Ir para o conteúdo principal

Helm

ℹ️ INFORMAÇÃO
Quando mencionado projeto, projeto atual ou meditation-timer; me refiro ao projeto usado como teste e acessivel na URL https://gitlab.setic.ufsc.br/marcosxavier/meditation-timer

Índice

  1. O que é o Helm e por que existe
  2. Os três conceitos fundamentais
  3. A estrutura de um Chart
  4. Como o Helm se encaixa no que já existe
  5. O problema que o Helm resolve no projeto atual
  6. Como ficaria um projeto com Helm
  7. Controle de acesso por perfil de usuário
  8. Como funciona ter vários Charts
  9. Quem usa qual Chart e como isso é garantido
  10. Referência de comandos Helm

1. O que é o Helm e por que existe

O Helm é um gerenciador de pacotes para Kubernetes. A analogia direta é o apt do Debian: assim como apt install nginx sabe que o nginx está instalado, em qual versão, e consegue desinstalar tudo de uma vez — o Helm sabe que uma aplicação está instalada no cluster, em qual versão, quais recursos ela criou, e consegue fazer rollback para a versão anterior com um único comando.

Sem o Helm, o deploy de uma aplicação no Kubernetes é feito com kubectl apply -f pasta/, que simplesmente empurra arquivos YAML estáticos para o cluster. Isso funciona, mas tem limitações sérias:

  • Os arquivos YAML têm valores hardcoded (nome da imagem, hostname, número de réplicas).
  • Para mudar qualquer valor é preciso editar o arquivo antes de aplicar.
  • O Kubernetes não guarda histórico de versões — se um deploy quebrar, voltar atrás significa re-executar o pipeline da versão anterior.
  • Para remover todos os recursos de uma aplicação é preciso saber exatamente quais foram criados.

O Helm resolve todos esses pontos de uma vez.


2. Os três conceitos fundamentais

Chart

É o pacote — o equivalente a um pacote .deb do apt. Contém os templates dos arquivos YAML da aplicação, os valores padrão, e as regras de validação. Um Chart é criado e mantido por quem entende de infraestrutura (a SeTIC), não pelo desenvolvedor da aplicação.

Values

São os parâmetros configuráveis do Chart — o equivalente às opções de configuração de um pacote. O desenvolvedor ou aluno preenche um arquivo values.yaml com os valores específicos do projeto dele (nome da imagem, hostname, número de réplicas). O Chart usa esses valores para gerar os YAMLs finais que vão para o cluster.

Release

É uma instalação específica de um Chart no cluster. Quando o Chart web-app é instalado para o projeto minha-aplicacao, isso cria uma Release chamada minha-aplicacao. O Helm rastreia o histórico completo dessa Release: qual versão do Chart foi usada, quais values foram passados, se o deploy teve sucesso ou falhou.

Chart (template)  +  values.yaml (parâmetros)  =  Release (instalação rastreada no cluster)

3. A estrutura de um Chart

Um Chart é um diretório com a seguinte estrutura:

meu-chart/
├── Chart.yaml            ← identidade do chart (nome, versão)
├── values.yaml           ← valores padrão de todos os parâmetros
├── values.schema.json    ← regras de validação dos parâmetros
└── templates/
    ├── deployment.yaml   ← template do Deployment
    ├── service.yaml      ← template do Service
    └── ingress.yaml      ← template do Ingress

Chart.yaml — identidade do Chart

apiVersion: v2
name: student-web-app
description: Chart padrão para projetos de alunos — UFSC SeTIC
version: 1.0.0      # versão do chart em si
appVersion: "main"  # versão padrão da aplicação

values.yaml — os parâmetros que o usuário pode preencher

image:
  repository: ""     # caminho da imagem no registry (obrigatório)
  tag: main          # tag da imagem

replicaCount: 1      # número de réplicas

resources:
  requests:
    cpu: 50m
    memory: 64Mi
  limits:
    cpu: 200m
    memory: 128Mi

ingress:
  host: ""           # hostname da aplicação (obrigatório)

templates/deployment.yaml — o YAML com variáveis

Em vez de valores fixos, o template usa {{ .Values.* }} para referenciar o values.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}         # ← nome da Release, definido no helm install
spec:
  replicas: {{ .Values.replicaCount }}
  template:
    spec:
      # SecurityContext fixo — não é um parâmetro, sempre igual
      securityContext:
        runAsNonRoot: true
        runAsUser: 101
        runAsGroup: 101
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: app
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: Always   # ← fixo no template, não é parâmetro
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          securityContext:
            allowPrivilegeEscalation: false   # ← fixo, não é parâmetro
            capabilities:
              drop: [ALL]                     # ← fixo, não é parâmetro

