Organización del despliegue en múltiples entornos k8s con helmfile

Helmfile — un envoltorio para helm, que permite describir en un solo lugar múltiples lanzamientos de helm, parametrizar sus gráficos para varios entornos y también establecer el orden de su implementación.

Sobre helmfile y ejemplos de su uso se puede leer en readme y guía de mejores prácticas.

Nos familiarizaremos con formas no obvias de describir lanzamientos en helmfile

Supongamos que tenemos un conjunto de gráficos de helm (por ejemplo, postgres y alguna aplicación de backend) y varios entornos (varios clústeres de kubernetes, varios namespaces o ambos). Tomamos helmfile, leemos la documentación y comenzamos a describir nuestros entornos y lanzamientos:

    .
    ├── envs
    │   ├── devel
    │   │   └── values
    │   │       ├── backend.yaml
    │   │       └── postgres.yaml
    │   └── production
    │       └── values
    │           ├── backend.yaml
    │           └── postgres.yaml
    └── helmfile.yaml

helmfile.yaml

environments:
  devel:
  production:

releases:
  - name: postgres
    labels:
      app: postgres
    wait: true
    chart: stable/postgresql
    version: 8.4.0
    values:
      - envs/{{ .Environment.Name }}/values/postgres.yaml
  - name: backend
    labels:
      app: backend
    wait: true
    chart: private-helm-repo/backend
    version: 1.0.5
    needs:
      - postgres
    values:
      - envs/{{ .Environment.Name }}/values/backend.yaml

Hemos creado 2 entornos: devel, producción — en cada uno hay sus propios valores para los gráficos de helm de los lanzamientos. Los implementaremos así:

helmfile -n  -e  apply

Diferentes versiones de gráficos de helm en diferentes entornos

¿Qué hacer si necesitamos desplegar diferentes versiones del backend en diferentes entornos? ¿Cómo parametrizar la versión del lanzamiento? Aquí entran en juego los valores del entorno, disponibles a través de {{ .Values }}

helmfile.yaml

environments:
  devel:
+   values:
+   - charts:
+       versions:
+         backend: 1.1.0
  production:
+   values:
+   - charts:
+       versions:
+         backend: 1.0.5
...
  - name: backend
    labels:
      app: backend
    wait: true
    chart: private-helm-repo/backend
-   version: 1.0.5
+   version: {{ .Values.charts.versions.backend }}
...

Diferente conjunto de aplicaciones en diferentes entornos

Genial, pero ¿qué pasa si no necesitamos producción desplegar postgres, porque sabemos que no debemos meter una base de datos en k8s y para producción tenemos un magnífico clúster de postgres aparte? Para resolver este problema tenemos etiquetas (labels)

helmfile -n  -e devel apply
helmfile -n  -e production -l app=backend apply

Es genial, pero personalmente prefiero describir qué aplicaciones desplegar en el entorno no mediante argumentos de lanzamiento, sino en la descripción de los propios entornos. ¿Qué hacer? Se puede colocar la descripción de los lanzamientos en una carpeta separada, en la descripción del entorno incluir una lista de los lanzamientos necesarios y "conectar" solo los lanzamientos necesarios, ignorando los demás.

    .
    ├── envs
    │   ├── devel
    │   │   └── valores
    │   │       ├── backend.yaml
    │   │       └── postgres.yaml
    │   └── producción
    │       └── valores
    │           ├── backend.yaml
    │           └── postgres.yaml
+   ├── lanzamientos
+   │   ├── backend.yaml
+   │   └── postgres.yaml
    └── helmfile.yaml

helmfile.yaml


  entornos:
    devel:
      valores:
      - gráficos:
          versiones:
            backend: 1.1.0
      - aplicaciones:
        - postgres
        - backend

    producción:
      valores:
      - gráficos:
          versiones:
            backend: 1.0.5
      - aplicaciones:
        - backend

- lanzamientos:
-    - nombre: postgres
-      etiquetas:
-        app: postgres
-      espera: true
-      gráfico: estable/postgresql
-      versión: 8.4.0
-      valores:
-        - envs/{{ .Environment.Name }}/valores/postgres.yaml
-    - nombre: backend
-      etiquetas:
-        app: backend
-      espera: true
-      gráfico: private-helm-repo/backend
-     versión: {{ .Values.charts.versions.backend }}
-     necesita:
-       - postgres
-     valores:
-       - envs/{{ .Environment.Name }}/valores/backend.yaml
+ ---
+ bases:
+ {{- range .Values.aplicaciones }}
+   - lanzamientos/{{ . }}.yaml
+ {{- end }}

