Pipeline de entrega contínua de nível corporativo para aplicações Node.js — do commit ao container em produção em menos de 60 segundos.
Processos de deploy manual são lentos, inconsistentes e introduzem risco humano. Este projeto elimina esses problemas ao automatizar todo o ciclo de vida do software — desde o git push até o container rodando em produção na AWS — com foco em segurança, imutabilidade e rastreabilidade.
Qualquer merge na branch main dispara automaticamente: validação de código → build da imagem Docker → scan de vulnerabilidades → push para o ECR com dupla tag → resumo auditável na interface do GitHub Actions.
Developer → GitHub (main) → GitHub Actions ─────────────────────────────┐
│ │
[test] → [build] → [push] │
│ │
Amazon ECR ← IAM Role ← EC2 ASG
(Registry) (Least (Container
Privilege) Runtime)
│
Internet (port 80)
Decisões de arquitetura e trade-offs:
| Decisão | Alternativa considerada | Por que esta escolha |
|---|---|---|
| Amazon ECR | Docker Hub | Latência zero dentro da AWS, controle de acesso via IAM, imutabilidade de imagens nativa |
| EC2 + ASG | ECS / EKS | Menor overhead operacional para o escopo do projeto; ECS seria a próxima evolução natural |
| Multi-stage Dockerfile | Build único | Imagem de produção ~60% menor, sem ferramentas de dev expostas em runtime |
| SHA tag + latest | Apenas latest | Permite rollback preciso para qualquer commit sem ambiguidade |
| GitHub Secrets + IAM Role | Credenciais hardcoded | Superfície de ataque de credenciais reduzida a zero |
- Runtime: Node.js 18 (LTS)
- Containerização: Docker com multi-stage build
- CI/CD: GitHub Actions
- Registry: Amazon ECR (privado, imagens imutáveis)
- Compute: Amazon EC2 dentro de Auto Scaling Group
- Segurança: IAM Roles, Resource-based Policies no ECR, GitHub Secrets
- Qualidade: Hadolint (lint de Dockerfile), Trivy (scan de vulnerabilidades)
.
├── index.js # Aplicação Node.js
├── package.json
├── Dockerfile # Multi-stage build (builder → production)
└── .github/
└── workflows/
└── deploy.yml # Pipeline completo (test → build → push)
O workflow é dividido em dois jobs independentes:
Job 1 — test (roda em PRs e pushes):
- Setup Node.js com cache de dependências
npm ci— instalação determinística- Lint do Dockerfile com Hadolint
Job 2 — build-and-push (apenas em merge na main):
- Configuração de credenciais AWS via GitHub Secrets
- Login no Amazon ECR
- Geração de tags:
latest+ SHA curto do commit (ex:a1b2c3d) - Build com Docker Buildx + cache entre runs (reduz tempo de CI em até 70%)
- Push das imagens para o ECR
- Scan de vulnerabilidades com Trivy (CRITICAL e HIGH)
- Resumo auditável publicado na interface do GitHub Actions
Controle de concorrência: cancel-in-progress: true garante que apenas um deploy rode por vez — pushes rápidos consecutivos não geram race conditions.
# Stage 1 — builder: instala todas as dependências, executa validações
FROM node:18-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# Stage 2 — production: apenas o necessário para rodar
FROM node:18-slim AS production
ENV NODE_ENV=production PORT=8080
COPY --from=builder /app/package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/index.js ./
USER node # Não roda como root
EXPOSE 8080
HEALTHCHECK ... # Orquestrador sabe se o container está vivo
CMD ["node", "index.js"]A separação em stages garante que ferramentas de desenvolvimento, cache do npm e arquivos temporários do build nunca chegam à imagem de produção.
| Controle | Implementação |
|---|---|
| Credenciais AWS | GitHub Secrets — nunca em arquivos ou logs |
| Acesso ECR | IAM Role na EC2 + Resource-based Policy no repositório |
| Princípio do menor privilégio | IAM policies granulares: EC2 lê ECR, não escreve |
| Runtime | Container roda como usuário node, não root |
| Imagens | Tags imutáveis no ECR — uma tag = um artefato exato |
| Vulnerabilidades | Scan automático com Trivy a cada build |
A autenticação atual usa AWS_ACCESS_KEY_ID e AWS_SECRET_ACCESS_KEY armazenadas como GitHub Secrets. Essa abordagem é segura quando as chaves têm escopo mínimo de permissão — o que foi implementado aqui.
A próxima evolução natural é adotar OpenID Connect (OIDC), que elimina completamente a existência de chaves estáticas:
# Como ficaria o workflow com OIDC
- name: Configure AWS Credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-role
aws-region: us-east-1
# Sem aws-access-key-id nem aws-secret-access-keyCom OIDC, o GitHub Actions solicita um token temporário diretamente à AWS no momento do run. Esse token expira em minutos e nunca é armazenado em lugar algum — superfície de ataque de credenciais reduzida a zero.
A migração exige criar um Identity Provider do GitHub no IAM e ajustar a trust policy da IAM Role. Foi uma decisão deliberada não implementar agora para manter o projeto focado no pipeline em si, mas está documentada como próximo passo de hardening.
A instância EC2 tinha uma IAM Role com permissão ecr:GetAuthorizationToken, mas o repositório ECR tinha uma Resource-based Policy que não listava explicitamente a entidade principal da EC2 como permitida. O erro aparecia apenas no pull, não no login — o que tornou o diagnóstico não trivial.
Resolução: adicionei a ARN da IAM Role da EC2 na Resource-based Policy do ECR com as ações mínimas necessárias (ecr:BatchGetImage, ecr:GetDownloadUrlForLayer). Isso separou claramente o controle de identidade (IAM Role) do controle de recurso (ECR Policy).
O container subia, passava pelo health check inicial e falhava alguns segundos depois com um erro de sintaxe. O problema era uma variável de ambiente injetada via ENV no Dockerfile que sobrescrevia um valor esperado pelo JSON.parse() interno da aplicação.
Resolução: docker logs -f + docker inspect para isolar a variável problemática. Corrigi localmente em ambiente isolado antes de commitar — reduzindo o ciclo de feedback de "push → aguardar CI → falha" para segundos.
Regra de segurança da instância EC2 expunha apenas a porta 80. A aplicação Node.js escutava na 8080. Abrir a 8080 no Security Group aumentaria a superfície de ataque desnecessariamente.
Resolução: mapeamento de portas nativo do Docker (-p 80:8080) — a porta 80 é exposta pelo host, o container permanece isolado na 8080. Sem mudanças no Security Group.
# Clone o repositório
git clone https://github.com/gustavogomes43/projeto-aws-docker-ci-cd.git
cd seu-repositorio
# Build da imagem
docker build -t projeto-docker-cicd .
# Execute o container
docker run -p 8080:8080 projeto-docker-cicd
# Acesse
curl http://localhost:8080Configure os seguintes secrets no repositório GitHub (Settings → Secrets → Actions):
| Secret | Finalidade |
|---|---|
AWS_ACCESS_KEY_ID |
Autenticação programática na AWS |
AWS_SECRET_ACCESS_KEY |
Chave privada para assinatura de requisições |
AWS_REGION |
Região do ECR e da EC2 (ex: us-east-1) |
ECR_REPOSITORY |
Nome do repositório no ECR (ex: projeto-docker-cicd) |
- Deploy automatizado: de 20 minutos manuais para menos de 60 segundos
- Zero credenciais AWS expostas em código, logs ou máquinas locais
- Rastreabilidade completa: cada imagem em produção é mapeável a um commit exato via SHA tag
- Ambientes imutáveis: a imagem que passa nos testes é exatamente a que vai para produção
Autor: Gustavo Gomes · Cloud & DevOps