Kubernetes Helm .NET DevOps Docker

Deploy de aplicações .NET no Kubernetes com Helm: do zero ao ambiente de produção

Guia completo de deploy .NET no Kubernetes com Helm: criação de charts, values por ambiente, ConfigMaps, Secrets, health checks, HPA.

N
Neryx Digital Architects
20 de outubro de 2025
14 min de leitura
230 profissionais leram
Categoria: .NET Público: Times de plataforma e operação Etapa: Decisão

Kubernetes é o padrão de facto para orquestração de containers em produção. Helm é o gerenciador de pacotes do Kubernetes — ele empacota todos os recursos (Deployment, Service, Ingress, ConfigMap, HPA) num chart reutilizável e versionado. Juntos, eliminam a duplicação de YAMLs entre ambientes e tornam o deploy de aplicações .NET reproduzível e auditável.

Dockerfile otimizado para .NET

Antes do Kubernetes, a imagem Docker precisa ser enxuta:

# Multi-stage build: imagem final sem SDK (.NET runtime apenas)
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

Copia apenas os arquivos de projeto primeiro (cache layer otimizado)

COPY [“src/NeryxApp.API/NeryxApp.API.csproj”, “src/NeryxApp.API/”] COPY [“src/NeryxApp.Application/NeryxApp.Application.csproj”, “src/NeryxApp.Application/”] COPY [“src/NeryxApp.Infrastructure/NeryxApp.Infrastructure.csproj”, “src/NeryxApp.Infrastructure/”] RUN dotnet restore “src/NeryxApp.API/NeryxApp.API.csproj”

Copia o código e faz o build

COPY . . WORKDIR “/src/src/NeryxApp.API” RUN dotnet build -c Release -o /app/build

FROM build AS publish RUN dotnet publish -c Release -o /app/publish —no-restore
/p:UseAppHost=false
/p:PublishTrimmed=false

Imagem final: apenas runtime (sem SDK) — muito menor

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final WORKDIR /app

Usuário não-root por segurança

RUN adduser —disabled-password —gecos "" appuser USER appuser

COPY —from=publish /app/publish .

Health check nativo do Docker (Kubernetes usa probes, mas é bom ter)

HEALTHCHECK —interval=30s —timeout=5s —start-period=10s —retries=3
CMD curl -f http://localhost:8080/health || exit 1

EXPOSE 8080 ENTRYPOINT [“dotnet”, “NeryxApp.API.dll”]

Criando o Helm Chart

# Estrutura de um chart .NET:
helm create neryx-api

# Estrutura gerada (e o que vamos customizar):
neryx-api/
├── Chart.yaml          # metadados do chart
├── values.yaml         # valores padrão
├── values-staging.yaml # overrides para staging
├── values-prod.yaml    # overrides para produção
└── templates/
    ├── deployment.yaml
    ├── service.yaml
    ├── ingress.yaml
    ├── configmap.yaml
    ├── hpa.yaml        # Horizontal Pod Autoscaler
    └── _helpers.tpl    # templates reutilizáveis
# Chart.yaml:
apiVersion: v2
name: neryx-api
description: API .NET da Neryx Digital Architects
type: application
version: 0.1.0
appVersion: "1.0.0"
# values.yaml — valores padrão (desenvolvimento):
replicaCount: 1

image:
  repository: ghcr.io/neryx/neryx-api
  pullPolicy: IfNotPresent
  tag: "latest"   # substituído no CI/CD pelo SHA do commit

service:
  type: ClusterIP
  port: 80
  targetPort: 8080

ingress:
  enabled: true
  className: nginx
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
  hosts:
    - host: api.neryx.com.br
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: neryx-api-tls
      hosts:
        - api.neryx.com.br

resources:
  requests:
    cpu: "100m"
    memory: "128Mi"
  limits:
    cpu: "500m"
    memory: "512Mi"

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70
  targetMemoryUtilizationPercentage: 80

# Configurações da aplicação (não-sensíveis)
config:
  ASPNETCORE_ENVIRONMENT: "Production"
  ASPNETCORE_URLS: "http://+:8080"
  Logging__LogLevel__Default: "Warning"

# Referências a Secrets do Kubernetes (criados separadamente)
secretName: neryx-api-secrets

Templates principais