lanzamientos/postgres.yaml

lanzamientos:
  - nombre: postgres
    etiquetas:
      app: postgres
    espera: true
    gráfico: estable/postgresql
    versión: 8.4.0
    valores:
      - envs/{{ .Environment.Name }}/valores/postgres.yaml

lanzamientos/backend.yaml

lanzamientos:
  - nombre: backend
    etiquetas:
      app: backend
    espera: true
    gráfico: private-helm-repo/backend
    versión: {{ .Values.charts.versions.backend }}
    necesita:
      - postgres
    valores:
      - envs/{{ .Environment.Name }}/valores/backend.yaml

Nota

Al usar bases: es necesario utilizar el delimitador yaml ---, para que se puedan templatear los lanzamientos (y otras partes, como helmDefaults) con valores de los entornos

En ese caso, el lanzamiento de postgres ni siquiera aparecería en la descripción para producción. ¡Es muy conveniente!

Valores globales anularábles para los lanzamientos

Por supuesto, es genial que se puedan definir valores para los gráficos de helm en cada entorno, pero ¿qué pasa si tenemos varios entornos descritos y queremos, por ejemplo, establecer uno igual para todos? afinidad, pero no queremos configurarlo por defecto en los propios gráficos, que están almacenados en los repos.

En ese caso, podríamos establecer para cada lanzamiento 2 archivos con valores: el primero con valores predeterminados, que determinarán los valores del propio gráfico, y el segundo con valores para el entorno, que a su vez anulará los predeterminados.

    .
    ├── envs
+   │   ├── default
+   │   │   └── values
+   │   │       ├── backend.yaml
+   │   │       └── postgres.yaml
    │   ├── devel
    │   │   └── values
    │   │       ├── backend.yaml
    │   │       └── postgres.yaml
    │   └── production
    │       └── values
    │           ├── backend.yaml
    │           └── postgres.yaml
    ├── releases
    │   ├── backend.yaml
    │   └── postgres.yaml
    └── helmfile.yaml

lanzamientos/backend.yaml

releases:
  - name: backend
    labels:
      app: backend
    wait: true
    chart: private-helm-repo/backend
    version: {{ .Values.charts.versions.backend }}
    needs:
      - postgres
    values:
+     - envs/default/values/backend.yaml
      - envs/{{ .Environment.Name }}/values/backend.yaml

envs/default/values/backend.yaml

affinity:
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
    - weight: 1
      podAffinityTerm:
        labelSelector:
          matchExpressions:
          - key: app.kubernetes.io/name
            operator: In
            values:
            - backend
        topologyKey: "kubernetes.io/hostname"

Definición de valores globales para los charts de helm de todos los lanzamientos a nivel de entorno

Supongamos que tenemos varios ingress en varios lanzamientos — podríamos definir manualmente para cada chart hosts:, pero en nuestro caso el dominio es el mismo, así que ¿por qué no sacarlo a una variable global y simplemente usar su valor en los charts? Para eso, los archivos de valores que queremos parametrizar deben tener la extensión .gotmpl, para que helmfile sepa que debe pasarlos por el motor de plantillas.

    .
    ├── envs
    │   ├── default
    │   │   └── values
-   │   │       ├── backend.yaml
-   │   │       ├── postgres.yaml
+   │   │       ├── backend.yaml.gotmpl
+   │   │       └── postgres.yaml.gotmpl
    │   ├── devel
    │   │   └── values
    │   │       ├── backend.yaml
    │   │       └── postgres.yaml
    │   └── production
    │       └── values
    │           ├── backend.yaml
    │           └── postgres.yaml
    ├── releases
    │   ├── backend.yaml
    │   └── postgres.yaml
    └── helmfile.yaml

helmfile.yaml

  environments:
    devel:
      values:
      - charts:
          versions:
            backend: 1.1.0
      - apps:
        - postgres
        - backend
+     - global:
+         ingressDomain: k8s.devel.domain

    production:
      values:
      - charts:
          versions:
            backend: 1.0.5
      - apps:
        - backend
+     - global:
+         ingressDomain: production.domain
  ---
  bases:
  {{- range .Values.apps }}
    - releases/{{ . }}.yaml
  {{- end }}

