Workshop completo: aulas 08-10 com Talos, n8n e GitLab na Hetzner

Aula 08 - Cluster Kubernetes HA:
- Setup interativo com OpenTofu para Talos na Hetzner
- CCM, CSI Driver, Cluster Autoscaler, Metrics Server
- NGINX Ingress com LoadBalancer (HTTP/HTTPS/SSH)

Aula 09 - n8n na Hetzner:
- Deploy via Helm com PostgreSQL e Redis
- Suporte multi-tenant com add-client.sh
- Integração com Hetzner CSI para volumes persistentes

Aula 10 - GitLab na Hetzner:
- Setup agnóstico: CloudFlare (trusted proxies) ou Let's Encrypt
- Anti-affinity para distribuir webservice/sidekiq em nós diferentes
- Container Registry e SSH via TCP passthrough
- Documentação do erro 422 e solução com trustedCIDRsForXForwardedFor

Melhorias gerais:
- READMEs atualizados com arquitetura e troubleshooting
- Scripts cleanup.sh para todas as aulas
- CLAUDE.md atualizado com contexto do projeto
This commit is contained in:
Allyson de Paula
2025-12-31 17:57:02 -03:00
parent 50dc74c1d8
commit 07b7ee62d3
35 changed files with 4665 additions and 311 deletions

1
.gitignore vendored
View File

@@ -14,6 +14,7 @@
**/talosconfig **/talosconfig
**/*.pem **/*.pem
**/*.key **/*.key
**/.env
# OS # OS
.DS_Store .DS_Store

View File

@@ -13,9 +13,11 @@ This is a workshop repository for teaching Docker and Kubernetes concepts, speci
- **aula-03/**: Kubernetes lesson - high availability with replicas and readiness probes - **aula-03/**: Kubernetes lesson - high availability with replicas and readiness probes
- **aula-04/**: Kubernetes lesson - NGINX Ingress with Keep Request (Lua) for zero-downtime - **aula-04/**: Kubernetes lesson - NGINX Ingress with Keep Request (Lua) for zero-downtime
- **aula-05/**: Kubernetes lesson - KEDA + Victoria Metrics for metrics-based auto-scaling - **aula-05/**: Kubernetes lesson - KEDA + Victoria Metrics for metrics-based auto-scaling
- **aula-06/**: Kubernetes lesson - n8n deployment via Helm with Queue Mode (workers, webhooks, PostgreSQL, Redis) - **aula-06/**: Kubernetes lesson - n8n deployment via Helm (LOCAL environment - Docker Desktop, minikube, kind)
- **aula-07/**: Talos Linux - creating custom Talos image for Hetzner Cloud - **aula-07/**: Talos Linux - creating custom Talos image for Hetzner Cloud
- **aula-08/**: OpenTofu - provisioning HA Talos Kubernetes cluster on Hetzner Cloud - **aula-08/**: OpenTofu - provisioning HA Talos Kubernetes cluster on Hetzner Cloud with CCM and LoadBalancer
- **aula-09/**: Kubernetes lesson - n8n deployment via Helm (Hetzner Cloud with CSI Driver and multi-tenant support)
- **aula-10/**: Kubernetes lesson - GitLab deployment via Helm with Container Registry and SSH
## Running the Examples ## Running the Examples
@@ -63,12 +65,12 @@ cd aula-05
``` ```
Installs Victoria Metrics (metrics collection), KEDA (event-driven autoscaling), and NGINX Ingress. The ScaledObject monitors metrics like unavailable pods and restart counts, automatically scaling the deployment from 5 to 30 replicas based on demand. Installs Victoria Metrics (metrics collection), KEDA (event-driven autoscaling), and NGINX Ingress. The ScaledObject monitors metrics like unavailable pods and restart counts, automatically scaling the deployment from 5 to 30 replicas based on demand.
### Aula 06 (Kubernetes - n8n via Helm) ### Aula 06 (Kubernetes - n8n via Helm - LOCAL)
```bash ```bash
cd aula-06 cd aula-06
./setup.sh ./setup.sh
``` ```
Deploys n8n workflow automation platform via Helm chart with Queue Mode architecture: main node, workers (2-5 replicas with HPA), webhooks (1-3 replicas with HPA), PostgreSQL, and Redis. Access via http://n8n.localhost (requires NGINX Ingress). Deploys n8n workflow automation platform via Helm chart in a LOCAL Kubernetes cluster (Docker Desktop, minikube, kind). Queue Mode architecture with main node, workers (2-5 replicas with HPA), webhooks (1-3 replicas with HPA), PostgreSQL, and Redis. Access via http://n8n.localhost (requires NGINX Ingress).
### Aula 07 (Talos Linux - Custom Image) ### Aula 07 (Talos Linux - Custom Image)
Follow the instructions in `aula-07/README.md` to create a custom Talos Linux image on Hetzner Cloud using Talos Factory. This is a prerequisite for Aula 08. Follow the instructions in `aula-07/README.md` to create a custom Talos Linux image on Hetzner Cloud using Talos Factory. This is a prerequisite for Aula 08.
@@ -98,8 +100,47 @@ Optional - Enable cluster autoscaling:
``` ```
This installs the Kubernetes Cluster Autoscaler configured for Hetzner Cloud, automatically scaling workers from 1 to 5 based on pending pods. This installs the Kubernetes Cluster Autoscaler configured for Hetzner Cloud, automatically scaling workers from 1 to 5 based on pending pods.
Optional - Install Hetzner Cloud Controller Manager and NGINX Ingress with LoadBalancer:
```bash
./install-ccm.sh
./install-nginx-ingress.sh
```
This enables automatic LoadBalancer provisioning and exposes HTTP/HTTPS/SSH via a single Hetzner LoadBalancer (~$5/month).
To destroy the infrastructure: `./cleanup.sh` To destroy the infrastructure: `./cleanup.sh`
### Aula 09 (Kubernetes - n8n via Helm - Hetzner Cloud)
```bash
cd aula-09
export KUBECONFIG=/path/to/aula-08/kubeconfig
./setup.sh
```
Deploys n8n workflow automation platform via Helm chart on Hetzner Cloud. Installs Hetzner CSI Driver for persistent volumes (10Gi minimum). Includes multi-tenant support with `add-client.sh` script for provisioning clients in separate namespaces.
Prerequisites:
- Completed Aula 08 (Talos cluster on Hetzner)
- Hetzner Cloud API token
### Aula 10 (Kubernetes - GitLab via Helm)
```bash
cd aula-10
./setup.sh
```
Deploys GitLab via official Helm chart with:
- Web UI at git.kube.quest
- Container Registry at registry.git.kube.quest
- SSH access via port 22 (TCP passthrough through NGINX)
- PostgreSQL, Redis, and MinIO for storage
- Resource requests of ~4GB to occupy one dedicated CAX11 worker
Prerequisites:
- Completed Aula 08 (Talos cluster)
- Hetzner CSI Driver (Aula 09)
- Hetzner CCM and NGINX Ingress with LoadBalancer (Aula 08)
- DNS configured pointing to LoadBalancer IP
To remove: `./cleanup.sh`
## App Behavior ## App Behavior
The Node.js app (`app.js`) is intentionally designed to: The Node.js app (`app.js`) is intentionally designed to:

122
aula-01/README.md Normal file
View File

@@ -0,0 +1,122 @@
# Aula 01 - O Problema: Apps que Travam mas Nao Morrem
## Objetivo
Demonstrar uma limitacao comum em ambientes de producao: aplicacoes que param de responder mas continuam com o processo rodando. O Docker nao consegue detectar esse tipo de falha.
## O Problema
Imagine uma aplicacao que:
- Tem um memory leak que causa travamento
- Entra em deadlock
- Perde conexao com o banco e fica em loop infinito
O processo continua vivo, mas a aplicacao nao responde. O Docker ve o processo rodando e nao faz nada.
## A Aplicacao "Bugada"
Nossa app Node.js simula esse comportamento:
```javascript
// Apos MAX_REQUESTS, para de responder mas continua rodando
if (requestCount > MAX_REQUESTS) {
console.log('App travado apos 3 requests');
return; // Nao responde, mas processo vivo
}
```
## Arquivos
```
aula-01/
├── docker-compose.yml # Configuracao do container
├── app.js # Aplicacao Node.js "bugada"
└── .env # MAX_REQUESTS=3
```
## Comandos
```bash
# Iniciar a aplicacao
docker compose up
# Parar a aplicacao
docker compose down
```
## Teste Pratico
1. Inicie a aplicacao:
```bash
docker compose up
```
2. Faca 3 requests (funcionam normalmente):
```bash
curl localhost:3000 # Req -> 1/3
curl localhost:3000 # Req -> 2/3
curl localhost:3000 # Req -> 3/3
```
3. Faca mais um request (vai travar):
```bash
curl localhost:3000 # Fica esperando para sempre...
```
4. Observe os logs - a app diz que travou:
```
App travado apos 3 requests
```
5. O container continua rodando:
```bash
docker ps # Container esta "Up", mas nao responde
```
## A Limitacao do `restart: always`
No `docker-compose.yml` temos:
```yaml
restart: always
```
Isso reinicia o container apenas quando o **processo morre**. Como nossa app trava mas o processo Node.js continua rodando, o Docker nao faz nada.
| Situacao | Processo | Docker reinicia? |
|----------|:--------:|:----------------:|
| App crasha (exit 1) | Morto | Sim |
| App trava (nosso caso) | Vivo | Nao |
## Healthcheck do Docker: Detecta mas Nao Resolve
Adicionamos um healthcheck no `docker-compose.yml`:
```yaml
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
interval: 2s
timeout: 2s
retries: 2
start_period: 5s
```
Apos a app travar, veja o status mudar para `unhealthy`:
```bash
docker ps
# STATUS: Up 1 minute (unhealthy)
```
**Porem, o Docker apenas DETECTA o problema - nao reinicia o container!**
O healthcheck serve para:
- Monitoramento e alertas
- Orquestradores externos tomarem acao
- Load balancers removerem o container do pool
Mas sozinho, nao resolve o problema.
## Proximo Passo
Na **Aula 02**, vamos resolver esse problema usando Kubernetes com **Liveness Probes** - verificacoes periodicas que detectam quando a aplicacao parou de responder **E reiniciam o container automaticamente**.

View File

@@ -9,3 +9,9 @@ services:
ports: ports:
- "3000:3000" - "3000:3000"
restart: always restart: always
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
interval: 2s
timeout: 2s
retries: 2
start_period: 5s

107
aula-02/README.md Normal file
View File

@@ -0,0 +1,107 @@
# Aula 02 - Do Docker Compose ao Kubernetes
## Objetivo
Migrar a aplicação da Aula 01 para Kubernetes, entendendo como os conceitos se traduzem entre as duas tecnologias.
## Comparação: Docker Compose vs Kubernetes
| Docker Compose | Kubernetes | Descrição |
|----------------|------------|-----------|
| `docker-compose.yml` | `deployment.yaml` | Define como rodar o container |
| `restart: always` | `livenessProbe` | Recuperação de falhas |
| `volumes:` (bind mount) | `ConfigMap` + `volumeMounts` | Injetar arquivos no container |
| `environment:` | `env:` + `configMapKeyRef` | Variáveis de ambiente |
| `ports:` | `Service` | Expor a aplicação |
## Mapeamento de Arquivos
```
aula-01/ aula-02/
├── docker-compose.yml → ├── deployment.yaml
├── app.js → ├── configmap.yaml (contém o app.js)
└── service.yaml
```
Na Aula 01, tínhamos 1 arquivo YAML. No Kubernetes, separamos em 3 arquivos com responsabilidades distintas:
- **configmap.yaml**: Configurações e código da aplicação
- **deployment.yaml**: Como rodar e monitorar o container
- **service.yaml**: Como expor a aplicação na rede
## Comandos
```bash
# Criar todos os recursos
kubectl apply -f .
# Remover todos os recursos
kubectl delete -f .
```
## O Problema do `restart: always`
Na Aula 01, usamos `restart: always` no Docker Compose:
```yaml
services:
node-app:
restart: always # Reinicia quando o processo morre
```
**Problema**: Se a aplicação travar mas o processo continuar rodando, o Docker não reinicia o container. É exatamente o que acontece com nossa app "bugada" - ela para de responder mas o processo Node.js continua vivo.
## A Solução: Liveness Probe
O Kubernetes resolve isso com **Liveness Probes** - verificações periódicas de saúde:
```yaml
livenessProbe:
httpGet:
path: /health # Endpoint a ser testado
port: 3000
initialDelaySeconds: 5 # Espera inicial antes de testar
periodSeconds: 2 # Intervalo entre testes
failureThreshold: 2 # Falhas consecutivas para reiniciar
```
**Como funciona**:
1. Kubernetes faz GET em `/health` a cada 2 segundos
2. Se falhar 2 vezes seguidas, o container é reiniciado
3. Diferente do `restart: always`, detecta processos "vivos mas travados"
## Teste Prático
1. Aplique os recursos:
```bash
kubectl apply -f .
```
2. Acompanhe os pods:
```bash
kubectl get pods -w
```
3. Em outro terminal, faça requests até travar:
```bash
curl localhost:3000
curl localhost:3000
curl localhost:3000
curl localhost:3000 # Este vai travar
```
4. Observe o pod ser reiniciado automaticamente (RESTARTS aumenta)
5. Remova tudo:
```bash
kubectl delete -f .
```
## Conclusão
| Abordagem | Detecta processo morto | Detecta processo travado |
|-----------|:---------------------:|:-----------------------:|
| `restart: always` | Sim | Nao |
| `livenessProbe` | Sim | Sim |
O Liveness Probe é a forma do Kubernetes garantir que sua aplicação está realmente funcionando, não apenas rodando.

30
aula-03/cleanup.sh Executable file
View File

@@ -0,0 +1,30 @@
#!/bin/bash
# =============================================================================
# Cleanup da Aula 03 - Remove aplicação node-bugado (HA)
# =============================================================================
set -e
# Cores para output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo ""
echo "============================================"
echo " Cleanup - Aula 03"
echo "============================================"
echo ""
log_info "Removendo recursos..."
kubectl delete -f "$SCRIPT_DIR/service.yaml" 2>/dev/null || true
kubectl delete -f "$SCRIPT_DIR/deployment.yaml" 2>/dev/null || true
kubectl delete -f "$SCRIPT_DIR/configmap.yaml" 2>/dev/null || true
log_success "Cleanup concluído!"
echo ""

43
aula-04/cleanup.sh Executable file
View File

@@ -0,0 +1,43 @@
#!/bin/bash
# =============================================================================
# Cleanup da Aula 04 - Remove NGINX Ingress e aplicação
# =============================================================================
set -e
# Cores para output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo ""
echo "============================================"
echo " Cleanup - Aula 04"
echo "============================================"
echo ""
# Remover recursos da aplicação
log_info "Removendo aplicação node-bugado..."
kubectl delete -f "$SCRIPT_DIR/ingress-nginx.yaml" 2>/dev/null || true
kubectl delete -f "$SCRIPT_DIR/service.yaml" 2>/dev/null || true
kubectl delete -f "$SCRIPT_DIR/deployment.yaml" 2>/dev/null || true
kubectl delete -f "$SCRIPT_DIR/configmap.yaml" 2>/dev/null || true
log_success "Aplicação removida"
# Remover NGINX Ingress
log_info "Removendo NGINX Ingress Controller..."
helm uninstall nginx-ingress -n ingress-nginx 2>/dev/null || true
kubectl delete namespace ingress-nginx 2>/dev/null || true
log_success "NGINX Ingress removido"
echo ""
log_success "Cleanup concluído!"
echo ""

56
aula-05/cleanup.sh Executable file
View File

@@ -0,0 +1,56 @@
#!/bin/bash
# =============================================================================
# Cleanup da Aula 05 - Remove KEDA, Victoria Metrics e aplicação
# =============================================================================
set -e
# Cores para output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo ""
echo "============================================"
echo " Cleanup - Aula 05"
echo "============================================"
echo ""
# Remover recursos da aplicação
log_info "Removendo aplicação node-bugado..."
kubectl delete -f "$SCRIPT_DIR/scaledobject.yaml" 2>/dev/null || true
kubectl delete -f "$SCRIPT_DIR/ingress-nginx.yaml" 2>/dev/null || true
kubectl delete -f "$SCRIPT_DIR/service.yaml" 2>/dev/null || true
kubectl delete -f "$SCRIPT_DIR/deployment.yaml" 2>/dev/null || true
kubectl delete -f "$SCRIPT_DIR/configmap.yaml" 2>/dev/null || true
log_success "Aplicação removida"
# Remover NGINX Ingress
log_info "Removendo NGINX Ingress Controller..."
helm uninstall nginx-ingress -n ingress-nginx 2>/dev/null || true
kubectl delete namespace ingress-nginx 2>/dev/null || true
log_success "NGINX Ingress removido"
# Remover KEDA
log_info "Removendo KEDA..."
helm uninstall keda -n keda 2>/dev/null || true
kubectl delete namespace keda 2>/dev/null || true
log_success "KEDA removido"
# Remover Victoria Metrics
log_info "Removendo Victoria Metrics..."
helm uninstall vm -n monitoring 2>/dev/null || true
kubectl delete namespace monitoring 2>/dev/null || true
log_success "Victoria Metrics removido"
echo ""
log_success "Cleanup concluído!"
echo ""

107
aula-06/README.md Normal file
View File

@@ -0,0 +1,107 @@
# Aula 06 - n8n via Helm (Ambiente LOCAL)
Deploy do n8n workflow automation em cluster Kubernetes local usando Helm.
## Arquitetura
```
┌─────────────────────────────────────────────────────────────┐
│ Cluster Local │
│ (Docker Desktop / minikube / kind / k3d) │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ NGINX Ingress │ │
│ │ http://n8n.localhost │ │
│ └────────────────────────┬────────────────────────────┘ │
│ │ │
│ ┌────────────────────────┼────────────────────────────┐ │
│ │ Namespace: n8n │ │
│ │ │ │ │
│ │ ┌────────────────────┼────────────────────┐ │ │
│ │ │ ▼ │ │ │
│ │ │ ┌──────────┐ │ │ │
│ │ │ │ Main │ │ │ │
│ │ │ │ (n8n) │ │ │ │
│ │ │ └────┬─────┘ │ │ │
│ │ │ │ │ │ │
│ │ │ ┌─────────────┼─────────────┐ │ │ │
│ │ │ ▼ ▼ ▼ │ │ │
│ │ │ ┌───────┐ ┌──────────┐ ┌────────┐ │ │ │
│ │ │ │Workers│ │ Webhooks │ │ MCP │ │ │ │
│ │ │ │ (2-5) │ │ (1-3) │ │Webhook │ │ │ │
│ │ │ └───────┘ └──────────┘ └────────┘ │ │ │
│ │ │ │ │ │
│ │ │ Queue Mode │ │ │
│ │ └────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌───────────────┼───────────────┐ │ │
│ │ ▼ ▼ │ │
│ │ ┌──────────┐ ┌──────────┐ │ │
│ │ │PostgreSQL│ │ Redis │ │ │
│ │ │ (1Gi) │ │ (1Gi) │ │ │
│ │ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## Pré-requisitos
- Cluster Kubernetes local:
- Docker Desktop com Kubernetes habilitado
- minikube (`minikube start`)
- kind (`kind create cluster`)
- k3d (`k3d cluster create`)
- kubectl configurado
- Helm 3.x instalado
## Instalação
```bash
cd aula-06
chmod +x setup.sh
./setup.sh
```
O script instala automaticamente:
1. NGINX Ingress Controller
2. n8n com todos os componentes
## Acesso
**URL:** http://n8n.localhost
Se `n8n.localhost` não resolver, adicione ao `/etc/hosts`:
```
127.0.0.1 n8n.localhost
```
## Componentes
| Componente | Réplicas | Recursos |
|------------|----------|----------|
| Main (n8n) | 1 | 256Mi-1Gi RAM |
| Workers | 2-5 (HPA) | 256Mi-512Mi RAM |
| Webhooks | 1-3 (HPA) | 128Mi-256Mi RAM |
| PostgreSQL | 1 | 1Gi volume |
| Redis | 1 | 1Gi volume |
## Comandos Úteis
```bash
# Ver pods
kubectl get pods -n n8n
# Ver logs do n8n
kubectl logs -f -n n8n deployment/n8n
# Ver autoscaling
kubectl get hpa -n n8n
# Desinstalar
helm uninstall n8n -n n8n
kubectl delete ns n8n
```
## Para Cluster Hetzner Cloud
Se você quer fazer deploy em um cluster Hetzner Cloud com volumes persistentes e LoadBalancer, veja a **aula-09**.

51
aula-06/cleanup.sh Executable file
View File

@@ -0,0 +1,51 @@
#!/bin/bash
# =============================================================================
# Cleanup da Aula 06 - Remove n8n (ambiente LOCAL)
# =============================================================================
set -e
# Cores para output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
echo ""
echo "============================================"
echo " Cleanup - Aula 06 (n8n LOCAL)"
echo "============================================"
echo ""
# Remover n8n
log_info "Removendo n8n..."
helm uninstall n8n -n n8n 2>/dev/null || true
log_success "Helm release removida"
# Remover PVCs
log_info "Removendo PVCs..."
kubectl delete pvc --all -n n8n 2>/dev/null || true
log_success "PVCs removidos"
# Remover namespace
log_info "Removendo namespace n8n..."
kubectl delete namespace n8n 2>/dev/null || true
log_success "Namespace removido"
# Opcional: remover NGINX Ingress
read -p "Remover NGINX Ingress também? (s/N): " remove_ingress
if [[ "$remove_ingress" =~ ^[Ss]$ ]]; then
log_info "Removendo NGINX Ingress..."
helm uninstall nginx-ingress -n ingress-nginx 2>/dev/null || true
kubectl delete namespace ingress-nginx 2>/dev/null || true
log_success "NGINX Ingress removido"
fi
echo ""
log_success "Cleanup concluído!"
echo ""

View File

@@ -1,10 +1,12 @@
# ============================================================================= # =============================================================================
# n8n Helm Chart - Custom Values # n8n Helm Chart - Custom Values (Ambiente LOCAL)
# ============================================================================= # =============================================================================
# Aula 06 - Deploy n8n via Helm # Aula 06 - Deploy n8n via Helm em cluster local
# #
# Chart: community-charts/n8n # Chart: community-charts/n8n
# Docs: https://community-charts.github.io/docs/charts/n8n/configuration # Docs: https://community-charts.github.io/docs/charts/n8n/configuration
#
# Para cluster Hetzner Cloud, veja aula-09/
# ============================================================================= # =============================================================================
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@@ -35,7 +37,7 @@ postgresql:
primary: primary:
persistence: persistence:
enabled: true enabled: true
size: 5Gi size: 1Gi # Ambiente local - sem mínimo
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Redis (necessário para Queue Mode) # Redis (necessário para Queue Mode)
@@ -46,6 +48,10 @@ redis:
auth: auth:
enabled: true enabled: true
password: "n8n-redis-workshop-2025" password: "n8n-redis-workshop-2025"
master:
persistence:
enabled: true
size: 1Gi # Ambiente local - sem mínimo
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Ingress NGINX # Ingress NGINX
@@ -58,7 +64,7 @@ ingress:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
hosts: hosts:
- host: n8n.kube.quest - host: n8n.localhost
paths: paths:
- path: / - path: /
pathType: Prefix pathType: Prefix
@@ -71,7 +77,7 @@ main:
N8N_SECURE_COOKIE: "false" # Permite HTTP sem HTTPS (apenas para dev/workshop) N8N_SECURE_COOKIE: "false" # Permite HTTP sem HTTPS (apenas para dev/workshop)
persistence: persistence:
enabled: true enabled: true
size: 2Gi size: 1Gi # Ambiente local - sem mínimo
mountPath: "/home/node/.n8n" mountPath: "/home/node/.n8n"
resources: resources:
requests: requests:
@@ -121,7 +127,7 @@ worker:
webhook: webhook:
mode: queue mode: queue
count: 1 count: 1
url: "https://n8n.kube.quest" url: "http://n8n.localhost"
extraEnvVars: extraEnvVars:
N8N_SECURE_COOKIE: "false" N8N_SECURE_COOKIE: "false"
resources: resources:

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# ============================================================================= # =============================================================================
# Setup da Aula 06 - n8n via Helm # Setup da Aula 06 - n8n via Helm (Ambiente LOCAL)
# ============================================================================= # =============================================================================
# #
# Este script instala e configura: # Este script instala e configura:
@@ -8,7 +8,7 @@
# 2. n8n com PostgreSQL, Redis, Workers e Webhooks # 2. n8n com PostgreSQL, Redis, Workers e Webhooks
# #
# Pré-requisitos: # Pré-requisitos:
# - Kubernetes cluster rodando (k3s, minikube, kind, etc) # - Kubernetes cluster LOCAL rodando (Docker Desktop, minikube, kind, k3d)
# - kubectl configurado # - kubectl configurado
# - Helm 3.x instalado # - Helm 3.x instalado
# #
@@ -52,6 +52,10 @@ log_success "kubectl encontrado"
# Verificar conexão com cluster # Verificar conexão com cluster
if ! kubectl cluster-info &> /dev/null; then if ! kubectl cluster-info &> /dev/null; then
log_error "Não foi possível conectar ao cluster Kubernetes." log_error "Não foi possível conectar ao cluster Kubernetes."
log_info "Verifique se seu cluster local está rodando:"
log_info " - Docker Desktop: Habilite Kubernetes nas configurações"
log_info " - minikube: minikube start"
log_info " - kind: kind create cluster"
exit 1 exit 1
fi fi
log_success "Conectado ao cluster Kubernetes" log_success "Conectado ao cluster Kubernetes"
@@ -67,68 +71,7 @@ log_success "Helm $(helm version --short) encontrado"
echo "" echo ""
# ============================================================================= # =============================================================================
# 1. INSTALAR HETZNER CSI DRIVER (para provisionar volumes) # 1. INSTALAR NGINX INGRESS (se não existir)
# =============================================================================
log_info "=== Configurando Hetzner CSI Driver ==="
# Verificar se secret já existe (evita pedir token novamente)
if kubectl get secret hcloud -n kube-system &> /dev/null; then
log_success "Secret hcloud já existe em kube-system"
else
# Pedir token via input interativo
echo ""
log_info "Token da Hetzner Cloud necessário para provisionar volumes."
log_info "Crie um token em: https://console.hetzner.cloud/projects/*/security/tokens"
echo ""
log_info "Cole o token e pressione ENTER:"
# Desabilita echo, lê linha completa, reabilita echo
stty -echo
IFS= read -r HCLOUD_TOKEN
stty echo
echo ""
if [ -z "$HCLOUD_TOKEN" ]; then
log_error "Token não pode ser vazio."
exit 1
fi
log_info "Criando secret hcloud em kube-system..."
kubectl create secret generic hcloud \
--namespace=kube-system \
--from-literal=token="$HCLOUD_TOKEN"
log_success "Secret hcloud criado"
fi
# Instalar Hetzner CSI Driver via Helm (se não instalado)
if helm status hcloud-csi -n kube-system &> /dev/null; then
log_success "Hetzner CSI Driver já está instalado"
else
log_info "Instalando Hetzner CSI Driver..."
helm repo add hcloud https://charts.hetzner.cloud 2>/dev/null || true
helm repo update hcloud
helm install hcloud-csi hcloud/hcloud-csi \
--namespace kube-system \
--wait \
--timeout 5m
log_success "Hetzner CSI Driver instalado"
fi
# Verificar StorageClass
log_info "Verificando StorageClass..."
if kubectl get storageclass hcloud-volumes &> /dev/null; then
log_success "StorageClass hcloud-volumes disponível"
else
log_error "StorageClass hcloud-volumes não encontrado"
exit 1
fi
echo ""
# =============================================================================
# 2. INSTALAR NGINX INGRESS (se não existir)
# ============================================================================= # =============================================================================
log_info "=== Verificando NGINX Ingress ===" log_info "=== Verificando NGINX Ingress ==="
@@ -152,7 +95,7 @@ fi
echo "" echo ""
# ============================================================================= # =============================================================================
# 3. CRIAR NAMESPACE E APLICAR SECRETS # 2. CRIAR NAMESPACE E APLICAR SECRETS
# ============================================================================= # =============================================================================
log_info "=== Configurando namespace n8n ===" log_info "=== Configurando namespace n8n ==="
@@ -169,7 +112,7 @@ fi
echo "" echo ""
# ============================================================================= # =============================================================================
# 4. INSTALAR n8n VIA HELM # 3. INSTALAR n8n VIA HELM
# ============================================================================= # =============================================================================
log_info "=== Instalando n8n ===" log_info "=== Instalando n8n ==="
@@ -201,7 +144,7 @@ fi
echo "" echo ""
# ============================================================================= # =============================================================================
# 5. AGUARDAR PODS FICAREM PRONTOS # 4. AGUARDAR PODS FICAREM PRONTOS
# ============================================================================= # =============================================================================
log_info "=== Aguardando pods ficarem prontos ===" log_info "=== Aguardando pods ficarem prontos ==="
@@ -248,7 +191,6 @@ echo -e "${GREEN} Setup Completo!${NC}"
echo "==============================================" echo "=============================================="
echo "" echo ""
echo "Componentes instalados:" echo "Componentes instalados:"
echo " - Hetzner CSI Driver (StorageClass: hcloud-volumes)"
echo " - NGINX Ingress Controller" echo " - NGINX Ingress Controller"
echo " - n8n (namespace: n8n)" echo " - n8n (namespace: n8n)"
echo " - Main node" echo " - Main node"
@@ -279,15 +221,6 @@ echo ""
echo " # Desinstalar" echo " # Desinstalar"
echo " helm uninstall n8n -n n8n" echo " helm uninstall n8n -n n8n"
echo "" echo ""
echo " # Fazer upgrade do helm chart"
echo " helm upgrade --reuse-values --values custom-values.yaml n8n community-charts/n8n --namespace n8n"
echo ""
echo " # Verificar historico de releases"
echo " helm history n8n -n n8n"
echo ""
echo " # Fazer rollback do historico de releases"
echo " helm rollback n8n <nº da release>"
echo ""
echo "==============================================" echo "=============================================="
echo "" echo ""

208
aula-08/README.md Normal file
View File

@@ -0,0 +1,208 @@
# Aula 08 - Cluster Kubernetes HA na Hetzner Cloud
## Objetivo
Provisionar um cluster Kubernetes Talos de producao na Hetzner Cloud usando OpenTofu, com opcoes de Alta Disponibilidade (HA) e LoadBalancer.
## Arquitetura
### Modo HA com LoadBalancer (Recomendado)
```
Internet
┌───────────────────────┐
│ Hetzner Load │
│ Balancer (LB11) │
│ IP: 1.2.3.4 │
└───────────────────────┘
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ CP-0 │ │ CP-1 │ │ CP-2 │
│ CAX11 │ │ CAX11 │ │ CAX11 │
│10.0.1.10│ │10.0.1.11│ │10.0.1.12│
└─────────┘ └─────────┘ └─────────┘
│ │ │
└───────────────┼───────────────┘
┌──────────┐
│ Worker-0 │
│ CAX11 │
│10.0.1.20 │
└──────────┘
```
### Servicos do LoadBalancer
O LoadBalancer centraliza todo o trafego externo:
| Porta | Destino | Uso |
|:-----:|---------|-----|
| 6443 | Control Planes | Kubernetes API |
| 50000 | Control Planes | Talos API |
| 80 | Workers | HTTP (NGINX Ingress) |
| 443 | Workers | HTTPS (NGINX Ingress) |
| 22 | Workers | SSH (GitLab) |
### Roteamento L7 (por dominio)
O LoadBalancer faz apenas roteamento L4 (por porta). O roteamento por dominio e feito pelo NGINX Ingress:
```
LB :443 → NGINX Ingress → n8n.kube.quest → n8n pods
→ git.kube.quest → gitlab pods
→ argocd.kube.quest → argocd pods
→ registry.git... → registry pods
```
## Custos Estimados
Precos baseados em [Hetzner Cloud](https://www.hetzner.com/cloud/) (NBG1 - Nuremberg):
| Configuracao | CPs | LB | Custo/mes |
|--------------|:---:|:--:|----------:|
| Single | 1 | Floating IP | ~$13 |
| HA | 3 | Floating IP | ~$22 |
| HA + LB | 3 | LB11 | ~$24 |
| HA + LB + Autoscaler (max 5 workers) | 3 | LB11 | ~$43 |
Recursos: CAX11 $4.59, LB11 $5.99, Floating IP $3.29
## Pre-requisitos
```bash
# macOS
brew install opentofu
brew install siderolabs/tap/talosctl
brew install kubectl
brew install helm
# Criar imagem Talos (aula-07)
# Obter ID: hcloud image list --type snapshot
```
## Comandos
```bash
# Provisionar cluster (interativo)
./setup.sh
# Destruir infraestrutura
./cleanup.sh
```
## Fluxo do Setup
```
1. Verifica pre-requisitos
2. Coleta credenciais:
- Token Hetzner Cloud
- Chave SSH
- ID da imagem Talos
3. Pergunta configuracao:
- Cluster HA? (S/n)
- LoadBalancer? (S/n) [se HA]
4. Cria terraform.tfvars
5. Executa tofu init/plan/apply
6. Aguarda cluster ficar pronto
7. Instala CCM (Cloud Controller Manager)
8. Instala Cluster Autoscaler
9. Instala Hetzner CSI Driver
10. Instala Metrics Server
11. Instala NGINX Ingress Controller
```
## Componentes Instalados
| Componente | Funcao |
|------------|--------|
| Hetzner CCM | Remove taints dos workers, provisiona LoadBalancers |
| Cluster Autoscaler | Escala workers de 1 a 5 automaticamente |
| Hetzner CSI Driver | Provisiona volumes persistentes (StorageClass: hcloud-volumes) |
| Metrics Server | Habilita kubectl top e HPA baseado em CPU/memoria |
| NGINX Ingress | Expoe servicos HTTP/HTTPS/SSH via LoadBalancer |
## Apos o Setup
### Configurar kubectl
```bash
export KUBECONFIG=$PWD/kubeconfig
kubectl get nodes
```
### Configurar talosctl
```bash
export TALOSCONFIG=$PWD/talosconfig
talosctl -n <IP> health
```
### Ver logs do Autoscaler
```bash
kubectl logs -n cluster-autoscaler -l app=cluster-autoscaler -f
```
### Ver IP do LoadBalancer
```bash
kubectl get svc -n ingress-nginx ingress-nginx-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
```
### Ver StorageClass
```bash
kubectl get storageclass
# Esperado: hcloud-volumes (default)
```
### Ver uso de recursos
```bash
kubectl top nodes # Uso de CPU/memoria por no
kubectl top pods -A # Uso de CPU/memoria por pod
```
### Testar Autoscaler
```bash
# Criar pods pending (vai escalar workers)
kubectl create deployment test --image=nginx --replicas=20
# Acompanhar
kubectl get nodes -w
# Limpar
kubectl delete deployment test
```
## Proximos Passos
1. **Configurar DNS** - Apontar dominio para o IP do LoadBalancer
2. **Deploy n8n** (aula-09) - Workflow automation com PostgreSQL e Redis
3. **Deploy GitLab** (aula-10) - Git + Container Registry + SSH
## Arquivos
```
aula-08/
├── main.tf # Recursos principais (servers, LB, Talos)
├── variables.tf # Variaveis de entrada
├── outputs.tf # Outputs do cluster
├── versions.tf # Versoes dos providers
├── setup.sh # Script de setup interativo
├── cleanup.sh # Script de destruicao
├── cluster-autoscaler.yaml # Manifesto do autoscaler
├── install-nginx-ingress.sh # Instala NGINX Ingress com LB
├── install-metrics-server.sh # Instala Metrics Server (kubectl top, HPA)
├── nginx-ingress-values.yaml # Configuracao do NGINX Ingress
└── talos-patches/ # Patches de configuracao Talos
├── control-plane.yaml
└── worker.yaml
```

View File

@@ -20,7 +20,7 @@ cd "$SCRIPT_DIR"
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; } log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; } log_error() { echo -e "${RED}[ERRO]${NC} $1"; }
echo "" echo ""
echo "============================================" echo "============================================"
@@ -57,6 +57,9 @@ fi
log_warn "ATENÇÃO: Esta operação irá DESTRUIR todos os recursos!" log_warn "ATENÇÃO: Esta operação irá DESTRUIR todos os recursos!"
echo "" echo ""
echo "Recursos que serão removidos:" echo "Recursos que serão removidos:"
echo " - NGINX Ingress Controller"
echo " - Hetzner CSI Driver"
echo " - LoadBalancer (Hetzner LB)"
echo " - 3x Control Plane nodes" echo " - 3x Control Plane nodes"
echo " - Workers (incluindo os criados pelo autoscaler)" echo " - Workers (incluindo os criados pelo autoscaler)"
echo " - Rede privada" echo " - Rede privada"
@@ -99,6 +102,76 @@ if [ -f "terraform.tfvars" ]; then
fi fi
fi fi
# ==========================================================================
# Remover LoadBalancer criado pelo CCM (não gerenciado pelo OpenTofu)
# ==========================================================================
# O LoadBalancer é criado pelo Hetzner CCM quando o NGINX Ingress é instalado.
# Ele está conectado à subnet e impede a destruição da rede se não for removido.
log_info "Verificando LoadBalancer do Hetzner CCM..."
# Tentar via kubectl primeiro (se cluster ainda acessível)
if [ -f "kubeconfig" ]; then
export KUBECONFIG="$SCRIPT_DIR/kubeconfig"
if kubectl cluster-info &>/dev/null; then
if kubectl get svc -n ingress-nginx nginx-ingress-ingress-nginx-controller &>/dev/null; then
log_info "Removendo Service LoadBalancer via kubectl..."
kubectl delete svc nginx-ingress-ingress-nginx-controller -n ingress-nginx --timeout=60s 2>/dev/null || true
sleep 10 # Aguardar CCM processar a remoção
fi
fi
fi
# Fallback: deletar diretamente via hcloud CLI
if [ -f "terraform.tfvars" ]; then
HCLOUD_TOKEN=$(grep 'hcloud_token' terraform.tfvars | cut -d'"' -f2)
if [ -n "$HCLOUD_TOKEN" ]; then
LB_NAME="k8s-ingress" # Nome definido no nginx-ingress-values.yaml
if HCLOUD_TOKEN="$HCLOUD_TOKEN" hcloud load-balancer describe "$LB_NAME" &>/dev/null; then
log_info "Removendo LoadBalancer '$LB_NAME' via hcloud CLI..."
HCLOUD_TOKEN="$HCLOUD_TOKEN" hcloud load-balancer delete "$LB_NAME" --quiet 2>/dev/null || true
log_success "LoadBalancer removido"
sleep 5 # Aguardar Hetzner processar
else
log_info "LoadBalancer '$LB_NAME' não encontrado (OK)"
fi
fi
fi
# ==========================================================================
# Remover NGINX Ingress Controller
# ==========================================================================
# Instalado pelo setup.sh para expor serviços HTTP/HTTPS/SSH
if [ -f "kubeconfig" ]; then
export KUBECONFIG="$SCRIPT_DIR/kubeconfig"
if kubectl cluster-info &>/dev/null; then
if helm status ingress-nginx -n ingress-nginx &>/dev/null; then
log_info "Removendo NGINX Ingress Controller..."
helm uninstall ingress-nginx -n ingress-nginx --wait 2>/dev/null || true
kubectl delete namespace ingress-nginx --wait=false 2>/dev/null || true
log_success "NGINX Ingress removido"
sleep 5 # Aguardar processamento
fi
fi
fi
# ==========================================================================
# Remover Hetzner CSI Driver
# ==========================================================================
# Instalado pelo setup.sh para provisionar volumes persistentes
if [ -f "kubeconfig" ]; then
export KUBECONFIG="$SCRIPT_DIR/kubeconfig"
if kubectl cluster-info &>/dev/null; then
if helm status hcloud-csi -n kube-system &>/dev/null; then
log_info "Removendo Hetzner CSI Driver..."
helm uninstall hcloud-csi -n kube-system --wait 2>/dev/null || true
log_success "Hetzner CSI Driver removido"
fi
fi
fi
echo "" echo ""
log_info "Destruindo infraestrutura via OpenTofu..." log_info "Destruindo infraestrutura via OpenTofu..."
echo "" echo ""

View File

@@ -106,17 +106,17 @@ spec:
app: cluster-autoscaler app: cluster-autoscaler
spec: spec:
serviceAccountName: cluster-autoscaler serviceAccountName: cluster-autoscaler
# Use host network to access external APIs (Hetzner)
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
# Workaround: Talos DNS proxy doesn't forward to upstream correctly
hostAliases:
- ip: "213.239.246.73"
hostnames:
- "api.hetzner.cloud"
containers: containers:
- name: cluster-autoscaler - name: cluster-autoscaler
image: registry.k8s.io/autoscaling/cluster-autoscaler:v1.31.0 image: registry.k8s.io/autoscaling/cluster-autoscaler:v1.31.0
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
runAsNonRoot: true
runAsUser: 65532
seccompProfile:
type: RuntimeDefault
command: command:
- ./cluster-autoscaler - ./cluster-autoscaler
- --cloud-provider=hetzner - --cloud-provider=hetzner

View File