# templates/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "neryx-api.fullname" . }}
  labels:
    {{- include "neryx-api.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "neryx-api.selectorLabels" . | nindent 6 }}
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0   # zero downtime deploy
  template:
    metadata:
      labels:
        {{- include "neryx-api.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - containerPort: 8080
      # Variáveis de configuração não-sensíveis via ConfigMap
      envFrom:
        - configMapRef:
            name: {{ include "neryx-api.fullname" . }}-config
        # Variáveis sensíveis via Secret (connection strings, API keys)
        - secretRef:
            name: {{ .Values.secretName }}

      # Liveness probe: reinicia o pod se a app travar
      livenessProbe:
        httpGet:
          path: /health/live
          port: 8080
        initialDelaySeconds: 10
        periodSeconds: 15
        failureThreshold: 3

      # Readiness probe: remove do balanceador se não pronto
      readinessProbe:
        httpGet:
          path: /health/ready
          port: 8080
        initialDelaySeconds: 5
        periodSeconds: 10
        failureThreshold: 3

      # Startup probe: dá tempo extra na inicialização a frio
      startupProbe:
        httpGet:
          path: /health/live
          port: 8080
        failureThreshold: 30
        periodSeconds: 5

      resources:
        {{- toYaml .Values.resources | nindent 12 }}

# templates/configmap.yaml — variáveis não-sensíveis:
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "neryx-api.fullname" . }}-config
data:
  {{- range $key, $value := .Values.config }}
  {{ $key }}: {{ $value | quote }}
  {{- end }}
# templates/hpa.yaml — escala automática por CPU/memória:
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: {{ include "neryx-api.fullname" . }}
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: {{ include "neryx-api.fullname" . }}
  minReplicas: {{ .Values.autoscaling.minReplicas }}
  maxReplicas: {{ .Values.autoscaling.maxReplicas }}
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}

Health checks em .NET para o Kubernetes

// Program.cs — health checks para liveness e readiness:
builder.Services.AddHealthChecks()
    .AddCheck("liveness", () => HealthCheckResult.Healthy(), tags: ["live"])
    .AddNpgsql(                           // verifica conexão com banco
        builder.Configuration.GetConnectionString("Default")!,
        name: "database",
        tags: ["ready"])
    .AddRedis(                            // verifica Redis
        builder.Configuration["Redis:ConnectionString"]!,
        name: "redis",
        tags: ["ready"])
    .AddUrlGroup(                         // verifica serviço externo
        new Uri("https://api.externa.com/health"),
        name: "api-externa",
        tags: ["ready"]);

// Endpoints separados para liveness e readiness: app.MapHealthChecks(“/health/live”, new HealthCheckOptions { // Liveness: apenas verifica se o processo está vivo (sem checks externos) Predicate = check => check.Tags.Contains(“live”), ResultStatusCodes = { [HealthStatus.Healthy] = StatusCodes.Status200OK, [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable, } });

app.MapHealthChecks(“/health/ready”, new HealthCheckOptions { // Readiness: verifica banco, cache e dependências externas Predicate = check => check.Tags.Contains(“ready”), ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse });

Pipeline CI/CD com GitHub Actions

# .github/workflows/deploy.yml:
name: Build e Deploy

on: push: branches: [main]

env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }}

jobs: build-and-deploy: runs-on: ubuntu-latest permissions: contents: read packages: write

steps:
  - uses: actions/checkout@v4

  - name: Login no GitHub Container Registry
    uses: docker/login-action@v3
    with:
      registry: ${{ env.REGISTRY }}
      username: ${{ github.actor }}
      password: ${{ secrets.GITHUB_TOKEN }}

  - name: Build e push da imagem Docker
    uses: docker/build-push-action@v5
    with:
      context: .
      push: true
      tags: |
        ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
        ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
      cache-from: type=gha
      cache-to: type=gha,mode=max

  - name: Configurar kubectl
    uses: azure/setup-kubectl@v3

  - name: Configurar kubeconfig
    run: |
      mkdir -p ~/.kube
      echo "${{ secrets.KUBECONFIG }}" > ~/.kube/config

  - name: Deploy com Helm
    run: |
      helm upgrade --install neryx-api ./helm/neryx-api \
        --namespace production \
        --create-namespace \
        --values ./helm/neryx-api/values-prod.yaml \
        --set image.tag=${{ github.sha }} \
        --set image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \
        --wait \                    # aguarda pods ficarem Ready
        --timeout 5m \
        --atomic                   # rollback automático se falhar

Gerenciando Secrets de forma segura

# Nunca commite Secrets no Git! Crie separado no Kubernetes:
kubectl create secret generic neryx-api-secrets \
  --namespace production \
  --from-literal=ConnectionStrings__Default="Host=db;Database=neryx;Username=app;Password=..." \
  --from-literal=Redis__ConnectionString="redis:6379,password=..." \
  --from-literal=Jwt__Secret="sua-chave-jwt-256-bits" \
  --dry-run=client -o yaml | kubectl apply -f -

Para ambientes AWS/Azure, use External Secrets Operator:

O operator busca os secrets do AWS Secrets Manager ou Azure Key Vault

e os injeta automaticamente como Kubernetes Secrets

apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: neryx-api-secrets spec: refreshInterval: 1h secretStoreRef: name: aws-secrets-manager kind: ClusterSecretStore target: name: neryx-api-secrets data: - secretKey: ConnectionStrings__Default remoteRef: key: production/neryx-api property: db-connection-string

Conclusão

Helm transforma o deploy de aplicações .NET em Kubernetes numa operação previsível e versionada. O chart com values por ambiente elimina a duplicação de YAMLs, o HPA escala automaticamente baseado em CPU/memória, e o pipeline CI/CD com --atomic garante rollback automático se algo der errado. O resultado: deploys com zero downtime e rastreabilidade completa.

Se você precisa containerizar e orquestrar aplicações .NET em Kubernetes — seja EKS, GKE ou AKS — a Neryx tem experiência com todo o processo, da criação dos Dockerfiles ao pipeline de deploy. Consultoria inicial gratuita.

Leitura complementar:

Quer sair do modo reativo e priorizar o que mais importa?

O diagnóstico de maturidade ajuda a transformar sintomas operacionais em um plano mais claro de evolução.

Avaliar maturidade

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.