envs/default/values/backend.yaml.gotmpl

ingress:
  enabled: true
  paths:
    - /api
  hosts:
    - {{ .Values.global.ingressDomain }}

envs/default/values/postgres.yaml.gotmpl

ingress:
  enabled: true
  paths:
    - /
  hosts:
    - postgres.{{ .Values.global.ingressDomain }}

Nota

Es obvio que el ingress en el chart de Postgres es algo bastante cuestionable, por lo que en el artículo se presenta simplemente como un ejemplo esférico en un vacío y para no introducir una nueva versión en el artículo solo por describir el ingress.

Interpolación de secretos (secrets) desde variables de entorno.

De manera similar al ejemplo anterior, se pueden interpolar valores cifrados utilizando helm secrets . En lugar de crear un archivo de secretos para cada versión, donde definiríamos los valores cifrados para el chart, podemos simplemente definir en el archivo default.yaml.gotmpl valores que se obtendrán de variables asignadas a nivel de entorno. Y los valores que no necesitamos ocultar de nadie se pueden redefinir en los valores de la versión en un entorno específico.

    .
    ├── envs
    │   ├── default
    │   │   └── values
    │   │       ├── backend.yaml
    │   │       └── postgres.yaml
    │   ├── devel
    │   │   ├── values
    │   │   │   ├── backend.yaml
    │   │   │   └── postgres.yaml
+   │   │   └── secrets.yaml
    │   └── production
    │       ├── values
    │       │   ├── backend.yaml
    │       │   └── postgres.yaml
+   │       └── secrets.yaml
    ├── releases
    │   ├── backend.yaml
    │   └── postgres.yaml
    └── helmfile.yaml

helmfile.yaml

  environments:
    devel:
      values:
      - charts:
          versions:
            backend: 1.1.0
      - apps:
        - postgres
        - backend
      - global:
          ingressDomain: k8s.devel.domain
+     secrets:
+       - envs/devel/secrets.yaml

    production:
      values:
      - charts:
          versions:
            backend: 1.0.5
      - apps:
        - backend
      - global:
          ingressDomain: production.domain
+     secrets:
+       - envs/production/secrets.yaml
  ---
  bases:
  {{- range .Values.apps }}
    - releases/{{ . }}.yaml
  {{- end }}

envs/devel/secrets.yaml

secrets:
    elastic:
        password: ENC[AES256_GCM,data:hjCB,iv:Z1P6/6xBJgJoKLJ0UUVfqZ80o4L84jvZfM+uH9gBelc=,tag:dGqQlCZnLdRAGoJSj63rBQ==,type:int]
...

envs/production/secrets.yaml

secrets:
    elastic:
        password: ENC[AES256_GCM,data:ZB/VpTFk8f0=,iv:EA//oT1Cb5wNFigTDOz3nA80qD9UwTjK5cpUwLnEXjs=,tag:hMdIUaqLRA8zuFBd82bz6A==,type:str]
...

envs/default/values/backend.yaml.gotmpl

elasticsearch:
  host: elasticsearch
  port: 9200
  password: {{ .Values | getOrNil "secrets.elastic.password" | default "password" }}

envs/devel/values/backend.yaml

elasticsearch:
  host: elastic-0.devel.domain

envs/production/values/backend.yaml

elasticsearch:
  host: elastic-0.production.domain

Nota

Por cierto, getOrNil — una función especial para las plantillas de Go en helmfile, que, incluso si .Values.secrets no existe, no lanza un error, sino que permite finalmente usar la función default para establecer un valor por defecto

Conclusión

Las cosas descritas parecen bastante obvias, pero la información sobre cómo realizar un despliegue conveniente en varios entornos utilizando helmfile es muy escasa, y a mí me gusta IaC (Infrastructure-as-Code) y quiero tener una descripción clara del estado del despliegue.

En conclusión, quiero añadir que las variables para el entorno por defecto se pueden parametrizar con variables de entorno del sistema operativo de algún runner desde el cual se iniciará el despliegue, lo que permitirá obtener entornos dinámicos.

helmfile.yaml

environments:
  default:
    values:
    - global:
        clusterDomain: {{ env "CLUSTER_DOMAIN" | default "cluster.local" }}
        ingressDomain: {{ env "INGRESS_DOMAIN" }}

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster