DevOps
CI/CD com GitHub Actions para Node.js: do zero ao deploy
15 de outubro de 2024·Paulo de Paula
Um bom pipeline de CI/CD é a diferença entre deploy com confiança e deploy com ansiedade. Vou mostrar como construir um pipeline completo para Node.js com GitHub Actions.
Estrutura do pipeline
O pipeline vai ter três estágios:
- CI — lint, testes e build (em todo PR e push)
- Build da imagem Docker — gera e publica no ECR (apenas na
main) - Deploy — atualiza o ECS com a nova imagem (apenas na
main)
Estágio 1: CI
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
name: Lint e Testes
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x, 22.x]
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Instalar dependências
run: npm ci
- name: Lint
run: npm run lint
- name: Type check
run: npm run type-check
- name: Testes
run: npm test -- --coverage
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
NODE_ENV: test
- name: Upload coverage
uses: codecov/codecov-action@v4
if: matrix.node-version == '20.x'
with:
token: ${{ secrets.CODECOV_TOKEN }}Estágio 2: Build e push da imagem
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
env:
AWS_REGION: us-east-1
ECR_REPOSITORY: minha-api
ECS_SERVICE: minha-api-service
ECS_CLUSTER: producao
CONTAINER_NAME: api
jobs:
build-and-push:
name: Build e Push para ECR
runs-on: ubuntu-latest
outputs:
image: ${{ steps.build-image.outputs.image }}
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Login no ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build, tag e push da imagem
id: build-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build \
--cache-from $ECR_REGISTRY/$ECR_REPOSITORY:latest \
--build-arg BUILDKIT_INLINE_CACHE=1 \
-t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG \
-t $ECR_REGISTRY/$ECR_REPOSITORY:latest \
.
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUTEstágio 3: Deploy no ECS
deploy:
name: Deploy no ECS
runs-on: ubuntu-latest
needs: build-and-push
environment: producao # requer aprovação manual
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Baixar task definition atual
run: |
aws ecs describe-task-definition \
--task-definition ${{ env.ECS_SERVICE }} \
--query taskDefinition > task-definition.json
- name: Atualizar imagem na task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: ${{ env.CONTAINER_NAME }}
image: ${{ needs.build-and-push.outputs.image }}
- name: Deploy para ECS
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: trueDockerfile otimizado para Node.js
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine AS runner
ENV NODE_ENV=production
WORKDIR /app
# Cria usuário não-root
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nodeuser
COPY --from=builder --chown=nodeuser:nodejs /app/dist ./dist
COPY --from=builder --chown=nodeuser:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodeuser:nodejs /app/package.json ./
USER nodeuser
EXPOSE 3000
CMD ["node", "dist/index.js"]Variáveis de ambiente e secrets
Nunca hardcode secrets. Use GitHub Secrets:
# No workflow
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}Para múltiplos ambientes, use GitHub Environments com proteção por review:
staging— deploy automáticoproducao— requer aprovação de pelo menos 1 reviewer
Cache de dependências
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # cache automático de node_modulesIsso reduz o tempo de npm ci de ~30s para ~3s em projetos médios.
Resultado
Com esse pipeline, todo PR passa por lint + testes automaticamente, e todo merge na main gera uma nova imagem e faz deploy automaticamente — com aprovação manual configurável para produção.
O tempo total típico: 4-6 minutos do push ao deploy em produção.