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:
339
aula-10/README.md
Normal file
339
aula-10/README.md
Normal 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/)
|
||||
Reference in New Issue
Block a user