CI/CD
From push to running container: build, ECR and replace.
A push to develop or main builds a Docker image with that environment’s settings, stores it in ECR and replaces the running container in the matching AWS account.
O fluxo completoThe full flow
O objetivo: depois de um commit chegar em develop ou main, o GitHub constrói uma imagem Docker com a configuração daquele ambiente, guarda a imagem no repositório de imagens (Amazon ECR) da conta AWS correta e troca o container em execução pelo novo. Ninguém entra em servidor, ninguém guarda chave de acesso.The goal: once a commit lands on develop or main, GitHub builds a Docker image with that environment’s configuration, stores it in the image registry (Amazon ECR) of the right AWS account and swaps the running container for the new one. Nobody logs in to a server, nobody stores an access key.
- Push. O merge de um PR em
develop(oumain) registra um eventopushe dispara o workflow.Push. Merging a PR intodevelop(ormain) records apushevent and triggers the workflow. - Build. O job escolhe o environment pelo nome da branch e constrói a imagem passando como
--build-argas variáveis desse environment.Build. The job picks the environment from the branch name and builds the image passing that environment’s variables as--build-arg. - Credenciais. O GitHub emite um token OIDC; a AWS o troca por credenciais temporárias da role da conta do ambiente (nota de OIDC).Credentials. GitHub issues an OIDC token; AWS exchanges it for temporary credentials of the environment account’s role (OIDC note).
- Registro. A imagem sobe para o ECR da mesma conta, com o SHA do commit como tag.Registry. The image is pushed to the ECR of the same account, tagged with the commit SHA.
- Troca. O workflow registra uma task definition nova apontando para a imagem nova e manda o serviço ECS usá-la. O ECS sobe o container novo, espera ele ficar saudável e para o antigo.Swap. The workflow registers a new task definition pointing to the new image and tells the ECS service to use it. ECS starts the new container, waits for it to be healthy and stops the old one.
- Verificação. O job espera o serviço estabilizar. Se a versão nova não ficar saudável, o job falha e o ECS pode desfazer sozinho.Check. The job waits for the service to stabilise. If the new version does not become healthy, the job fails and ECS can roll back on its own.
docker stop e docker rm. O serviço faz a troca (rolling update): a documentação diz que o scheduler “substitui as tasks em execução por tasks novas”. A task antiga é parada e o container dela é descartado junto. Se você usa Docker puro numa EC2, o mesmo passo é manual: veja a alternativa com EC2.And “stop and delete the current container”? On ECS you do not run docker stop and docker rm. The service does the swap (a rolling update): the docs say the scheduler “replaces the currently running tasks with new tasks”. The old task is stopped and its container is discarded with it. If you run plain Docker on an EC2 instance, the same step is manual: see the EC2 alternative.Uma branch, um ambiente, uma contaOne branch, one environment, one account
O desenho que o resto da nota segue. Os nomes são iguais nos dois lados; o que muda é o valor guardado em cada environment.The layout the rest of the note follows. The names are the same on both sides; what changes is the value stored in each environment.
develop→ conta dev. Deploy automático, sem aprovação.develop→ dev account. Automatic deploy, no approval.main→ conta prod. O environmentproductionpode exigir um revisor antes de o job continuar.main→ prod account. Theproductionenvironment can require a reviewer before the job continues.- Mais ambientes? Para um terceiro (por exemplo
staging), crie outra branch, outro environment, outra conta (ou o mesmo par de contas com outro cluster) e acrescente a branch aoon.push.branchese ao mapeamento doenvironment:no workflow. Nada mais muda.More environments? For a third one (for examplestaging), create another branch, another environment, another account (or the same pair of accounts with another cluster) and add the branch toon.push.branchesand to theenvironment:mapping in the workflow. Nothing else changes.
Antes de começarBefore you start
Esta nota continua duas outras e não repete o que elas já explicam. Cada item abaixo precisa estar pronto antes do primeiro push.This note continues two others and does not repeat what they already explain. Each item below must be ready before the first push.
| Onde verWhere to look | O que precisa existirWhat must exist |
|---|---|
| Nota de AWSAWS note | Organization e as duas contas (dev e prod), com um perfil da CLI para cada.Organization and the two accounts (dev and prod), with a CLI profile for each. |
| Nota de OIDC, 1 a 3OIDC note, 1 to 3 | Provedor OIDC e uma role de deploy em cada conta; environments develop e production no GitHub, com AWS_ROLE_ARN.OIDC provider and a deploy role in each account; develop and production environments on GitHub, with AWS_ROLE_ARN. |
| Nota de OIDC, pré-requisitosOIDC note, prerequisites | Cluster ECS, serviço ECS rodando (mesmo com imagem de teste), task execution role e task role em cada conta.ECS cluster, ECS service running (even with a placeholder image), task execution role and task role in each account. |
| Nota de OIDC, 4OIDC note, 4 | Segredos da aplicação no Secrets Manager de cada conta.Application secrets in each account’s Secrets Manager. |
O que esta nota acrescenta, nesta ordem: (1) como separar a configuração do Dockerfile por ambiente, (2) o repositório ECR de cada conta, (3) como o serviço ECS troca o container, (4) as variáveis novas dos environments, (5) o workflow completo, (6) o primeiro deploy e (7) a alternativa sem ECS.What this note adds, in this order: (1) how to split the Dockerfile’s configuration per environment, (2) each account’s ECR repository, (3) how the ECS service swaps the container, (4) the new environment variables, (5) the full workflow, (6) the first deploy and (7) the alternative without ECS.
Variáveis usadas nos comandos (troque pelos seus valores):Variables used in the commands (replace with your values):
GH_ORG=minha-org
GH_REPO=meu-repo
REGION=sa-east-1
APP=meu-app # ECR repository name
# profiles: --profile dev and --profile prod
1. Configuração por ambiente no Dockerfile1. Per-environment configuration in the Dockerfile
Você pediu uma imagem “com todas as envs corretas de dev ou prod”. Há dois tipos de configuração e cada um entra num momento diferente:You asked for an image “with all the correct dev or prod envs”. There are two kinds of configuration and each enters at a different moment:
| TipoKind | ExemploExample | Entra quando e comoEnters when and how |
|---|---|---|
| Pública, conhecida no buildPublic, known at build time | URL pública da API, nome do ambiente, ID de analytics, variáveis embutidas no bundle do front-endPublic API URL, environment name, analytics ID, variables embedded in the front-end bundle | Build. GitHub Environment variables → --build-arg → ARG/ENV no Dockerfile.Build. GitHub Environment variables → --build-arg → ARG/ENV in the Dockerfile. |
| Não secreta, só necessária ao rodarNon-secret, needed only at run time | Nível de log, nome da fila, flagsLog level, queue name, flags | Runtime. Campo environment da task definition.Runtime. The task definition’s environment field. |
| SegredoSecret | Senha do banco, chave de API, tokenDatabase password, API key, token | Runtime. Secrets Manager → campo secrets da task definition. Nunca no build.Runtime. Secrets Manager → the task definition’s secrets field. Never at build time. |
Dockerfile de exemploExample Dockerfile
Um app Node com build em duas etapas, só para mostrar onde ficam os ARG. Adapte ao seu stack; a ideia vale para qualquer linguagem.A Node app with a two-stage build, only to show where the ARGs go. Adapt it to your stack; the idea holds for any language.
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
ARG APP_ENV
ARG PUBLIC_API_URL
RUN APP_ENV=$APP_ENV PUBLIC_API_URL=$PUBLIC_API_URL npm run build
FROM node:22-alpine
WORKDIR /app
ARG APP_ENV
ENV NODE_ENV=production APP_ENV=$APP_ENV
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]
ARG e ENV ficam gravados nos metadados e nas camadas da imagem, e qualquer pessoa com permissão de baixá-la os lê (por exemplo com docker history). A documentação do Docker recomenda secret mounts (RUN --mount=type=secret) para segredos de build, que não ficam na imagem. Para a senha do banco, use o caminho de runtime da tabela.A secret at build time is a leak. ARG and ENV values are recorded in the image metadata and layers, and anyone allowed to pull it can read them (for example with docker history). Docker’s documentation recommends secret mounts (RUN --mount=type=secret) for build secrets, which do not stay in the image. For the database password, use the runtime path from the table.2. O repositório de imagens (ECR)2. The image registry (ECR)
Cada conta tem o seu repositório ECR, com o mesmo nome nas duas. O workflow envia a imagem para o repositório da conta em que a role foi assumida, então dev e prod nunca compartilham imagens.Each account has its own ECR repository, with the same name in both. The workflow pushes the image to the repository of the account whose role was assumed, so dev and prod never share images.
Criar o repositório, passo a passoCreating the repository, step by step
- Na conta dev, abra Amazon ECR → Private registry → Repositories → Create repository.In the dev account, open Amazon ECR → Private registry → Repositories → Create repository.
- Em Repository name, informe
meu-app. Ative Tag immutability: uma tag, uma vez enviada, não pode ser sobrescrita.Under Repository name, entermeu-app. Turn on Tag immutability: once pushed, a tag cannot be overwritten. - Ative Scan on push para analisar vulnerabilidades a cada envio e crie o repositório.Turn on Scan on push to scan for vulnerabilities on every push, and create the repository.
- Repita na conta prod, com o mesmo nome.Repeat in the prod account, with the same name.
for P in dev prod; do
aws ecr create-repository --repository-name $APP \
--image-tag-mutability IMMUTABLE \
--image-scanning-configuration scanOnPush=true \
--region $REGION --profile $P
done
Limpeza automáticaAutomatic cleanup
Cada deploy acrescenta uma imagem. Uma lifecycle policy expira as mais antigas. Segundo a documentação, as imagens expiram em até 24 horas depois de cumprir o critério, e a pré-visualização (preview) mostra o que seria apagado antes de você aplicar.Every deploy adds an image. A lifecycle policy expires the oldest ones. According to the docs, images expire within 24 hours of meeting the criteria, and the preview shows what would be deleted before you apply it.
{
"rules": [
{
"rulePriority": 1,
"description": "Keep the last 30 images",
"selection": {
"tagStatus": "any",
"countType": "imageCountMoreThan",
"countNumber": 30
},
"action": { "type": "expire" }
}
]
}
aws ecr put-lifecycle-policy --repository-name $APP \
--lifecycle-policy-text file://lifecycle.json --profile dev
# repeat with --profile prod
ecr:DescribeImages ao statement EcrPush da política da role (nota de OIDC, etapa 2), no mesmo ARN de repositório.One more permission on the deploy role. The workflow below queries ECR before building. Add ecr:DescribeImages to the EcrPush statement of the role policy (OIDC note, step 2), on the same repository ARN.3. Como o ECS troca o container3. How ECS swaps the container
É aqui que o container antigo é parado e descartado. O comportamento é controlado por dois números do serviço, descritos na documentação do ECS:This is where the old container is stopped and discarded. The behaviour is controlled by two numbers on the service, described in the ECS documentation:
minimumHealthyPercenté o limite inferior de tasks saudáveis durante a troca, em % do número desejado (arredondado para cima).minimumHealthyPercentis the lower limit of healthy tasks during the swap, as a % of the desired count (rounded up).maximumPercenté o limite superior de tasks em execução durante a troca (arredondado para baixo).maximumPercentis the upper limit of running tasks during the swap (rounded down).
Com 1 task desejada, o padrão (100/200) sobe a nova antes de parar a antiga: sem indisponibilidade, desde que a nova passe no health check. Com 0/100 o ECS é forçado a parar a antiga antes: é o equivalente literal de “parar, excluir e criar de novo”, ao custo de uma janela fora do ar.With 1 desired task, the default (100/200) starts the new one before stopping the old one: no downtime, as long as the new one passes the health check. With 0/100 ECS is forced to stop the old one first: the literal equivalent of “stop, delete and create again”, at the cost of a window offline.
Configurar, passo a passoConfiguring, step by step
- No console: ECS → Clusters → seu cluster → seu serviço → Update service, seção de opções de deployment.In the console: ECS → Clusters → your cluster → your service → Update service, deployment options section.
- Defina
Minimum healthy percenteMaximum percentconforme o desenho acima (a primeira opção é a recomendada).SetMinimum healthy percentandMaximum percentas drawn above (the first option is the recommended one). - Ative o deployment circuit breaker com rollback: se as tasks novas não conseguirem iniciar, o ECS marca o deploy como falho e volta para a revisão anterior. A documentação o indica para parar um deploy quando as tasks não sobem.Turn on the deployment circuit breaker with rollback: if the new tasks cannot start, ECS marks the deploy as failed and returns to the previous revision. The docs recommend it for stopping a deployment when tasks cannot start.
- Repita no serviço da outra conta (em prod, mantenha o padrão 100/200 para não ter janela fora do ar).Repeat on the other account’s service (in prod, keep the default 100/200 to avoid an offline window).
aws ecs update-service --cluster meu-cluster --service meu-servico \
--deployment-configuration "minimumHealthyPercent=100,maximumPercent=200,deploymentCircuitBreaker={enable=true,rollback=true}" \
--profile dev
O health check decide quando a antiga pode pararThe health check decides when the old one may stop
O ECS só considera a task nova saudável se o container passar no health check. Defina um na task definition (o comando depende do que existe na imagem; este exemplo usa wget, presente na imagem Alpine):ECS only treats the new task as healthy if the container passes its health check. Define one in the task definition (the command depends on what the image contains; this example uses wget, present in the Alpine image):
"healthCheck": {
"command": ["CMD-SHELL", "wget -qO- http://localhost:3000/health || exit 1"],
"interval": 15,
"timeout": 5,
"retries": 3,
"startPeriod": 20
}
Se o serviço está atrás de um load balancer, o health check do target group também vale. Sem nenhum health check, o ECS só sabe que o processo iniciou, não que a aplicação responde.If the service is behind a load balancer, the target group’s health check counts too. With no health check, ECS only knows the process started, not that the application responds.
4. Variáveis novas nos environments4. New variables in the environments
A nota de OIDC criou cinco variáveis por environment. Acrescente as duas que alimentam o build. Mesmo nome nos dois environments, valor diferente.The OIDC note created five variables per environment. Add the two that feed the build. Same name in both environments, different value.
| VariávelVariable | Em developIn develop | Em productionIn production |
|---|---|---|
APP_ENVAPP_ENV | developmentdevelopment | productionproduction |
PUBLIC_API_URLPUBLIC_API_URL | https://api-dev.exemplo.comhttps://api-dev.example.com | https://api.exemplo.comhttps://api.example.com |
- No repositório: Settings → Environments → develop → Environment variables → Add variable.In the repository: Settings → Environments → develop → Environment variables → Add variable.
- Crie
APP_ENVePUBLIC_API_URLcom os valores de dev.CreateAPP_ENVandPUBLIC_API_URLwith the dev values. - Repita em production com os valores de prod.Repeat in production with the prod values.
gh variable set APP_ENV --env develop --body "development"
gh variable set PUBLIC_API_URL --env develop --body "https://api-dev.example.com"
gh variable set APP_ENV --env production --body "production"
gh variable set PUBLIC_API_URL --env production --body "https://api.example.com"
5. O workflow completo5. The full workflow
Um único arquivo atende os dois ambientes. É o workflow da nota de OIDC com duas diferenças: as --build-arg vindas das variáveis do environment e a checagem de “imagem já existe”.One file serves both environments. It is the OIDC note’s workflow with two differences: the --build-args coming from the environment variables and the “image already exists” check.
.github/workflows/deploy.yml
Dockerfile
.aws/task-definition.json # container named "app"
name: Build and deploy
on:
push:
branches:
- develop
- main
# One deploy at a time per branch; a newer push waits for the running one.
concurrency:
group: deploy-${{ github.ref_name }}
cancel-in-progress: false
permissions:
id-token: write # lets the job request the OIDC token
contents: read # actions/checkout
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ github.ref_name == 'main' && 'production' || 'develop' }}
env:
AWS_REGION: ${{ vars.AWS_REGION }}
ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }}
ECS_CLUSTER: ${{ vars.ECS_CLUSTER }}
ECS_SERVICE: ${{ vars.ECS_SERVICE }}
ECS_TASK_DEFINITION: .aws/task-definition.json
CONTAINER_NAME: app
steps:
- uses: actions/checkout@v6
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
role-session-name: gh-${{ github.run_id }}
aws-region: ${{ env.AWS_REGION }}
- name: Log in to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push the image
id: build-image
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
APP_ENV: ${{ vars.APP_ENV }}
PUBLIC_API_URL: ${{ vars.PUBLIC_API_URL }}
run: |
IMAGE="$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
if aws ecr describe-images --repository-name "$ECR_REPOSITORY" \
--image-ids imageTag="$IMAGE_TAG" > /dev/null 2>&1; then
echo "Image $IMAGE_TAG already in ECR, skipping build"
else
docker build \
--build-arg APP_ENV="$APP_ENV" \
--build-arg PUBLIC_API_URL="$PUBLIC_API_URL" \
-t "$IMAGE" .
docker push "$IMAGE"
fi
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
- name: Render the task definition
id: render
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: ${{ env.ECS_TASK_DEFINITION }}
container-name: ${{ env.CONTAINER_NAME }}
image: ${{ steps.build-image.outputs.image }}
- name: Deploy to Amazon ECS (replaces the running container)
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.render.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: true
O que cada parte fazWhat each part does
on.push.branches. Sódevelopemaindisparam. Uma branch de feature não faz deploy.on.push.branches. Onlydevelopandmaintrigger. A feature branch does not deploy.environment:. A expressão escolheproductionparamainedeveloppara o resto. É isso que seleciona as variáveis (vars.*), a role e as regras de aprovação.environment:. The expression picksproductionformainanddevelopotherwise. It is what selects the variables (vars.*), the role and the approval rules.- Build. As variáveis do environment viram
--build-arg. A imagem nasce com os valores do ambiente certo.Build. The environment’s variables become--build-arg. The image is born with the right environment’s values. - Checagem de existência. Com tags imutáveis, enviar de novo a mesma tag dá erro. Se você reexecutar um job do mesmo commit, o workflow pula o build e reaproveita a imagem. Consequência: se mudar uma variável do environment e reexecutar o mesmo commit, a imagem não é refeita; faça um commit novo.Existence check. With immutable tags, pushing the same tag again is an error. If you re-run a job for the same commit, the workflow skips the build and reuses the image. Consequence: if you change an environment variable and re-run the same commit, the image is not rebuilt; make a new commit.
- Render + deploy. A primeira ação grava a imagem nova na task definition; a segunda registra a revisão e atualiza o serviço. A partir daí, o ECS faz a troca descrita na etapa 3.Render + deploy. The first action writes the new image into the task definition; the second registers the revision and updates the service. From there ECS does the swap described in step 3.
wait-for-service-stability. O job só termina quando o serviço estabiliza. Se a versão nova não ficar saudável, o job falha e você vê no GitHub.wait-for-service-stability. The job finishes only when the service stabilises. If the new version does not become healthy, the job fails and you see it on GitHub.
@v6, @v2, @v1) por legibilidade. Em produção, fixe por SHA de commit, como recomenda a documentação do GitHub.These actions are not certified by GitHub and the examples use tags (@v6, @v2, @v1) for readability. In production, pin to a commit SHA, as GitHub’s docs recommend.6. Primeiro deploy, passo a passo6. First deploy, step by step
- Confira o checklist. Repositório ECR e serviço ECS em cada conta; as sete variáveis em cada environment (as cinco da nota de OIDC mais
APP_ENVePUBLIC_API_URL);ecr:DescribeImagesna política da role.Check the list. ECR repository and ECS service in each account; the seven variables in each environment (the five from the OIDC note plusAPP_ENVandPUBLIC_API_URL);ecr:DescribeImagesin the role policy. - Commit na
develop. Mergeie um PR (ou faça um commit) e abra a aba Actions. O job mostra o environmentdevelop.Commit todevelop. Merge a PR (or commit) and open the Actions tab. The job shows thedevelopenvironment. - Acompanhe os passos. Credenciais, login no ECR, build e push, render, deploy. Se o de credenciais falhar, veja os problemas comuns da nota de OIDC.Follow the steps. Credentials, ECR login, build and push, render, deploy. If credentials fail, see the OIDC note’s common problems.
- Confirme a imagem no ECR da conta dev:Confirm the image in the dev account’s ECR:
aws ecr describe-images --repository-name $APP --profile dev \ --query 'sort_by(imageDetails,&imagePushedAt)[-1].{tags:imageTags,pushed:imagePushedAt}' - Veja a troca acontecer. Durante o deploy, o serviço lista duas revisões; quando termina, só a nova:Watch the swap happen. During the deploy the service lists two revisions; when it ends, only the new one:
aws ecs describe-services --cluster meu-cluster --services meu-servico --profile dev \ --query 'services[0].deployments[].[status,rolloutState,runningCount,taskDefinition]' # PRIMARY IN_PROGRESS ... new task definition # ACTIVE COMPLETED ... old task definition (disappears when done) - Confirme o container novo. Em ECS → serviço → Tasks, a task antiga aparece como parada e a nova em execução. A aplicação deve responder com a configuração de dev (por exemplo a URL de API de dev).Confirm the new container. Under ECS → service → Tasks, the old task shows as stopped and the new one running. The application must answer with the dev configuration (for example the dev API URL).
- Promova para produção. Abra um PR de
developparamaine mergeie. O job fica em “Waiting” até um revisor aprovar em Review deployments; depois constrói outra imagem, com os valores de prod, no ECR da conta prod.Promote to production. Open a PR fromdeveloptomainand merge it. The job sits in “Waiting” until a reviewer approves under Review deployments; it then builds another image, with the prod values, in the prod account’s ECR.
7. Alternativa: Docker puro numa EC27. Alternative: plain Docker on an EC2 instance
Se você não usa ECS e roda o container direto numa EC2, o “parar, excluir e criar” é um script na instância, e o workflow o dispara pelo AWS Systems Manager Run Command, sem abrir SSH. Os passos do build, o ECR e o OIDC são os mesmos; só mudam os dois últimos passos do workflow.If you do not use ECS and run the container directly on an EC2 instance, “stop, delete and create” is a script on the instance, and the workflow triggers it through AWS Systems Manager Run Command, with no SSH open. The build, ECR and OIDC steps are the same; only the last two workflow steps change.
Preparar a instância, uma vez por contaPreparing the instance, once per account
- A instância precisa de um instance profile com a política gerenciada
AmazonSSMManagedInstanceCore(para o SSM Agent) e permissão de leitura no ECR (por exemploAmazonEC2ContainerRegistryReadOnly). Em EC2 → Instances → Actions → Security → Modify IAM role.The instance needs an instance profile with the managed policyAmazonSSMManagedInstanceCore(for the SSM Agent) and read permission on ECR (for exampleAmazonEC2ContainerRegistryReadOnly). Under EC2 → Instances → Actions → Security → Modify IAM role. - Confirme em Systems Manager → Fleet Manager que a instância aparece como gerenciada. Docker e AWS CLI instalados nela.Confirm under Systems Manager → Fleet Manager that the instance shows as managed. Docker and the AWS CLI installed on it.
- Salve o script abaixo em
/opt/meu-app/deploy.sh(chmod +x).Save the script below at/opt/meu-app/deploy.sh(chmod +x). - Na role de deploy (OIDC), troque as permissões do ECS por
ssm:SendCommand(nos ARNs do documentoAWS-RunShellScripte da instância) essm:GetCommandInvocation. Crie a variávelEC2_INSTANCE_IDem cada environment.On the deploy role (OIDC), replace the ECS permissions withssm:SendCommand(on the ARNs of theAWS-RunShellScriptdocument and the instance) andssm:GetCommandInvocation. Create theEC2_INSTANCE_IDvariable in each environment.
#!/usr/bin/env bash
# /opt/meu-app/deploy.sh <image>
set -euo pipefail
IMAGE="$1"
REGISTRY="${IMAGE%%/*}"
REGION="$(echo "$REGISTRY" | cut -d. -f4)"
aws ecr get-login-password --region "$REGION" \
| docker login --username AWS --password-stdin "$REGISTRY"
docker pull "$IMAGE" # download first: shortens the downtime
docker stop app || true # stop the running container
docker rm app || true # delete it
docker run -d --name app --restart unless-stopped -p 80:3000 "$IMAGE"
No workflow, remova os passos “Render” e “Deploy to Amazon ECS” e acrescente este:In the workflow, remove the “Render” and “Deploy to Amazon ECS” steps and add this one:
- name: Replace the container on the instance
env:
IMAGE: ${{ steps.build-image.outputs.image }}
INSTANCE_ID: ${{ vars.EC2_INSTANCE_ID }}
run: |
COMMAND_ID=$(aws ssm send-command \
--document-name AWS-RunShellScript \
--instance-ids "$INSTANCE_ID" \
--parameters commands="bash /opt/meu-app/deploy.sh $IMAGE" \
--query Command.CommandId --output text)
aws ssm wait command-executed \
--command-id "$COMMAND_ID" --instance-id "$INSTANCE_ID"
docker stop e o docker run) e nenhum rollback automático: se a versão nova não subir, você precisa reexecutar o deploy de um commit anterior. É o que o ECS resolve para você. Segredos: leia-os do Secrets Manager no próprio script, com a role da instância; não os passe pelo workflow.Here there is always a window with no container (between docker stop and docker run) and no automatic rollback: if the new version does not start, you must redeploy an earlier commit. This is what ECS solves for you. Secrets: read them from Secrets Manager in the script itself, with the instance role; do not pass them through the workflow.Voltar atrás e problemas comunsRolling back and common problems
Voltar para uma versão anteriorGoing back to an earlier version
- Automático. Com o circuit breaker e rollback ativos, uma versão que não sobe é desfeita pelo ECS.Automatic. With the circuit breaker and rollback on, a version that does not start is undone by ECS.
- Manual. Reverta o commit na branch (
git revert) e faça o push, ou use Re-run jobs numa execução antiga que deu certo. Como as tags são imutáveis e únicas por commit, a imagem antiga ainda está no ECR e o workflow a reaproveita.Manual. Revert the commit on the branch (git revert) and push, or use Re-run jobs on an old successful run. Since tags are immutable and unique per commit, the old image is still in ECR and the workflow reuses it.
Problemas comunsCommon problems
- “tag invalid: The image tag ... already exists” no push. O repositório tem tags imutáveis e a tag já existe. A checagem do workflow evita isso; se aparecer, confira se
ecr:DescribeImagesestá na política (sem ela a checagem falha e o workflow tenta construir de novo).“tag invalid: The image tag ... already exists” on push. The repository has immutable tags and the tag already exists. The workflow check avoids it; if it appears, check thatecr:DescribeImagesis in the policy (without it the check fails and the workflow tries to build again). - A task nova não sobe:
CannotPullContainerError. A task execution role não pode baixar do ECR, ou a task está numa sub-rede privada sem caminho até o ECR (NAT ou VPC endpoints). Confira também se a imagem existe com aquela tag.The new task does not start:CannotPullContainerError. The task execution role cannot pull from ECR, or the task is in a private subnet with no path to ECR (NAT or VPC endpoints). Also check that the image exists with that tag. - O deploy fica parado e o serviço registra “unable to stop or start tasks”. Os percentuais do serviço não deixam o scheduler iniciar nem parar nenhuma task (por exemplo 100/100 com 1 task). Ajuste
minimumHealthyPercentoumaximumPercent.The deploy stalls and the service logs “unable to stop or start tasks”. The service percentages do not let the scheduler start or stop any task (for example 100/100 with 1 task). AdjustminimumHealthyPercentormaximumPercent. - A aplicação em dev usa a URL de prod (ou o contrário). Confira as variáveis do environment e o ARN em
AWS_ROLE_ARN: o ID da conta no ARN decide o destino. ARNs trocados entre os dois environments é o erro mais caro desta configuração.The application in dev uses the prod URL (or the reverse). Check the environment variables and the ARN inAWS_ROLE_ARN: the account ID in the ARN decides the destination. Swapped ARNs between the two environments is the costliest mistake in this setup. - Mudei uma variável e o deploy continua com o valor antigo. Reexecutar o mesmo commit reaproveita a imagem. Faça um commit novo (pode ser vazio:
git commit --allow-empty).I changed a variable and the deploy still has the old value. Re-running the same commit reuses the image. Make a new commit (it can be empty:git commit --allow-empty). - O segredo mudou e a app não viu. O ECS lê o segredo quando o container inicia. Faça um novo deploy ou “Force new deployment” no serviço.The secret changed and the app did not see it. ECS reads the secret when the container starts. Run a new deploy or “Force new deployment” on the service.
aws ecr create-repository e aws ecs update-service foram escritos pela referência da CLI e não foram executados aqui: confira os parâmetros com --help na sua versão.Personal note, not verified in the sources: this combination (OIDC, build with per-environment variables, ECR with immutable tags and an ECS swap) does not appear in a single official example; it joins the pages listed below. The aws ecr create-repository and aws ecs update-service commands were written from the CLI reference and were not run here: check the parameters with --help on your version.- AWS Docs · Deploy Amazon ECS services by replacing tasks (rolling update, minimumHealthyPercent, maximumPercent)
- AWS Docs · How the Amazon ECS deployment circuit breaker detects failures
- AWS Docs · Automate the cleanup of images by using lifecycle policies in Amazon ECR
- AWS Docs · Preventing image tags from being overwritten in Amazon ECR
- AWS Docs · AWS Systems Manager Run Command
- Docker Docs · Build secrets with GitHub Actions
- GitHub Docs · Configuring OpenID Connect in Amazon Web Services
- GitHub Docs · Store information in variables
- GitHub Docs · Deploying to Amazon Elastic Container Service
- aws-actions/configure-aws-credentials · amazon-ecr-login · amazon-ecs-render-task-definition · amazon-ecs-deploy-task-definition