Читать книгу: «Guía práctica de Kubernetes», страница 4
Mejores prácticas en el despliegue de servicios
Kubernetes es un sistema potente que puede parecer complicado. Pero poner en marcha una aplicación básica y tener éxito puede resultar fácil si utilizamos las siguientes prácticas:
• La mayoría de los servicios deberían desplegarse como recursos Deployments. Los Deployments crean réplicas idénticas en redundancia y escala.
• Los Deployments se pueden presentar utilizando un Service, que es un equilibrador de carga. Un Service se puede presentar dentro de un clúster (por defecto) o externamente. Si deseamos presentar una aplicación HTTP, podemos utilizar un controlador Ingress para agregar cosas como solicitar enrutamiento y SSL.
• Eventualmente, estaremos interesados en parametrizar la aplicación para hacer su configuración más reutilizable en diferentes entornos. Las herramientas de empaquetado como Helm son la mejor opción para este tipo de parametrización.
Resumen
La aplicación creada en este capítulo es sencilla, pero contiene casi todos los conceptos que necesitaremos para crear aplicaciones más grandes y complejas. Comprender cómo encajan las piezas y cómo utilizar los componentes más importantes de Kubernetes es clave para tener éxito cuando trabajamos con esta herramienta.
Sentar unas buenas bases en el control de versiones, la revisión de código y la ininterrumpida entrega del servicio nos asegurará que lo que creemos se creará de una manera sólida. A medida que progresemos sobre los temas más avanzados en los siguientes capítulos, habrá que tener en cuenta esta información fundamental.
CAPÍTULO 2
Flujos de trabajo para desarrolladores
Kubernetes se creó para operar software de manera confiable. Simplifica el despliegue y la gestión de software con una API orientada a la aplicación, con propiedades de autorregeneración y herramientas útiles como Deployments (implementaciones) para la puesta en marcha de software con tiempo de inactividad cero. Aunque todas estas herramientas tienen su utilidad, no contribuyen de forma importante a facilitar el desarrollo de aplicaciones en Kubernetes. Además, a pesar de que muchos clústeres están diseñados para ejecutar aplicaciones en el entorno de producción —y, por lo tanto, rara vez se accede a ellos mediante los flujos de trabajo de los desarrolladores—, también es fundamental permitir que los flujos de trabajo de desarrollo tengan como objetivo Kubernetes, y esto normalmente significa tener un clúster o al menos una parte de un clúster destinado a desarrollo. La configuración de este tipo de clústeres para facilitar el desarrollo de aplicaciones en Kubernetes es un aspecto necesario para asegurar el éxito. Si no hay ningún código que se cree para nuestro clúster, el clúster por sí mismo no va a conseguir mucho.
Objetivos
Antes de describir las mejores prácticas para la creación de clústeres de desarrollo, merece la pena establecer los objetivos para dichos clústeres. Obviamente, el objetivo final es hacer posible que los desarrolladores puedan crear aplicaciones en Kubernetes fácilmente y de forma rápida. Pero ¿qué significa eso en la práctica y cómo se refleja en las características prácticas del clúster de desarrollo?
Es útil identificar las fases de interacción del desarrollador con el clúster.
La primera fase es la de incorporación. Esto ocurre cuando un nuevo desarrollador se une al equipo de trabajo. Esta fase incluye facilitar al usuario el acceso al clúster, así como proporcionarle orientación en su primer despliegue. El objetivo para esta fase es conseguir que el desarrollador adquiera experiencia en un corto espacio de tiempo. Para este proceso deberíamos establecer el objetivo de Key Performance Indicator (indicador clave de desempeño) (KPI). Un objetivo razonable sería que el usuario pudiera pasar de la nada a poder dirigir la ejecución de la aplicación en curso en menos de media hora. Cada vez que alguien se incorpore al equipo de trabajo, comprobaremos cómo nos va con este objetivo.
La segunda fase es la de desarrollo. Esta es la actividad diaria del desarrollador. El objetivo de esta fase es asegurar una iteración y una depuración rápidas. Los desarrolladores necesitan pasar código al clúster rápidamente y de forma repetitiva. También deben ser capaces de probar su código y depurarlo cuando no funciona correctamente. El KPI para esta fase es más difícil de medir, pero se puede estimar midiendo el tiempo en obtener una pull request (petición de validación) (PR) o el tiempo empleado en el cambio y la ejecución en el clúster, o con encuestas sobre la productividad que percibe el usuario, o ambas. También podremos medirlo en la productividad global de los equipos de trabajo.
La tercera fase es de la de pruebas. Esta fase se intercala con la de desarrollo y se utiliza para validar el código antes de su envío y fusión. El objetivo de esta fase es doble. En primer lugar, el desarrollador debe ser capaz de realizar todas las pruebas de su entorno antes de presentar la PR. En segundo lugar, todas las pruebas deben ejecutarse automáticamente antes de que el código se fusione en el repositorio. Además de estos objetivos, también debemos establecer un KPI para la duración del tiempo que tardan las pruebas en realizarse. A medida que el proyecto se vuelve más complejo, es natural que un mayor número de pruebas necesite más tiempo. Cuando esto sucede, puede ser rentable identificar un conjunto más reducido de pruebas de humo que el desarrollador puede utilizar para la validación inicial antes de enviar la PR. También debemos tener un KPI muy estricto en cuanto a la debilidad de las pruebas. Una prueba problemática es una prueba que ocasionalmente (o no tan ocasionalmente) falla. En cualquier proyecto activo aceptable, un índice de debilidad de más de un fallo por cada mil ejecuciones hará que el desarrollador tenga problemas. Necesitamos asegurar que el entorno del clúster no conduce a pruebas problemáticas. A veces las pruebas problemáticas ocurren debido a problemas en el código, pero también pueden ocurrir debido a interferencias en el entorno de desarrollo (por ejemplo, quedarnos sin recursos o experimentar vecinos ruidosos). Debemos tener la seguridad de que nuestro entorno de desarrollo está libre de tales problemas midiendo la debilidad y actuando rápidamente para arreglarlo.
Creación de un clúster de desarrollo
Cuando alguien empieza a pensar en un desarrollo en Kubernetes, una de las primeras dudas que aparecen es si crear un único gran clúster de desarrollo o tener un clúster de desarrollo por desarrollador. Hay que tener en cuenta que esta opción solo tiene sentido en un entorno en el que la creación de clústeres dinámicos resulta fácil, como es la nube pública. En entornos físicos, es posible que la única opción sea un clúster grande.
Si tenemos opciones, debemos considerar los pros y los contras de cada opción. Si optamos por tener un clúster de desarrollo por usuario, el principal inconveniente de este enfoque es que será más costoso y menos eficiente, y tendremos que gestionar un gran número de clústeres de desarrollo diferentes. Los costes adicionales provienen del hecho de que es probable que cada clúster esté muy infrautilizado. Además, con los desarrolladores trabajando en diferentes clústeres, se hace más difícil rastrear y recolectar recursos que ya no están en uso.
La ventaja del enfoque del clúster por usuario es su simplicidad: cada desarrollador puede gestionar su propio clúster y, debido a su aislamiento, es mucho más difícil para los diferentes desarrolladores interferirse entre ellos.
Por otra parte, un único clúster de desarrollo será mucho más eficiente. Es probable que podamos mantener el mismo número de desarrolladores en un clúster compartido por la tercera parte del precio (o menos). Además, es mucho más fácil para nosotros instalar servicios en clústeres compartidos, por ejemplo la monitorización y el registro, lo que facilita considerablemente producir un clúster amigable con el desarrollador. La desventaja de un clúster de desarrollo compartido es el proceso de gestión de usuarios y la posible interferencia entre desarrolladores. Debido a que actualmente el proceso de añadir nuevos usuarios y namespaces (espacios de nombres) al clúster de Kubernetes no es ágil, deberemos activar un proceso para incorporar nuevos desarrolladores. Aunque la gestión de recursos de Kubernetes y el Role-Based Access Control (control de acceso basado en roles) (RBAC) pueden reducir la probabilidad de que dos desarrolladores entren en conflicto, siempre es posible que un usuario pueda bloquear el clúster de desarrollo al consumir demasiados recursos —de manera que otras aplicaciones y desarrolladores no los pueden planificar—. Además, deberemos asegurarnos de que los desarrolladores no pierdan ni olviden los recursos que han creado. Esto es algo más fácil, sin embargo, que el enfoque en el que los desarrolladores crean sus propios clústeres.
Ambos enfoques son factibles, pero nuestra recomendación es tener un único clúster grande para todos los desarrolladores. Aunque puedan existir problemas de interferencia entre ellos, estos se pueden gestionar y, en última instancia, la rentabilidad y la posibilidad de agregar fácilmente capacidades al clúster a nivel de toda la organización compensan los riesgos que supone la interferencia. Pero tendremos que invertir en los procesos de incorporación de nuevos desarrolladores, de gestión de recursos y de recolección de basura. Nuestra recomendación es intentarlo con un único clúster grande como primera opción. A medida que la organización crezca (o si ya es una organización grande), podríamos considerar tener un clúster por equipo de trabajo o grupo (de 10 a 20 personas), en lugar de un clúster gigante para cientos de usuarios. Esto puede simplificar tanto la facturación como la gestión.
Configuración de un clúster compartido por varios desarrolladores
Cuando se configura un clúster grande, el objetivo principal es asegurar que lo puedan utilizar varios usuarios simultáneamente sin que interfieran entre ellos. La manera obvia de separar a los diferentes desarrolladores es con los espacios de nombres de Kubernetes. Los espacios de nombres pueden servir como marco para el despliegue de servicios, de manera que el servicio de frontend de un usuario no interfiera con el servicio de frontend de otro usuario. Los espacios de nombres son también el marco para RBAC, con lo que se asegura que un desarrollador no pueda borrar accidentalmente el trabajo realizado por otro desarrollador. Por lo tanto, en un clúster compartido tiene sentido utilizar un espacio de nombres como el espacio de trabajo de cada desarrollador. Los procesos para la incorporación de usuarios y la creación y protección de un espacio de nombres se describen en las secciones siguientes.
Registro de usuarios
Antes de que podamos asignar un usuario a un espacio de nombres, debemos registrarlo en el propio clúster de Kubernetes. Para hacerlo hay dos opciones. Podemos utilizar la autenticación basada en certificados, crear un nuevo certificado para el usuario y proporcionarle un archivo kubeconfig que puede usar para iniciar sesión. O podemos configurar nuestro clúster para que utilice un sistema de identidad externo (por ejemplo, Microsoft Azure Active Directory o AWS Identity and Access Management [IAM]) para el acceso al clúster.
En general, el uso de un sistema de identidad externo es una buena práctica porque no requiere mantener dos fuentes de identidad diferentes, pero en algunos casos esto no es posible y necesitamos usar certificados. Afortunadamente, podemos usar la API de certificados de Kubernetes para la creación y gestión de dichos certificados. A continuación, veremos el proceso para añadir un nuevo usuario a un clúster existente. En primer lugar, necesitamos crear una solicitud de firma de certificado para generar un nuevo certificado. A continuación, veremos un sencillo programa en Go que permite realizarlo:
package main import ( "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/pem" "os" ) func main() { name := os.Args[1] user := os.Args[2] key, err := rsa.GenerateKey(rand.Reader, 1024) if err != nil { panic(err) } keyDer := x509.MarshalPKCS1PrivateKey(key) keyBlock := pem.Block{ Type: "RSA PRIVATE KEY", Bytes: keyDer, } keyFile, err := os.Create(name + "-key.pem") if err != nil { panic(err) } pem.Encode(keyFile, &keyBlock) keyFile.Close() commonName := user // You may want to update these too emailAddress := "someone@myco.com" org := "My Co, Inc." orgUnit := "Widget Farmers" city := "Seattle" state := "WA" country := "US" subject := pkix.Name{ CommonName: commonName, Country: []string{country}, Locality: []string{city}, Organization: []string{org}, OrganizationalUnit: []string{orgUnit}, Province: []string{state}, } asn1, err := asn1.Marshal(subject.ToRDNSequence()) if err != nil { panic(err) } csr := x509.CertificateRequest{ RawSubject: asn1, EmailAddresses: []string{emailAddress}, SignatureAlgorithm: x509.SHA256WithRSA, } bytes, err := x509.CreateCertificateRequest(rand.Reader, &csr, key) if err != nil { panic(err) } csrFile, err := os.Create(name + ".csr") if err != nil { panic(err) } pem.Encode(csrFile, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: bytes}) csrFile.Close() }
Podemos ejecutarlo de la siguiente manera:
go run csr-gen.go client <user-name>
Con esto creamos los archivos llamados client-key.pem y client.csr. A continuación, podemos ejecutar el siguiente script para crear y descargar un nuevo certificado:
#!/bin/bash csr_name="my-client-csr" name="${1:-my-user}" csr="${2}" cat <<EOF | kubectl create -f - apiVersion: certificates.k8s.io/v1beta1 kind: CertificateSigningRequest metadata: name: ${csr_name} spec: groups: - system:authenticated request: $(cat ${csr} | base64 | tr -d '\n') usages: - digital signature - key encipherment - client auth EOF echo echo "Approving signing request." kubectl certificate approve ${csr_name} echo echo "Downloading certificate." kubectl get csr ${csr_name} -o jsonpath='{.status.certificate}' \ | base64 --decode > $(basename ${csr} .csr).crt echo echo "Cleaning up" kubectl delete csr ${csr_name} echo echo "Add the following to the 'users' list in your kubeconfig file:" echo "- name: ${name}" echo " user:" echo " client-certificate: ${PWD}/$(basename ${csr} .csr).crt" echo " client-key: ${PWD}/$(basename ${csr} .csr)-key.pem" echo echo "Next you may want to add a role-binding for this user."
Este script imprime la información final que podemos añadir al archivo kubeconfig para habilitar al usuario. Por supuesto, el usuario no tiene privilegios de acceso, por lo que tendremos que aplicar RBAC de Kubernetes al usuario con el fin de dotar de privilegios al espacio de nombres.
Creación y dotación de seguridad a un espacio de nombres
El primer paso para proporcionar un espacio de nombres es crearlo. Podemos hacerlo usando kubectl create namespace my-namespace.
Pero la verdad es que cuando creamos un espacio de nombres, necesitamos adjuntar un montón de metadatos, como por ejemplo la información de contacto del equipo de trabajo que crea el componente que se ha implementado en el espacio de nombres. Generalmente, esto se hace en forma de anotaciones. Podemos generar el archivo YAML usando alguna plantilla, como por ejemplo Jinja u otras, o bien podemos crear el espacio de nombres y, a continuación, hacer la anotación. Veamos un sencillo script para hacer esto:
ns='my-namespace' kubectl create namespace ${ns} kubectl annotate namespace ${ns} annotation_key= annotation_value
Cuando se crea el espacio de nombres, necesitamos dotarlo de seguridad para tener la garantía de que podemos conceder acceso a un usuario específico. Para ello, podemos vincular un rol a un usuario en el contexto de ese espacio de nombres. Esto lo conseguimos creando el objeto RoleBinding dentro del propio espacio de nombres. RoleBindig podría tener este aspecto:
apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: example namespace: my-namespace roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: edit subjects: - apiGroup: rbac.authorization.k8s.io kind: User name: myuser
Para crearlo, lo único que tenemos que hacer es ejecutar kubectl create -f role-binding.yaml. Hay que tener en cuenta que podemos reutilizar este vínculo tanto como deseemos, siempre y cuando actualicemos el espacio de nombres en el vínculo para apuntar al espacio de nombres correcto. Si tenemos la seguridad de que el usuario no tiene ningún otro vínculo de rol, podemos estar seguros de que este espacio de nombres es la única parte del clúster a la que el usuario tiene acceso. Otra práctica razonable es conceder acceso a los lectores a todo el clúster; de esta manera, los desarrolladores pueden ver lo que otros están haciendo en caso de que interfieran con su trabajo. Sin embargo, debemos tener cuidado al conceder dicho acceso de lectura porque este incluirá el acceso a recursos secretos en el clúster. Generalmente, en un clúster de desarrollo esto está bien porque todo el mundo está en la misma organización y los datos secretos se utilizan solo en desarrollo. Sin embargo, si es motivo de preocupación, podemos crear un rol más detallado en el que se elimine la posibilidad de leer datos secretos.
Si deseamos limitar la cantidad de recursos que consume un espacio de nombres determinado, podemos utilizar el recurso ResourceQuota para fijar un límite al número total de recursos que consume ese espacio de nombres en particular. Por ejemplo, la siguiente cuota limita el espacio de nombres a 10 núcleos y 100 GB de memoria, para Request y Limit, para las cápsulas en ese espacio de nombres:
apiVersion: v1 kind: ResourceQuota metadata: name: limit-compute namespace: my-namespace spec: hard: requests.cpu: "10" requests.memory: 100Gi limits.cpu: "10" limits.memory: 100Gi
Administración de espacios de nombres
Ahora que hemos visto cómo incorporar a un nuevo usuario y cómo crear un espacio de nombres para utilizarlo como espacio de trabajo, la pregunta sigue siendo cómo asignar un desarrollador a un espacio de nombres. Como con muchas otras cosas, no hay una sola respuesta; en este caso hay dos enfoques. El primero es dar a cada usuario su propio espacio de nombres como parte del proceso de integración. Esto es útil porque después de que un usuario se haya registrado, siempre tiene un espacio de trabajo donde puede desarrollar y gestionar sus aplicaciones. Sin embargo, hacer que el espacio de nombres del desarrollador se extienda demasiado en el tiempo anima a este a dejar las cosas en el espacio de nombres después de haber terminado de usarlas, y la recogida de basura y la contabilidad de los recursos individuales se complican. Un enfoque alternativo consiste en crear y asignar temporalmente un espacio de nombres con un time to live (tiempo de vida) (TTL) limitado. Esta opción asegura que el desarrollador tenga en cuenta que los recursos en el clúster son transitorios y que es fácil automatizar la eliminación de espacios de nombres completos cuando su TTL expire.
En este modelo, cuando el desarrollador quiere iniciar un nuevo proyecto, utiliza una herramienta para asignar un nuevo espacio de nombres al proyecto. Cuando creamos el espacio de nombres, este tiene una selección de metadatos asociados a él para la gestión y la contabilidad. Obviamente, estos metadatos incluyen el TTL para el espacio de nombres, pero también incluyen al desarrollador al que está asignado, los recursos que deben asignarse al espacio de nombres (por ejemplo, CPU y memoria), el equipo de trabajo y el propósito del espacio de nombres. Estos metadatos garantizan que podamos realizar un seguimiento del uso de los recursos y eliminar el espacio de nombres en el momento adecuado.
El desarrollo de herramientas para asignar espacios de nombres a petición puede parecer un reto, pero las herramientas sencillas son relativamente fáciles de desarrollar. Por ejemplo, se puede lograr la asignación de un nuevo espacio de nombres con un sencillo script que crea el espacio de nombres y solicita los metadatos relevantes para adjuntarlos al mismo.
Si queremos tener una mayor integración con Kubernetes, podemos usar las custom resources definitions (definiciones de recursos personalizados) (CRD), que permiten a los usuarios crear y asignar dinámicamente nuevos espacios de nombres mediante la herramienta kubectl. Si tienes tiempo y ganas, esta es definitivamente una buena opción porque hace que la gestión de los espacios de nombres sea declarativa y también permite el uso de RBAC de Kubernetes.
Una vez disponemos de las herramientas para habilitar la asignación de espacios de nombres, necesitamos añadir algunas herramientas para reutilizarlos cuando su TTL haya expirado. Una vez más, podemos conseguirlo con un sencillo script que examina los espacios de nombres y borra aquellos que tienen una TTL caducada.
Podemos crear este script en un contenedor y usar ScheduledJob para ejecutarlo con una frecuencia de una hora. Combinadas entre sí, estas herramientas pueden garantizar que los desarrolladores puedan asignar fácilmente recursos independientes para su proyecto en la medida en que lo necesiten. Pero esos recursos también se obtendrán en el intervalo adecuado para tener la seguridad de que no estamos malgastando recursos y de que los recursos antiguos no se interponen en el camino del nuevo desarrollo.