Buenas, después de un parón sin escribir mucho, vuelvo a las andadas. La intención es ir sacando una serie de posts con diferentes temáticas y modalidades de Kubernetes. Desde un despliegue simple hasta como gestionar una integración continua. Para empezar, vamos a crear unos YAML que desplieguen un stack de WordPress básico. Este constará de; un Nginx, un PHP-FPM y un MySQL. La idea, es utilizar diferentes opciones para que se pueda ver las diferencias entre ellas y sea lo mas completo posible.
Todo esto, lo levantaremos sobre un Minikube. Se puede ver como instalarlo en el siguiente post.
Empezamos
Una vez desplegado Minikube, vamos a empezar a declarar los YAML que vamos a necesitar. Por comodidad, la nomenclatura de los ficheros empezarán por un número que tendrá que ver con el servicio, seguido del nombre del servicio y el recurso que vamos a utilizar.
Ej: 11-nginx-configmap.yaml
Los YAML los tengo definidos ya y subidos a github:
https://github.com/ichasco/Kubernetes-WP-test
Lo que iremos haciendo, es ir analizándolos la sintaxis de estos, uno a uno.
YAMLs transversales
Hay YAMLs que no están directamente asociados a un servicio, como pueden ser: los de namespace, roles, PDB… En este caso, solo vamos a utilizar uno, que será el de namespace. En este caso es el que tiene nombre: 00-namespace.yaml
En este fichero, vamos a definir el namespace que vamos a crear. Es recomendable crear un namespace nuevo para separar las aplicaciones. En este caso va a ser un namespace de producción
---
apiVersion: v1
kind: Namespace
metadata:
name: production
Nginx
Para el servicio de Nginx, vamos a utilizar un configmap, un service y un deployment. A continuación explico que es cada cosa.
Configmap
En este caso, el archivo de configmap se llamará: 11-nginx-configmap.yaml. En este fichero, definiremos los ficheros de configuración que queramos que tenga Nginx, luego estos se invocarán desde el fichero de deployment.
Hay varios modos aparte de este, como crear la imagen de Docker con la configuración incluida. Yo no soy muy partidario de esto, ya que las imágenes de Docker deberían de ser lo mas estándar posibles.
En este caso, vamos a declarar los siguientes ficheros:
- nginx.conf
- virtualhost.conf
- security.conf
- cache.conf
El formato sería el siguiente:
---
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx
namespace: production
labels:
app: nginx
data:
nginx.conf: |
...
virtualhost.conf: |
...
security.conf: |
...
cache.conf: |
...
Service
Ahora vamos a definir el servicio. En este caso, se va a llamar 13-nginx-service.yaml. Esto es simplemente definir por que puerto se van a realizar las conexiones y que tipo de servicio queremos. En este caso va a ser NodePort porque nos vamos a conectar desde fuera del cluster. En un entorno productivo esto será de tipo ingress o loadblancer. O generar otro ingress que sea el que gestione los accesos.
Mas info sobre los servicios:
https://medium.com/google-cloud/kubernetes-nodeport-vs-loadbalancer-vs-ingress-when-should-i-use-what-922f010849e0
---
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: production
spec:
selector:
app: nginx
type: NodePort
ports:
- name: nginx-port
port: 80
targetPort: 80
nodePort: 30036
protocol: TCP
Deployment
Los deployment, como su propio nombre indica se utilizan para desplegar PODs. Por norma general, los deployment se utilizan para PODs stateless y los statefulset para stateful. Esto lo veremos también en el servicio de MySQL. En este caso, el YAML se llamará 14-nginx-deployment.yaml. En este, se declarará toda la lógica y comportamiento del POD:
- Estrategia de despliegue: Rolling Update (Es lo mismo que Ramped)
- Resources: Se establecen los limites de CPU y RAM. En el CPU 1000m equivale a 1 core. Por lo que 100m equivale a un 10% de 1 core.
- request: Los recursos que se necesitan para levantar el container.
- limits: El limite de recursos que puede coger el container.
- livenessProbe: healthcheck de que el servicio funciona correctamente. Si no está OK reinicia el contenedor.
- readinessProbe: healthcheck que comprueba que el contenedor se ha levantado correctamente.
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: nginx
namespace: production
spec:
replicas: 1
selector:
matchLabels:
app: nginx
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
type: RollingUpdate
template:
metadata:
labels:
app: nginx
roles: web
stages: production
spec:
containers:
- name: nginx
image: "nginx:latest"
imagePullPolicy: IfNotPresent
env:
- name: TZ
value: "Europe/Madrid"
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "200m"
memory: "254Mi"
ports:
- name: nginx-port
containerPort: 80
livenessProbe:
tcpSocket:
port: nginx-port
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
tcpSocket:
port: nginx-port
initialDelaySeconds: 5
periodSeconds: 10
volumeMounts:
- name: nginx-conf
mountPath: /etc/nginx/conf.d
- name: wordpress-files
mountPath: /var/www/html
- name: nginx
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
volumes:
- name: nginx-conf
configMap:
name: nginx-conf
items:
- key: virtualhost.conf
path: virtualhost.conf
- key: security.conf
path: security.conf
- key: cache.conf
path: cache.conf
- name: nginx
configMap:
name: nginx-conf
items:
- key: nginx.conf
path: nginx.conf
- name: wordpress-files
persistentVolumeClaim:
claimName: wordpress-files
Como se puede ver en el deployment, hay varios volúmenes definidos (esto está abajo del todo en la parte de volumes). En este caso, estamos invocando 2 veces al configmap que hemos definido antes. ¿Por qué? Porque los vamos a montar en paths distintos. Unos van sobre la carpeta conf.d y otro va sobre la raíz de la carpeta de Nginx. Si montamos todos en la raíz de Nginx, se pisarán todos los ficheros que haya. Cuando se quiere montar un solo fichero en un path sin borrar el resto, hay que usar la opción subPath en el volumeMounts.
El tercero, es el volumen que va a tener los ficheros de WordPress. Que estarán compartidos con con el POD de PHP-FPM.
Una vez definidos los volúmenes, hay que montarlos con VolumeMounts.
PHP-FPM
El servicio de PHP-FPM es muy parecido al de Nginx cambiando un par de cosas.
Secrets
A diferencia del servicio de Nginx, el de PHP-FPM tiene secretos. Kubernetes a día de hoy esto no lo tiene muy seguro. Lo secretos pueden ir tanto en base64 como en plano. En futuros posts, miraremos como gestionar esto con Vault. Haremos los 2 ejemplos. En este caso, el YAML se llamará 20-phpfpm-secrets.yaml. En este ejemplo, irán en plano. Y la forma de definirlos es como si fuese una variable.
Para usarlo en texto plano, hay que usar el modo stringData
apiVersion: v1
kind: Secret
metadata:
name: phpfpm-secrets
namespace: production
type: Opaque
stringData:
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress
Configmap
En este caso, no vamos a definir ficheros en el configmap, si no variables (que no sean sensibles). Hay 2 formas de definirlas, puede ser en el configmap o directamente en el deployment/statefulset. Las ventajas y desventajas son; que el configmap puede ser usado por varios servicios y que es mas limpio, pero cada vez que se cambie algo, además de aplicar los cambios del configmap, hay que reiniciar el POD para que se apliquen los cambios. En el caso de definirlas en el POD, la sintaxis es un poco mas extensa pero cada vez que se cambie una variable, se reiniciará solo el POD.
En este caso el YAML se llamará 21-phpfpm-configmap.yaml.
---
apiVersion: v1
kind: ConfigMap
metadata:
name: phpfpm-conf
namespace: production
labels:
app: phpfpm
data:
WORDPRESS_DB_HOST: mysql
WORDPRESS_DB_NAME: wordpress
Service
El servicio, en este caso será ClusterIP ya que no es necesario que esté expuesto desde fuera, solo para los servicios del clúster. El fichero es 22-phpfpm-service.yaml.
---
apiVersion: v1
kind: Service
metadata:
name: phpfpm
namespace: production
spec:
selector:
app: phpfpm
type: ClusterIP
ports:
- name: phpfpm-port
port: 9000
targetPort: 9000
Deployment
El deployment en este caso es muy parecido al de Nginx. Por lo que no lo voy a pegar entero. Solo los cachos que sean interesantes. El YAML se llama 23-phpfpm-deployment.yaml.
...
env:
- name: TZ
value: "Europe/Madrid"
envFrom:
- configMapRef:
name: phpfpm-conf
- secretRef:
name: phpfpm-secrets
...
volumeMounts:
- name: wordpress-files
mountPath: /var/www/html
volumes:
- name: wordpress-files
persistentVolumeClaim:
claimName: wordpress-files
Aquí se puede ver lo que hemos comentado antes de las variables. Las variables, cuando se definen en el deployment, se define con el env y hay que hacer un name y value por cada una, por lo que si hay muchas, el fichero puede ser muy largo. En el caso de que definan en un configmap o un secret, solo hay que invocarlos con el envFrom y se cargarán todas las variables.
Hay otra opción de llamar a variables concretas de un configmap o de un secret. Esta forma también es muy interesante si se quieren coger variables del secret/configmap de otro servicio invocándolas con el nombre que queramos y así no duplicarlas. Sería del siguiente modo:
...
env:
- name: WORDPRESS_DB_PASSWORD
valueFrom:
secretKeyRef:
name: phpfpm-secret
key: WORDPRESS_DB_PASSWORD
...
En el caso de los volúmenes, es lo mismo que en Nginx, en el siguiente YAML definiremos el PVC que se monta.
PersistentVolumeClaim
Este es nuevo, aquí lo que se define es el PVC que vamos a utilizar para guardar los ficheros de WordPress. El YAML se llama 24-phpfpm-pvc.yaml.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: wordpress-files
namespace: production
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
MySQL
Y por último, queda definir el servicio de MySQL.
Secrets
En el anterior servicio, hemos creado los secrets en plano. En este caso para cambiar, los vamos a definir en base64 que es la otra forma de hacerlo. Para generar el base64 del secreto que queramos hay que lanzar el siguiente comando:
echo "secreto" | base64
Y si se quiere pasar a texto plano:
echo c2VjcmV0bwo= | base64 -d
El YAML se llama 30-mysql-secrets.yaml. Para añadir los secretos en base64, el modo que hay que usar es data.
apiVersion: v1
kind: Secret
metadata:
name: mysql-secrets
namespace: production
type: Opaque
data:
MYSQL_USER: d29yZHByZXNz
MYSQL_PASSWORD: d29yZHByZXNz
MYSQL_ROOT_PASSWORD: Y2hhbmdlbWU=
Configmap
El configmap, es muy parecido al de PHP-FPM. Se llama 31-mysql-configmap.yaml.
Service
Igual que el configmap, este también muy parecido al del PHP-FPM. 32-mysql-service.yaml.
Statefulset
Como hemos comentado antes, el statefulset está pensado para servicios que sean stateful, es decir, que puedan albergar datos que cambien. La declaración de estos, es muy parecida a la del deployment, pero varia en algunas cosas. El YAML se llama 33-mysql-statefulset.yaml.
apiVersion: apps/v1beta2
kind: StatefulSet
...
spec:
serviceName: mysql
...
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
subPath: mysql
...
volumeClaimTemplates:
- metadata:
name: mysql-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
Principalmente, lo que varia es el kind, que es Statefulset, que añade el serviceName y luego que los PVC se puede declarar como volumeClaimTemplates en el mismo YAML. Lo que se hace es declarar un PVC nuevo y luego montarlo en el path que se quiera.
Despliegue
Para desplegar esto, una vez creados todos los archivos y teniendo el Minikube levantado, solo hay que lanzar lo siguiente:
kubectl apply -f .
namespace/production created
configmap/nginx-conf created
service/nginx created
deployment.apps/nginx created
secret/phpfpm-secrets created
configmap/phpfpm-conf created
service/phpfpm created
deployment.apps/phpfpm created
persistentvolumeclaim/wordpress-files created
secret/mysql-secrets created
configmap/mysql-conf created
service/mysql created
statefulset.apps/mysql created
Una vez desplegado, podemos conectarnos al Nginx por el puerto que hemos destinado a ello:
http://IP_Minikube:30036
Para ver la IP de Minikube:
minikube status
host: Running
kubelet: Running
apiserver: Running
kubectl: Correctly Configured: pointing to minikube-vm at 192.168.99.108
Troubleshooting
PODs
Para ver los PODs hay que usar el siguiente comando
kubectl -n production get pods
NAME READY STATUS RESTARTS AGE
mysql-0 1/1 Running 0 85s
nginx-69945748c6-nls7z 1/1 Running 0 86s
phpfpm-697d4f9f9d-pfsdn 1/1 Running 0 86s
Servicios
kubectl -n production get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
mysql ClusterIP 10.101.164.170 <none> 3306/TCP 102s
nginx NodePort 10.109.3.47 <none> 80:30036/TCP 103s
phpfpm ClusterIP 10.111.192.13 <none> 9000/TCP 103s
PersistentVolumes
kubectl -n production get pv
pvc-4a839582-88fb-11e9-8971-0800271416ea 1Gi RWX Delete Bound production/wordpress-files standard 114s
pvc-4a9edb1c-88fb-11e9-8971-0800271416ea 10Gi RWO Delete Bound production/mysql-data-mysql-0 standard 114s
kubectl -n production get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
mysql-data-mysql-0 Bound pvc-4a9edb1c-88fb-11e9-8971-0800271416ea 10Gi RWO standard 2m5s
wordpress-files Bound pvc-4a839582-88fb-11e9-8971-0800271416ea 1Gi RWX standard 2m6s
Secrets
kubectl -n production get secrets
NAME TYPE DATA AGE
default-token-fw6j6 kubernetes.io/service-account-token 3 2m21s
mysql-secrets Opaque 3 2m20s
phpfpm-secrets Opaque 2 2m21s
Logs
Ver los últimos logs
kubectl -n production logs nginx-69945748c6-nls7z
Ver los logs en tiempo real
kubectl -n production logs -f nginx-69945748c6-nls7z
Ver todos los logs de los contenedores de un POD
kubectl -n production logs -f nginx-69945748c6-nls7z --all-containers=true
Port Forwarding
Traerse un puerto de un POD al local
kubectl -n production port-forward phpfpm-697d4f9f9d-pfsdn 9000:9000
Conectarse al POD
kubectl -n production exec -it nginx-69945748c6-nls7z -- bash
Resumen
En este post hemos visto como empezar a definir un Stack de servicios conectados entre ellos. Utilizando diferentes recursos y tipos propios de Kubernetes. En los siguientes posts intentaré explicar:
- RBAC y autenticación
- Ingress Controller
- Secrets con Vault
- CNIs
- Monitorización con Prometheus
- Gestión de Logs
- HELM
- Istio aplicado
- GitOps
- Continuous Deployment
Excelente post, directo y claro
Gracias 🙂