Perceba a diferença: replicaCount, image.repository, image.tag, e resources são variáveis que o usuário controla. securityContext, allowPrivilegeEscalation, e imagePullPolicy são valores fixos no template — o usuário não tem como alterá-los porque eles simplesmente não existem no values.yaml.


4. Como o Helm se encaixa projeto do meditation-timer

O Helm não substitui nenhuma das peças do cenário atual (somente GitLab-managed Kubernetes resources)— ele se posiciona como uma nova camada entre o pipeline e o cluster.

Antes (sem Helm):

Pipeline GitLab
  └── kubectl apply -f k8s/   →   Cluster (manifests estáticos aplicados diretamente)


Depois (com Helm):

Pipeline GitLab
  └── helm upgrade --install  →   Chart (template parametrizado)  →   Cluster

O que não muda:

  • GitLab-managed resources continua criando namespace e RoleBindings automaticamente quando o environment inicia. O Helm não interfere nisso.
  • GitLab Agent continua sendo o mecanismo de autenticação — o helm usa o mesmo kubeconfig que o kubectl.
  • Namespace por projeto (gl-<project_id>) continua sendo o padrão.
  • Registry de imagens (registry.setic.ufsc.br) continua o mesmo.

O que muda é só a forma como os manifests da aplicação chegam no cluster: em vez de YAMLs estáticos com kubectl apply, são templates parametrizados com helm upgrade.


5. O problema que o Helm resolve no projeto atual

Um projeto típico tem hoje um problema concreto: a tag da imagem está hardcoded no deployment.yaml:

# k8s/deployment.yaml — hoje
image: registry.setic.ufsc.br/<grupo>/<projeto>:main

Por isso o pipeline precisa de dois comandos para fazer o que deveria ser um:

# .gitlab-ci.yml — hoje
- kubectl apply -f k8s/ --namespace="$KUBE_NAMESPACE"
  # ↑ aplica o YAML com a imagem hardcoded (tag errada)

- kubectl set image deployment/"$APP_NAME" "$APP_CONTAINER=$REGISTRY_IMAGE" --namespace="$KUBE_NAMESPACE"
  # ↑ corrige a tag em cima do que foi aplicado

Com Helm, o deployment.yaml recebe {{ .Values.image.tag }} no lugar da tag hardcoded, e o pipeline passa o valor correto no momento do deploy:

# .gitlab-ci.yml — com Helm
- helm upgrade --install minha-aplicacao ./chart \
    --namespace "$KUBE_NAMESPACE" \
    --set image.tag="$CI_COMMIT_REF_SLUG" \
    --wait --timeout 3m

Um único comando. O YAML sai do template já com a tag correta. O --wait aguarda todos os pods ficarem prontos e substitui o kubectl rollout status que existia separado.

O job de parada também simplifica:

# hoje
- kubectl scale deployment "$APP_NAME" --replicas=0 --namespace="$KUBE_NAMESPACE"

# com Helm
- helm upgrade minha-aplicacao ./chart \
    --namespace "$KUBE_NAMESPACE" \
    --set replicaCount=0 \
    --reuse-values

O que o Helm adiciona que o kubectl apply não tem

Histórico de versões rastreado no cluster:

helm history minha-aplicacao -n gl-<project_id>

# REVISION  STATUS     CHART              DESCRIPTION
# 1         superseded web-app-1.0.0      Install complete
# 2         superseded web-app-1.0.0      Upgrade complete
# 3         deployed   web-app-1.0.0      Upgrade complete

Rollback com um comando:

helm rollback minha-aplicacao 2 -n gl-<project_id>
# volta exatamente para o estado do deploy 2, sem re-executar pipeline

Remoção limpa de todos os recursos:

helm uninstall minha-aplicacao -n gl-<project_id>
# remove Deployment + Service + Ingress de uma vez, sem precisar listar

Visualizar o diff antes de aplicar (com o plugin helm-diff):

helm diff upgrade minha-aplicacao ./chart \
    --namespace gl-<project_id> \
    --set image.tag="nova-tag"
# mostra exatamente o que vai mudar antes de mudar

6. Como ficaria um projeto com Helm

Estrutura do repositório

