Organização de deploy em múltiplos ambientes k8s usando helmfile

Helmfile — uma camada para helm, que permite descrever múltiplos lançamentos helm em um só lugar, parametrizar seus charts para vários ambientes, além de definir a ordem de seu deploy.

Você pode ler sobre o helmfile e exemplos de uso na readme e guia de melhores práticas.

Vamos explorar maneiras menos óbvias de descrever lançamentos no helmfile.

Suponha que temos um conjunto de charts helm (por exemplo, postgres e uma aplicação backend) e vários ambientes (vários clusters Kubernetes, vários namespaces ou ambos). Pegamos o helmfile, lemos a documentação e começamos a descrever nossos ambientes e lançamentos:

    .
    ├── 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

Temos 2 ambientes: devel, production — cada um contém seus próprios valores para os charts helm dos lançamentos. Vamos realizar o deploy deles assim:

helmfile -n  -e  apply

Versões diferentes dos charts helm em diferentes ambientes

O que fazer se precisarmos lançar versões diferentes do backend em diferentes ambientes? Como parametrizar a versão do lançamento? Os valores do ambiente, disponíveis atravé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 }}
...

Conjunto variado de aplicações em diferentes ambientes

Ótimo, mas e se não precisarmos production implantar o postgres, porque sabemos que não devemos colocar um banco de dados no k8s e para produção temos um belo cluster postgres separado? Para resolver esse problema, temos labels (etiquetas)

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

É ótimo, mas pessoalmente prefiro descrever quais aplicativos implantar no ambiente não por meio de argumentos de execução, mas na descrição dos próprios ambientes. O que fazer? Podemos colocar a descrição das versões em uma pasta separada, criar uma lista das versões necessárias na descrição do ambiente e "conectar" apenas as versões necessárias, ignorando as demais.

    .
    ├── envs
    │   ├── 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

    production:
      values:
      - charts:
          versions:
            backend: 1.0.5
      - apps:
        - backend

- 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: {{ .Values.charts.versions.backend }}
-      needs:
-        - postgres
-      values:
-        - envs/{{ .Environment.Name }}/values/backend.yaml
+ ---
+ bases:
+ {{- range .Values.apps }}
+   - releases/{{ . }}.yaml
+ {{- end }}

releases/postgres.yaml

releases:
  - name: postgres
    labels:
      app: postgres
    wait: true
    chart: stable/postgresql
    version: 8.4.0
    values:
      - envs/{{ .Environment.Name }}/values/postgres.yaml

releases/backend.yaml

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

Nota

Ao usar bases: é necessário usar obrigatoriamente o delimitador yaml ---, para que seja possível usar templates para releases (e outras partes, como helmDefaults) com valores dos environments

Nesse caso, a release postgres nem será incluída na descrição para produção. Muito conveniente!

Valores globais substituíveis para releases

Claro, é ótimo que podemos definir valores para os charts helm em cada ambiente, mas e se tivermos vários ambientes descritos e quisermos, por exemplo, definir um valor idêntico para todos afinidade, mas não queremos configurá-lo por padrão nos próprios charts, que estão armazenados nos repositórios.

Nesse caso, poderíamos especificar 2 arquivos com values para cada release: o primeiro com os valores padrão, que determinarão os valores do próprio chart, e o segundo com os valores para o ambiente, que, por sua vez, substituirá os padrões.

    .
    ├── envs
+   │   ├── padrão
+   │   │   └── valores
+   │   │       ├── backend.yaml
+   │   │       └── postgres.yaml
    │   ├── desenvolvimento
    │   │   └── valores
    │   │       ├── backend.yaml
    │   │       └── postgres.yaml
    │   └── produção
    │       └── valores
    │           ├── backend.yaml
    │           └── postgres.yaml
    ├── releases
    │   ├── backend.yaml
    │   └── postgres.yaml
    └── helmfile.yaml

releases/backend.yaml

releases:
  - nome: backend
    etiquetas:
      app: backend
    wait: true
    chart: private-helm-repo/backend
    version: {{ .Values.charts.versions.backend }}
    needs:
      - postgres
    values:
+     - envs/padrão/valores/backend.yaml
      - envs/{{ .Environment.Name }}/valores/backend.yaml

envs/padrão/valores/backend.yaml

afinidade:
  podAntiAffinity:
    preferidoDuranteAgendamentoIgnoradoDuranteExecução:
    - peso: 1
      podAffinityTerm:
        labelSelector:
          matchExpressions:
          - key: app.kubernetes.io/nome
            operator: In
            valores:
            - backend
        topologyKey: "kubernetes.io/hostname"

Definição de valores globais para helm charts de todas as releases a nível de ambiente

Suponha que temos várias releases gerando vários ingress — poderíamos definir manualmente para cada chart. hosts:, mas no nosso caso o domínio é o mesmo, por que não transformá-lo numa variável global e simplesmente inserir seu valor nos charts? Para isso, os arquivos com valores que queremos parametrizar devem ter a extensão .gotmpl, para que o helmfile saiba que ele deve ser processado pelo template.

    .
    ├── envs
    │   ├── padrão
    │   │   └── valores
-   │   │       ├── backend.yaml
-   │   │       ├── postgres.yaml
+   │   │       ├── backend.yaml.gotmpl
+   │   │       └── postgres.yaml.gotmpl
    │   ├── desenvolvimento
    │   │   └── valores
    │   │       ├── backend.yaml
    │   │       └── postgres.yaml
    │   └── produção
    │       └── valores
    │           ├── backend.yaml
    │           └── postgres.yaml
    ├── releases
    │   ├── backend.yaml
    │   └── postgres.yaml
    └── helmfile.yaml

helmfile.yaml

  ambientes:
    desenvolvimento:
      valores:
      - charts:
          versões:
            backend: 1.1.0
      - apps:
        - postgres
        - backend
+     - global:
+         ingressDomain: k8s.devel.domain

    produção:
      valores:
      - charts:
          versões:
            backend: 1.0.5
      - apps:
        - backend
+     - global:
+         ingressDomain: production.domain
  ---
  bases:
  {{- range .Values.apps }}
    - releases/{{ . }}.yaml
  {{- end }}

envs/padrão/valores/backend.yaml.gotmpl

ingress:
  ativado: true
  caminhos:
    - /api
  hosts:
    - {{ .Values.global.ingressDomain }}

envs/padrão/valores/postgres.yaml.gotmpl

ingress:
  ativado: true
  caminhos:
    - /
  hosts:
    - postgres.{{ .Values.global.ingressDomain }}

Nota

É óbvio que o ingress no chart postgres é algo extremamente duvidoso, então no artigo isso é apresentado apenas como um exemplo esférico em um vácuo e para não introduzir um novo lançamento apenas para descrever o ingress.

Substituição de segredos (secrets) a partir de variáveis de ambiente.

Analogamente ao exemplo acima, podemos substituir e também usar os valores criptografados com helm secrets . Ao invés de criar um arquivo secrets para cada lançamento, onde definimos os valores criptografados para o chart, podemos simplesmente definir no arquivo default.yaml.gotmpl os valores que serão extraídos das variáveis definidas a nível de ambientes. E os valores que não precisamos esconder de ninguém podem ser facilmente sobrescritos nos valores do lançamento em um ambiente 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/padrão/valores/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

A propósito, getOrNil — uma função especial para templates go no helmfile, que mesmo se .Values.secrets não existir, não lançará um erro, mas permitirá usar a função padrão para substituir o valor padrão.

Conclusão

As coisas descritas parecem bastante óbvias, mas as informações sobre uma descrição conveniente de deployment em múltiplos ambientes usando helmfile são escassas, e eu adoro IaC (Infrastructure-as-Code) e quero ter uma descrição clara do estado do deployment.

Por fim, gostaria de adicionar que as variáveis para o ambiente padrão podem, por sua vez, ser parametrizadas por variáveis de ambiente do sistema operacional de um runner, a partir do qual o deployment será iniciado, e assim obter ambientes dinâmicos.

helmfile.yaml

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

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster