Aula 10 - Plataforma de Desenvolvimento (Gitea)
Git Server + Container Registry + CI/CD Runner — tudo num único deploy.
Por que esta aula existe
Nas aulas anteriores construímos um cluster Kubernetes na Hetzner com autoscaling, ingress e storage. Agora precisamos de uma plataforma de desenvolvimento para:
- Hospedar código (Git)
- Armazenar imagens Docker (Container Registry)
- Automatizar builds e deploys (CI/CD)
Normalmente, isso exigiria 3 ferramentas separadas. Nesta aula, mostramos como o Gitea entrega as 3 em um único binário.
Por que Gitea (e não GitLab, Harbor, Tekton)?
A escolha de ferramentas é uma decisão de arquitetura. No ecossistema Kubernetes, existem opções especializadas para cada peça:
De → Para
| Função | Opção "Enterprise" | O que usamos | Por quê |
|---|---|---|---|
| Git Server | GitLab CE/EE | Gitea | GitLab pede ~5Gi RAM, 8 pods, 2 nodes. Gitea pede ~500Mi, 1 pod, 1 node. |
| Container Registry | Harbor | Gitea (integrado) | Harbor é um projeto separado (~1Gi RAM, PostgreSQL, Redis, storage backend). Gitea já tem registry OCI embutido. |
| CI/CD | Tekton / GitLab CI | Gitea Actions | Tekton é poderoso mas complexo (CRDs, pipelines, tasks, triggers — curva de aprendizado alta). Gitea Actions usa sintaxe GitHub Actions, que a maioria já conhece. |
A conta fecha assim
Abordagem "enterprise" (GitLab + Harbor + Tekton):
GitLab: ~5Gi RAM, 8 pods, 40Gi storage, 2 nodes ($12.92/mês)
Harbor: ~1Gi RAM, 5 pods, 20Gi storage ($5/mês)
Tekton: ~512Mi RAM, 4 CRDs, curva de aprendizado
Total: ~6.5Gi RAM, 17+ pods (~$18/mês)
Abordagem Gitea (tudo integrado):
Gitea: ~500Mi RAM, 3 pods, 21Gi storage, 1 node ($7.41/mês)
Total: ~500Mi RAM, 3 pods (~$7/mês)
Para um workshop com cluster pequeno na Hetzner (CAX11 com 4GB RAM), a conta não fecha com a abordagem enterprise.
Mas e a sintaxe do CI?
Esse é o ponto decisivo. Gitea Actions usa sintaxe GitHub Actions — o padrão de fato da indústria:
# .gitea/workflows/ci.yml — mesma sintaxe do GitHub Actions
name: Build
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t minha-app .
Se você sabe GitHub Actions, sabe Gitea Actions. Se você sabe Gitea Actions, sabe GitHub Actions. Tekton tem sua própria DSL (PipelineRun, TaskRun, etc.) que não transfere pra nenhuma outra plataforma.
Comparação visual: GitLab vs Gitea
| GitLab | Gitea | |
|---|---|---|
| RAM | ~5 Gi (request) | ~500 Mi |
| Pods | 8 (webservice, sidekiq, gitaly, shell, postgres, redis, minio, registry) | 3 (gitea, postgresql, valkey) |
| Storage | 40 Gi (4 PVCs) | 21 Gi (3 PVCs) |
| Nodes mínimos | 2 (antiAffinity webservice/sidekiq) | 1 |
| Install time | 10-15 min | 2-3 min |
| CI Syntax | .gitlab-ci.yml (proprietária) |
.gitea/workflows/*.yml (GitHub Actions) |
| Registry | Componente separado (+ MinIO/S3) | Integrado (OCI nativo) |
| Runner | gitlab-runner (Helm chart separado) |
act_runner (Helm chart separado) |
Arquitetura
Hetzner LoadBalancer
│
┌───────────────────┼──────────────┐
│ │ │
:80/:443 :22 │
│ │ │
▼ ▼ │
┌───────────────┐ ┌─────────────┐ │
│ NGINX Ingress │ │ TCP Service │ │
└───────┬───────┘ └──────┬──────┘ │
│ │ │
▼ ▼ │
┌──────────────────────────────────────────┐
│ Namespace: gitea │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Gitea Pod │ │
│ │ Web UI + API + Git + SSH │◄────┘
│ │ Container Registry (OCI) │
│ │ Gitea Actions (habilitado) │
│ └──────────────┬──────────────────┘
│ │
│ ┌──────────────┼──────────────┐
│ │ │ │
│ ▼ ▼ ▼
│ PostgreSQL Valkey act_runner
│ (10Gi) (1Gi) + DinD
│ (builds Docker)
└──────────────────────────────────────────┘
O act_runner em detalhe
O runner é instalado como um StatefulSet com 2 containers:
| Container | O que faz |
|---|---|
act-runner |
Conecta ao Gitea, escuta por jobs, executa workflows |
dind (Docker-in-Docker) |
Daemon Docker privilegiado — permite docker build e docker push dentro dos workflows |
Quando você faz git push num repo que tem .gitea/workflows/ci.yml, o fluxo é:
git push → Gitea recebe → act_runner detecta workflow
→ Runner cria container temporário
→ Executa steps (checkout, build, push)
→ Imagem vai pro Gitea Container Registry
→ Kubernetes puxa a imagem no deploy
Pré-requisitos
- Cluster Talos na Hetzner (aula-08) com:
- Hetzner CSI Driver (StorageClass: hcloud-volumes)
- NGINX Ingress Controller com LoadBalancer
- kubectl instalado
- Helm 3.x instalado
Instalação
cd aula-10
export KUBECONFIG=$(pwd)/../aula-08/kubeconfig
chmod +x setup.sh
./setup.sh
O script é interativo e pergunta:
- Hostname do Gitea (ex:
gitea.kube.quest) - FQDN completo - Usa CloudFlare? (com proxy/CDN)
- Ativar Let's Encrypt? (se não usar CloudFlare)
O que o setup.sh instala
1. cert-manager (se Let's Encrypt)
2. Gitea via Helm (gitea-charts/gitea)
- Web UI + API + Git + SSH
- Container Registry (packages)
- PostgreSQL standalone
- Valkey (cache)
3. Gitea Actions Runner via Helm (gitea-charts/actions)
- act_runner + Docker-in-Docker
- Registration token obtido automaticamente
4. TCP passthrough no NGINX para SSH (porta 22)
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) |
Componentes e Recursos
| Componente | Chart | Memory Request | Memory Limit | Volume |
|---|---|---|---|---|
| Gitea | gitea-charts/gitea | 512Mi | 1Gi | 10Gi |
| PostgreSQL | (subchart) | 128Mi | 256Mi | 10Gi |
| Valkey | (subchart) | 32Mi | 64Mi | 1Gi |
| act_runner + DinD | gitea-charts/actions | 128Mi | 256Mi | 5Gi |
| Total | ~800Mi | ~1.6Gi | ~26Gi |
Funcionalidades
1. Container Registry (OCI)
O Gitea inclui um Container Registry OCI integrado. Sem Harbor, sem MinIO, sem componentes extras.
# Login no registry
docker login gitea.kube.quest
# Push de imagem
docker tag minha-app:v1 gitea.kube.quest/usuario/minha-app:v1
docker push gitea.kube.quest/usuario/minha-app:v1
2. Gitea Actions (CI/CD)
CI/CD integrado com sintaxe GitHub Actions. Sem Tekton, sem CRDs extras, sem DSL proprietária.
# .gitea/workflows/build.yml
name: Build
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login gitea.kube.quest -u ${{ gitea.actor }} --password-stdin
docker build -t gitea.kube.quest/${{ gitea.repository }}:${{ github.sha }} .
docker push gitea.kube.quest/${{ gitea.repository }}:${{ github.sha }}
3. SSH
# Clonar via SSH
git clone git@gitea.kube.quest:usuario/projeto.git
# Configurar chave SSH: Settings → SSH/GPG Keys
Acesso
Web UI
URL: https://gitea.kube.quest
Usuário: gitea_admin
Senha: (exibida no final do setup.sh)
Container Registry
docker login gitea.kube.quest
# Username: gitea_admin
# Password: (mesma senha do admin)
Verificar Runner
# Pod do runner
kubectl get pods -n gitea -l app.kubernetes.io/name=actions
# Logs
kubectl logs -n gitea -l app.kubernetes.io/name=actions -c act-runner -f
# Workflows executados
# Via UI: https://gitea.kube.quest/{owner}/{repo}/actions
Comandos Úteis
# Ver todos os pods
kubectl get pods -n gitea
# Ver logs do Gitea
kubectl logs -n gitea -l app.kubernetes.io/name=gitea -f
# Reiniciar Gitea
kubectl rollout restart deployment -n gitea gitea
# Ver certificados (se Let's Encrypt)
kubectl get certificate -n gitea
# Desinstalar
./cleanup.sh
Troubleshooting
Pods não iniciam
kubectl get events -n gitea --sort-by='.lastTimestamp'
kubectl get pvc -n gitea
SSH não conecta
# Verificar TCP passthrough
kubectl get configmap tcp-services -n ingress-nginx -o yaml
Registry não funciona
curl -v https://gitea.kube.quest/v2/
Runner não executa workflows
# Verificar se o runner está registrado
kubectl logs -n gitea -l app.kubernetes.io/name=actions -c act-runner --tail=20
# Verificar se DinD está rodando
kubectl logs -n gitea -l app.kubernetes.io/name=actions -c dind --tail=10
# Erro "Docker connection failed" → DinD não compartilha socket
# Verificar se extraVolumeMounts do dind inclui docker-socket
Runner crashando (OOM)
O Gitea pode usar mais memória durante migrações de repos grandes. Se ocorrer OOM:
# Aumentar limit temporariamente
helm upgrade gitea gitea-charts/gitea -n gitea \
--reuse-values \
--set resources.limits.memory=1Gi
Custos
| Recurso | Custo/mês |
|---|---|
| Worker (1x CAX11) | ~$4.59 |
| Volumes (~26Gi) | ~$1.26 |
| LoadBalancer (compartilhado) | ~$1.80 (1/3) |
| Total | ~$7.65 |
Storage: Disco Local vs S3 (Object Storage)
Por padrão, o Gitea armazena tudo em disco local (PVC). Isso é simples e funciona pro workshop. Mas em produção com muitas imagens Docker, o PVC de 10Gi enche rápido.
O Gitea suporta S3-compatible storage (MinIO, AWS S3, Hetzner Object Storage, etc.) como backend para tudo: container images, LFS, avatars, attachments, actions artifacts.
O que usamos (default)
Gitea Pod → PVC 10Gi (hcloud-volumes)
├── /data/git/repositories (código)
├── /data/gitea/packages (container images)
├── /data/gitea/lfs (large files)
└── /data/gitea/avatars (fotos)
Simples, sem dependências externas. Suficiente pra clusters pequenos.
Quando migrar pra S3
- PVC passando de 80% (o pvc-autoresizer da aula-12 ajuda, mas tem limite)
- Muitas imagens Docker grandes (container registry crescendo)
- Precisa de storage "ilimitado" sem gerenciar volumes
Como configurar S3 (Hetzner Object Storage)
- Criar bucket no Hetzner Cloud Console → Object Storage
- Gerar Access Key + Secret Key
- Adicionar no
gitea-values.yaml:
gitea:
config:
storage:
STORAGE_TYPE: minio
MINIO_ENDPOINT: nbg1.your-objectstorage.com
MINIO_ACCESS_KEY_ID: "sua-access-key"
MINIO_SECRET_ACCESS_KEY: "sua-secret-key"
MINIO_BUCKET: gitea
MINIO_LOCATION: nbg1
MINIO_USE_SSL: "true"
SERVE_DIRECT: "true"
helm upgradepra aplicar
SERVE_DIRECT: como funciona (e por que é seguro)
Com SERVE_DIRECT: true, o Gitea não faz proxy dos downloads. Em vez disso, gera URLs pré-assinadas com expiração:
docker pull gitea.kube.quest/org/app-privada:v1
│
├─ 1. Client autentica no Gitea (token/password)
│
├─ 2. Gitea verifica permissão (repo privado? user tem acesso?)
│ → Se não tem acesso: 403 Forbidden. Para aqui.
│
├─ 3. Gitea gera URL S3 pré-assinada (expira em ~10 minutos)
│ https://s3.example.com/gitea/packages/sha256:abc...
│ ?X-Amz-Signature=xxx&X-Amz-Expires=600
│
└─ 4. Client baixa direto do S3 via URL assinada
→ URL expira, não pode ser reutilizada
→ Repo privado permanece privado
Sem SERVE_DIRECT (default):
Client → Gitea (proxy) → S3 → Gitea (proxy) → Client
^^^^^^^^^^^^ ^^^^^^^^^^^^
CPU + RAM + bandwidth passam pelo Gitea
Repos privados continuam privados — a URL assinada só é gerada após autenticação e verificação de permissão. É o mesmo mecanismo usado por AWS S3, GitHub Packages e Google Cloud Storage.
Único cenário onde não usar SERVE_DIRECT: se o bucket S3 está em rede privada e os clients não têm acesso direto à rede do S3.
Ou só pra packages (container images)
Se quiser manter repos git em disco local mas imagens no S3:
gitea:
config:
storage.packages:
STORAGE_TYPE: minio
MINIO_ENDPOINT: nbg1.your-objectstorage.com
MINIO_ACCESS_KEY_ID: "sua-access-key"
MINIO_SECRET_ACCESS_KEY: "sua-secret-key"
MINIO_BUCKET: gitea
MINIO_USE_SSL: "true"
MINIO_BASE_PATH: "packages/"
SERVE_DIRECT: "true"
Custo S3 vs PVC
| Storage | Custo | Limite |
|---|---|---|
| PVC (Hetzner Volume) | ~$0.048/GB/mês | 10TB max |
| Hetzner Object Storage | ~€0.006/GB/mês (~8x mais barato) | Ilimitado |
Para o workshop usamos PVC — zero configuração extra, zero secrets de S3.
Lições do Workshop
- Nem sempre precisa da ferramenta "enterprise" — Gitea substitui GitLab + Harbor + parcialmente Tekton com 10x menos recursos
- Registry integrado é suficiente para a maioria dos casos — Harbor justifica-se quando você precisa de vulnerability scanning, replicação multi-site, ou compliance (assinatura de imagens)
- Sintaxe GitHub Actions é portável — o que você aprende aqui transfere direto pro GitHub, e vice-versa
- Docker-in-Docker no Kubernetes requer namespace com PodSecurity
privileged— é o trade-off pra ter builds Docker no cluster