minha-aplicacao/
├── Dockerfile
├── src/
├── .gitlab-ci.yml
├── values.yaml          ← parâmetros específicos deste projeto
└── chart/               ← Chart (poderia vir de um repositório central)
    ├── Chart.yaml
    ├── values.yaml
    ├── values.schema.json
    └── templates/
        ├── deployment.yaml
        ├── service.yaml
        └── ingress.yaml

values.yaml do projeto

image:
  repository: registry.setic.ufsc.br/<grupo>/<projeto>
  tag: main

replicaCount: 1

resources:
  requests:
    cpu: 50m
    memory: 64Mi
  limits:
    cpu: 200m
    memory: 128Mi

ingress:
  host: minha-aplicacao.setic.ufsc.br

.gitlab-ci.yml com Helm

deploy:
  image: alpine/helm:latest
  script:
    - kubectl config use-context "$KUBE_CONTEXT"
    - helm upgrade --install minha-aplicacao ./chart \
        --namespace "$KUBE_NAMESPACE" \
        --values values.yaml \
        --set image.tag="$CI_COMMIT_REF_SLUG" \
        --wait --timeout 3m

stop-producao:
  script:
    - kubectl config use-context "$KUBE_CONTEXT"
    - helm upgrade minha-aplicacao ./chart \
        --namespace "$KUBE_NAMESPACE" \
        --set replicaCount=0 \
        --reuse-values
  when: manual

7. Controle de acesso por perfil de usuário

Esta é a principal razão para ter Charts diferentes para perfis diferentes. O Helm permite três níveis de controle sobre o que um usuário pode configurar:

Nível 1 — Bloqueado: o usuário não pode alterar de jeito nenhum

Esses valores são hardcoded diretamente no template e não aparecem no values.yaml. O usuário nem sabe que existem como parâmetro porque simplesmente não há como passá-los.

Exemplos do que bloquear:

O que Como fica no template
allowPrivilegeEscalation Sempre false, sem variável
capabilities.drop Sempre [ALL], sem variável
seccompProfile Sempre RuntimeDefault, sem variável
ingressClassName Sempre nginx, sem variável
imagePullPolicy Sempre Always, sem variável
Prefixo do registry Template concatena registry.setic.ufsc.br/ com o valor do usuário
# templates/deployment.yaml — valores bloqueados
securityContext:
  runAsNonRoot: true           # bloqueado — não está no values.yaml
  allowPrivilegeEscalation: false  # bloqueado
  capabilities:
    drop: [ALL]                # bloqueado

image: "registry.setic.ufsc.br/{{ .Values.image.repository }}:{{ .Values.image.tag }}"
#       ↑ prefixo fixo         ↑ usuário só fornece esta parte

Nível 2 — Liberado com restrição: o usuário pode alterar dentro de limites

Esses valores aparecem no values.yaml mas o values.schema.json define regras rígidas. Se o usuário passar um valor fora das regras, o Helm rejeita antes de qualquer coisa chegar no cluster, com mensagem de erro clara.

O values.schema.json é escrito em JSON Schema e suporta:

  • minimum / maximum — limites numéricos
  • enum — lista de valores permitidos
  • pattern — expressão regular (para validar formato de strings)
  • additionalProperties: false — proíbe qualquer chave fora do schema

Exemplo para alunos:

{
  "$schema": "http://json-schema.org/schema#",
  "additionalProperties": false,
  "properties": {
    "replicaCount": {
      "type": "integer",
      "minimum": 1,
      "maximum": 2,
      "description": "Máximo 2 réplicas por projeto de aluno."
    },
    "resources": {
      "properties": {
        "limits": {
          "properties": {
            "cpu":    { "type": "string", "enum": ["200m", "500m"] },
            "memory": { "type": "string", "enum": ["128Mi", "256Mi"] }
          }
        }
      }
    },
    "ingress": {
      "properties": {
        "host": {
          "type": "string",
          "pattern": "^[a-z0-9-]+\\.inf\\.ufsc\\.br$",
          "description": "O hostname deve terminar em .inf.ufsc.br"
        }
      }
    }
  }
}

Se um aluno tentar replicaCount: 10 ou ingress.host: meusite.com:

Error: values don't meet the specifications of the schema(s):
- replicaCount: Must be less than or equal to 2
- ingress.host: Does not match pattern '^[a-z0-9-]+\.inf\.ufsc\.br$'

Nível 3 — Liberado totalmente: o usuário controla sem restrição de valor

Esses valores o usuário preenche livremente. O schema só valida o tipo para evitar erro humano (passar um número onde deveria ser string, por exemplo).

Exemplos:

"image": {
  "type": "object",
  "required": ["repository", "tag"],
  "properties": {
    "repository": { "type": "string" },
    "tag":        { "type": "string" }
  }
}

Tabela resumo dos três níveis

O que Nível Mecanismo
securityContext completo Bloqueado Hardcoded no template, sem variável exposta
ingressClassName Bloqueado Hardcoded no template
imagePullPolicy Bloqueado Hardcoded no template
Prefixo do registry Bloqueado Template concatena o prefixo fixo com o valor do usuário
Número de réplicas Restrito schema.json com minimum e maximum
Limites de CPU/memória Restrito schema.json com enum de tiers permitidos
Hostname do Ingress Restrito schema.json com pattern que força sufixo *.ufsc.br
Tag da imagem Livre schema.json só valida que é uma string
Variáveis de ambiente Livre Lista de pares chave/valor sem restrição de conteúdo
Nome da aplicação Livre String com validação de formato Kubernetes

8. Como funciona ter vários Charts

Cada Chart é um diretório independente com seus próprios templates e values.schema.json. Você pode ter quantos quiser. Eles ficam armazenados no registry do GitLab como artefatos OCI — o mesmo registry que já armazena imagens Docker, sem nenhuma infraestrutura adicional.

Estrutura de repositórios recomendada

gitlab.setic.ufsc.br/
│
├── dtr/
│   └── helm-charts/                ← projeto da SeTIC (privado)
│       ├── student-web-app/        ← chart para alunos
│       │   ├── Chart.yaml
│       │   ├── values.yaml
│       │   ├── values.schema.json  ← restrições rígidas
│       │   └── templates/
│       └── web-app/                ← chart para desenvolvedores UFSC
│           ├── Chart.yaml
│           ├── values.yaml
│           ├── values.schema.json  ← restrições mais relaxadas
│           └── templates/
│
├── disciplinas/
│   └── inf-xxx/                    ← grupo da disciplina
│       ├── <usuario-a>/            ← repositório do aluno
│       │   ├── Dockerfile
│       │   ├── src/
│       │   └── values.yaml         ← ÚNICO arquivo de deploy que o aluno escreve
│       └── <usuario-b>/
│           └── values.yaml
│
└── CSS/                            ← grupo dos desenvolvedores UFSC
    └── <projeto>/
        ├── Dockerfile
        ├── src/
        └── values.yaml

O que cada Chart tem de diferente

student-web-app web-app
Réplicas máximas 2 10
CPU limit máximo 500m 2000m
Memória limit máxima 256Mi 1Gi
Padrão do hostname *.inf.ufsc.br *.setic.ufsc.br
Variáveis de ambiente extras Não exposto Exposto
additionalProperties false (nenhuma chave extra) Flexível

Os templates em si podem ser idênticos — a diferença está no values.schema.json de cada um.


9. Quem usa qual Chart e como isso é garantido

O controle de qual Chart um usuário pode acessar tem duas camadas que funcionam juntas.

Camada 1 — Acesso ao registry (quem pode fazer pull do Chart)

O projeto dtr/helm-charts é privado. O acesso ao registry é herdado do papel do membro no projeto GitLab. Quando um pipeline tenta fazer pull de um Chart, o GitLab verifica se o CI job token do projeto tem permissão.

Você configura isso uma vez nas configurações de CI/CD token access de cada grupo:

  • Projetos dentro de disciplinas/ → acesso de pull apenas em student-web-app
  • Projetos dentro de CSS/ → acesso de pull em ambos os charts

Se um aluno tentar apontar o pipeline dele para web-app (chart dos devs), o GitLab retorna 401 Unauthorized e o pipeline falha.

Camada 2 — O .gitlab-ci.yml controlado pela SeTIC (quem aponta para qual Chart)

Esta é a camada mais importante para o cenário de disciplina. O aluno não escreve o .gitlab-ci.yml — ele herda um template centralizado que a SeTIC mantém em dtr/k8s-agents, exatamente como já funciona hoje com o gitlab-k8s-group-rbac.yml:

# .gitlab-ci.yml do aluno — ele NÃO escreve isso, herda do template
include:
  - project: dtr/k8s-agents
    ref: main
    file: templates/student-deploy.yml   # ← template que a SeTIC controla

O student-deploy.yml na SeTIC hardcoda o Chart:

# dtr/k8s-agents/templates/student-deploy.yml — controlado pela SeTIC
.student_deploy:
  image: alpine/helm:latest
  script:
    - kubectl config use-context "$KUBE_CONTEXT"
    - helm upgrade --install "$CI_PROJECT_NAME" \
        oci://registry.setic.ufsc.br/dtr/helm-charts/student-web-app \
        --version 1.0.0 \
        --namespace "$KUBE_NAMESPACE" \
        --values values.yaml \
        --set image.tag="$CI_COMMIT_REF_SLUG" \
        --wait --timeout 3m

O aluno nunca toca nessa linha. Ele só preenche o values.yaml do próprio repositório — que é validado pelo values.schema.json do student-web-app automaticamente quando o helm upgrade executa.

Para desenvolvedores UFSC, o template incluído é diferente e aponta para o web-app.

O que o aluno vê e faz

O aluno tem um repositório com:

<usuario>/
├── Dockerfile      ← escreve normalmente
├── src/            ← escreve normalmente
└── values.yaml     ← ÚNICO arquivo relacionado ao deploy que ele preenche

O values.yaml que ele preenche:

image:
  repository: disciplinas/inf-xxx/<usuario>/<projeto>
  tag: main

replicaCount: 1

resources:
  requests:
    cpu: 50m
    memory: 64Mi
  limits:
    cpu: 200m     # só pode escolher 200m ou 500m
    memory: 128Mi # só pode escolher 128Mi ou 256Mi

ingress:
  host: <projeto>.inf.ufsc.br  # deve terminar em .inf.ufsc.br

Não tem securityContext. Não tem ingressClassName. Não tem nada de infraestrutura — só o que é da aplicação dele. Se ele tentar colocar qualquer chave que não esteja no schema, o Helm rejeita com mensagem clara antes de qualquer coisa chegar no cluster.

Fluxo completo do deploy do aluno

Aluno faz push no GitLab
        │
        ▼
Pipeline executa (usando template da SeTIC)
        │
        ├── job build: docker build + push no registry
        │
        └── job deploy:
                │
                ├── helm lê o values.yaml do aluno
                │
                ├── helm valida contra o values.schema.json do student-web-app
                │        │
                │        ├── OK: continua
                │        └── ERRO: pipeline falha com mensagem clara, nada chega no cluster
                │
                ├── helm gera os YAMLs finais combinando template + values
                │        (com securityContext correto, ingressClassName correto, etc.)
                │
                └── helm aplica os YAMLs no namespace gl-<project_id>
                         │
                         ▼
                   Cluster Kubernetes
                   (namespace isolado, criado pelo GitLab-managed resources)

10. Referência de comandos Helm

Instalar / atualizar uma aplicação

# instala se não existir, atualiza se já existir
helm upgrade --install <nome-da-release> <caminho-ou-url-do-chart> \
  --namespace <namespace> \
  --values values.yaml \
  --set image.tag="abc123" \
  --wait --timeout 3m

Listar releases instaladas

helm list -n gl-<project_id>

Ver o histórico de versões de uma release

helm history <nome-da-release> -n gl-<project_id>

Fazer rollback para uma versão anterior

helm rollback <nome-da-release> 2 -n gl-<project_id>
# o número é o REVISION do helm history

Ver os values que foram passados no último deploy

helm get values <nome-da-release> -n gl-<project_id>

Ver os YAMLs exatos que o Helm aplicou no cluster

helm get manifest <nome-da-release> -n gl-<project_id>

Testar o que seria gerado sem aplicar nada (dry-run)

helm upgrade --install <nome-da-release> ./chart \
  --namespace gl-<project_id> \
  --values values.yaml \
  --dry-run

Ver o diff entre o que está no cluster e o que seria aplicado

# requer o plugin helm-diff
helm diff upgrade <nome-da-release> ./chart \
  --namespace gl-<project_id> \
  --set image.tag="nova-tag"

Remover uma release e todos os recursos que ela criou

helm uninstall <nome-da-release> -n gl-<project_id>

Fazer push de um Chart para o registry do GitLab

# empacotar o chart em um .tgz
helm package ./chart

# autenticar no registry
helm registry login registry.setic.ufsc.br \
  -u "$CI_REGISTRY_USER" \
  -p "$CI_REGISTRY_PASSWORD"

# publicar
helm push <nome-do-chart>-1.0.0.tgz \
  oci://registry.setic.ufsc.br/dtr/helm-charts

Instalar a partir do registry (sem ter o chart localmente)

helm upgrade --install <nome-da-release> \
  oci://registry.setic.ufsc.br/dtr/helm-charts/<nome-do-chart> \
  --version 1.0.0 \
  --namespace gl-<project_id> \
  --values values.yaml