@@ -1,139 +0,0 @@
#!/bin/bash
############################################################
# Instala o Cluster Autoscaler no cluster Talos
# Requer: cluster provisionado via setup.sh
############################################################
set -e
# Cores
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
echo ""
echo "============================================"
echo " Instalando Cluster Autoscaler"
echo "============================================"
echo ""
# Verificar pré-requisitos
if [ ! -f "kubeconfig" ]; then
log_error "kubeconfig não encontrado! Execute setup.sh primeiro."
exit 1
fi
if [ ! -f "terraform.tfvars" ]; then
log_error "terraform.tfvars não encontrado!"
exit 1
fi
export KUBECONFIG="$SCRIPT_DIR/kubeconfig"
# Verificar conexão com cluster
log_info "Verificando conexão com o cluster..."
if ! kubectl get nodes &>/dev/null; then
log_error "Não foi possível conectar ao cluster!"
exit 1
fi
log_success "Conectado ao cluster"
# Obter valores do OpenTofu
log_info "Obtendo configurações do OpenTofu..."
WORKER_CONFIG_BASE64=$(tofu output -raw autoscaler_worker_config 2>/dev/null)
TALOS_IMAGE_ID=$(tofu output -raw autoscaler_image_id 2>/dev/null)
CLUSTER_NAME=$(tofu output -raw cluster_name 2>/dev/null)
NETWORK_ID=$(tofu output -raw network_id 2>/dev/null)
FIREWALL_ID=$(tofu output -raw firewall_id 2>/dev/null)
SSH_KEY_NAME=$(tofu output -raw ssh_key_name 2>/dev/null)
# Obter token do terraform.tfvars
HCLOUD_TOKEN=$(grep 'hcloud_token' terraform.tfvars | cut -d'"' -f2)
if [ -z "$WORKER_CONFIG_BASE64" ] || [ -z "$HCLOUD_TOKEN" ]; then
log_error "Não foi possível obter as configurações necessárias!"
exit 1
fi
log_success "Configurações obtidas"
echo " - Cluster: $CLUSTER_NAME"
echo " - Image ID: $TALOS_IMAGE_ID"
echo " - Network ID: $NETWORK_ID"
echo " - SSH Key: $SSH_KEY_NAME"
echo ""
# Criar namespace com política privileged (necessário para hostNetwork)
log_info "Criando namespace cluster-autoscaler..."
kubectl create namespace cluster-autoscaler --dry-run=client -o yaml | kubectl apply -f -
kubectl label namespace cluster-autoscaler pod-security.kubernetes.io/enforce=privileged --overwrite
# Criar secret com credenciais
log_info "Criando secret com credenciais..."
kubectl create secret generic hcloud-autoscaler \
--namespace cluster-autoscaler \
--from-literal=token="$HCLOUD_TOKEN" \
--from-literal=cloud-init="$WORKER_CONFIG_BASE64" \
--dry-run=client -o yaml | kubectl apply -f -
log_success "Secret criado"
# Aplicar RBAC e Deployment
log_info "Aplicando manifesto do cluster-autoscaler..."
# Substituir variáveis no template e aplicar
cat cluster-autoscaler.yaml | \
sed "s|\${TALOS_IMAGE_ID}|$TALOS_IMAGE_ID|g" | \
sed "s|\${NETWORK_NAME}|$CLUSTER_NAME-network|g" | \
sed "s|\${FIREWALL_NAME}|$CLUSTER_NAME-firewall|g" | \
sed "s|\${SSH_KEY_NAME}|$SSH_KEY_NAME|g" | \
kubectl apply -f -
log_success "Cluster Autoscaler instalado!"
# Aguardar pod ficar pronto
log_info "Aguardando pod do autoscaler..."
kubectl wait --for=condition=ready pod \
-l app=cluster-autoscaler \
-n cluster-autoscaler \
--timeout=120s
echo ""
log_success "Cluster Autoscaler pronto!"
echo ""
echo "============================================"
echo " Configuração do Autoscaler"
echo "============================================"
echo ""
echo " Pool: worker-pool"
echo " Tipo: CAX11 (ARM64)"
echo " Região: nbg1 (Nuremberg)"
echo " Min nodes: 1"
echo " Max nodes: 5"
echo ""
echo " Scale down após: 5 minutos"
echo " Utilização mínima: 50%"
echo ""
echo "Comandos úteis:"
echo ""
echo " # Ver logs do autoscaler"
echo " kubectl logs -n cluster-autoscaler -l app=cluster-autoscaler -f"
echo ""
echo " # Ver status dos nodes"
echo " kubectl get nodes"
echo ""
echo " # Testar scale up (criar pods pending)"
echo " kubectl create deployment test --image=nginx --replicas=10"
echo ""

121
aula-08/install-metrics-server.sh Executable file
View File

@@ -0,0 +1,121 @@
#!/bin/bash
# =============================================================================
# Instala o Metrics Server para suporte a HPA e kubectl top
# =============================================================================
#
# O Metrics Server é necessário para:
# - Horizontal Pod Autoscaler (HPA) baseado em CPU/memória
# - kubectl top pods/nodes
# - Decisões de scheduling baseadas em métricas
#
# Pré-requisitos:
# - Cluster Kubernetes rodando
# - Helm 3.x instalado
#
# Uso:
# ./install-metrics-server.sh
#
# =============================================================================
set -e
# Cores para output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERRO]${NC} $1"; }
# =============================================================================
# VERIFICAÇÕES
# =============================================================================
log_info "Verificando pré-requisitos..."
if ! command -v kubectl &> /dev/null; then
log_error "kubectl não encontrado"
exit 1
fi
if ! command -v helm &> /dev/null; then
log_error "Helm não encontrado"
exit 1
fi
# Verificar conexão com o cluster
if ! kubectl cluster-info &> /dev/null; then
log_error "Não foi possível conectar ao cluster"
log_error "Verifique se KUBECONFIG está configurado corretamente"
exit 1
fi
log_success "Pré-requisitos OK"
# =============================================================================
# INSTALAR METRICS SERVER
# =============================================================================
log_info "Adicionando repositório Helm..."
helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/ 2>/dev/null || true
helm repo update
log_info "Instalando Metrics Server..."
# Nota: --kubelet-insecure-tls é necessário para Talos Linux
# pois os certificados do kubelet não são assinados pela CA do cluster
helm upgrade --install metrics-server metrics-server/metrics-server \
-n kube-system \
--set 'args[0]=--kubelet-insecure-tls' \
--wait \
--timeout 2m
log_success "Metrics Server instalado!"
# =============================================================================
# VERIFICAR INSTALAÇÃO
# =============================================================================
log_info "Aguardando Metrics Server ficar pronto..."
for i in {1..30}; do
if kubectl get --raw "/apis/metrics.k8s.io/v1beta1/nodes" &> /dev/null; then
break
fi
echo -n "."
sleep 2
done
echo ""
# Testar se as métricas estão disponíveis
if kubectl get --raw "/apis/metrics.k8s.io/v1beta1/nodes" &> /dev/null; then
log_success "Metrics API disponível!"
echo ""
log_info "Testando kubectl top nodes..."
kubectl top nodes 2>/dev/null || log_warn "Métricas ainda sendo coletadas, aguarde alguns segundos..."
else
log_warn "Metrics API ainda não disponível"
log_warn "Aguarde alguns segundos e execute: kubectl top nodes"
fi
# =============================================================================
# RESUMO
# =============================================================================
echo ""
echo "=============================================="
echo -e "${GREEN} Metrics Server Instalado!${NC}"
echo "=============================================="
echo ""
echo "Agora você pode usar:"
echo " kubectl top nodes # Ver uso de recursos dos nós"
echo " kubectl top pods # Ver uso de recursos dos pods"
echo ""
echo "HPAs agora podem escalar baseado em CPU/memória!"
echo ""
echo "=============================================="

139
aula-08/install-nginx-ingress.sh Executable file
View File

@@ -0,0 +1,139 @@
#!/bin/bash
# =============================================================================
# Instala/Atualiza o NGINX Ingress Controller com LoadBalancer Hetzner
# =============================================================================
#
# Este script configura o NGINX Ingress para:
# - Usar LoadBalancer da Hetzner (requer CCM instalado)
# - Suportar TCP passthrough para SSH do GitLab
# - Comunicação via rede privada
#
# Pré-requisitos:
# - Cluster Kubernetes rodando
# - Hetzner CCM instalado (./install-ccm.sh)
# - Helm 3.x instalado
#
# Uso:
# ./install-nginx-ingress.sh
#
# =============================================================================
set -e
# Cores para output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERRO]${NC} $1"; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# =============================================================================
# VERIFICAÇÕES
# =============================================================================
log_info "Verificando pré-requisitos..."
if ! command -v kubectl &> /dev/null; then
log_error "kubectl não encontrado"
exit 1
fi
if ! command -v helm &> /dev/null; then
log_error "Helm não encontrado"
exit 1
fi
# Verificar se CCM está instalado
if ! kubectl get deployment hccm-hcloud-cloud-controller-manager -n kube-system &> /dev/null; then
log_warn "Hetzner CCM não parece estar instalado"
log_warn "Execute ./install-ccm.sh primeiro para LoadBalancer automático"
read -p "Continuar mesmo assim? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
log_success "Pré-requisitos OK"
# =============================================================================
# INSTALAR NGINX INGRESS
# =============================================================================
log_info "Adicionando repositório Helm..."
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx 2>/dev/null || true
helm repo update
log_info "Instalando/Atualizando NGINX Ingress Controller..."
helm upgrade --install nginx-ingress ingress-nginx/ingress-nginx \
-n ingress-nginx \
--create-namespace \
-f "$SCRIPT_DIR/nginx-ingress-values.yaml" \
--wait \
--timeout 5m
log_success "NGINX Ingress instalado!"
# =============================================================================
# AGUARDAR LOADBALANCER
# =============================================================================
log_info "Aguardando LoadBalancer receber IP externo..."
echo "(pode levar 1-2 minutos)"
for i in {1..60}; do
EXTERNAL_IP=$(kubectl get svc nginx-ingress-ingress-nginx-controller \
-n ingress-nginx \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "")
if [ -n "$EXTERNAL_IP" ] && [ "$EXTERNAL_IP" != "<pending>" ]; then
break
fi
echo -n "."
sleep 2
done
echo ""
if [ -n "$EXTERNAL_IP" ] && [ "$EXTERNAL_IP" != "<pending>" ]; then
log_success "LoadBalancer IP: $EXTERNAL_IP"
else
log_warn "LoadBalancer ainda não tem IP. Verifique com:"
echo " kubectl get svc -n ingress-nginx"
fi
# =============================================================================
# RESUMO
# =============================================================================
echo ""
echo "=============================================="
echo -e "${GREEN} NGINX Ingress Configurado!${NC}"
echo "=============================================="
echo ""
echo "LoadBalancer IP: ${EXTERNAL_IP:-<pendente>}"
echo ""
echo "Portas expostas:"
echo " - 80 (HTTP)"
echo " - 443 (HTTPS)"
echo " - 22 (SSH - para GitLab)"
echo ""
echo "Próximos passos:"
echo " 1. Configure DNS apontando para o IP acima:"
echo " - n8n.kube.quest → $EXTERNAL_IP"
echo " - git.kube.quest → $EXTERNAL_IP"
echo " - registry.git.kube.quest → $EXTERNAL_IP"
echo ""
echo " 2. Instale o GitLab:"
echo " cd ../aula-09 && ./setup.sh"
echo ""
echo "=============================================="

View File

@@ -33,6 +33,11 @@ resource "random_string" "cluster_id" {
locals { locals {
cluster_name = "talos-${random_string.cluster_id.result}" cluster_name = "talos-${random_string.cluster_id.result}"
control_plane_count = var.enable_ha ? 3 : 1
# Endpoint: LoadBalancer IP if enabled, otherwise Floating IP
cluster_endpoint_ip = var.enable_loadbalancer ? hcloud_load_balancer.cluster[0].ipv4 : hcloud_floating_ip.control_plane[0].ip_address
common_labels = { common_labels = {
cluster = local.cluster_name cluster = local.cluster_name
environment = var.environment environment = var.environment
@@ -191,7 +196,7 @@ resource "hcloud_placement_group" "cluster" {
############################################################ ############################################################
resource "hcloud_server" "control_plane" { resource "hcloud_server" "control_plane" {
count = 3 count = local.control_plane_count
name = "${local.cluster_name}-cp-${count.index}" name = "${local.cluster_name}-cp-${count.index}"
server_type = "cax11" server_type = "cax11"
image = data.hcloud_image.talos.id image = data.hcloud_image.talos.id
@@ -218,14 +223,15 @@ resource "hcloud_server" "control_plane" {
} }
resource "hcloud_server_network" "control_plane" { resource "hcloud_server_network" "control_plane" {
count = 3 count = local.control_plane_count
server_id = hcloud_server.control_plane[count.index].id server_id = hcloud_server.control_plane[count.index].id
network_id = hcloud_network.cluster.id network_id = hcloud_network.cluster.id
ip = "10.0.1.${10 + count.index}" ip = "10.0.1.${10 + count.index}"
} }
# Floating IP for stable control plane access # Floating IP for stable control plane access (only if LoadBalancer is disabled)
resource "hcloud_floating_ip" "control_plane" { resource "hcloud_floating_ip" "control_plane" {
count = var.enable_loadbalancer ? 0 : 1
type = "ipv4" type = "ipv4"
name = "${local.cluster_name}-cp-ip" name = "${local.cluster_name}-cp-ip"
home_location = "nbg1" home_location = "nbg1"
@@ -233,10 +239,139 @@ resource "hcloud_floating_ip" "control_plane" {
} }
resource "hcloud_floating_ip_assignment" "control_plane" { resource "hcloud_floating_ip_assignment" "control_plane" {
floating_ip_id = hcloud_floating_ip.control_plane.id count = var.enable_loadbalancer ? 0 : 1
floating_ip_id = hcloud_floating_ip.control_plane[0].id
server_id = hcloud_server.control_plane[0].id server_id = hcloud_server.control_plane[0].id
} }
############################################################
# LOAD BALANCER (for HA access to control plane and ingress)
############################################################
resource "hcloud_load_balancer" "cluster" {
count = var.enable_loadbalancer ? 1 : 0
name = "${local.cluster_name}-lb"
load_balancer_type = "lb11"
location = "nbg1"
labels = local.common_labels
}
resource "hcloud_load_balancer_network" "cluster" {
count = var.enable_loadbalancer ? 1 : 0
load_balancer_id = hcloud_load_balancer.cluster[0].id
network_id = hcloud_network.cluster.id
ip = "10.0.1.2"
depends_on = [hcloud_network_subnet.cluster]
}
# Kubernetes API (6443) -> Control Planes
resource "hcloud_load_balancer_service" "kubernetes_api" {
count = var.enable_loadbalancer ? 1 : 0
load_balancer_id = hcloud_load_balancer.cluster[0].id
protocol = "tcp"
listen_port = 6443
destination_port = 6443
health_check {
protocol = "tcp"
port = 6443
interval = 10
timeout = 5
retries = 3
}
}
# Talos API (50000) -> Control Planes
resource "hcloud_load_balancer_service" "talos_api" {
count = var.enable_loadbalancer ? 1 : 0
load_balancer_id = hcloud_load_balancer.cluster[0].id
protocol = "tcp"
listen_port = 50000
destination_port = 50000
health_check {
protocol = "tcp"
port = 50000
interval = 10
timeout = 5
retries = 3
}
}
# HTTP (80) -> Workers (NGINX Ingress)
resource "hcloud_load_balancer_service" "http" {
count = var.enable_loadbalancer ? 1 : 0
load_balancer_id = hcloud_load_balancer.cluster[0].id
protocol = "tcp"
listen_port = 80
destination_port = 80
health_check {
protocol = "tcp"
port = 80
interval = 10
timeout = 5
retries = 3
}
}
# HTTPS (443) -> Workers (NGINX Ingress)
resource "hcloud_load_balancer_service" "https" {
count = var.enable_loadbalancer ? 1 : 0
load_balancer_id = hcloud_load_balancer.cluster[0].id
protocol = "tcp"
listen_port = 443
destination_port = 443
health_check {
protocol = "tcp"
port = 443
interval = 10
timeout = 5
retries = 3
}
}
# SSH (22) -> Workers (GitLab SSH)
resource "hcloud_load_balancer_service" "ssh" {
count = var.enable_loadbalancer ? 1 : 0
load_balancer_id = hcloud_load_balancer.cluster[0].id
protocol = "tcp"
listen_port = 22
destination_port = 22
health_check {
protocol = "tcp"
port = 22
interval = 10
timeout = 5
retries = 3
}
}
# LB Targets: Control Planes (for 6443 and 50000)
resource "hcloud_load_balancer_target" "control_plane" {
count = var.enable_loadbalancer ? local.control_plane_count : 0
type = "server"
load_balancer_id = hcloud_load_balancer.cluster[0].id
server_id = hcloud_server.control_plane[count.index].id
use_private_ip = true
depends_on = [hcloud_load_balancer_network.cluster]
}
# LB Targets: Workers (for 80, 443, and 22)
resource "hcloud_load_balancer_target" "worker" {
count = var.enable_loadbalancer ? 1 : 0
type = "server"
load_balancer_id = hcloud_load_balancer.cluster[0].id
server_id = hcloud_server.worker[count.index].id
use_private_ip = true
depends_on = [hcloud_load_balancer_network.cluster]
}
############################################################ ############################################################
# WORKER NODE (Single CAX11) # WORKER NODE (Single CAX11)
############################################################ ############################################################
@@ -288,15 +423,15 @@ resource "talos_machine_secrets" "this" {
data "talos_client_configuration" "this" { data "talos_client_configuration" "this" {
cluster_name = local.cluster_name cluster_name = local.cluster_name
client_configuration = talos_machine_secrets.this.client_configuration client_configuration = talos_machine_secrets.this.client_configuration
endpoints = [hcloud_floating_ip.control_plane.ip_address] endpoints = [local.cluster_endpoint_ip]
} }
# Control plane configuration # Control plane configuration
data "talos_machine_configuration" "control_plane" { data "talos_machine_configuration" "control_plane" {
count = 3 count = local.control_plane_count
cluster_name = local.cluster_name cluster_name = local.cluster_name
machine_type = "controlplane" machine_type = "controlplane"
cluster_endpoint = "https://${hcloud_floating_ip.control_plane.ip_address}:6443" cluster_endpoint = "https://${local.cluster_endpoint_ip}:6443"
machine_secrets = talos_machine_secrets.this.machine_secrets machine_secrets = talos_machine_secrets.this.machine_secrets
talos_version = var.talos_version talos_version = var.talos_version
@@ -304,15 +439,16 @@ data "talos_machine_configuration" "control_plane" {
templatefile("${path.module}/talos-patches/control-plane.yaml", { templatefile("${path.module}/talos-patches/control-plane.yaml", {
cluster_name = local.cluster_name cluster_name = local.cluster_name
node_name = hcloud_server.control_plane[count.index].name node_name = hcloud_server.control_plane[count.index].name
is_ha = true is_ha = var.enable_ha
is_first_cp = count.index == 0 is_first_cp = count.index == 0
etcd_peers = [for i in range(3) : "10.0.1.${10 + i}"] etcd_peers = [for i in range(local.control_plane_count) : "10.0.1.${10 + i}"]
floating_ip = hcloud_floating_ip.control_plane.ip_address floating_ip = local.cluster_endpoint_ip
}) })
] ]
depends_on = [ depends_on = [
hcloud_server.control_plane, hcloud_server.control_plane,
hcloud_load_balancer.cluster,
hcloud_floating_ip_assignment.control_plane hcloud_floating_ip_assignment.control_plane
] ]
} }
@@ -322,7 +458,7 @@ data "talos_machine_configuration" "worker" {
count = 1 count = 1
cluster_name = local.cluster_name cluster_name = local.cluster_name
machine_type = "worker" machine_type = "worker"
cluster_endpoint = "https://${hcloud_floating_ip.control_plane.ip_address}:6443" cluster_endpoint = "https://${local.cluster_endpoint_ip}:6443"
machine_secrets = talos_machine_secrets.this.machine_secrets machine_secrets = talos_machine_secrets.this.machine_secrets
talos_version = var.talos_version talos_version = var.talos_version
@@ -335,6 +471,7 @@ data "talos_machine_configuration" "worker" {
depends_on = [ depends_on = [
hcloud_server.worker, hcloud_server.worker,
hcloud_load_balancer.cluster,
hcloud_floating_ip_assignment.control_plane hcloud_floating_ip_assignment.control_plane
] ]
} }
@@ -344,7 +481,7 @@ data "talos_machine_configuration" "worker" {
############################################################ ############################################################
resource "talos_machine_configuration_apply" "control_plane" { resource "talos_machine_configuration_apply" "control_plane" {
count = 3 count = local.control_plane_count
client_configuration = talos_machine_secrets.this.client_configuration client_configuration = talos_machine_secrets.this.client_configuration
machine_configuration_input = data.talos_machine_configuration.control_plane[count.index].machine_configuration machine_configuration_input = data.talos_machine_configuration.control_plane[count.index].machine_configuration
endpoint = hcloud_server.control_plane[count.index].ipv4_address endpoint = hcloud_server.control_plane[count.index].ipv4_address
@@ -400,11 +537,11 @@ resource "talos_cluster_kubeconfig" "this" {
############################################################ ############################################################
resource "local_sensitive_file" "kubeconfig" { resource "local_sensitive_file" "kubeconfig" {
# Replace the internal hostname with the floating IP for external access # Replace the internal hostname with the LB/Floating IP for external access
content = replace( content = replace(
talos_cluster_kubeconfig.this.kubeconfig_raw, talos_cluster_kubeconfig.this.kubeconfig_raw,
"https://${local.cluster_name}.local:6443", "https://${local.cluster_name}.local:6443",
"https://${hcloud_floating_ip.control_plane.ip_address}:6443" "https://${local.cluster_endpoint_ip}:6443"
) )
filename = "${path.root}/kubeconfig" filename = "${path.root}/kubeconfig"
} }

View File

@@ -0,0 +1,88 @@
# =============================================================================
# NGINX Ingress Controller - Configuração para Hetzner Cloud
# =============================================================================
#
# Este values configura o NGINX Ingress com:
# - LoadBalancer da Hetzner (provisionado automaticamente pelo CCM)
# - Suporte a TCP para SSH do GitLab (porta 22)
# - Uso de rede privada para comunicação com os nodes
#
# Uso:
# helm upgrade --install nginx-ingress ingress-nginx/ingress-nginx \
# -n ingress-nginx --create-namespace \
# -f nginx-ingress-values.yaml
#
# =============================================================================
controller:
# Configuração do Service LoadBalancer
service:
type: LoadBalancer
# Annotations específicas para Hetzner Cloud
annotations:
# Nome do LoadBalancer no painel Hetzner
load-balancer.hetzner.cloud/name: "k8s-ingress"
# Localização do LoadBalancer (mesmo datacenter do cluster)
load-balancer.hetzner.cloud/location: "nbg1"
# Usar rede privada para comunicação com nodes
# Mais seguro e sem custo de tráfego
load-balancer.hetzner.cloud/use-private-ip: "true"
# Tipo do LoadBalancer (lb11 é o menor/mais barato)
load-balancer.hetzner.cloud/type: "lb11"
# Health check
load-balancer.hetzner.cloud/health-check-interval: "5s"
load-balancer.hetzner.cloud/health-check-timeout: "3s"
load-balancer.hetzner.cloud/health-check-retries: "3"
# ==========================================================================
# TCP Services - Para SSH do GitLab
# ==========================================================================
# Mapeia porta externa -> namespace/service:porta
# O GitLab Shell roda no namespace gitlab, service gitlab-gitlab-shell
tcp:
22: "gitlab/gitlab-gitlab-shell:22"
# Configuração do controller
config:
# Habilitar proxy protocol se necessário
# use-proxy-protocol: "true"
# Timeouts
proxy-connect-timeout: "10"
proxy-read-timeout: "120"
proxy-send-timeout: "120"
# Body size para uploads grandes (GitLab, n8n)
proxy-body-size: "0"
# Keepalive
keep-alive: "75"
keep-alive-requests: "1000"
# Recursos do controller
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
# Métricas para monitoramento
metrics:
enabled: true
serviceMonitor:
enabled: false # Habilitar se usar Prometheus Operator
# Admission webhook
admissionWebhooks:
enabled: true
# Default backend (opcional)
defaultBackend:
enabled: false

View File

@@ -26,8 +26,13 @@ output "network_cidr" {
# Control Plane Information # Control Plane Information
output "control_plane_ip" { output "control_plane_ip" {
description = "Public IP address of the control plane" description = "Public IP address of the control plane (LB or Floating IP)"
value = hcloud_floating_ip.control_plane.ip_address value = local.cluster_endpoint_ip
}
output "load_balancer_ip" {
description = "Public IP of the Load Balancer (if enabled)"
value = var.enable_loadbalancer ? hcloud_load_balancer.cluster[0].ipv4 : null
} }
output "control_plane_private_ips" { output "control_plane_private_ips" {
@@ -70,22 +75,23 @@ output "talosconfig_path" {
# API Endpoints # API Endpoints
output "kubernetes_api_endpoint" { output "kubernetes_api_endpoint" {
description = "Kubernetes API server endpoint" description = "Kubernetes API server endpoint"
value = "https://${hcloud_floating_ip.control_plane.ip_address}:6443" value = "https://${local.cluster_endpoint_ip}:6443"
} }
output "talos_api_endpoint" { output "talos_api_endpoint" {
description = "Talos API endpoint for management" description = "Talos API endpoint for management"
value = "https://${hcloud_floating_ip.control_plane.ip_address}:50000" value = "https://${local.cluster_endpoint_ip}:50000"
} }
# Cost Information # Cost Information
output "estimated_monthly_cost" { output "estimated_monthly_cost" {
description = "Estimated monthly cost for the infrastructure (EUR)" description = "Estimated monthly cost for the infrastructure (USD)"
value = { value = {
control_plane = 3 * 3.79 # 3x CAX11 control_plane = local.control_plane_count * 4.59
worker = 1 * 3.79 # 1x CAX11 worker = 1 * 4.59
floating_ip = 3.00 # Floating IPv4 load_balancer = var.enable_loadbalancer ? 5.99 : 0
total = (4 * 3.79) + 3.00 # ~€18.16 floating_ip = var.enable_loadbalancer ? 0 : 3.29
total = (local.control_plane_count + 1) * 4.59 + (var.enable_loadbalancer ? 5.99 : 3.29)
} }
} }
@@ -104,16 +110,15 @@ output "connection_instructions" {
2. Configure talosctl: 2. Configure talosctl:
export TALOSCONFIG=${local_sensitive_file.talosconfig.filename} export TALOSCONFIG=${local_sensitive_file.talosconfig.filename}
talosctl --nodes ${hcloud_floating_ip.control_plane.ip_address} health talosctl --nodes ${local.cluster_endpoint_ip} health
3. Access Kubernetes API: 3. Access Kubernetes API:
${"https://${hcloud_floating_ip.control_plane.ip_address}:6443"} https://${local.cluster_endpoint_ip}:6443
4. Nodes: 4. Nodes:
Control Plane: 3x CAX11 (ARM64) Control Plane: ${local.control_plane_count}x CAX11 (ARM64)
Workers: 1x CAX11 (ARM64) Workers: 1x CAX11 (ARM64)
${var.enable_loadbalancer ? "Load Balancer: LB11" : "Floating IP: IPv4"}
5. Total Monthly Cost: ~€18/month
==================================== ====================================
EOT EOT

View File

@@ -32,7 +32,7 @@ log_warn() {
} }
log_error() { log_error() {
echo -e "${RED}[ERROR]${NC} $1" echo -e "${RED}[ERRO]${NC} $1"
} }
############################################################ ############################################################
@@ -84,6 +84,18 @@ if ! command -v kubectl &> /dev/null; then
fi fi
log_success "kubectl $(kubectl version --client -o yaml 2>/dev/null | grep gitVersion | awk '{print $2}' || echo 'instalado')" log_success "kubectl $(kubectl version --client -o yaml 2>/dev/null | grep gitVersion | awk '{print $2}' || echo 'instalado')"
# Verificar Helm
if ! command -v helm &> /dev/null; then
log_error "Helm não encontrado!"
echo ""
echo "Instale o Helm:"
echo " brew install helm # macOS"
echo " snap install helm --classic # Linux"
echo ""
exit 1
fi
log_success "Helm $(helm version --short 2>/dev/null | head -1)"
# Verificar hcloud CLI (opcional, mas útil) # Verificar hcloud CLI (opcional, mas útil)
if command -v hcloud &> /dev/null; then if command -v hcloud &> /dev/null; then
log_success "hcloud CLI instalado" log_success "hcloud CLI instalado"
@@ -170,6 +182,52 @@ if [ "$SKIP_CREDENTIALS" != "true" ]; then
log_success "Image ID: $TALOS_IMAGE_ID" log_success "Image ID: $TALOS_IMAGE_ID"
echo "" echo ""
# Configuração do Cluster
echo "============================================"
echo " Configuração do Cluster"
echo "============================================"
echo ""
# Cluster HA?
echo "4. Modo de Alta Disponibilidade (HA)"
echo ""
echo " HA = 3 Control Planes (tolerância a falhas)"
echo " Single = 1 Control Plane (menor custo)"
echo ""
read -p " Cluster HA? (S/n): " enable_ha
if [[ "$enable_ha" =~ ^[Nn]$ ]]; then
ENABLE_HA="false"
ENABLE_LB="false"
log_info "Modo Single: 1 Control Plane"
else
ENABLE_HA="true"
log_success "Modo HA: 3 Control Planes"
echo ""
# LoadBalancer?
echo "5. LoadBalancer para o Control Plane"
echo ""
echo " Com LB: HA real (qualquer CP pode cair)"
echo " Sem LB: Floating IP (se CP-0 cair, cluster inacessível)"
echo ""
echo " O LoadBalancer também serve para:"
echo " - HTTP/HTTPS (NGINX Ingress)"
echo " - SSH (GitLab)"
echo " - Talos API"
echo ""
echo " Custo adicional: ~\$6/mes"
echo ""
read -p " Usar LoadBalancer? (S/n): " enable_lb
if [[ "$enable_lb" =~ ^[Nn]$ ]]; then
ENABLE_LB="false"
log_info "Usando Floating IP (sem HA real do CP)"
else
ENABLE_LB="true"
log_success "LoadBalancer habilitado"
fi
fi
echo ""
# Criar terraform.tfvars # Criar terraform.tfvars
log_info "Criando terraform.tfvars..." log_info "Criando terraform.tfvars..."
cat > terraform.tfvars << EOF cat > terraform.tfvars << EOF
@@ -180,8 +238,9 @@ hcloud_token = "$HCLOUD_TOKEN"
ssh_public_key = "$SSH_PUBLIC_KEY" ssh_public_key = "$SSH_PUBLIC_KEY"
talos_image_id = $TALOS_IMAGE_ID talos_image_id = $TALOS_IMAGE_ID
environment = "workshop" environment = "prod"
enable_monitoring = true enable_ha = $ENABLE_HA
enable_loadbalancer = $ENABLE_LB
EOF EOF
log_success "terraform.tfvars criado" log_success "terraform.tfvars criado"
fi fi
@@ -219,16 +278,39 @@ echo ""
log_success "Plano criado!" log_success "Plano criado!"
echo "" echo ""
# Mostrar resumo # Mostrar resumo baseado na configuração
echo "============================================" echo "============================================"
echo " Recursos a serem criados:" echo " Recursos a serem criados:"
echo "============================================" echo "============================================"
echo "" echo ""
echo " - 4x CAX11 (3 CP + 1 Worker) = 4 x €3.79 = €15.16"
echo " - 1x Floating IPv4 = €3.00" # Ler configuração do tfvars
echo " - Rede/Firewall/Placement = Grátis" ENABLE_HA_CONFIG=$(grep 'enable_ha' terraform.tfvars 2>/dev/null | grep -o 'true\|false' || echo "true")
ENABLE_LB_CONFIG=$(grep 'enable_loadbalancer' terraform.tfvars 2>/dev/null | grep -o 'true\|false' || echo "true")
if [ "$ENABLE_HA_CONFIG" = "true" ]; then
CP_COUNT=3
echo " - 3x CAX11 Control Planes = 3 x \$4.59 = \$13.77"
else
CP_COUNT=1
echo " - 1x CAX11 Control Plane = 1 x \$4.59 = \$4.59"
fi
echo " - 1x CAX11 Worker = 1 x \$4.59 = \$4.59"
if [ "$ENABLE_LB_CONFIG" = "true" ]; then
echo " - 1x Load Balancer LB11 = \$5.99"
echo " - Rede/Firewall/Placement = Gratis"
LB_COST=5.99
else
echo " - 1x Floating IPv4 = \$3.29"
echo " - Rede/Firewall/Placement = Gratis"
LB_COST=3.29
fi
TOTAL_COST=$(echo "scale=2; ($CP_COUNT + 1) * 4.59 + $LB_COST" | bc)
echo "" echo ""
echo " Custo estimado: ~€18.16/mês (sem VAT)" echo " Custo estimado: ~\$${TOTAL_COST}/mes"
echo "" echo ""
############################################################ ############################################################
@@ -312,6 +394,203 @@ fi
echo "" echo ""
############################################################
# INSTALAÇÃO DO CCM (Cloud Controller Manager)
############################################################
echo "============================================"
echo " Instalando Hetzner Cloud Controller Manager"
echo "============================================"
echo ""
# Obter token do terraform.tfvars
HCLOUD_TOKEN=$(grep 'hcloud_token' terraform.tfvars | cut -d'"' -f2)
NETWORK_ID=$(tofu output -raw network_id 2>/dev/null || echo "")
if [ -z "$HCLOUD_TOKEN" ]; then
log_error "Não foi possível obter HCLOUD_TOKEN!"
exit 1
fi
# Criar secret para o CCM
log_info "Criando secret hcloud..."
SECRET_DATA="--from-literal=token=$HCLOUD_TOKEN"
if [ -n "$NETWORK_ID" ]; then
SECRET_DATA="$SECRET_DATA --from-literal=network=$NETWORK_ID"
fi
kubectl create secret generic hcloud \
$SECRET_DATA \
-n kube-system \
--dry-run=client -o yaml | kubectl apply -f -
log_success "Secret criado"
# Instalar CCM via Helm
log_info "Instalando CCM via Helm..."
helm repo add hcloud https://charts.hetzner.cloud 2>/dev/null || true
helm repo update hcloud
HELM_ARGS="--set networking.enabled=true"
HELM_ARGS="$HELM_ARGS --set networking.clusterCIDR=10.244.0.0/16"
if [ -n "$NETWORK_ID" ]; then
HELM_ARGS="$HELM_ARGS --set networking.network.id=$NETWORK_ID"
fi
helm upgrade --install hccm hcloud/hcloud-cloud-controller-manager \
-n kube-system \
$HELM_ARGS \
--wait
log_success "CCM instalado!"
# Aguardar taint ser removido dos workers
log_info "Aguardando CCM inicializar workers..."
for i in {1..30}; do
if ! kubectl get nodes -o jsonpath='{.items[*].spec.taints[*].key}' 2>/dev/null | grep -q "node.cloudprovider.kubernetes.io/uninitialized"; then
log_success "Workers inicializados!"
break
fi
echo -n "."
sleep 5
done
echo ""
############################################################
# INSTALAÇÃO DO CLUSTER AUTOSCALER
############################################################
echo ""
echo "============================================"
echo " Instalando Cluster Autoscaler"
echo "============================================"
echo ""
# Obter configurações do OpenTofu
log_info "Obtendo configurações do OpenTofu..."
WORKER_CONFIG_BASE64=$(tofu output -raw autoscaler_worker_config 2>/dev/null)
TALOS_IMAGE_ID=$(tofu output -raw autoscaler_image_id 2>/dev/null)
CLUSTER_NAME=$(tofu output -raw cluster_name 2>/dev/null)
FIREWALL_ID=$(tofu output -raw firewall_id 2>/dev/null)
SSH_KEY_NAME=$(tofu output -raw ssh_key_name 2>/dev/null)
if [ -z "$WORKER_CONFIG_BASE64" ]; then
log_error "Não foi possível obter configuração do worker!"
exit 1
fi
log_success "Configurações obtidas"
echo " - Cluster: $CLUSTER_NAME"
echo " - Image ID: $TALOS_IMAGE_ID"
echo " - Network ID: $NETWORK_ID"
echo ""
# Criar namespace
log_info "Criando namespace cluster-autoscaler..."
kubectl create namespace cluster-autoscaler --dry-run=client -o yaml | kubectl apply -f -
kubectl label namespace cluster-autoscaler pod-security.kubernetes.io/enforce=privileged --overwrite
# Criar secret
log_info "Criando secret do autoscaler..."
kubectl create secret generic hcloud-autoscaler \
--namespace cluster-autoscaler \
--from-literal=token="$HCLOUD_TOKEN" \
--from-literal=cloud-init="$WORKER_CONFIG_BASE64" \
--dry-run=client -o yaml | kubectl apply -f -
log_success "Secret criado"
# Aplicar manifesto
log_info "Aplicando manifesto do cluster-autoscaler..."
cat cluster-autoscaler.yaml | \
sed "s|\${TALOS_IMAGE_ID}|$TALOS_IMAGE_ID|g" | \
sed "s|\${NETWORK_NAME}|$CLUSTER_NAME-network|g" | \
sed "s|\${FIREWALL_NAME}|$CLUSTER_NAME-firewall|g" | \
sed "s|\${SSH_KEY_NAME}|$SSH_KEY_NAME|g" | \
kubectl apply -f -
# Aguardar pod ficar pronto
log_info "Aguardando pod do autoscaler..."
kubectl wait --for=condition=ready pod \
-l app=cluster-autoscaler \
-n cluster-autoscaler \
--timeout=120s
log_success "Cluster Autoscaler instalado!"
echo ""
############################################################
# INSTALAÇÃO DO HETZNER CSI DRIVER
############################################################
echo "============================================"
echo " Instalando Hetzner CSI Driver"
echo "============================================"
echo ""
log_info "Instalando CSI Driver via Helm..."
helm upgrade --install hcloud-csi hcloud/hcloud-csi \
-n kube-system \
--wait \
--timeout 5m
log_success "Hetzner CSI Driver instalado!"
# Verificar StorageClass
log_info "Verificando StorageClass..."
kubectl get storageclass hcloud-volumes
echo ""
############################################################
# INSTALAÇÃO DO NGINX INGRESS CONTROLLER
############################################################
echo "============================================"
echo " Instalando NGINX Ingress Controller"
echo "============================================"
echo ""
# Detectar localização do cluster para o LoadBalancer
CLUSTER_LOCATION=$(kubectl get nodes -o jsonpath='{.items[0].metadata.labels.topology\.kubernetes\.io/zone}' 2>/dev/null | cut -d'-' -f1)
if [ -z "$CLUSTER_LOCATION" ]; then
CLUSTER_LOCATION="nbg1" # Default para Nuremberg
fi
log_info "Localização do cluster: $CLUSTER_LOCATION"
log_info "Instalando NGINX Ingress via Helm..."
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx 2>/dev/null || true
helm repo update ingress-nginx
helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--create-namespace \
--set controller.allowSnippetAnnotations=true \
--set controller.config.annotations-risk-level=Critical \
--set controller.admissionWebhooks.enabled=false \
--set "controller.service.annotations.load-balancer\.hetzner\.cloud/location=${CLUSTER_LOCATION}" \
--set "controller.service.annotations.load-balancer\.hetzner\.cloud/use-private-ip=true" \
--wait --timeout 5m
log_success "NGINX Ingress Controller instalado!"
# Aguardar LoadBalancer obter IP
log_info "Aguardando LoadBalancer obter IP externo..."
for i in {1..30}; do
LB_IP=$(kubectl get svc -n ingress-nginx ingress-nginx-controller \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null)
if [ -n "$LB_IP" ]; then
log_success "LoadBalancer IP: $LB_IP"
break
fi
echo -n "."
sleep 5
done
echo ""
############################################################ ############################################################
# RESUMO FINAL # RESUMO FINAL
############################################################ ############################################################
@@ -327,6 +606,19 @@ tofu output -raw kubernetes_api_endpoint 2>/dev/null && echo "" || true
tofu output -raw talos_api_endpoint 2>/dev/null && echo "" || true tofu output -raw talos_api_endpoint 2>/dev/null && echo "" || true
echo "" echo ""
echo "Componentes instalados:"
echo " - Hetzner Cloud Controller Manager (CCM)"
echo " - Cluster Autoscaler (1-5 workers)"
echo " - Hetzner CSI Driver (StorageClass: hcloud-volumes)"
echo " - NGINX Ingress Controller + LoadBalancer"
echo ""
# Mostrar IP do LoadBalancer
LB_IP=$(kubectl get svc -n ingress-nginx ingress-nginx-controller \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "pendente")
echo "LoadBalancer IP: $LB_IP"
echo ""
echo "Arquivos gerados:" echo "Arquivos gerados:"
echo " - kubeconfig : Configuração do kubectl" echo " - kubeconfig : Configuração do kubectl"
echo " - talosconfig : Configuração do talosctl" echo " - talosconfig : Configuração do talosctl"
@@ -338,24 +630,12 @@ echo " # Usar kubectl com este cluster"
echo " export KUBECONFIG=$SCRIPT_DIR/kubeconfig" echo " export KUBECONFIG=$SCRIPT_DIR/kubeconfig"
echo " kubectl get nodes" echo " kubectl get nodes"
echo "" echo ""
echo " # Usar talosctl com este cluster" echo " # Ver logs do autoscaler"
echo " export TALOSCONFIG=$SCRIPT_DIR/talosconfig" echo " kubectl logs -n cluster-autoscaler -l app=cluster-autoscaler -f"
echo " talosctl -n <IP> health"
echo ""
echo " # Ver outputs do OpenTofu"
echo " tofu output"
echo "" echo ""
echo " # Destruir infraestrutura (CUIDADO!)" echo " # Destruir infraestrutura (CUIDADO!)"
echo " ./cleanup.sh" echo " ./cleanup.sh"
echo "" echo ""
log_success "Setup concluído!" log_success "Setup concluído!"
echo ""
echo "============================================"
echo " Próximo passo (opcional)"
echo "============================================"
echo ""
echo " Para habilitar autoscaling de 1-5 workers:"
echo " ./install-autoscaler.sh"
echo "" echo ""

View File

@@ -26,6 +26,7 @@ machine:
# Kubelet configuration # Kubelet configuration
kubelet: kubelet:
extraArgs: extraArgs:
cloud-provider: external
max-pods: "110" max-pods: "110"
kube-reserved: "cpu=200m,memory=300Mi" kube-reserved: "cpu=200m,memory=300Mi"
system-reserved: "cpu=200m,memory=200Mi" system-reserved: "cpu=200m,memory=200Mi"

View File

@@ -16,6 +16,7 @@ machine:
# Kubelet configuration # Kubelet configuration
kubelet: kubelet:
extraArgs: extraArgs:
cloud-provider: external
max-pods: "110" max-pods: "110"
kube-reserved: "cpu=100m,memory=200Mi" kube-reserved: "cpu=100m,memory=200Mi"
system-reserved: "cpu=100m,memory=100Mi" system-reserved: "cpu=100m,memory=100Mi"

View File

@@ -2,28 +2,56 @@
# Variables for Hetzner Talos Kubernetes Cluster # Variables for Hetzner Talos Kubernetes Cluster
############################################################ ############################################################
# Authentication # ==========================================================
# AUTENTICAÇÃO
# ==========================================================
variable "hcloud_token" { variable "hcloud_token" {
type = string type = string
description = "Hetzner Cloud API token" description = "Hetzner Cloud API token"
sensitive = true sensitive = true
} }
# Cluster Configuration # ==========================================================
# CONFIGURAÇÃO DO CLUSTER
# ==========================================================
variable "enable_ha" {
type = bool
description = "Enable HA mode with 3 control plane nodes"
default = true
}
variable "enable_loadbalancer" {
type = bool
description = "Enable Hetzner Load Balancer for HA access to control plane and ingress"
default = true
}
variable "environment" { variable "environment" {
type = string type = string
description = "Environment name (prod, staging, dev)" description = "Environment name (prod, staging, dev)"
default = "prod" default = "prod"
validation {
condition = contains(["prod", "staging", "dev"], var.environment)
error_message = "Environment deve ser: prod, staging ou dev."
}
} }
# ==========================================================
# SSH
# ==========================================================
# SSH Configuration
variable "ssh_public_key" { variable "ssh_public_key" {
type = string type = string
description = "Public SSH key for emergency access to nodes" description = "Public SSH key for emergency access to nodes"
} }
# Talos Configuration # ==========================================================
# TALOS
# ==========================================================
variable "talos_image_id" { variable "talos_image_id" {
type = number type = number
description = "ID da imagem Talos customizada na Hetzner (criada na aula-07). Obtenha com: hcloud image list --type snapshot" description = "ID da imagem Talos customizada na Hetzner (criada na aula-07). Obtenha com: hcloud image list --type snapshot"
@@ -32,30 +60,18 @@ variable "talos_image_id" {
variable "talos_version" { variable "talos_version" {
type = string type = string
description = "Talos version to use" description = "Talos version to use"
default = "v1.11.2" # Match the official image version default = "v1.11.2"
validation {
condition = can(regex("^v[0-9]+\\.[0-9]+\\.[0-9]+$", var.talos_version))
error_message = "talos_version deve seguir o formato semântico: v1.2.3"
}
} }
# Monitoring Configuration # ==========================================================
variable "enable_monitoring" { # LABELS CUSTOMIZADAS
type = bool # ==========================================================
description = "Enable Victoria Metrics monitoring stack"
default = true
}
# Auto-scaling Configuration
variable "scale_up_threshold" {
type = number
description = "CPU percentage to trigger scale up"
default = 70
}
variable "scale_down_threshold" {
type = number
description = "CPU percentage to trigger scale down"
default = 30
}
# Tags for resource management
variable "custom_labels" { variable "custom_labels" {
type = map(string) type = map(string)
description = "Custom labels to add to all resources" description = "Custom labels to add to all resources"

233
aula-09/README.md Normal file
View File

@@ -0,0 +1,233 @@
# Aula 09 - n8n via Helm (Cluster Hetzner Cloud)
Deploy do n8n workflow automation em cluster Kubernetes na Hetzner Cloud com volumes persistentes, LoadBalancer e TLS configurável.
## Arquitetura
```
Internet
┌─────────────────┐
│ LoadBalancer │
│ (Hetzner LB) │
└────────┬────────┘
┌────────────────────────┐
│ NGINX Ingress │
│ n8n.{domain}:443 │
└───────────┬────────────┘
┌─────────────────────────┼─────────────────────────┐
│ Namespace: n8n │
│ │ │
│ ┌────────────────────┼────────────────────┐ │
│ │ ▼ │ │
│ │ ┌──────────┐ │ │
│ │ │ Main │ │ │
│ │ │ (n8n) │ │ │
│ │ └────┬─────┘ │ │
│ │ │ │ │
│ │ ┌─────────────┼─────────────┐ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌───────┐ ┌──────────┐ ┌────────┐ │ │
│ │ │Workers│ │ Webhooks │ │ MCP │ │ │
│ │ │ (2-5) │ │ (1-3) │ │Webhook │ │ │
│ │ └───────┘ └──────────┘ └────────┘ │ │
│ │ │ │
│ │ Queue Mode │ │
│ └────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │PostgreSQL│ │ Redis │ │
│ │ (10Gi) │ │ (10Gi) │ │
│ │ hcloud │ │ hcloud │ │
│ └──────────┘ └──────────┘ │
└───────────────────────────────────────────────────┘
Hetzner Cloud
```
## Pré-requisitos
1. **Cluster Talos na Hetzner** (aula-08) com:
- Hetzner CSI Driver (StorageClass: hcloud-volumes)
- NGINX Ingress Controller com LoadBalancer
2. **kubectl** instalado
3. **Helm** 3.x instalado
## Contexto do Cluster
Esta aula usa o cluster Kubernetes provisionado na **aula-08**. O `kubeconfig` foi gerado automaticamente pelo OpenTofu e está em `aula-08/kubeconfig`.
```bash
# Verificar se o cluster está acessível
export KUBECONFIG=$(pwd)/../aula-08/kubeconfig
kubectl cluster-info
# Esperado:
# Kubernetes control plane is running at https://<floating-ip>:6443
```
## Instalação
```bash
cd aula-09
# Configurar acesso ao cluster (kubeconfig da aula-08)
export KUBECONFIG=$(pwd)/../aula-08/kubeconfig
# Executar setup interativo
chmod +x setup.sh
./setup.sh
```
O script é interativo e pergunta:
1. **Hostname do n8n** (ex: `n8n.kube.quest`) - FQDN completo
2. **Usa CloudFlare?** (com proxy/CDN)
3. **Ativar Let's Encrypt?** (se não usar CloudFlare)
### Opções de TLS
| Cenário | TLS Provider | Configuração |
|---------|--------------|--------------|
| **CloudFlare (proxy)** | CloudFlare Edge | Sem cert-manager, TLS na borda |
| **Outro DNS + Let's Encrypt** | cert-manager | HTTP-01 challenge automático |
| **Outro DNS + HTTP** | Nenhum | Apenas HTTP (dev/workshop) |
### Exemplo de Instalação
```
═══════════════════════════════════════════════════
n8n no Kubernetes (Hetzner Cloud)
═══════════════════════════════════════════════════
Digite o hostname do n8n (ex: n8n.kube.quest): n8n.kube.quest
Você usa CloudFlare para DNS?
1) Sim (com proxy/CDN ativado - ícone laranja)
2) Não
Escolha [1/2]: 1
[INFO] CloudFlare irá gerenciar TLS automaticamente na edge
[OK] Configuração salva em .env
...instalação...
═══════════════════════════════════════════════════
Configure o DNS
═══════════════════════════════════════════════════
No painel do CloudFlare (https://dash.cloudflare.com):
Tipo: A
Nome: n8n
Conteúdo: 123.45.67.89
Proxy: ✓ (ícone laranja)
O CloudFlare cuida do TLS automaticamente!
Acesse: https://n8n.kube.quest
```
## Componentes Instalados
**Da aula-08 (infraestrutura):**
- Hetzner CSI Driver (StorageClass: hcloud-volumes)
- NGINX Ingress Controller com LoadBalancer
**Instalados por este script:**
- **cert-manager** (se Let's Encrypt ativado)
- **n8n** com todos os componentes
| Componente | Réplicas | Recursos | Volume |
|------------|----------|----------|--------|
| Main (n8n) | 1 | 256Mi-1Gi RAM | 10Gi |
| Workers | 2-5 (HPA) | 256Mi-512Mi RAM | - |
| Webhooks | 1-3 (HPA) | 128Mi-256Mi RAM | - |
| PostgreSQL | 1 | - | 10Gi |
| Redis | 1 | - | 10Gi |
**Total volumes:** 30Gi (~$1.45/mês)
## Multi-Tenant: Adicionar Clientes
Para provisionar n8n para múltiplos clientes em namespaces separados:
```bash
# Adicionar novo cliente
./add-client.sh acme
# Isso cria:
# - Namespace: acme-n8n
# - n8n em: https://acme-n8n.{domain}
# - Grupo GitLab: /acme/ (se GitLab instalado - aula-10)
```
O script `add-client.sh` herda a configuração do `.env` gerado pelo `setup.sh`.
### Padrão de Domínio
Os clientes usam o padrão `{cliente}-n8n.{domain}`:
- `acme-n8n.kube.quest`
- `empresa-n8n.kube.quest`
Se você configurou um wildcard DNS (`*.kube.quest`), os novos clientes funcionam automaticamente.
## Arquivo de Configuração
O `setup.sh` gera um arquivo `.env` com as configurações:
```bash
# aula-09/.env (gerado automaticamente)
N8N_HOST=n8n.kube.quest
DOMAIN=kube.quest
USE_CLOUDFLARE=true
USE_LETSENCRYPT=false
LETSENCRYPT_EMAIL=
```
Este arquivo é usado pelo `add-client.sh` e em re-execuções do `setup.sh`.
## Comandos Úteis
```bash
# Ver pods
kubectl get pods -n n8n
# Ver logs do n8n
kubectl logs -f -n n8n deployment/n8n
# Ver autoscaling
kubectl get hpa -n n8n
# Ver volumes
kubectl get pvc -n n8n
# Ver certificado (se Let's Encrypt)
kubectl get certificate -n n8n
# Desinstalar (apenas n8n)
./cleanup.sh
# Ou manualmente:
helm uninstall n8n -n n8n
kubectl delete pvc --all -n n8n
kubectl delete ns n8n
```
**Nota:** O `cleanup.sh` remove apenas n8n, clientes e cert-manager. A infraestrutura (CSI Driver, NGINX Ingress, LoadBalancer) é mantida pois pertence à aula-08.
## Custos Estimados
| Recurso | Custo/mês |
|---------|-----------|
| Volumes (3x 10Gi) | ~$1.45 |
| Worker (compartilhado) | ~$0.80 |
| **Total por cliente** | ~$2.25 |
## Para Ambiente Local
Se você quer testar em um cluster local (Docker Desktop, minikube), veja a **aula-06**.

299
aula-09/add-client.sh Executable file
View File

@@ -0,0 +1,299 @@
#!/bin/bash
# =============================================================================
# Script para Adicionar Novo Cliente
# =============================================================================
#
# Este script provisiona um novo cliente com:
# - n8n em namespace separado (cliente-n8n)
# - Grupo no GitLab compartilhado (opcional)
#
# Uso:
# ./add-client.sh <nome-cliente>
#
# Exemplo:
# ./add-client.sh acme
# -> Cria namespace acme-n8n com n8n
# -> Cria grupo /acme no GitLab (se disponível)
#
# Pré-requisitos:
# - ./setup.sh já executado (gera .env com configurações)
# - kubectl configurado
# - Helm instalado
#
# =============================================================================
set -e
# Cores para output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERRO]${NC} $1"; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CLIENT=$1
# =============================================================================
# CARREGAR CONFIGURAÇÃO
# =============================================================================
if [[ ! -f "$SCRIPT_DIR/.env" ]]; then
log_error "Arquivo .env não encontrado!"
log_error "Execute ./setup.sh primeiro para configurar o ambiente."
exit 1
fi
source "$SCRIPT_DIR/.env"
# Suporte ao novo formato (N8N_HOST) e legado (DOMAIN)
if [[ -n "$N8N_HOST" && -z "$DOMAIN" ]]; then
# Extrair DOMAIN do N8N_HOST (ex: n8n.kube.quest → kube.quest)
DOMAIN=$(echo "$N8N_HOST" | sed 's/^[^.]*\.//')
fi
if [[ -z "$DOMAIN" ]]; then
log_error "Variável DOMAIN ou N8N_HOST não definida no .env"
log_error "Execute ./setup.sh novamente."
exit 1
fi
# Carregar configuração do GitLab se disponível
GITLAB_HOST=""
if [[ -f "$SCRIPT_DIR/../aula-10/.env" ]]; then
source "$SCRIPT_DIR/../aula-10/.env"
fi
# =============================================================================
# VALIDAÇÕES
# =============================================================================
if [ -z "$CLIENT" ]; then
echo "Uso: $0 <nome-cliente>"
echo ""
echo "Exemplo:"
echo " $0 acme"
echo ""
echo "Configuração atual (de .env):"
echo " Domínio: ${DOMAIN}"
echo " CloudFlare: ${USE_CLOUDFLARE}"
echo " Let's Encrypt: ${USE_LETSENCRYPT}"
echo ""
echo "Isso vai criar:"
echo " - Namespace: acme-n8n"
PROTOCOL="https"
[[ "$USE_CLOUDFLARE" == "false" && "$USE_LETSENCRYPT" == "false" ]] && PROTOCOL="http"
echo " - n8n em: ${PROTOCOL}://acme-n8n.${DOMAIN}"
if [[ -n "$GITLAB_HOST" ]]; then
echo " - GitLab grupo: https://${GITLAB_HOST}/acme/ (se configurado)"
fi
exit 1
fi
# Validar nome do cliente (só letras minúsculas, números e hífens)
if ! [[ "$CLIENT" =~ ^[a-z0-9-]+$ ]]; then
log_error "Nome do cliente inválido: $CLIENT"
log_error "Use apenas letras minúsculas, números e hífens"
exit 1
fi
log_info "Verificando pré-requisitos..."
if ! command -v kubectl &> /dev/null; then
log_error "kubectl não encontrado"
exit 1
fi
if ! command -v helm &> /dev/null; then
log_error "Helm não encontrado"
exit 1
fi
if ! kubectl cluster-info &> /dev/null; then
log_error "Não foi possível conectar ao cluster"
exit 1
fi
log_success "Pré-requisitos OK"
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
echo -e "${CYAN} Adicionando Cliente: $CLIENT${NC}"
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
echo ""
# =============================================================================
# 1. CRIAR NAMESPACE PARA N8N
# =============================================================================
log_info "Criando namespace ${CLIENT}-n8n..."
kubectl create namespace ${CLIENT}-n8n --dry-run=client -o yaml | kubectl apply -f -
log_success "Namespace criado"
# =============================================================================
# 2. INSTALAR N8N
# =============================================================================
log_info "Instalando n8n para ${CLIENT}..."
# Verificar se base-n8n-values.yaml existe
if [ ! -f "$SCRIPT_DIR/base-n8n-values.yaml" ]; then
log_error "Arquivo base-n8n-values.yaml não encontrado em $SCRIPT_DIR"
exit 1
fi
# Adicionar repo se necessário
helm repo add community-charts https://community-charts.github.io/helm-charts 2>/dev/null || true
helm repo update
# Construir argumentos do Helm dinamicamente
HELM_ARGS=""
# Configurar host do ingress
HELM_ARGS="$HELM_ARGS --set ingress.enabled=true"
HELM_ARGS="$HELM_ARGS --set ingress.className=nginx"
HELM_ARGS="$HELM_ARGS --set ingress.hosts[0].host=${CLIENT}-n8n.${DOMAIN}"
HELM_ARGS="$HELM_ARGS --set ingress.hosts[0].paths[0].path=/"
HELM_ARGS="$HELM_ARGS --set ingress.hosts[0].paths[0].pathType=Prefix"
# Configurar webhook URL
if [[ "$USE_CLOUDFLARE" == "true" || "$USE_LETSENCRYPT" == "true" ]]; then
HELM_ARGS="$HELM_ARGS --set webhook.url=https://${CLIENT}-n8n.${DOMAIN}"
else
HELM_ARGS="$HELM_ARGS --set webhook.url=http://${CLIENT}-n8n.${DOMAIN}"
fi
# Configurar TLS (se Let's Encrypt)
if [[ "$USE_LETSENCRYPT" == "true" ]]; then
HELM_ARGS="$HELM_ARGS --set ingress.annotations.cert-manager\\.io/cluster-issuer=letsencrypt"
HELM_ARGS="$HELM_ARGS --set ingress.tls[0].hosts[0]=${CLIENT}-n8n.${DOMAIN}"
HELM_ARGS="$HELM_ARGS --set ingress.tls[0].secretName=${CLIENT}-n8n-tls"
fi
# Configurar N8N_SECURE_COOKIE (se HTTPS)
if [[ "$USE_CLOUDFLARE" == "true" || "$USE_LETSENCRYPT" == "true" ]]; then
HELM_ARGS="$HELM_ARGS --set main.extraEnvVars.N8N_SECURE_COOKIE=true"
HELM_ARGS="$HELM_ARGS --set worker.extraEnvVars.N8N_SECURE_COOKIE=true"
HELM_ARGS="$HELM_ARGS --set webhook.extraEnvVars.N8N_SECURE_COOKIE=true"
fi
# Instalar n8n
eval helm upgrade --install ${CLIENT}-n8n community-charts/n8n \
--namespace ${CLIENT}-n8n \
-f "$SCRIPT_DIR/base-n8n-values.yaml" \
$HELM_ARGS \
--wait \
--timeout 5m
log_success "n8n instalado"
# =============================================================================
# 3. CRIAR GRUPO NO GITLAB (opcional)
# =============================================================================
log_info "Verificando GitLab..."
# Obter token do GitLab
if [ -z "$GITLAB_TOKEN" ]; then
GITLAB_TOKEN=$(kubectl get secret gitlab-gitlab-initial-root-password \
-n gitlab \
-o jsonpath='{.data.password}' 2>/dev/null | base64 -d || echo "")
fi
if [ -z "$GITLAB_TOKEN" ]; then
log_warn "GitLab não encontrado ou não configurado"
log_warn "Pule esta etapa ou crie o grupo manualmente"
else
log_info "Criando grupo no GitLab..."
# Usar GITLAB_HOST se disponível (da aula-10/.env)
if [[ -n "$GITLAB_HOST" ]]; then
GITLAB_URL="https://${GITLAB_HOST}"
else
GITLAB_URL="https://git.${DOMAIN}"
fi
# Criar grupo via API
RESULT=$(curl -s --request POST "${GITLAB_URL}/api/v4/groups" \
--header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
--form "name=${CLIENT}" \
--form "path=${CLIENT}" \
--form "visibility=private" \
--write-out "%{http_code}" \
--output /tmp/gitlab-group-result.json 2>/dev/null || echo "000")
if [ "$RESULT" == "201" ]; then
log_success "Grupo criado no GitLab"
elif [ "$RESULT" == "400" ]; then
log_warn "Grupo já existe ou erro de validação"
else
log_warn "Não foi possível criar grupo (HTTP $RESULT)"
log_warn "Crie manualmente em ${GITLAB_URL}/admin/groups/new"
fi
fi
# =============================================================================
# RESUMO
# =============================================================================
# Obter IP do LoadBalancer
LB_IP=$(kubectl get svc ingress-nginx-controller \
-n ingress-nginx \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "<pendente>")
# Determinar protocolo
PROTOCOL="https"
[[ "$USE_CLOUDFLARE" == "false" && "$USE_LETSENCRYPT" == "false" ]] && PROTOCOL="http"
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
echo -e "${GREEN} Cliente $CLIENT Provisionado!${NC}"
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
echo ""
echo "Serviços:"
echo -e " n8n: ${GREEN}${PROTOCOL}://${CLIENT}-n8n.${DOMAIN}${NC}"
if [ -n "$GITLAB_TOKEN" ]; then
if [[ -n "$GITLAB_HOST" ]]; then
echo -e " GitLab: ${GREEN}https://${GITLAB_HOST}/${CLIENT}/${NC}"
else
echo -e " GitLab: ${GREEN}https://git.${DOMAIN}/${CLIENT}/${NC}"
fi
fi
echo ""
if [[ "$USE_CLOUDFLARE" == "true" ]]; then
echo "Configure no CloudFlare:"
echo -e " ${YELLOW}Tipo:${NC} A"
echo -e " ${YELLOW}Nome:${NC} ${CLIENT}-n8n"
echo -e " ${YELLOW}Conteúdo:${NC} ${GREEN}${LB_IP}${NC}"
echo -e " ${YELLOW}Proxy:${NC} ✓ (ícone laranja)"
echo ""
echo "(Ou use o wildcard *.${DOMAIN} se já configurado)"
else
echo "Configure o DNS:"
echo -e " ${YELLOW}Tipo:${NC} A"
echo -e " ${YELLOW}Nome:${NC} ${CLIENT}-n8n"
echo -e " ${YELLOW}Valor:${NC} ${GREEN}${LB_IP}${NC}"
echo ""
echo "(Ou use o wildcard *.${DOMAIN} se já configurado)"
fi
if [ -n "$GITLAB_TOKEN" ]; then
echo ""
echo "Git clone:"
if [[ -n "$GITLAB_HOST" ]]; then
echo " git clone git@${GITLAB_HOST}:${CLIENT}/meu-projeto.git"
else
echo " git clone git@git.${DOMAIN}:${CLIENT}/meu-projeto.git"
fi
fi
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"

View File

@@ -0,0 +1,140 @@
# =============================================================================
# Base Values para n8n de Clientes
# =============================================================================
#
# Este arquivo contém configurações base para instalações de n8n por cliente.
# O script add-client.sh usa este arquivo como template.
#
# Personalizações por cliente são feitas via --set no helm install.
#
# =============================================================================
# Imagem
image:
repository: n8nio/n8n
tag: "2.0.3"
pullPolicy: IfNotPresent
# Chave de criptografia (será sobrescrita por cliente se necessário)
# IMPORTANTE: Em produção, use uma chave única por cliente
encryptionKey: "workshop-n8n-encryption-key-32ch"
# =============================================================================
# BANCO DE DADOS
# =============================================================================
db:
type: postgresdb
postgresql:
enabled: true
auth:
database: n8n
username: n8n
# Em produção, use secrets únicos por cliente
password: "n8n-postgres-workshop-2025"
primary:
persistence:
enabled: true
size: 10Gi # Mínimo Hetzner ($0.0484/GB)
storageClass: hcloud-volumes
# =============================================================================
# REDIS
# =============================================================================
redis:
enabled: true
architecture: standalone
auth:
enabled: true
password: "n8n-redis-workshop-2025"
master:
persistence:
enabled: true
size: 10Gi # Mínimo Hetzner ($0.0484/GB)
storageClass: hcloud-volumes
# =============================================================================
# INGRESS
# =============================================================================
# Host é configurado dinamicamente pelo add-client.sh
ingress:
enabled: true
className: nginx
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
# =============================================================================
# MAIN NODE
# =============================================================================
# N8N_SECURE_COOKIE é configurado dinamicamente pelo add-client.sh via --set
main:
persistence:
enabled: true
size: 10Gi # Mínimo Hetzner ($0.0484/GB)
storageClass: hcloud-volumes
mountPath: "/home/node/.n8n"
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
# =============================================================================
# WORKERS (Queue Mode)
# =============================================================================
worker:
mode: queue
count: 1
concurrency: 10
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
autoscaling:
enabled: true
minReplicas: 1
maxReplicas: 3
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
# =============================================================================
# WEBHOOKS
# =============================================================================
webhook:
mode: queue
count: 1
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 250m
memory: 256Mi
autoscaling:
enabled: false # Manter simples para clientes menores
# =============================================================================
# SEGURANÇA
# =============================================================================
securityContext:
runAsNonRoot: true
runAsUser: 1000

185
aula-09/cleanup.sh Executable file
View File

@@ -0,0 +1,185 @@
#!/bin/bash
# =============================================================================
# Cleanup da Aula 09 - Remove n8n e clientes
# =============================================================================
#
# Este script remove:
# - n8n principal (namespace n8n)
# - Todos os clientes (namespaces *-n8n)
# - cert-manager (se instalado por esta aula)
# - Arquivo .env
#
# NÃO remove (gerenciados pela aula-08):
# - NGINX Ingress Controller
# - Hetzner CSI Driver
# - LoadBalancer
#
# =============================================================================
set -e
# Cores para output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERRO]${NC} $1"; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo ""
echo -e "${CYAN}============================================${NC}"
echo -e "${CYAN} Cleanup - Aula 09 (n8n)${NC}"
echo -e "${CYAN}============================================${NC}"
echo ""
log_warn "ATENÇÃO: Isso vai remover os recursos da aula 09:"
echo " - n8n principal e todos os clientes"
echo " - Volumes persistentes (PostgreSQL, Redis, n8n)"
echo " - cert-manager (se instalado)"
echo " - Arquivo .env"
echo ""
echo "Mantido (aula-08):"
echo " - NGINX Ingress Controller"
echo " - Hetzner CSI Driver"
echo " - LoadBalancer"
echo ""
read -p "Continuar? (digite 'sim' para confirmar): " confirm
if [ "$confirm" != "sim" ]; then
log_info "Operação cancelada"
exit 0
fi
echo ""
# =============================================================================
# 1. REMOVER n8n PRINCIPAL
# =============================================================================
log_info "=== Removendo n8n principal ==="
if helm status n8n -n n8n &> /dev/null; then
log_info "Removendo Helm release n8n..."
helm uninstall n8n -n n8n --wait 2>/dev/null || true
log_success "n8n removido"
else
log_info "n8n não está instalado"
fi
# Remover PVCs
if kubectl get pvc -n n8n &> /dev/null 2>&1; then
log_info "Removendo PVCs do namespace n8n..."
kubectl delete pvc --all -n n8n --wait=false 2>/dev/null || true
fi
# Remover namespace
if kubectl get namespace n8n &> /dev/null; then
log_info "Removendo namespace n8n..."
kubectl delete namespace n8n --wait=false 2>/dev/null || true
log_success "Namespace n8n removido"
fi
# =============================================================================
# 2. REMOVER CLIENTES (namespaces *-n8n)
# =============================================================================
log_info "=== Removendo clientes ==="
CLIENT_NAMESPACES=$(kubectl get namespaces -o name 2>/dev/null | grep -E ".*-n8n$" | sed 's/namespace\///' || true)
if [ -n "$CLIENT_NAMESPACES" ]; then
for ns in $CLIENT_NAMESPACES; do
log_info "Removendo cliente: $ns"
# Remover helm release
RELEASE_NAME=$(echo "$ns" | sed 's/-n8n$//')
helm uninstall "${RELEASE_NAME}-n8n" -n "$ns" --wait 2>/dev/null || true
# Remover PVCs
kubectl delete pvc --all -n "$ns" --wait=false 2>/dev/null || true
# Remover namespace
kubectl delete namespace "$ns" --wait=false 2>/dev/null || true
log_success "Cliente $ns removido"
done
else
log_info "Nenhum cliente encontrado"
fi
# =============================================================================
# 3. REMOVER CERT-MANAGER (se instalado)
# =============================================================================
log_info "=== Verificando cert-manager ==="
if helm status cert-manager -n cert-manager &> /dev/null; then
log_info "Removendo cert-manager..."
# Remover ClusterIssuer primeiro
kubectl delete clusterissuer --all 2>/dev/null || true
# Remover helm release
helm uninstall cert-manager -n cert-manager --wait 2>/dev/null || true
# Remover CRDs
kubectl delete crd \
certificates.cert-manager.io \
certificaterequests.cert-manager.io \
challenges.acme.cert-manager.io \
clusterissuers.cert-manager.io \
issuers.cert-manager.io \
orders.acme.cert-manager.io \
2>/dev/null || true
# Remover namespace
kubectl delete namespace cert-manager --wait=false 2>/dev/null || true
log_success "cert-manager removido"
else
log_info "cert-manager não está instalado"
fi
# =============================================================================
# 4. REMOVER ARQUIVO .env
# =============================================================================
log_info "=== Removendo arquivo de configuração ==="
if [ -f "$SCRIPT_DIR/.env" ]; then
rm -f "$SCRIPT_DIR/.env"
log_success "Arquivo .env removido"
else
log_info "Arquivo .env não existe"
fi
# =============================================================================
# RESUMO
# =============================================================================
echo ""
echo -e "${CYAN}============================================${NC}"
echo -e "${GREEN} Cleanup Concluído!${NC}"
echo -e "${CYAN}============================================${NC}"
echo ""
echo "Removido:"
echo " - n8n principal e clientes"
echo " - cert-manager (se existia)"
echo " - Arquivo .env"
echo ""
echo "Mantido (aula-08):"
echo " - NGINX Ingress Controller"
echo " - Hetzner CSI Driver"
echo " - LoadBalancer"
echo " - Cluster Kubernetes"
echo ""
log_warn "Volumes Hetzner podem demorar alguns minutos para serem"
log_warn "completamente removidos. Verifique no painel da Hetzner."
echo ""

148
aula-09/custom-values.yaml Normal file
View File

@@ -0,0 +1,148 @@
# =============================================================================
# n8n Helm Chart - Custom Values
# =============================================================================
# Aula 06 - Deploy n8n via Helm
#
# Chart: community-charts/n8n
# Docs: https://community-charts.github.io/docs/charts/n8n/configuration
# =============================================================================
# -----------------------------------------------------------------------------
# Imagem
# -----------------------------------------------------------------------------
image:
repository: n8nio/n8n
tag: "2.0.3"
pullPolicy: IfNotPresent
# -----------------------------------------------------------------------------
# Chave de Criptografia
# -----------------------------------------------------------------------------
encryptionKey: "workshop-n8n-encryption-key-32ch"
# -----------------------------------------------------------------------------
# Banco de Dados PostgreSQL
# -----------------------------------------------------------------------------
db:
type: postgresdb
postgresql:
enabled: true
auth:
database: n8n
username: n8n
password: "n8n-postgres-workshop-2025"
primary:
persistence:
enabled: true
size: 10Gi # Mínimo Hetzner ($0.0484/GB)
# -----------------------------------------------------------------------------
# Redis (necessário para Queue Mode)
# -----------------------------------------------------------------------------
redis:
enabled: true
architecture: standalone
auth:
enabled: true
password: "n8n-redis-workshop-2025"
master:
persistence:
enabled: true
size: 10Gi # Mínimo Hetzner ($0.0484/GB)
# -----------------------------------------------------------------------------
# Ingress NGINX
# -----------------------------------------------------------------------------
# Host e TLS são configurados dinamicamente pelo setup.sh via --set
ingress:
enabled: true
className: nginx
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
# -----------------------------------------------------------------------------
# Main Node
# -----------------------------------------------------------------------------
# N8N_SECURE_COOKIE é configurado dinamicamente pelo setup.sh via --set
main:
persistence:
enabled: true
size: 10Gi # Mínimo Hetzner ($0.0484/GB)
mountPath: "/home/node/.n8n"
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 1000m
memory: 1Gi
# -----------------------------------------------------------------------------
# Workers (Queue Mode)
# -----------------------------------------------------------------------------
worker:
mode: queue
count: 2
concurrency: 10
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 5
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
# -----------------------------------------------------------------------------
# Webhooks (Queue Mode)
# -----------------------------------------------------------------------------
# URL é configurada dinamicamente pelo setup.sh via --set
webhook:
mode: queue
count: 1
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 250m
memory: 256Mi
autoscaling:
enabled: true
minReplicas: 1
maxReplicas: 3
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
# -----------------------------------------------------------------------------
# Configurações de Segurança
# -----------------------------------------------------------------------------
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault

526
aula-09/setup.sh Executable file
View File

@@ -0,0 +1,526 @@
#!/bin/bash
# =============================================================================
# Setup da Aula 09 - n8n via Helm (Hetzner Cloud)
# =============================================================================
#
# Este script instala e configura:
# 1. cert-manager (opcional, para Let's Encrypt)
# 2. n8n com PostgreSQL, Redis, Workers e Webhooks
#
# Pré-requisitos:
# - Kubernetes cluster Talos na Hetzner (aula-08) com:
# - Hetzner CSI Driver (StorageClass: hcloud-volumes)
# - NGINX Ingress Controller com LoadBalancer
# - kubectl configurado (KUBECONFIG=../aula-08/kubeconfig)
# - Helm 3.x instalado
#
# Uso:
# export KUBECONFIG=$(pwd)/../aula-08/kubeconfig
# chmod +x setup.sh
# ./setup.sh
#
# =============================================================================
set -e # Para na primeira falha
# Cores para output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Funções de log
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERRO]${NC} $1"; }
# Diretório do script
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Variáveis de configuração (serão preenchidas pelo usuário ou .env)
N8N_HOST=""
DOMAIN="" # Extraído automaticamente do N8N_HOST
USE_CLOUDFLARE=""
USE_LETSENCRYPT=""
LETSENCRYPT_EMAIL=""
# =============================================================================
# FUNÇÕES DE CONFIGURAÇÃO
# =============================================================================
save_config() {
cat > "$SCRIPT_DIR/.env" <<EOF
# Configuração gerada pelo setup.sh
# $(date)
N8N_HOST=${N8N_HOST}
DOMAIN=${DOMAIN}
USE_CLOUDFLARE=${USE_CLOUDFLARE}
USE_LETSENCRYPT=${USE_LETSENCRYPT}
LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL}
EOF
log_success "Configuração salva em .env"
}
load_config() {
if [[ -f "$SCRIPT_DIR/.env" ]]; then
source "$SCRIPT_DIR/.env"
# Compatibilidade: se não tem N8N_HOST, deriva do DOMAIN
if [[ -z "$N8N_HOST" && -n "$DOMAIN" ]]; then
N8N_HOST="n8n.${DOMAIN}"
fi
return 0
fi
return 1
}
# Extrai domínio base de um FQDN (ex: n8n.kube.quest → kube.quest)
extract_domain() {
local fqdn="$1"
echo "$fqdn" | sed 's/^[^.]*\.//'
}
collect_user_input() {
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
echo -e "${CYAN} n8n no Kubernetes (Hetzner Cloud)${NC}"
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
echo ""
# Verificar se já existe configuração
if load_config; then
echo -e "Configuração existente encontrada:"
echo -e " Hostname: ${GREEN}${N8N_HOST}${NC}"
echo -e " CloudFlare: ${GREEN}${USE_CLOUDFLARE}${NC}"
echo -e " Let's Encrypt: ${GREEN}${USE_LETSENCRYPT}${NC}"
echo ""
echo -e "Deseja usar esta configuração?"
echo -e " 1) Sim, continuar com a configuração existente"
echo -e " 2) Não, reconfigurar"
echo -n "Escolha [1/2]: "
read -r choice
if [[ "$choice" == "1" ]]; then
return 0
fi
fi
# Coletar hostname completo
echo ""
echo -n "Digite o hostname do n8n (ex: n8n.kube.quest): "
read -r N8N_HOST
if [[ -z "$N8N_HOST" ]]; then
log_error "Hostname não pode ser vazio"
exit 1
fi
# Extrair domínio base automaticamente
DOMAIN=$(extract_domain "$N8N_HOST")
# Perguntar sobre CloudFlare
echo ""
echo "Você usa CloudFlare para DNS?"
echo " 1) Sim (com proxy/CDN ativado - ícone laranja)"
echo " 2) Não"
echo -n "Escolha [1/2]: "
read -r choice
if [[ "$choice" == "1" ]]; then
USE_CLOUDFLARE=true
USE_LETSENCRYPT=false
log_info "CloudFlare irá gerenciar TLS automaticamente na edge"
else
USE_CLOUDFLARE=false
# Perguntar sobre Let's Encrypt
echo ""
echo "Deseja ativar HTTPS com Let's Encrypt?"
echo " 1) Sim (recomendado para produção)"
echo " 2) Não (apenas HTTP)"
echo -n "Escolha [1/2]: "
read -r choice
if [[ "$choice" == "1" ]]; then
USE_LETSENCRYPT=true
echo ""
echo -n "Digite seu email para Let's Encrypt: "
read -r LETSENCRYPT_EMAIL
if [[ -z "$LETSENCRYPT_EMAIL" ]]; then
log_error "Email é obrigatório para Let's Encrypt"
exit 1
fi
else
USE_LETSENCRYPT=false
fi
fi
# Salvar configuração
save_config
}
# =============================================================================
# FUNÇÕES DE INSTALAÇÃO
# =============================================================================
install_cert_manager() {
log_info "Instalando cert-manager..."
helm repo add jetstack https://charts.jetstack.io 2>/dev/null || true
helm repo update jetstack
if helm status cert-manager -n cert-manager &> /dev/null; then
log_success "cert-manager já está instalado"
else
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--version v1.16.2 \
--set crds.enabled=true \
--wait \
--timeout 5m
log_success "cert-manager instalado"
fi
}
create_cluster_issuer() {
log_info "Criando ClusterIssuer para Let's Encrypt..."
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ${LETSENCRYPT_EMAIL}
privateKeySecretRef:
name: letsencrypt-key
solvers:
- http01:
ingress:
class: nginx
EOF
log_success "ClusterIssuer 'letsencrypt' criado"
}
show_dns_instructions() {
# Obter IP do LoadBalancer
LB_IP=$(kubectl get svc -n ingress-nginx ingress-nginx-controller \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "<pendente>")
# Extrair nome do host (parte antes do primeiro ponto)
HOST_NAME=$(echo "$N8N_HOST" | cut -d. -f1)
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
echo -e "${CYAN} Configure o DNS${NC}"
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
if [[ "$USE_CLOUDFLARE" == "true" ]]; then
echo ""
echo "No painel do CloudFlare (https://dash.cloudflare.com):"
echo ""
echo -e " ${YELLOW}Tipo:${NC} A"
echo -e " ${YELLOW}Nome:${NC} ${HOST_NAME}"
echo -e " ${YELLOW}Conteúdo:${NC} ${GREEN}${LB_IP}${NC}"
echo -e " ${YELLOW}Proxy:${NC} ✓ (ícone laranja)"
echo ""
echo -e " ${YELLOW}Tipo:${NC} A"
echo -e " ${YELLOW}Nome:${NC} * (wildcard, para multi-tenant)"
echo -e " ${YELLOW}Conteúdo:${NC} ${GREEN}${LB_IP}${NC}"
echo -e " ${YELLOW}Proxy:${NC} ✓ (ícone laranja)"
echo ""
echo -e "${GREEN}O CloudFlare cuida do TLS automaticamente!${NC}"
else
echo ""
echo "No seu provedor DNS:"
echo ""
echo -e " ${YELLOW}Tipo:${NC} A"
echo -e " ${YELLOW}Nome:${NC} ${HOST_NAME}"
echo -e " ${YELLOW}Valor:${NC} ${GREEN}${LB_IP}${NC}"
echo ""
echo -e " ${YELLOW}Tipo:${NC} A"
echo -e " ${YELLOW}Nome:${NC} * (wildcard, para multi-tenant)"
echo -e " ${YELLOW}Valor:${NC} ${GREEN}${LB_IP}${NC}"
if [[ "$USE_LETSENCRYPT" == "true" ]]; then
echo ""
echo -e "${GREEN}Let's Encrypt irá emitir certificados automaticamente!${NC}"
echo -e "Aguarde ~2 minutos após configurar o DNS."
fi
fi
# Determinar protocolo
PROTOCOL="https"
if [[ "$USE_CLOUDFLARE" == "false" && "$USE_LETSENCRYPT" == "false" ]]; then
PROTOCOL="http"
fi
echo ""
echo -e "Acesse: ${GREEN}${PROTOCOL}://${N8N_HOST}${NC}"
}
# =============================================================================
# VERIFICAÇÕES INICIAIS
# =============================================================================
log_info "Verificando pré-requisitos..."
# Verificar kubectl
if ! command -v kubectl &> /dev/null; then
log_error "kubectl não encontrado. Instale o kubectl primeiro."
exit 1
fi
log_success "kubectl encontrado"
# Verificar conexão com cluster
if ! kubectl cluster-info &> /dev/null; then
log_error "Não foi possível conectar ao cluster Kubernetes."
exit 1
fi
log_success "Conectado ao cluster Kubernetes"
# Verificar helm
if ! command -v helm &> /dev/null; then
log_error "Helm não encontrado. Instale o Helm >= 3.14 primeiro."
log_info "Instalação: https://helm.sh/docs/intro/install/"
exit 1
fi
log_success "Helm $(helm version --short) encontrado"
# =============================================================================
# COLETAR INPUT DO USUÁRIO
# =============================================================================
collect_user_input
echo ""
# =============================================================================
# 1. VERIFICAR PRÉ-REQUISITOS DA AULA-08
# =============================================================================
log_info "=== Verificando infraestrutura (aula-08) ==="
# Verificar Hetzner CSI Driver
if ! kubectl get storageclass hcloud-volumes &> /dev/null; then
log_error "StorageClass hcloud-volumes não encontrado!"
log_error "Execute primeiro o setup.sh da aula-08 para instalar o CSI Driver."
exit 1
fi
log_success "Hetzner CSI Driver instalado (hcloud-volumes)"
# Verificar NGINX Ingress
if ! kubectl get ingressclass nginx &> /dev/null; then
log_error "NGINX Ingress não encontrado!"
log_error "Execute primeiro o setup.sh da aula-08 para instalar o Ingress Controller."
exit 1
fi
log_success "NGINX Ingress Controller instalado"
echo ""
# =============================================================================
# 2. INSTALAR CERT-MANAGER (se Let's Encrypt)
# =============================================================================
if [[ "$USE_LETSENCRYPT" == "true" ]]; then
log_info "=== Configurando cert-manager ==="
install_cert_manager
create_cluster_issuer
echo ""
fi
# =============================================================================
# 3. CRIAR NAMESPACE E APLICAR SECRETS
# =============================================================================
log_info "=== Configurando namespace n8n ==="
# Criar namespace se não existir
if kubectl get namespace n8n &> /dev/null; then
log_success "Namespace n8n já existe"
else
log_info "Criando namespace n8n..."
kubectl create namespace n8n
log_success "Namespace n8n criado"
fi
echo ""
# =============================================================================
# 4. INSTALAR n8n VIA HELM
# =============================================================================
log_info "=== Instalando n8n ==="
# Adicionar repo do community-charts
log_info "Adicionando repositório Helm do community-charts..."
helm repo add community-charts https://community-charts.github.io/helm-charts 2>/dev/null || true
helm repo update
# Construir argumentos do Helm dinamicamente
HELM_ARGS=""
# Configurar host do ingress
HELM_ARGS="$HELM_ARGS --set ingress.hosts[0].host=${N8N_HOST}"
HELM_ARGS="$HELM_ARGS --set ingress.hosts[0].paths[0].path=/"
HELM_ARGS="$HELM_ARGS --set ingress.hosts[0].paths[0].pathType=Prefix"
# Configurar webhook URL
if [[ "$USE_CLOUDFLARE" == "true" || "$USE_LETSENCRYPT" == "true" ]]; then
HELM_ARGS="$HELM_ARGS --set webhook.url=https://${N8N_HOST}"
else
HELM_ARGS="$HELM_ARGS --set webhook.url=http://${N8N_HOST}"
fi
# Configurar TLS
if [[ "$USE_LETSENCRYPT" == "true" ]]; then
HELM_ARGS="$HELM_ARGS --set ingress.annotations.cert-manager\\.io/cluster-issuer=letsencrypt"
HELM_ARGS="$HELM_ARGS --set ingress.tls[0].hosts[0]=${N8N_HOST}"
HELM_ARGS="$HELM_ARGS --set ingress.tls[0].secretName=n8n-tls"
fi
# Configurar N8N_SECURE_COOKIE
if [[ "$USE_CLOUDFLARE" == "true" || "$USE_LETSENCRYPT" == "true" ]]; then
HELM_ARGS="$HELM_ARGS --set main.extraEnvVars.N8N_SECURE_COOKIE=true"
HELM_ARGS="$HELM_ARGS --set worker.extraEnvVars.N8N_SECURE_COOKIE=true"
HELM_ARGS="$HELM_ARGS --set webhook.extraEnvVars.N8N_SECURE_COOKIE=true"
fi
# Verificar se já está instalado
if helm status n8n -n n8n &> /dev/null; then
log_warn "n8n já está instalado. Atualizando..."
eval helm upgrade n8n community-charts/n8n \
--namespace n8n \
--values "$SCRIPT_DIR/custom-values.yaml" \
$HELM_ARGS \
--wait \
--timeout 10m
log_success "n8n atualizado com sucesso!"
else
log_info "Instalando n8n..."
eval helm install n8n community-charts/n8n \
--namespace n8n \
--values "$SCRIPT_DIR/custom-values.yaml" \
$HELM_ARGS \
--wait \
--timeout 10m
log_success "n8n instalado com sucesso!"
fi
echo ""
# =============================================================================
# 5. AGUARDAR PODS FICAREM PRONTOS
# =============================================================================
log_info "=== Aguardando pods ficarem prontos ==="
log_info "Aguardando PostgreSQL..."
kubectl wait --for=condition=ready pod \
-l app.kubernetes.io/name=postgresql \
-n n8n \
--timeout=180s 2>/dev/null || log_warn "Timeout aguardando PostgreSQL"
log_info "Aguardando Redis..."
kubectl wait --for=condition=ready pod \
-l app.kubernetes.io/name=redis \
-n n8n \
--timeout=120s 2>/dev/null || log_warn "Timeout aguardando Redis"
log_info "Aguardando n8n main..."
kubectl wait --for=condition=ready pod \
-l app.kubernetes.io/component=main \
-n n8n \
--timeout=180s 2>/dev/null || log_warn "Timeout aguardando n8n main"
log_info "Aguardando n8n workers..."
kubectl wait --for=condition=ready pod \
-l app.kubernetes.io/component=worker \
-n n8n \
--timeout=120s 2>/dev/null || log_warn "Timeout aguardando workers"
log_info "Aguardando n8n webhooks..."
kubectl wait --for=condition=ready pod \
-l app.kubernetes.io/component=webhook \
-n n8n \
--timeout=120s 2>/dev/null || log_warn "Timeout aguardando webhooks"
log_success "Todos os componentes estão rodando!"
echo ""
# =============================================================================
# RESUMO FINAL
# =============================================================================
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
echo -e "${GREEN} Setup Completo!${NC}"
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
echo ""
echo "Componentes instalados:"
if [[ "$USE_LETSENCRYPT" == "true" ]]; then
echo " - cert-manager (ClusterIssuer: letsencrypt)"
fi
echo " - n8n (namespace: n8n)"
echo " - Main node"
echo " - Workers (2-5 réplicas, autoscaling)"
echo " - Webhooks (1-3 réplicas, autoscaling)"
echo " - PostgreSQL"
echo " - Redis"
echo ""
echo "Configuração:"
echo " Hostname: ${N8N_HOST}"
echo " CloudFlare: ${USE_CLOUDFLARE}"
echo " Let's Encrypt: ${USE_LETSENCRYPT}"
# Mostrar instruções de DNS
show_dns_instructions
echo ""
echo "Comandos úteis:"
echo " # Ver todos os pods"
echo " kubectl get pods -n n8n"
echo ""
echo " # Ver logs do n8n"
echo " kubectl logs -f -n n8n deployment/n8n"
echo ""
echo " # Ver HPA (autoscaler)"
echo " kubectl get hpa -n n8n"
echo ""
echo " # Ver ingress"
echo " kubectl get ingress -n n8n"
echo ""
if [[ "$USE_LETSENCRYPT" == "true" ]]; then
echo " # Ver status do certificado"
echo " kubectl get certificate -n n8n"
echo ""
fi
echo " # Adicionar novo cliente"
echo " ./add-client.sh <nome-cliente>"
echo ""
echo " # Desinstalar"
echo " helm uninstall n8n -n n8n"
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
echo ""
# Mostrar status atual
log_info "Status atual dos pods:"
echo ""
kubectl get pods -n n8n
echo ""
log_info "Status do HPA:"
echo ""
kubectl get hpa -n n8n 2>/dev/null || echo "HPA ainda não criado"
echo ""
log_info "Status do Ingress:"
echo ""
kubectl get ingress -n n8n
echo ""

339
aula-10/README.md Normal file
View File

@@ -0,0 +1,339 @@
# Aula 10 - GitLab via Helm (Cluster Hetzner Cloud)
Deploy do GitLab em Kubernetes usando Helm chart oficial, com Container Registry, SSH e TLS configurável.
## Arquitetura
```
Hetzner LoadBalancer
┌───────────────────┼───────────────────┐
│ │ │
:80/:443 :22 │
│ │ │
▼ ▼ │
┌───────────────┐ ┌─────────────┐ │
│ NGINX Ingress │ │ TCP Service │ │
└───────┬───────┘ └──────┬──────┘ │
│ │ │
▼ ▼ │
┌─────────────────────────────────────────┐ │
│ GitLab (namespace: gitlab) │ │
│ ┌──────────┐ ┌────────┐ ┌─────────┐ │ │
│ │Webservice│ │Sidekiq │ │ Shell │◄─┼─────┘
│ │ (Rails) │ │ (Jobs) │ │ (SSH) │ │
│ └────┬─────┘ └───┬────┘ └─────────┘ │
│ │ │ │
│ ┌────▼────────────▼────┐ │
│ │ Gitaly │ │
│ │ (Git Storage) │ │
│ └──────────────────────┘ │
│ │
│ ┌──────────┐ ┌───────┐ ┌──────────┐ │
│ │PostgreSQL│ │ Redis │ │ MinIO │ │
│ │ (10Gi) │ │(10Gi) │ │ (10Gi) │ │
│ └──────────┘ └───────┘ └──────────┘ │
└─────────────────────────────────────────┘
```
## Pré-requisitos
1. **Cluster Talos na Hetzner** (aula-08) com:
- Hetzner CSI Driver (StorageClass: hcloud-volumes)
- NGINX Ingress Controller com LoadBalancer
2. **kubectl** instalado
3. **Helm** 3.x instalado
## Contexto do Cluster
Esta aula usa o cluster Kubernetes provisionado na **aula-08**. O `kubeconfig` foi gerado automaticamente pelo OpenTofu.
```bash
# Verificar se o cluster está acessível
export KUBECONFIG=$(pwd)/../aula-08/kubeconfig
kubectl cluster-info
```
## Instalação
```bash
cd aula-10
# Configurar acesso ao cluster (kubeconfig da aula-08)
export KUBECONFIG=$(pwd)/../aula-08/kubeconfig
# Executar setup interativo
chmod +x setup.sh
./setup.sh
```
O script é interativo e pergunta:
1. **Hostname do GitLab** (ex: `git.kube.quest`) - FQDN completo
2. **Usa CloudFlare?** (com proxy/CDN) - pode herdar da aula-09
3. **Ativar Let's Encrypt?** (se não usar CloudFlare)
### Opções de TLS
| Cenário | TLS Provider | Configuração |
|---------|--------------|--------------|
| **CloudFlare (proxy)** | CloudFlare Edge | Sem cert-manager, TLS na borda |
| **Outro DNS + Let's Encrypt** | cert-manager | HTTP-01 challenge automático |
| **Outro DNS + HTTP** | Nenhum | Apenas HTTP (dev/workshop) |
### CloudFlare e Trusted Proxies (Erro 422)
Quando você usa CloudFlare com proxy ativo (ícone laranja), o tráfego passa pelos servidores do CloudFlare antes de chegar ao GitLab:
```
Usuário → CloudFlare (TLS) → LoadBalancer → NGINX → GitLab
Adiciona headers:
X-Forwarded-For: IP-do-usuario
X-Forwarded-Proto: https
```
**Problema:** Por padrão, o GitLab (Workhorse) não confia nesses headers. Resultado:
- GitLab pensa que a requisição veio via HTTP (não HTTPS)
- CSRF token é gerado para HTTPS mas validado como HTTP
- **Erro 422: "The change you requested was rejected"**
**Solução:** O `setup.sh` configura automaticamente os IPs do CloudFlare como proxies confiáveis:
```yaml
gitlab.webservice.workhorse.trustedCIDRsForXForwardedFor:
- 173.245.48.0/20 # CloudFlare
- 103.21.244.0/22
- 103.22.200.0/22
# ... (todos os CIDRs do CloudFlare)
```
Com isso, o Workhorse confia nos headers `X-Forwarded-*` vindos do CloudFlare e o login funciona corretamente.
> **Lição:** Proxies reversos (CloudFlare, NGINX, HAProxy) adicionam headers para preservar informações do cliente original. O backend precisa ser configurado para confiar nesses headers, caso contrário terá problemas de protocolo e autenticação.
## Componentes Instalados
**Da aula-08 (infraestrutura):**
- Hetzner CSI Driver (StorageClass: hcloud-volumes)
- NGINX Ingress Controller com LoadBalancer
**Instalados por este script:**
- **cert-manager** (se Let's Encrypt ativado)
- **GitLab** com todos os componentes
| Componente | Memory Request | Memory Limit | Volume |
|------------|----------------|--------------|--------|
| Webservice | 2Gi | 2.5Gi | - |
| Sidekiq | 1.5Gi | 2Gi | - |
| Gitaly | 512Mi | 1Gi | 10Gi |
| PostgreSQL | 512Mi | 1Gi | 10Gi |
| Redis | 256Mi | 512Mi | 10Gi |
| MinIO | 128Mi | 256Mi | 10Gi |
| Shell | 64Mi | 128Mi | - |
| Toolbox | 256Mi | 512Mi | - |
| **Total** | ~5Gi | ~8Gi | 40Gi |
## Pod Anti-Affinity (Distribuicao Inteligente)
O GitLab configura **antiAffinity** para garantir que webservice e sidekiq rodem em nos diferentes:
```yaml
gitlab:
webservice:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: sidekiq
topologyKey: kubernetes.io/hostname
```
### Por que isso importa?
Sem antiAffinity, webservice (~2.5Gi) + sidekiq (~2Gi) = 4.5Gi no mesmo no CAX11 (4GB):
- **Resultado**: OOMKilled constantes, servico instavel
Com antiAffinity:
- Kubernetes detecta que nao pode agendar no mesmo no
- Pod fica **Pending**
- Cluster Autoscaler cria um novo no automaticamente
- Pods distribuidos = servico estavel
### Licao do Workshop
> "Kubernetes nao e so rodar containers - e **orquestracao inteligente**.
> Quando configuramos antiAffinity, o scheduler entendeu que precisava de mais
> recursos e o autoscaler provisionou automaticamente."
## Padrão de Domínios
| Serviço | Domínio |
|---------|---------|
| Web UI | {GITLAB_HOST} (ex: git.kube.quest) |
| Registry | registry.{DOMAIN} (ex: registry.kube.quest) |
| SSH | {GITLAB_HOST}:22 |
Exemplo com hostname `git.kube.quest`:
- https://git.kube.quest
- https://registry.kube.quest
- git@git.kube.quest
## Arquivo de Configuração
O `setup.sh` gera um arquivo `.env` (herda configuração de TLS da aula-09):
```bash
# aula-10/.env (gerado automaticamente)
GITLAB_HOST=git.kube.quest
REGISTRY_HOST=registry.kube.quest
DOMAIN=kube.quest
USE_CLOUDFLARE=true
USE_LETSENCRYPT=false
LETSENCRYPT_EMAIL=
```
## Acesso
### Web UI
```
URL: https://git.{domain}
Usuário: root
Senha: (ver abaixo)
```
### Obter senha do root
```bash
kubectl get secret gitlab-gitlab-initial-root-password \
-n gitlab \
-o jsonpath='{.data.password}' | base64 -d; echo
```
### SSH
```bash
# Clonar repositório via SSH
git clone git@git.kube.quest:grupo/projeto.git
# Configurar chave SSH no GitLab
# Settings → SSH Keys → Adicionar sua chave pública
```
### Container Registry
```bash
# Login no registry
docker login registry.kube.quest
# Push de imagem
docker tag minha-app:v1 registry.kube.quest/grupo/projeto:v1
docker push registry.kube.quest/grupo/projeto:v1
```
## Comandos Úteis
```bash
# Ver todos os pods
kubectl get pods -n gitlab
# Ver logs do webservice
kubectl logs -n gitlab -l app=webservice -f
# Ver logs do sidekiq
kubectl logs -n gitlab -l app=sidekiq -f
# Reiniciar componente
kubectl rollout restart deployment -n gitlab gitlab-webservice-default
# Rails console
kubectl exec -it -n gitlab \
$(kubectl get pod -n gitlab -l app=toolbox -o name | head -1) \
-- gitlab-rails console
# Backup
kubectl exec -it -n gitlab \
$(kubectl get pod -n gitlab -l app=toolbox -o name | head -1) \
-- backup-utility
# Ver certificados (se Let's Encrypt)
kubectl get certificate -n gitlab
# Desinstalar (apenas GitLab)
./cleanup.sh
```
**Nota:** O `cleanup.sh` remove apenas GitLab, clientes e cert-manager. A infraestrutura (CSI Driver, NGINX Ingress, LoadBalancer) é mantida pois pertence à aula-08.
## Configurações Adicionais
### Adicionar GitLab Runner
```bash
# Obter registration token
kubectl get secret gitlab-gitlab-runner-secret \
-n gitlab \
-o jsonpath='{.data.runner-registration-token}' | base64 -d; echo
# Instalar runner
helm install gitlab-runner gitlab/gitlab-runner \
--namespace gitlab \
--set gitlabUrl=https://git.kube.quest \
--set runnerRegistrationToken=<token>
```
## Troubleshooting
### Pods não iniciam
```bash
# Verificar eventos
kubectl get events -n gitlab --sort-by='.lastTimestamp'
# Verificar PVCs
kubectl get pvc -n gitlab
# Verificar se CSI driver está funcionando
kubectl get pods -n kube-system -l app=hcloud-csi
```
### SSH não conecta
```bash
# Verificar se porta 22 está no ConfigMap
kubectl get configmap tcp-services -n ingress-nginx -o yaml
# Verificar gitlab-shell pod
kubectl get pods -n gitlab -l app=gitlab-shell
```
### Registry não funciona
```bash
# Verificar pod do registry
kubectl get pods -n gitlab -l app=registry
kubectl logs -n gitlab -l app=registry
# Testar internamente
kubectl run test --rm -it --image=busybox -- wget -qO- http://gitlab-registry:5000/v2/
```
## Custos
| Recurso | Custo/mês |
|---------|-----------|
| Workers (2x CAX11 - antiAffinity) | ~$9.18 |
| Volumes (4x 10Gi = 40Gi) | ~$1.94 |
| LoadBalancer (compartilhado) | ~$1.80 (1/3) |
| **Total GitLab** | ~$12.92 |
**Nota:** O antiAffinity requer 2 workers minimos (webservice e sidekiq em nos separados).
O Cluster Autoscaler provisiona automaticamente quando necessario.
## Referências
- [GitLab Helm Chart](https://docs.gitlab.com/charts/)
- [GitLab Helm Chart Values](https://gitlab.com/gitlab-org/charts/gitlab/-/blob/master/values.yaml)
- [External Ingress](https://docs.gitlab.com/charts/advanced/external-ingress/)
- [NGINX TCP Services](https://kubernetes.github.io/ingress-nginx/user-guide/exposing-tcp-udp-services/)

184
aula-10/cleanup.sh Executable file
View File

@@ -0,0 +1,184 @@
#!/bin/bash
# =============================================================================
# Cleanup da Aula 10 - Remove GitLab
# =============================================================================
#
# Este script remove:
# - GitLab (Helm release)
# - PVCs (dados persistentes)
# - Secrets
# - Namespace gitlab
# - cert-manager (se instalado por esta aula)
# - Arquivo .env
#
# NÃO remove (gerenciados pela aula-08):
# - NGINX Ingress Controller
# - Hetzner CSI Driver
# - LoadBalancer
#
# ATENÇÃO: Todos os dados do GitLab serão perdidos!
#
# =============================================================================
set -e
# Cores para output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERRO]${NC} $1"; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo ""
echo -e "${CYAN}============================================${NC}"
echo -e "${CYAN} Cleanup - Aula 10 (GitLab)${NC}"
echo -e "${CYAN}============================================${NC}"
echo ""
log_warn "ATENÇÃO: Isso vai remover os recursos da aula 10:"
echo " - GitLab e todos os componentes"
echo " - Volumes persistentes (PostgreSQL, Redis, MinIO, Gitaly)"
echo " - cert-manager (se instalado)"
echo " - Arquivo .env"
echo ""
echo "Mantido (aula-08):"
echo " - NGINX Ingress Controller"
echo " - Hetzner CSI Driver"
echo " - LoadBalancer"
echo ""
read -p "Continuar? (digite 'sim' para confirmar): " confirm
if [ "$confirm" != "sim" ]; then
log_info "Operação cancelada"
exit 0
fi
echo ""
# =============================================================================
# 1. REMOVER TCP SERVICES DO NGINX
# =============================================================================
log_info "=== Removendo configuração TCP do NGINX ==="
if kubectl get configmap tcp-services -n ingress-nginx &> /dev/null; then
kubectl patch configmap tcp-services \
-n ingress-nginx \
--type json \
-p '[{"op": "remove", "path": "/data/22"}]' 2>/dev/null || true
log_success "Configuração TCP removida"
else
log_info "ConfigMap tcp-services não encontrado"
fi
# =============================================================================
# 2. DESINSTALAR GITLAB
# =============================================================================
log_info "=== Removendo GitLab ==="
if helm status gitlab -n gitlab &> /dev/null; then
log_info "Desinstalando GitLab via Helm..."
helm uninstall gitlab -n gitlab --wait 2>/dev/null || true
log_success "GitLab removido"
else
log_info "GitLab não está instalado"
fi
# Remover PVCs
if kubectl get pvc -n gitlab &> /dev/null 2>&1; then
log_info "Removendo PVCs do namespace gitlab..."
kubectl delete pvc --all -n gitlab --wait=false 2>/dev/null || true
fi
# Aguardar PVs serem liberados
sleep 5
# Remover secrets restantes
log_info "Removendo secrets..."
kubectl delete secret --all -n gitlab 2>/dev/null || true
# Remover namespace
if kubectl get namespace gitlab &> /dev/null; then
log_info "Removendo namespace gitlab..."
kubectl delete namespace gitlab --wait=false 2>/dev/null || true
log_success "Namespace gitlab removido"
fi
# =============================================================================
# 3. REMOVER CERT-MANAGER (se instalado)
# =============================================================================
log_info "=== Verificando cert-manager ==="
if helm status cert-manager -n cert-manager &> /dev/null; then
log_info "Removendo cert-manager..."
# Remover ClusterIssuer primeiro
kubectl delete clusterissuer --all 2>/dev/null || true
# Remover helm release
helm uninstall cert-manager -n cert-manager --wait 2>/dev/null || true
# Remover CRDs
kubectl delete crd \
certificates.cert-manager.io \
certificaterequests.cert-manager.io \
challenges.acme.cert-manager.io \
clusterissuers.cert-manager.io \
issuers.cert-manager.io \
orders.acme.cert-manager.io \
2>/dev/null || true
# Remover namespace
kubectl delete namespace cert-manager --wait=false 2>/dev/null || true
log_success "cert-manager removido"
else
log_info "cert-manager não está instalado"
fi
# =============================================================================
# 4. REMOVER ARQUIVO .env
# =============================================================================
log_info "=== Removendo arquivo de configuração ==="
if [ -f "$SCRIPT_DIR/.env" ]; then
rm -f "$SCRIPT_DIR/.env"
log_success "Arquivo .env removido"
else
log_info "Arquivo .env não existe"
fi
# =============================================================================
# RESUMO
# =============================================================================
echo ""
echo -e "${CYAN}============================================${NC}"
echo -e "${GREEN} Cleanup Concluído!${NC}"
echo -e "${CYAN}============================================${NC}"
echo ""
echo "Removido:"
echo " - GitLab e todos os componentes"
echo " - cert-manager (se existia)"
echo " - Arquivo .env"
echo ""
echo "Mantido (aula-08):"
echo " - NGINX Ingress Controller"
echo " - Hetzner CSI Driver"
echo " - LoadBalancer"
echo " - Cluster Kubernetes"
echo ""
log_warn "Volumes Hetzner podem demorar alguns minutos para serem"
log_warn "completamente removidos. Verifique no painel da Hetzner."
echo ""

298
aula-10/gitlab-values.yaml Normal file
View File

@@ -0,0 +1,298 @@
# =============================================================================
# GitLab Helm Chart - Configuração Base para Hetzner CAX11
# =============================================================================
#
# Esta configuração:
# - Usa NGINX Ingress Controller externo (instalado na aula-08)
# - Define ~5GB de recursos distribuídos em 2 workers CAX11 (antiAffinity)
# - Desabilita componentes não essenciais para economizar recursos
# - Configura Registry para container images
#
# Valores dinâmicos (configurados via --set no setup.sh):
# - global.hosts.domain
# - global.hosts.gitlab.name
# - global.hosts.registry.name
# - global.hosts.minio.name
# - global.hosts.https
# - global.ingress.tls.enabled
# - global.ingress.configureCertmanager
#
# Cenário CloudFlare (proxy ativo):
# - global.ingress.tls.enabled=false
# - global.hosts.https=true
# - gitlab.webservice.workhorse.trustedCIDRsForXForwardedFor=[CloudFlare IPs]
#
# Cenário Let's Encrypt:
# - global.ingress.tls.enabled=true
# - global.hosts.https=true
# - global.ingress.configureCertmanager=true
# - global.ingress.annotations.cert-manager.io/cluster-issuer=letsencrypt-prod
#
# =============================================================================
global:
# Usar Ingress Controller externo
ingress:
class: nginx
# configureCertmanager é definido via --set no setup.sh (true para Let's Encrypt)
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/proxy-read-timeout: "900"
nginx.ingress.kubernetes.io/proxy-connect-timeout: "900"
nginx.ingress.kubernetes.io/proxy-buffering: "off"
# cert-manager.io/cluster-issuer é adicionado via --set quando Let's Encrypt
# Timezone
time_zone: America/Sao_Paulo
# Pod Security - evitar warnings de PodSecurity
pod:
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
# Email (opcional - configurar depois)
# email:
# from: gitlab@kube.quest
# display_name: GitLab
# reply_to: noreply@kube.quest
# =============================================================================
# NGINX BUNDLED - DESABILITAR (usamos o externo)
# =============================================================================
nginx-ingress:
enabled: false
# =============================================================================
# CERT-MANAGER - DESABILITAR BUNDLED
# =============================================================================
# O cert-manager é instalado separadamente se Let's Encrypt for escolhido.
# global.ingress.configureCertmanager controla a integração.
# =============================================================================
# GITLAB COMPONENTS
# =============================================================================
gitlab:
# Webservice (Rails app - UI e API)
# NOTA: antiAffinity garante que webservice e sidekiq rodem em nós diferentes
# Isso evita OOM quando ambos competem por memória no mesmo nó CAX11 (4GB)
webservice:
minReplicas: 1
maxReplicas: 1
resources:
requests:
memory: 2Gi
cpu: 200m
limits:
memory: 2.5Gi
cpu: 1
workerProcesses: 1
puma:
threads:
min: 1
max: 2
# Anti-affinity: não rodar no mesmo nó que sidekiq
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: sidekiq
topologyKey: kubernetes.io/hostname
# Sidekiq (background jobs)
# Anti-affinity: não rodar no mesmo nó que webservice
sidekiq:
minReplicas: 1
maxReplicas: 1
resources:
requests:
memory: 1.5Gi
cpu: 100m
limits:
memory: 2Gi
cpu: 500m
# Desabilitar memory watchdog interno do GitLab (deixa o OOM killer do K8s gerenciar)
memoryKiller:
maxRss: 2000000000 # 2GB - maior que o limite para evitar kills prematuros
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: webservice
topologyKey: kubernetes.io/hostname
# Gitaly (Git storage)
gitaly:
resources:
requests:
memory: 512Mi
cpu: 100m
limits:
memory: 1Gi
cpu: 500m
persistence:
size: 10Gi # Mínimo Hetzner ($0.0484/GB)
storageClass: hcloud-volumes
# GitLab Shell (SSH)
gitlab-shell:
minReplicas: 1
maxReplicas: 1
service:
type: ClusterIP # TCP passthrough pelo NGINX
resources:
requests:
memory: 64Mi
cpu: 50m
limits:
memory: 128Mi
cpu: 100m
# Toolbox (backup, rake tasks)
toolbox:
enabled: true
resources:
requests:
memory: 256Mi
cpu: 50m
limits:
memory: 512Mi
cpu: 200m
# GitLab Exporter (métricas)
gitlab-exporter:
enabled: false # Economiza recursos
# Migrations
migrations:
resources:
requests:
memory: 256Mi
cpu: 100m
# KAS (Kubernetes Agent Server) - desabilitar
kas:
enabled: false
# =============================================================================
# POSTGRESQL
# =============================================================================
postgresql:
install: true
primary:
resources:
requests:
memory: 512Mi
cpu: 100m
limits:
memory: 1Gi
cpu: 500m
persistence:
size: 10Gi # Mínimo Hetzner ($0.0484/GB)
storageClass: hcloud-volumes
# =============================================================================
# REDIS
# =============================================================================
redis:
install: true
master:
resources:
requests:
memory: 256Mi
cpu: 50m
limits:
memory: 512Mi
cpu: 200m
persistence:
size: 10Gi # Mínimo Hetzner ($0.0484/GB)
storageClass: hcloud-volumes
# =============================================================================
# MINIO (Object Storage)
# =============================================================================
# NOTA: As imagens padrão do GitLab chart não suportam ARM64.
# Usamos as imagens oficiais multi-arch do MinIO.
minio:
install: true
image: minio/minio
imageTag: RELEASE.2024-06-13T22-53-53Z
minioMc:
image: minio/mc
tag: RELEASE.2024-06-12T14-34-03Z
resources:
requests:
memory: 128Mi
cpu: 50m
limits:
memory: 256Mi
cpu: 200m
persistence:
size: 10Gi
storageClass: hcloud-volumes
# Ou usar object storage externo (S3, etc):
# global:
# minio:
# enabled: false
# appConfig:
# object_store:
# enabled: true
# connection:
# secret: gitlab-object-storage
# key: connection
# =============================================================================
# REGISTRY (Container Registry)
# =============================================================================
registry:
enabled: true
hpa:
minReplicas: 1
maxReplicas: 1
resources:
requests:
memory: 128Mi
cpu: 50m
limits:
memory: 256Mi
cpu: 200m
# Storage usa MinIO bundled automaticamente quando minio.install=true
# =============================================================================
# COMPONENTES DESABILITADOS (economia de recursos)
# =============================================================================
# GitLab Runner - instalar separadamente se necessário
gitlab-runner:
install: false
# Prometheus - usar Victoria Metrics da aula-05
prometheus:
install: false
# Grafana
grafana:
install: false
# GitLab Pages
gitlab-pages:
enabled: false
# Praefect (Gitaly HA) - não necessário para instalação pequena
praefect:
enabled: false
# Spamcheck
spamcheck:
enabled: false
# =============================================================================
# UPGRADECHECK
# =============================================================================
upgradeCheck:
enabled: false

569
aula-10/setup.sh Executable file
View File

@@ -0,0 +1,569 @@
#!/bin/bash
# =============================================================================
# Setup da Aula 10 - GitLab via Helm (Hetzner Cloud)
# =============================================================================
#
# Este script instala e configura:
# 1. cert-manager (opcional, para Let's Encrypt)
# 2. GitLab com todos os componentes
#
# Pré-requisitos:
# - Kubernetes cluster Talos na Hetzner (aula-08) com:
# - Hetzner CSI Driver (StorageClass: hcloud-volumes)
# - NGINX Ingress Controller com LoadBalancer
# - kubectl configurado (KUBECONFIG=../aula-08/kubeconfig)
# - Helm 3.x instalado
#
# Uso:
# export KUBECONFIG=$(pwd)/../aula-08/kubeconfig
# chmod +x setup.sh
# ./setup.sh
#
# =============================================================================
set -e
# Cores para output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERRO]${NC} $1"; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Variáveis de configuração
GITLAB_HOST=""
REGISTRY_HOST="" # Derivado automaticamente
DOMAIN="" # Extraído automaticamente do GITLAB_HOST
USE_CLOUDFLARE=""
USE_LETSENCRYPT=""
LETSENCRYPT_EMAIL=""
# =============================================================================
# FUNÇÕES DE CONFIGURAÇÃO
# =============================================================================
save_config() {
cat > "$SCRIPT_DIR/.env" <<EOF
# Configuração gerada pelo setup.sh
# $(date)
GITLAB_HOST=${GITLAB_HOST}
REGISTRY_HOST=${REGISTRY_HOST}
DOMAIN=${DOMAIN}
USE_CLOUDFLARE=${USE_CLOUDFLARE}
USE_LETSENCRYPT=${USE_LETSENCRYPT}
LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL}
EOF
log_success "Configuração salva em .env"
}
load_config() {
# Tentar carregar .env local primeiro
if [[ -f "$SCRIPT_DIR/.env" ]]; then
source "$SCRIPT_DIR/.env"
return 0
fi
# Tentar herdar da aula-09 (apenas CloudFlare/Let's Encrypt)
if [[ -f "$SCRIPT_DIR/../aula-09/.env" ]]; then
source "$SCRIPT_DIR/../aula-09/.env"
log_info "Configuração de TLS herdada da aula-09"
# Limpar hosts (serão perguntados novamente)
GITLAB_HOST=""
REGISTRY_HOST=""
return 0
fi
return 1
}
# Extrai domínio base de um FQDN (ex: git.kube.quest → kube.quest)
extract_domain() {
local fqdn="$1"
echo "$fqdn" | sed 's/^[^.]*\.//'
}
collect_user_input() {
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
echo -e "${CYAN} GitLab no Kubernetes (Hetzner Cloud)${NC}"
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
echo ""
# Carregar configuração existente
load_config
# Se já tem GITLAB_HOST configurado, oferecer usar
if [[ -n "$GITLAB_HOST" ]]; then
echo -e "Configuração existente encontrada:"
echo -e " GitLab: ${GREEN}${GITLAB_HOST}${NC}"
echo -e " Registry: ${GREEN}${REGISTRY_HOST}${NC}"
echo -e " CloudFlare: ${GREEN}${USE_CLOUDFLARE}${NC}"
echo -e " Let's Encrypt: ${GREEN}${USE_LETSENCRYPT}${NC}"
echo ""
echo -e "Deseja usar esta configuração?"
echo -e " 1) Sim, continuar com a configuração existente"
echo -e " 2) Não, reconfigurar"
echo -n "Escolha [1/2]: "
read -r choice
if [[ "$choice" == "1" ]]; then
return 0
fi
fi
# Coletar hostname do GitLab
echo ""
echo -n "Digite o hostname do GitLab (ex: git.kube.quest): "
read -r GITLAB_HOST
if [[ -z "$GITLAB_HOST" ]]; then
log_error "Hostname não pode ser vazio"
exit 1
fi
# Derivar Domain e Registry automaticamente
DOMAIN=$(extract_domain "$GITLAB_HOST")
REGISTRY_HOST="registry.${DOMAIN}"
log_info "Registry será: ${REGISTRY_HOST}"
# Se já tem configuração de TLS, oferecer usar
if [[ -n "$USE_CLOUDFLARE" ]]; then
echo ""
echo -e "Configuração de TLS encontrada:"
echo -e " CloudFlare: ${GREEN}${USE_CLOUDFLARE}${NC}"
echo -e " Let's Encrypt: ${GREEN}${USE_LETSENCRYPT}${NC}"
echo ""
echo -e "Deseja usar esta configuração de TLS?"
echo -e " 1) Sim"
echo -e " 2) Não, reconfigurar"
echo -n "Escolha [1/2]: "
read -r choice
if [[ "$choice" == "1" ]]; then
save_config
return 0
fi
fi
# Perguntar sobre CloudFlare
echo ""
echo "Você usa CloudFlare para DNS?"
echo " 1) Sim (com proxy/CDN ativado - ícone laranja)"
echo " 2) Não"
echo -n "Escolha [1/2]: "
read -r choice
if [[ "$choice" == "1" ]]; then
USE_CLOUDFLARE=true
USE_LETSENCRYPT=false
log_info "CloudFlare irá gerenciar TLS automaticamente na edge"
else
USE_CLOUDFLARE=false
# Perguntar sobre Let's Encrypt
echo ""
echo "Deseja ativar HTTPS com Let's Encrypt?"
echo " 1) Sim (recomendado para produção)"
echo " 2) Não (apenas HTTP)"
echo -n "Escolha [1/2]: "
read -r choice
if [[ "$choice" == "1" ]]; then
USE_LETSENCRYPT=true
echo ""
echo -n "Digite seu email para Let's Encrypt: "
read -r LETSENCRYPT_EMAIL
if [[ -z "$LETSENCRYPT_EMAIL" ]]; then
log_error "Email é obrigatório para Let's Encrypt"
exit 1
fi
else
USE_LETSENCRYPT=false
fi
fi
# Salvar configuração
save_config
}
install_cert_manager() {
log_info "Instalando cert-manager..."
helm repo add jetstack https://charts.jetstack.io 2>/dev/null || true
helm repo update jetstack
if helm status cert-manager -n cert-manager &> /dev/null; then
log_success "cert-manager já está instalado"
else
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--version v1.16.2 \
--set crds.enabled=true \
--wait \
--timeout 5m
log_success "cert-manager instalado"
fi
}
create_cluster_issuer() {
log_info "Criando ClusterIssuer para Let's Encrypt..."
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ${LETSENCRYPT_EMAIL}
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
ingress:
class: nginx
EOF
log_success "ClusterIssuer 'letsencrypt-prod' criado"
}
show_dns_instructions() {
# Obter IP do LoadBalancer
LB_IP=$(kubectl get svc -n ingress-nginx ingress-nginx-controller \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "<pendente>")
# Extrair nome do host (ex: git.kube.quest → git)
GITLAB_NAME=$(echo "$GITLAB_HOST" | cut -d. -f1)
REGISTRY_NAME=$(echo "$REGISTRY_HOST" | sed "s/\.${DOMAIN}$//" )
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
echo -e "${CYAN} Configure o DNS${NC}"
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
if [[ "$USE_CLOUDFLARE" == "true" ]]; then
echo ""
echo "No painel do CloudFlare (https://dash.cloudflare.com):"
echo ""
echo -e " ${YELLOW}Tipo:${NC} A"
echo -e " ${YELLOW}Nome:${NC} ${GITLAB_NAME}"
echo -e " ${YELLOW}Conteúdo:${NC} ${GREEN}${LB_IP}${NC}"
echo -e " ${YELLOW}Proxy:${NC} ✓ (ícone laranja)"
echo ""
echo -e " ${YELLOW}Tipo:${NC} A"
echo -e " ${YELLOW}Nome:${NC} ${REGISTRY_NAME}"
echo -e " ${YELLOW}Conteúdo:${NC} ${GREEN}${LB_IP}${NC}"
echo -e " ${YELLOW}Proxy:${NC} ✓ (ícone laranja)"
echo ""
echo -e "${GREEN}O CloudFlare cuida do TLS automaticamente!${NC}"
else
echo ""
echo "No seu provedor DNS:"
echo ""
echo -e " ${YELLOW}Tipo:${NC} A"
echo -e " ${YELLOW}Nome:${NC} ${GITLAB_NAME}"
echo -e " ${YELLOW}Valor:${NC} ${GREEN}${LB_IP}${NC}"
echo ""
echo -e " ${YELLOW}Tipo:${NC} A"
echo -e " ${YELLOW}Nome:${NC} ${REGISTRY_NAME}"
echo -e " ${YELLOW}Valor:${NC} ${GREEN}${LB_IP}${NC}"
if [[ "$USE_LETSENCRYPT" == "true" ]]; then
echo ""
echo -e "${GREEN}Let's Encrypt irá emitir certificados automaticamente!${NC}"
echo -e "Aguarde ~2 minutos após configurar o DNS."
fi
fi
# Determinar protocolo
PROTOCOL="https"
if [[ "$USE_CLOUDFLARE" == "false" && "$USE_LETSENCRYPT" == "false" ]]; then
PROTOCOL="http"
fi
echo ""
echo -e "Acesse: ${GREEN}${PROTOCOL}://${GITLAB_HOST}${NC}"
}
# =============================================================================
# VERIFICAÇÕES INICIAIS
# =============================================================================
log_info "Verificando pré-requisitos..."
if ! command -v kubectl &> /dev/null; then
log_error "kubectl não encontrado"
exit 1
fi
log_success "kubectl encontrado"
if ! command -v helm &> /dev/null; then
log_error "Helm não encontrado"
exit 1
fi
log_success "Helm $(helm version --short) encontrado"
if ! kubectl cluster-info &> /dev/null; then
log_error "Não foi possível conectar ao cluster"
exit 1
fi
log_success "Conectado ao cluster Kubernetes"
# =============================================================================
# 1. VERIFICAR PRÉ-REQUISITOS DA AULA-08
# =============================================================================
log_info "=== Verificando infraestrutura (aula-08) ==="
# Verificar Hetzner CSI Driver
if ! kubectl get storageclass hcloud-volumes &> /dev/null; then
log_error "StorageClass hcloud-volumes não encontrado!"
log_error "Execute primeiro o setup.sh da aula-08 para instalar o CSI Driver."
exit 1
fi
log_success "Hetzner CSI Driver instalado (hcloud-volumes)"
# Verificar NGINX Ingress
if ! kubectl get ingressclass nginx &> /dev/null; then
log_error "NGINX Ingress não encontrado!"
log_error "Execute primeiro o setup.sh da aula-08 para instalar o Ingress Controller."
exit 1
fi
log_success "NGINX Ingress Controller instalado"
echo ""
# =============================================================================
# 2. COLETAR INPUT DO USUÁRIO
# =============================================================================
collect_user_input
echo ""
# =============================================================================
# 3. INSTALAR CERT-MANAGER (se Let's Encrypt)
# =============================================================================
if [[ "$USE_LETSENCRYPT" == "true" ]]; then
log_info "=== Configurando cert-manager ==="
install_cert_manager
create_cluster_issuer
echo ""
fi
# =============================================================================
# 4. ADICIONAR REPOSITÓRIO HELM
# =============================================================================
log_info "=== Preparando instalação do GitLab ==="
log_info "Adicionando repositório Helm do GitLab..."
helm repo add gitlab https://charts.gitlab.io/ 2>/dev/null || true
helm repo update
# =============================================================================
# 5. CRIAR NAMESPACE
# =============================================================================
log_info "Criando namespace gitlab..."
kubectl create namespace gitlab --dry-run=client -o yaml | kubectl apply -f -
echo ""
# =============================================================================
# 6. INSTALAR GITLAB VIA HELM
# =============================================================================
log_info "=== Instalando GitLab (isso pode levar 10-15 minutos) ==="
# Construir argumentos do Helm dinamicamente
HELM_ARGS=""
# Configurar hosts
HELM_ARGS="$HELM_ARGS --set global.hosts.domain=${DOMAIN}"
HELM_ARGS="$HELM_ARGS --set global.hosts.gitlab.name=${GITLAB_HOST}"
HELM_ARGS="$HELM_ARGS --set global.hosts.registry.name=${REGISTRY_HOST}"
HELM_ARGS="$HELM_ARGS --set global.hosts.minio.name=minio.${GITLAB_HOST}"
# Configurar TLS
if [[ "$USE_LETSENCRYPT" == "true" ]]; then
# Let's Encrypt: TLS gerenciado pelo cert-manager
HELM_ARGS="$HELM_ARGS --set global.ingress.configureCertmanager=true"
HELM_ARGS="$HELM_ARGS --set global.ingress.tls.enabled=true"
HELM_ARGS="$HELM_ARGS --set global.hosts.https=true"
HELM_ARGS="$HELM_ARGS --set certmanager-issuer.email=${LETSENCRYPT_EMAIL}"
# Adicionar annotation do ClusterIssuer
HELM_ARGS="$HELM_ARGS --set global.ingress.annotations.cert-manager\\.io/cluster-issuer=letsencrypt-prod"
elif [[ "$USE_CLOUDFLARE" == "true" ]]; then
# CloudFlare: TLS na edge, backend HTTP
# Workhorse precisa confiar nos IPs do CloudFlare para X-Forwarded-For
HELM_ARGS="$HELM_ARGS --set global.ingress.tls.enabled=false"
HELM_ARGS="$HELM_ARGS --set global.hosts.https=true"
# CloudFlare IPv4 CIDRs (https://www.cloudflare.com/ips-v4)
CLOUDFLARE_CIDRS='["173.245.48.0/20","103.21.244.0/22","103.22.200.0/22","103.31.4.0/22","141.101.64.0/18","108.162.192.0/18","190.93.240.0/20","188.114.96.0/20","197.234.240.0/22","198.41.128.0/17","162.158.0.0/15","104.16.0.0/13","104.24.0.0/14","172.64.0.0/13","131.0.72.0/22"]'
HELM_ARGS="$HELM_ARGS --set-json gitlab.webservice.workhorse.trustedCIDRsForXForwardedFor='${CLOUDFLARE_CIDRS}'"
else
# Apenas HTTP
HELM_ARGS="$HELM_ARGS --set global.ingress.tls.enabled=false"
HELM_ARGS="$HELM_ARGS --set global.hosts.https=false"
fi
# Verificar se já está instalado
if helm status gitlab -n gitlab &> /dev/null; then
log_warn "GitLab já está instalado. Atualizando..."
eval helm upgrade gitlab gitlab/gitlab \
--namespace gitlab \
-f "$SCRIPT_DIR/gitlab-values.yaml" \
$HELM_ARGS \
--timeout 15m \
--wait
log_success "GitLab atualizado com sucesso!"
else
log_info "Instalando GitLab..."
eval helm install gitlab gitlab/gitlab \
--namespace gitlab \
-f "$SCRIPT_DIR/gitlab-values.yaml" \
$HELM_ARGS \
--timeout 15m \
--wait
log_success "GitLab instalado com sucesso!"
fi
echo ""
# =============================================================================
# 7. CONFIGURAR TCP PASSTHROUGH PARA SSH
# =============================================================================
log_info "=== Configurando TCP passthrough para SSH ==="
# Verificar se ConfigMap existe
if kubectl get configmap tcp-services -n ingress-nginx &> /dev/null; then
kubectl patch configmap tcp-services \
-n ingress-nginx \
--type merge \
-p '{"data":{"22":"gitlab/gitlab-gitlab-shell:22"}}'
else
kubectl create configmap tcp-services \
-n ingress-nginx \
--from-literal="22=gitlab/gitlab-gitlab-shell:22"
fi
# Reiniciar NGINX para aplicar
kubectl rollout restart deployment ingress-nginx-controller -n ingress-nginx
log_success "TCP passthrough configurado (porta 22 → GitLab Shell)"
echo ""
# =============================================================================
# 8. OBTER SENHA INICIAL
# =============================================================================
log_info "Obtendo senha inicial do root..."
# Aguardar secret ser criado e obter senha (até 2 min)
ROOT_PASSWORD=""
for i in {1..60}; do
ROOT_PASSWORD=$(kubectl get secret gitlab-gitlab-initial-root-password \
-n gitlab \
-o jsonpath='{.data.password}' 2>/dev/null | base64 -d 2>/dev/null)
if [[ -n "$ROOT_PASSWORD" ]]; then
log_success "Senha obtida"
break
fi
sleep 2
done
# =============================================================================
# RESUMO FINAL
# =============================================================================
# Determinar protocolo
PROTOCOL="https"
if [[ "$USE_CLOUDFLARE" == "false" && "$USE_LETSENCRYPT" == "false" ]]; then
PROTOCOL="http"
fi
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
echo -e "${GREEN} GitLab Instalado com Sucesso!${NC}"
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
echo ""
echo "Componentes instalados:"
if [[ "$USE_LETSENCRYPT" == "true" ]]; then
echo " - cert-manager (ClusterIssuer: letsencrypt)"
fi
echo " - GitLab (namespace: gitlab)"
echo " - Webservice (UI + API)"
echo " - Sidekiq (background jobs)"
echo " - Gitaly (Git storage)"
echo " - GitLab Shell (SSH)"
echo " - PostgreSQL"
echo " - Redis"
echo " - MinIO (object storage)"
echo " - Container Registry"
echo ""
echo "Configuração:"
echo " GitLab: ${GITLAB_HOST}"
echo " Registry: ${REGISTRY_HOST}"
echo " Domínio: ${DOMAIN}"
echo " CloudFlare: ${USE_CLOUDFLARE}"
echo " Let's Encrypt: ${USE_LETSENCRYPT}"
echo ""
echo "URLs:"
echo " Web: ${PROTOCOL}://${GITLAB_HOST}"
echo " Registry: ${PROTOCOL}://${REGISTRY_HOST}"
echo " SSH: git@${GITLAB_HOST} (porta 22)"
echo ""
echo "Credenciais:"
echo " Usuário: root"
if [ -n "$ROOT_PASSWORD" ]; then
echo " Senha: $ROOT_PASSWORD"
else
echo " Senha: (execute o comando abaixo)"
echo ""
echo " kubectl get secret gitlab-gitlab-initial-root-password -n gitlab \\"
echo " -o jsonpath='{.data.password}' | base64 -d; echo"
fi
# Mostrar instruções de DNS
show_dns_instructions
echo ""
echo "Comandos úteis:"
echo " # Ver pods"
echo " kubectl get pods -n gitlab"
echo ""
echo " # Ver logs"
echo " kubectl logs -n gitlab -l app=webservice -f"
echo ""
echo " # Reiniciar componente"
echo " kubectl rollout restart deployment -n gitlab gitlab-webservice-default"
echo ""
if [[ "$USE_LETSENCRYPT" == "true" ]]; then
echo " # Ver status dos certificados"
echo " kubectl get certificate -n gitlab"
echo ""
fi
echo " # Desinstalar"
echo " ./cleanup.sh"
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════${NC}"
echo ""
# Mostrar status dos pods
log_info "Status dos pods:"
kubectl get pods -n gitlab