CI/CDCI/CD

Do push ao container no ar: OIDC, Docker, ECR e ECS.From push to running container: OIDC, Docker, ECR and ECS.

Um push em develop ou main constrói uma imagem Docker com a configuração do ambiente, guarda no ECR e troca o container em execução na conta AWS correspondente, sem chaves de acesso guardadas.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, with no stored access keys.

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.

tudo isto acontece na conta AWS do ambiente (dev ou prod)all of this happens in the environment’s AWS account (dev or prod)1git pushgit pushdevelop ou maindevelop or main2GitHub ActionsGitHub Actionsbuild da imagemimage build3OIDC → STSOIDC → STScredenciais de 1 h1 h credentials4Amazon ECRAmazon ECRguarda a imagemstores the image5Amazon ECSAmazon ECStroca a taskreplaces the taskCada passo só roda se o anterior deu certo; se qualquer um falhar, o container que está no ar continua intocado.Each step runs only if the previous one succeeded; if any fails, the running container stays untouched.A imagem leva como tag o SHA do commit: cada versão é rastreável e pode ser reimplantada.The image is tagged with the commit SHA: every version is traceable and can be redeployed.
O caminho de um commit até o container novo rodando.The path from a commit to the new container running.
  1. Push. O merge de um PR em develop (ou main) registra um evento push e dispara o workflow.Push. Merging a PR into develop (or main) records a push event and triggers the workflow.
  2. Build. O job escolhe o environment pelo nome da branch e constrói a imagem passando como --build-arg as 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.
  3. Credenciais. O GitHub emite um token OIDC; a AWS o troca por credenciais temporárias da role da conta do ambiente (etapas 1 a 3).Credentials. GitHub issues an OIDC token; AWS exchanges it for temporary credentials of the environment account’s role (steps 1 to 3).
  4. 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.
  5. 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.
  6. 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.
E o “parar e excluir o container atual”? No ECS você não roda 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 (etapa 10).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 (step 10).

Como o OIDC funcionaHow OIDC works

Em vez de guardar chaves de acesso da AWS nos secrets do GitHub, o GitHub Actions emite um token OIDC de curta duração para cada execução. A AWS confia no GitHub como provedor de identidade e troca esse token por credenciais temporárias de uma role IAM.Instead of storing AWS access keys in GitHub secrets, GitHub Actions issues a short-lived OIDC token for each run. AWS trusts GitHub as an identity provider and exchanges that token for temporary credentials of an IAM role.

  1. O job pede um token ao GitHub (precisa de id-token: write).The job asks GitHub for a token (it needs id-token: write).
  2. A action aws-actions/configure-aws-credentials envia o token à AWS (sts:AssumeRoleWithWebIdentity).The aws-actions/configure-aws-credentials action sends the token to AWS (sts:AssumeRoleWithWebIdentity).
  3. A AWS confere a trust policy da role: emissor, audiência (aud) e quem está pedindo (sub).AWS checks the role’s trust policy: issuer, audience (aud) and who is asking (sub).
  4. Se bater, devolve credenciais temporárias (por padrão, 1 hora) que os passos seguintes usam.If it matches, AWS returns temporary credentials (1 hour by default) that the following steps use.

Não há segredo para vazar nem para rotacionar. O controle de quem pode fazer deploy passa a morar na trust policy.There is no secret to leak or rotate. Control over who can deploy now lives in the trust policy.

Os nomes: issuer, audience, STSThe names: issuer, audience, STS

Os valores token.actions.githubusercontent.com e sts.amazonaws.com parecem endereços para onde algo é enviado, mas na maior parte do tempo são identificadores que precisam ser iguais nos dois lados. Esta seção separa o que é padrão aberto do que é convenção da AWS.The values token.actions.githubusercontent.com and sts.amazonaws.com look like addresses something is sent to, but most of the time they are identifiers that must be identical on both sides. This section separates what is an open standard from what is an AWS convention.

GlossárioGlossary

TermoTermO que éWhat it is
token.actions.githubusercontent.comO emissor (issuer) dos tokens do GitHub Actions: o serviço do GitHub que assina o JWT de cada execução. Vai no campo iss do token e é o “Provider URL” que você cadastra na AWS. Como tem esse nome, é também um servidor real e público, de onde a AWS busca as chaves de verificação (veja abaixo). É o mesmo emissor para todos os repositórios do GitHub, e é por isso que a condição em sub é obrigatória.The issuer of GitHub Actions tokens: the GitHub service that signs each run’s JWT. It goes in the token’s iss field and is the “Provider URL” you register in AWS. It is also a real, public server, from which AWS fetches the verification keys (see below). It is the same issuer for every repository on GitHub, which is why the condition on sub is mandatory.
sts.amazonaws.comDois papéis com o mesmo nome. (1) É o endereço da API do STS (Security Token Service), o serviço da AWS que emite credenciais temporárias. (2) Neste fluxo, é o valor da audiência (aud): para quem o token se destina. O GitHub coloca esse texto no token, e a AWS compara com a lista de “Audience” do provedor. Nenhuma chamada vai para um “endereço de audiência”; é só uma etiqueta combinada.Two roles with the same name. (1) It is the address of the STS API (Security Token Service), the AWS service that issues temporary credentials. (2) In this flow it is the audience (aud) value: who the token is meant for. GitHub puts that text in the token and AWS compares it with the provider’s “Audience” list. No call goes to an “audience address”; it is just an agreed label.
STSAWS Security Token Service. Recebe uma prova de identidade (aqui, o token do GitHub) e devolve um trio temporário: AccessKeyId, SecretAccessKey e SessionToken, com data de expiração. Existe um endpoint global (sts.amazonaws.com) e endpoints regionais (por exemplo sts.sa-east-1.amazonaws.com).AWS Security Token Service. It receives a proof of identity (here, GitHub’s token) and returns a temporary trio: AccessKeyId, SecretAccessKey and SessionToken, with an expiry time. There is a global endpoint (sts.amazonaws.com) and regional ones (for example sts.sa-east-1.amazonaws.com).
JWTJSON Web Token: três partes em base64url separadas por ponto (cabeçalho, conteúdo e assinatura). O conteúdo é uma lista de claims (afirmações). Qualquer um lê, mas só quem tem a chave privada do emissor consegue assinar.JSON Web Token: three base64url parts separated by dots (header, payload and signature). The payload is a list of claims. Anyone can read it, but only the holder of the issuer’s private key can sign it.
iss · aud · subClaims do JWT. iss: quem emitiu. aud: para quem é. sub: quem é o “sujeito”, aqui montado pelo GitHub a partir do repositório e do environment/branch (tabela na etapa 2).JWT claims. iss: who issued it. aud: who it is for. sub: the “subject”, here built by GitHub from the repository and environment/branch (table in step 2).
JWKSJSON Web Key Set: a lista de chaves públicas do emissor, publicada numa URL (jwks_uri). A AWS baixa essa lista para verificar a assinatura do token.JSON Web Key Set: the issuer’s list of public keys, published at a URL (jwks_uri). AWS downloads this list to verify the token’s signature.
sts:AssumeRoleWithWebIdentityA chamada de API do STS que troca o token por credenciais de uma role. Ela não é assinada com credenciais AWS (não há nenhuma ainda): o próprio token é a credencial. É também o nome da Action na trust policy.The STS API call that exchanges the token for a role’s credentials. It is not signed with AWS credentials (there are none yet): the token itself is the credential. It is also the name of the Action in the trust policy.
Provedor OIDC (IAM)OIDC provider (IAM)O registro, dentro da sua conta, dizendo “aceito tokens deste emissor, com esta audiência”. Não guarda segredo nenhum; guarda só a URL do emissor e a lista de audiências.The record, inside your account, saying “I accept tokens from this issuer, with this audience”. It holds no secret; only the issuer URL and the audience list.

O que a AWS faz com o token, em ordemWhat AWS does with the token, in order

  1. O runner do GitHub pede o token ao emissor (a action faz isso por você quando o job tem id-token: write) e o envia ao STS com AssumeRoleWithWebIdentity, indicando o ARN da role.The GitHub runner asks the issuer for the token (the action does it for you when the job has id-token: write) and sends it to STS with AssumeRoleWithWebIdentity, naming the role ARN.
  2. O STS lê o iss e procura, na conta da role, um provedor OIDC com essa URL. Se não houver, falha.STS reads iss and looks, in the role’s account, for an OIDC provider with that URL. If there is none, it fails.
  3. Busca https://token.actions.githubusercontent.com/.well-known/openid-configuration, de onde tira o jwks_uri, e baixa as chaves públicas. A conexão é validada pelas autoridades raiz confiáveis da AWS; só se o certificado não for de uma delas entram os thumbprints cadastrados.It fetches https://token.actions.githubusercontent.com/.well-known/openid-configuration, takes the jwks_uri from it and downloads the public keys. The connection is validated against AWS’s trusted root CAs; the configured thumbprints only come into play if the certificate is not from one of them.
  4. Verifica a assinatura com a chave pública certa e confere a validade do token (iat e expiração).It verifies the signature with the right public key and checks the token’s validity (iat and expiry).
  5. Confere a audiência: o aud do token precisa estar na lista de Audience do provedor.It checks the audience: the token’s aud must be in the provider’s Audience list.
  6. Avalia a trust policy da role: Principal.Federated é este provedor? As condições em :aud e :sub batem? Só então emite as credenciais temporárias.It evaluates the role’s trust policy: is Principal.Federated this provider? Do the conditions on :aud and :sub match? Only then does it issue the temporary credentials.

Os passos 2 a 5 são a parte “padrão OIDC”; o passo 6 é IAM puro. Por isso, quem controla quem pode fazer deploy é a trust policy: a assinatura só prova que o token veio mesmo do GitHub, não que veio do seu repositório.Steps 2 to 5 are the “standard OIDC” part; step 6 is pure IAM. That is why the trust policy controls who may deploy: the signature only proves the token really came from GitHub, not that it came from your repository.

Dá para olhar com as próprias mãosYou can look at it yourself

As duas URLs abaixo são públicas e não exigem login:The two URLs below are public and need no login:

curl -s https://token.actions.githubusercontent.com/.well-known/openid-configuration
curl -s https://token.actions.githubusercontent.com/.well-known/jwks

A primeira é o documento de descoberta (issuer, jwks_uri, algoritmos de assinatura); a segunda, as chaves públicas. Dentro de um job com id-token: write você também consegue imprimir as claims do token (sem a assinatura) para ver o sub real, útil para depurar uma trust policy:The first is the discovery document (issuer, jwks_uri, signing algorithms); the second, the public keys. Inside a job with id-token: write you can also print the token’s claims (without the signature) to see the real sub, handy for debugging a trust policy:

TOKEN=$(curl -s -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
    "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | jq -r .value)
  echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .

O resultado tem esta forma (valores de exemplo, lista abreviada):The result has this shape (example values, shortened list):

{
    "iss": "https://token.actions.githubusercontent.com",
    "aud": "sts.amazonaws.com",
    "sub": "repo:minha-org/meu-repo:environment:production",
    "repository": "minha-org/meu-repo",
    "ref": "refs/heads/main",
    "environment": "production",
    "job_workflow_ref": "minha-org/meu-repo/.github/workflows/deploy.yml@refs/heads/main",
    "iat": 1790000000,
    "exp": 1790000900
  }
Não imprima o token inteiro no log nem o cole em lugar nenhum. Sem a assinatura (só o trecho do meio) é inofensivo, mas o token completo vale como credencial até expirar.Do not print the whole token in a log or paste it anywhere. Without the signature (only the middle part) it is harmless, but the full token works as a credential until it expires.

É protocolo da AWS?Is it an AWS protocol?

Não. A base é aberta; a AWS só fornece o lado que recebe.No. The base is open; AWS only provides the receiving side.

PartePartDe quem éWhose it is
Formato JWT, JWKS, documento .well-known/openid-configuration, claims iss/aud/subJWT format, JWKS, the .well-known/openid-configuration document, iss/aud/sub claimsPadrões abertos (OpenID Connect Discovery, RFCs de JWT/JWK). Os mesmos que o Google e o Cognito usam; GCP e Azure têm equivalentes para o GitHub (Workload Identity Federation, federated credentials).Open standards (OpenID Connect Discovery, the JWT/JWK RFCs). The same ones Google and Cognito use; GCP and Azure have equivalents for GitHub (Workload Identity Federation, federated credentials).
Quais claims existem e o que cada uma contém (repository, environment, job_workflow_ref...)Which claims exist and what each holds (repository, environment, job_workflow_ref...)GitHub.GitHub.
AssumeRoleWithWebIdentity, STS, role, trust policy, ARN do provedorAssumeRoleWithWebIdentity, STS, role, trust policy, provider ARNAWS (a ideia de trocar um token por credenciais temporárias existe em todas as nuvens, mas cada uma tem a sua API).AWS (swapping a token for temporary credentials exists in every cloud, but each has its own API).
O texto sts.amazonaws.com como audiênciaThe text sts.amazonaws.com as the audienceConvenção. O OIDC deixa a audiência livre. A action configure-aws-credentials pede esse valor por padrão e a documentação da AWS o usa nos exemplos; se você trocar nos dois lados (e na opção audience da action), funciona do mesmo jeito. Um valor próprio só serve para evitar que um token emitido para outro destino seja aceito aqui.Convention. OIDC leaves the audience free. The configure-aws-credentials action asks for this value by default and the AWS docs use it in the examples; if you change it on both sides (and in the action’s audience option) it works the same. A custom value only helps keep a token issued for another destination from being accepted here.
As chaves de condição token.actions.githubusercontent.com:subThe condition keys token.actions.githubusercontent.com:subAWS: é a URL do provedor sem https://, seguida de : e da claim. Para o GitHub, a AWS só expõe aud, sub, amr, email e oaud; claims como repository ou job_workflow_ref não podem ser usadas direto na trust policy.AWS: it is the provider URL without https://, followed by : and the claim. For GitHub, AWS only exposes aud, sub, amr, email and oaud; claims such as repository or job_workflow_ref cannot be used directly in the trust policy.

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.

BranchBranchGitHub EnvironmentGitHub EnvironmentConta AWSAWS accountdevelopenvironment: developvars: role, região, ECR, ECSvars: role, Region, ECR, ECSconta devdev accountECRECRECS (serviço)ECS (service)mainenvironment: production+ aprovação manual+ manual approvalconta prodprod accountECRECRECS (serviço)ECS (service)Mesmo workflow, mesmos nomes de variáveis, valores diferentes: é o valor de AWS_ROLE_ARN que decide a conta.Same workflow, same variable names, different values: AWS_ROLE_ARN decides the account.
Uma branch aponta para um environment do GitHub, e o environment aponta (pela role) para uma conta AWS.One branch maps to a GitHub environment, and the environment maps (through the role) to an AWS account.
  • develop → conta dev. Deploy automático, sem aprovação.develop → dev account. Automatic deploy, no approval.
  • main → conta prod. O environment production pode exigir um revisor antes de o job continuar.main → prod account. The production environment 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 ao on.push.branches e ao mapeamento do environment: no workflow. Nada mais muda.More environments? For a third one (for example staging), create another branch, another environment, another account (or the same pair of accounts with another cluster) and add the branch to on.push.branches and to the environment: mapping in the workflow. Nothing else changes.

O exemplo faz deploy de uma imagem Docker no Amazon ECR e atualiza um serviço do Amazon ECS, o mesmo caminho da nota de AWS. Se o seu destino for outro (S3, EC2, Lambda), as etapas 1 a 3 não mudam; só as permissões da role e os passos de deploy do workflow.The example deploys a Docker image to Amazon ECR and updates an Amazon ECS service, the same path as the AWS notes. If your target is something else (S3, EC2, Lambda), steps 1 to 3 stay the same; only the role’s permissions and the workflow’s deploy steps change.

Se develop e produção ficam em contas AWS diferentes, repita as etapas 1 e 2 em cada conta: o provedor OIDC usado na trust policy precisa estar na mesma conta da role.If develop and production live in different AWS accounts, repeat steps 1 and 2 in each account: the OIDC provider used in a trust policy must be in the same account as the role.

Antes de começarBefore you start

O workflow não cria a infraestrutura: ele só publica a versão nova numa infraestrutura que já existe. Confira se cada ambiente tem o que está abaixo; sem isso, o primeiro deploy falha.The workflow does not create the infrastructure: it only publishes the new version onto infrastructure that already exists. Check that each environment has the items below; without them the first deploy fails.

OndeWhereO que precisa existirWhat must exist
Duas contas AWS (dev e prod)Two AWS accounts (dev and prod)Criadas dentro de uma Organization. Passo a passo na nota de AWS, seção Organizations. Você precisa dos dois IDs de conta e de um perfil da CLI para cada uma (--profile dev e --profile prod).Created inside an Organization. Step by step in the AWS note, Organizations section. You need both account IDs and a CLI profile for each (--profile dev and --profile prod).
Em cada contaIn each accountUm repositório ECR, um cluster e um serviço ECS já rodando (mesmo que com uma imagem de teste), e as duas roles do ECS: task execution role e task role. Nunca criou nada disso? ECS do zero monta tudo passo a passo.An ECR repository, an ECS cluster and a service already running (even with a placeholder image), and the two ECS roles: the task execution role and the task role. Never created these? ECS from zero builds all of them step by step.
No repositórioIn the repositoryUm Dockerfile na raiz, o arquivo .aws/task-definition.json (com um container chamado app) e as branches develop e main.A Dockerfile at the root, the .aws/task-definition.json file (with a container named app) and the develop and main branches.
Na sua máquinaOn your machineAWS CLI v2 e GitHub CLI (gh) autenticados. Opcionais: tudo o que a CLI faz aqui também dá para fazer pelo console.AWS CLI v2 and GitHub CLI (gh) authenticated. Both optional: everything the CLI does here can also be done in the console.

Variáveis usadas nos comandos abaixo (troque pelos seus valores):Variables used in the commands below (replace with your values):

GH_ORG=minha-org            # GitHub owner (user or organization)
  GH_REPO=meu-repo
  REGION=sa-east-1
  # AWS account IDs: <ID_CONTA_DEV> and <ID_CONTA_PROD>

1. Provedor OIDC na AWS1. OIDC provider in AWS

O provedor diz à AWS “confio nos tokens emitidos pelo GitHub Actions”. Ele vive dentro de cada conta, então esta etapa é feita duas vezes: uma na conta dev e outra na prod. Uma conta só precisa dele uma vez, não importa quantos repositórios ou roles use.The provider tells AWS “I trust tokens issued by GitHub Actions”. It lives inside each account, so this step is done twice: once in the dev account and once in prod. An account needs it only once, no matter how many repositories or roles use it.

O que é um identity provider OIDCWhat an OIDC identity provider is

Um identity provider (IdP, provedor de identidade) é um serviço que sabe provar quem é alguém e que outros sistemas aceitam como testemunha. Você já usou vários: ao clicar em “Entrar com Google”, o Google é o IdP e o site que você abriu confia na palavra dele em vez de pedir uma senha nova. OIDC (OpenID Connect) é o protocolo padrão dessa conversa: o IdP entrega um token assinado (um JWT) dizendo “este sujeito é fulano, emitido por mim, para tal destino”, e quem recebe só precisa conferir a assinatura.An identity provider (IdP) is a service that can prove who someone is and that other systems accept as a witness. You have used plenty: when you click “Sign in with Google”, Google is the IdP and the site you opened trusts its word instead of asking for a new password. OIDC (OpenID Connect) is the standard protocol for that conversation: the IdP hands over a signed token (a JWT) saying “this subject is so-and-so, issued by me, for this destination”, and the receiver only has to check the signature.

Aqui o “fulano” não é uma pessoa, é uma execução do GitHub Actions. O GitHub faz o papel de IdP para os seus workflows: a cada job ele assina um token com a identidade daquela execução (repositório, branch, environment, workflow). A AWS faz o papel de quem recebe e precisa decidir se acredita.Here the “someone” is not a person, it is a GitHub Actions run. GitHub plays the IdP for your workflows: for each job it signs a token carrying that run’s identity (repository, branch, environment, workflow). AWS plays the receiver that has to decide whether to believe it.

PapelRoleQuem é aquiWho it is hereO que fazWhat it does
Identity provider (emissor)Identity provider (issuer)GitHub Actions
token.actions.githubusercontent.com
Assina o token de cada execução com a sua chave privada e publica a chave pública.Signs each run’s token with its private key and publishes the public key.
Relying party (quem confia)Relying party (who trusts)AWS (STS)Recebe o token, verifica a assinatura com a chave pública e decide se entrega credenciais.Receives the token, verifies the signature with the public key and decides whether to hand out credentials.
Sujeito (quem se identifica)Subject (who is identified)O job do workflowThe workflow jobCarrega o token do GitHub até a AWS. Não possui nenhuma senha própria.Carries the token from GitHub to AWS. It owns no password of its own.

Então o que é o “provedor OIDC” que criamos na AWS?So what is the “OIDC provider” we create in AWS?

O nome engana: o provedor de identidade de verdade é o GitHub, e ele já existe. O que você cria dentro da conta AWS (IAM → Identity providers) é um registro local que diz “nesta conta, aceito tokens deste emissor, para esta audiência”. É uma lista de confiança, parecida com a lista de autoridades certificadoras do navegador: sem a entrada, o STS recusa qualquer token do GitHub, mesmo com assinatura perfeita, porque não tem motivo para acreditar nele.The name is misleading: the real identity provider is GitHub, and it already exists. What you create inside the AWS account (IAM → Identity providers) is a local record saying “in this account, I accept tokens from this issuer, for this audience”. It is a trust list, similar to the browser’s list of certificate authorities: without the entry, STS rejects any GitHub token, even one with a perfect signature, because it has no reason to believe it.

O registro guarda só duas coisas públicas, a URL do emissor e a audiência. Não há chave, senha nem segredo. Na hora da validação, a AWS descobre sozinha as chaves públicas do GitHub em /.well-known/openid-configuration (o jwks_uri), então você não copia nem rotaciona nada.The record holds only two public things, the issuer URL and the audience. There is no key, password or secret. At validation time, AWS discovers GitHub’s public keys by itself at /.well-known/openid-configuration (the jwks_uri), so you copy and rotate nothing.

Por que isso substitui as chaves de acessoWhy this replaces access keys

  • Antes: uma chave de acesso IAM de longa duração fica guardada nos secrets do GitHub. Quem a copiar (log vazado, fork malicioso, colaborador que sai) usa a conta até alguém notar e rotacionar.Before: a long-lived IAM access key sits in GitHub secrets. Whoever copies it (leaked log, malicious fork, departing collaborator) uses the account until someone notices and rotates it.
  • Agora: nada de valioso é guardado. O token dura minutos, serve só para a audiência sts.amazonaws.com e as credenciais que a AWS devolve expiram em cerca de 1 hora. Um vazamento depois do job terminar não dá acesso a nada.Now: nothing valuable is stored. The token lasts minutes, works only for the sts.amazonaws.com audience, and the credentials AWS returns expire in about 1 hour. A leak after the job ends grants nothing.
  • Quem decide o acesso: o provedor só estabelece que o token é genuíno. Quem restringe qual repositório, branch ou environment pode assumir a role é a trust policy (etapa 2). Sem condições no sub, qualquer repositório do GitHub passaria.Who decides access: the provider only establishes that the token is genuine. What restricts which repository, branch or environment may assume the role is the trust policy (step 2). Without conditions on sub, any GitHub repository would pass.
Cuidado: “o token é do GitHub” não significa “o token é do seu repositório”. Todo repositório do mundo recebe tokens assinados pelo mesmo emissor. Por isso a trust policy precisa fixar o sub (por exemplo, repo:minha-org/meu-repo:environment:production).Careful: “the token is from GitHub” does not mean “the token is from your repository”. Every repository in the world gets tokens signed by the same issuer. That is why the trust policy must pin sub (for example, repo:my-org/my-repo:environment:production).

Criando o provedorCreating the provider

  1. Entre na conta (portal do Identity Center, escolha a conta dev e a role de administrador). Abra IAM → Identity providers → Add provider.Sign in to the account (Identity Center portal, pick the dev account and the administrator role). Open IAM → Identity providers → Add provider.
  2. Em Provider type, escolha OpenID Connect.Under Provider type, choose OpenID Connect.
  3. Provider URL · https://token.actions.githubusercontent.com
  4. Audience · sts.amazonaws.com
  5. Clique em Add provider. Confirme que apareceu na lista, com o ARN arn:aws:iam::<ID_CONTA>:oidc-provider/token.actions.githubusercontent.com.Click Add provider. Confirm it appears in the list, with the ARN arn:aws:iam::<ACCOUNT_ID>:oidc-provider/token.actions.githubusercontent.com.
  6. Troque para a conta prod e repita os passos 1 a 5.Switch to the prod account and repeat steps 1 to 5.

Pela CLI, o comando é o abaixo (confira na referência da CLI se ele pede a lista de thumbprints na sua versão).From the CLI, the command is the one below (check the CLI reference for whether your version asks for a thumbprint list).

for P in dev prod; do
    aws iam create-open-id-connect-provider \
      --url https://token.actions.githubusercontent.com \
      --client-id-list sts.amazonaws.com \
      --profile $P
  done

Se o comando falhar com “EntityAlreadyExists”, o provedor já existe naquela conta e não há nada a fazer.If the command fails with “EntityAlreadyExists”, the provider already exists in that account and there is nothing to do.

Sobre thumbprints: a AWS valida o certificado do endpoint do provedor com a sua biblioteca de autoridades raiz confiáveis, e só recorre aos thumbprints cadastrados se o certificado não for assinado por uma delas.About thumbprints: AWS verifies the provider endpoint’s certificate with its library of trusted root CAs, and only falls back to the configured thumbprints if the certificate is not signed by one of them.

2. Roles IAM e trust policy2. IAM roles and trust policy

Crie uma role por ambiente (por exemplo github-deploy-develop e github-deploy-production). A trust policy diz quem pode assumir a role. A condição em sub é obrigatória: sem ela, qualquer repositório do GitHub poderia pedir acesso.Create one role per environment (for example github-deploy-develop and github-deploy-production). The trust policy says who may assume the role. The sub condition is required: without it, any repository on GitHub could ask for access.

{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Principal": {
          "Federated": "arn:aws:iam::<ACCOUNT_ID>:oidc-provider/token.actions.githubusercontent.com"
        },
        "Action": "sts:AssumeRoleWithWebIdentity",
        "Condition": {
          "StringEquals": {
            "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
            "token.actions.githubusercontent.com:sub": "repo:<ORG>/<REPO>:environment:develop"
          }
        }
      }
    ]
  }

Na role de produção, troque o final do sub por environment:production.On the production role, change the end of sub to environment:production.

Como o sub é montadoHow sub is built

Quando o jobWhen the jobsub
referencia um environmentreferences an environmentrepo:ORG/REPO:environment:NAME
roda de uma branch (sem environment)runs from a branch (no environment)repo:ORG/REPO:ref:refs/heads/BRANCH
roda por pull request (sem environment)runs for a pull request (no environment)repo:ORG/REPO:pull_request
roda de uma tag (sem environment)runs from a tag (no environment)repo:ORG/REPO:ref:refs/tags/TAG
Quando o job referencia um environment, o sub usa a forma environment:NAME e não mais a da branch. Por isso a trust policy acima usa environment:develop, e não ref:refs/heads/develop.When a job references an environment, sub uses the environment:NAME form, not the branch form. That is why the trust policy above uses environment:develop rather than ref:refs/heads/develop.
Repositórios criados depois de 15/07/2026, ou que optaram por isso, usam o formato imutável com IDs do dono e do repositório. Nesse caso uma trust policy só com nomes falha com “Not authorized to perform sts:AssumeRoleWithWebIdentity”. O valor passa a ser, por exemplo: repo:<ORG>@<ORG_ID>/<REPO>@<REPO_ID>:environment:develop.Repositories created after July 15, 2026, or that opted in, use the immutable format with the owner and repository IDs. In that case a name-only trust policy fails with “Not authorized to perform sts:AssumeRoleWithWebIdentity”. The value becomes, for example: repo:<ORG>@<ORG_ID>/<REPO>@<REPO_ID>:environment:develop.

Permissões da roleRole permissions

A role só precisa do mínimo para este deploy. A lista abaixo junta as permissões que as documentações das actions de ECR e ECS indicam; ajuste ARNs e, se usar CodeDeploy, acrescente o que a documentação da action pedir.The role only needs the minimum for this deploy. The list below combines the permissions that the ECR and ECS action docs call for; adjust the ARNs and, if you use CodeDeploy, add what the action docs ask for.

{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Sid": "EcrLogin",
        "Effect": "Allow",
        "Action": "ecr:GetAuthorizationToken",
        "Resource": "*"
      },
      {
        "Sid": "EcrPush",
        "Effect": "Allow",
        "Action": [
          "ecr:BatchGetImage",
          "ecr:DescribeImages",
          "ecr:BatchCheckLayerAvailability",
          "ecr:CompleteLayerUpload",
          "ecr:GetDownloadUrlForLayer",
          "ecr:InitiateLayerUpload",
          "ecr:PutImage",
          "ecr:UploadLayerPart"
        ],
        "Resource": "arn:aws:ecr:<REGION>:<ACCOUNT_ID>:repository/<ECR_REPOSITORY>"
      },
      {
        "Sid": "EcsRegister",
        "Effect": "Allow",
        "Action": "ecs:RegisterTaskDefinition",
        "Resource": "*"
      },
      {
        "Sid": "EcsPassRoles",
        "Effect": "Allow",
        "Action": "iam:PassRole",
        "Resource": [
          "arn:aws:iam::<ACCOUNT_ID>:role/<TASK_ROLE>",
          "arn:aws:iam::<ACCOUNT_ID>:role/<TASK_EXECUTION_ROLE>"
        ]
      },
      {
        "Sid": "EcsDeploy",
        "Effect": "Allow",
        "Action": [
          "ecs:UpdateService",
          "ecs:DescribeServices"
        ],
        "Resource": "arn:aws:ecs:<REGION>:<ACCOUNT_ID>:service/<CLUSTER>/<SERVICE>"
      }
    ]
  }

Criar a role, passo a passoCreating the role, step by step

Uma role por conta: github-deploy-develop na conta dev e github-deploy-production na conta prod. Pelo console:One role per account: github-deploy-develop in the dev account and github-deploy-production in the prod account. In the console:

  1. Na conta dev: IAM → Policies → Create policy → JSON. Cole a política de permissões acima (com os ARNs da conta dev) e salve como github-deploy-develop-policy.In the dev account: IAM → Policies → Create policy → JSON. Paste the permissions policy above (with the dev account ARNs) and save it as github-deploy-develop-policy.
  2. IAM → Roles → Create role. Em Trusted entity type, escolha Web identity; em Identity provider, token.actions.githubusercontent.com; em Audience, sts.amazonaws.com. Preencha a organização e o repositório do GitHub.IAM → Roles → Create role. Under Trusted entity type, choose Web identity; under Identity provider, token.actions.githubusercontent.com; under Audience, sts.amazonaws.com. Fill in the GitHub organization and repository.
  3. Anexe a política criada no passo 1, dê o nome github-deploy-develop e crie a role.Attach the policy created in step 1, name the role github-deploy-develop and create it.
  4. Abra a role → aba Trust relationships → Edit trust policy e ajuste a condição sub para repo:<ORG>/<REPO>:environment:develop, como na trust policy mostrada antes. O assistente do console só oferece a forma por branch; a por environment você escreve à mão.Open the role → Trust relationships tab → Edit trust policy and set the sub condition to repo:<ORG>/<REPO>:environment:develop, as in the trust policy shown earlier. The console wizard only offers the branch form; you write the environment form by hand.
  5. Copie o ARN da role (algo como arn:aws:iam::<ID_CONTA_DEV>:role/github-deploy-develop). Ele vai para a variável AWS_ROLE_ARN do environment develop (etapa 3).Copy the role’s ARN (something like arn:aws:iam::<ID_CONTA_DEV>:role/github-deploy-develop). It goes into the AWS_ROLE_ARN variable of the develop environment (step 3).
  6. Na conta prod, repita tudo com os ARNs da prod, o nome github-deploy-production e environment:production no sub.In the prod account, repeat everything with the prod ARNs, the name github-deploy-production and environment:production in sub.

O mesmo pela CLI, com os JSONs salvos em arquivos (trust-develop.json e deploy-policy-develop.json):The same from the CLI, with the JSONs saved to files (trust-develop.json and deploy-policy-develop.json):

aws iam create-role --role-name github-deploy-develop \
    --assume-role-policy-document file://trust-develop.json --profile dev
  aws iam put-role-policy --role-name github-deploy-develop \
    --policy-name deploy --policy-document file://deploy-policy-develop.json --profile dev
  aws iam get-role --role-name github-deploy-develop --profile dev \
    --query Role.Arn --output text      # <- value for AWS_ROLE_ARN

  # then repeat with --profile prod, github-deploy-production and the prod files
Cada trust policy referencia o provedor da própria conta (o ARN tem o ID da conta). Se você copiar o arquivo da dev para a prod sem trocar o ID, a role cria, mas ninguém consegue assumi-la.Each trust policy references the provider of its own account (the ARN contains the account ID). If you copy the dev file to prod without changing the ID, the role is created but nobody can assume it.

3. Environments e variáveis no GitHub3. GitHub environments and variables

Um Environment é a “porta” entre o repositório e uma role da AWS: é nele que ficam as variáveis daquele ambiente e as regras de quem pode fazer deploy. Passo a passo no repositório:An Environment is the “door” between the repository and an AWS role: it holds that environment’s variables and the rules about who may deploy. Step by step in the repository:

  1. Settings → Environments → New environment, nome develop (exatamente igual ao usado no sub da trust policy), Configure environment.Settings → Environments → New environment, name develop (exactly the same as used in the trust policy’s sub), Configure environment.
  2. Em Deployment branches and tags, escolha Selected branches and tags → Add deployment branch or tag rule → develop.Under Deployment branches and tags, choose Selected branches and tags → Add deployment branch or tag rule → develop.
  3. Em Environment variables → Add variable, crie as cinco variáveis abaixo com os valores da conta dev.Under Environment variables → Add variable, create the five variables below with the dev account values.
  4. Volte e crie o environment production: branch permitida main, Required reviewers marcado com quem aprova, e as mesmas cinco variáveis com os valores da conta prod.Go back and create the production environment: allowed branch main, Required reviewers checked with who approves, and the same five variables with the prod account values.

Os nomes das variáveis são iguais nos dois ambientes; o que muda é o valor. É isso que faz o mesmo workflow mandar o develop para uma conta e o main para outra.The variable names are the same in both environments; the value is what changes. That is what makes the same workflow send develop to one account and main to another.

  • Deployment branches. Em develop, permita só a branch develop; em production, só a main. É o que impede outra branch de obter acesso à role.Deployment branches. On develop, allow only the develop branch; on production, only main. This is what stops another branch from reaching the role.
  • Required reviewers em production (até 6 pessoas ou times; basta uma aprovação), se quiser aprovação manual antes do deploy.Required reviewers on production (up to 6 people or teams; one approval is enough), if you want manual approval before deploying.
  • Variables por ambiente: AWS_ROLE_ARN, AWS_REGION, ECR_REPOSITORY, ECS_CLUSTER, ECS_SERVICE. Ficam disponíveis só para jobs que usam o environment, pelo contexto vars.Variables per environment: AWS_ROLE_ARN, AWS_REGION, ECR_REPOSITORY, ECS_CLUSTER, ECS_SERVICE. They are available only to jobs that use the environment, through the vars context.
O ARN de uma role não é segredo, então variáveis bastam: não há chave nenhuma para guardar.A role ARN is not a secret, so variables are enough: there is no key to store.

A AWS recomenda proteger environments usados em políticas OIDC com regras de proteção, como a restrição de branches e tags de deploy.AWS recommends protecting environments used in OIDC policies with protection rules, such as deployment branch and tag restrictions.

O mesmo pela CLI do GitHubThe same from the GitHub CLI

# create the environment and restrict deployments to one branch
  gh api -X PUT repos/$GH_ORG/$GH_REPO/environments/develop \
    -F 'deployment_branch_policy[protected_branches]=false' \
    -F 'deployment_branch_policy[custom_branch_policies]=true'
  gh api -X POST repos/$GH_ORG/$GH_REPO/environments/develop/deployment-branch-policies \
    -f name=develop -f type=branch

  # variables of the environment (dev account values)
  gh variable set AWS_ROLE_ARN    --env develop --body "arn:aws:iam::<ID_CONTA_DEV>:role/github-deploy-develop"
  gh variable set AWS_REGION      --env develop --body "$REGION"
  gh variable set ECR_REPOSITORY  --env develop --body "meu-app"
  gh variable set ECS_CLUSTER     --env develop --body "meu-cluster"
  gh variable set ECS_SERVICE     --env develop --body "meu-servico"

  gh variable list --env develop     # check

  # repeat for "production" (branch: main; prod account values)

Os corpos da API REST de environments e de branch policies estão na referência “REST API endpoints for deployment environments / branch policies”. Os revisores obrigatórios são mais simples de configurar pela interface.The REST bodies for environments and branch policies are in the “REST API endpoints for deployment environments / branch policies” reference. Required reviewers are simpler to set up in the UI.

As duas variáveis do buildThe two build variables

Além das cinco variáveis acima, acrescente as duas que alimentam o build (etapa 5). Mesmo nome nos dois environments, valor diferente.Besides the five variables above, add the two that feed the build (step 5). Same name in both environments, different value.

VariávelVariableEm developIn developEm productionIn production
APP_ENVAPP_ENVdevelopmentdevelopmentproductionproduction
PUBLIC_API_URLPUBLIC_API_URLhttps://api-dev.exemplo.comhttps://api-dev.example.comhttps://api.exemplo.comhttps://api.example.com
  1. No repositório: Settings → Environments → develop → Environment variables → Add variable.In the repository: Settings → Environments → develop → Environment variables → Add variable.
  2. Crie APP_ENV e PUBLIC_API_URL com os valores de dev.Create APP_ENV and PUBLIC_API_URL with the dev values.
  3. 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"
Use variables, não secrets, para esses dois valores: são públicos (acabam dentro da imagem) e em variável você consegue conferir o que está configurado. Segredos continuam no Secrets Manager.Use variables, not secrets, for these two values: they are public (they end up inside the image) and with a variable you can check what is configured. Secrets stay in Secrets Manager.

4. Secrets: onde ficam e por quê4. Secrets: where they live and why

Pergunta frequente: “a senha do banco vai nos secrets do GitHub ou na AWS?”. Depende de quem precisa ler o valor. Separe três tipos de informação:A common question: “does the database password go in GitHub secrets or in AWS?”. It depends on who needs to read the value. Separate three kinds of information:

O que éWhat it isOnde guardarWhere to keep itPor quêWhy
Credencial da AWS para o deployAWS credential for the deployEm lugar nenhumNowhere (OIDC)O token é emitido por execução e expira. Não existe chave para vazar nem para rotacionar.The token is issued per run and expires. There is no key to leak or rotate.
ARN da role, região, nomes de ECR/cluster/serviçoRole ARN, Region, ECR/cluster/service namesGitHub · Environment variablesNão são segredo e mudam por ambiente. Em variável você consegue ver e conferir o valor.They are not secret and change per environment. In a variable you can see and check the value.
Senha do banco, chaves de API, tokens que a aplicação usa ao rodarDatabase password, API keys, tokens the application uses at runtimeAWS · Secrets Manager (ou SSM Parameter Store SecureString), na conta de cada ambiente(or SSM Parameter Store SecureString), in each environment’s accountQuem lê é o ECS, na hora de iniciar o container, não o pipeline. Veja abaixo.The reader is ECS, when it starts the container, not the pipeline. See below.
Token de terceiro que só o CI usa (registry privado de pacotes, scanner, etc.)Third-party token used only by CI (private package registry, scanner, etc.)GitHub · Environment secrets (ou do repositório)(or repository)A AWS não precisa dele. Só o workflow o lê, então é lá que ele deve ficar.AWS does not need it. Only the workflow reads it, so that is where it belongs.

Por que o segredo da aplicação fica na AWS e não no GitHubWhy the application secret lives in AWS, not GitHub

  • Quem lê é o runtime. A aplicação precisa do valor sempre que uma task sobe: no deploy, mas também em autoscaling, reinício ou troca de host, quando o GitHub não está envolvido. Se o valor só existe nos secrets do GitHub, você teria de reimplantar para qualquer task nova.The reader is the runtime. The application needs the value whenever a task starts: at deploy, but also on autoscaling, restarts or host replacement, when GitHub is not involved. If the value exists only in GitHub secrets, you would have to redeploy for every new task.
  • O pipeline nunca vê o valor. Com o segredo na AWS, o workflow só carrega o ARN dele. Quem tem permissão de rodar workflows (ou uma action de terceiros comprometida) não consegue lê-lo. Segredos do GitHub chegam ao runner como variáveis do job; a documentação avisa que o mascaramento nos logs não é garantido e que valores transformados podem escapar.The pipeline never sees the value. With the secret in AWS, the workflow carries only its ARN. Anyone who can run workflows (or a compromised third-party action) cannot read it. GitHub secrets reach the runner as job variables; the docs warn that log masking is not guaranteed and that transformed values can slip through.
  • Um valor por ambiente, na conta dele. O segredo de prod existe só na conta prod, e o IAM dessa conta decide quem lê. Um desenvolvedor com acesso à dev nunca alcança a senha de produção. O CloudTrail registra cada leitura.One value per environment, in its own account. The prod secret exists only in the prod account, and that account’s IAM decides who reads it. A developer with dev access never reaches the production password. CloudTrail logs every read.
  • Rotação e criptografia nativas. O Secrets Manager rotaciona segredos e criptografa com o KMS. Os secrets do GitHub são criptografados antes de chegar ao GitHub (libsodium), mas ele é um cofre para o CI, sem rotação automática.Native rotation and encryption. Secrets Manager rotates secrets and encrypts them with KMS. GitHub secrets are encrypted before reaching GitHub (libsodium), but it is a vault for CI, with no automatic rotation.
Regra prática: se a aplicação lê, o segredo fica na AWS. Se só o CI lê, fica no GitHub. Se ninguém precisa ler (credencial de deploy), use OIDC e não guarde nada.Rule of thumb: if the application reads it, the secret lives in AWS. If only CI reads it, it lives in GitHub. If nobody needs to read it (deploy credential), use OIDC and store nothing.

Ligando o segredo ao serviço, passo a passoWiring the secret to the service, step by step

  1. Criar o segredo em cada conta, com valores diferentes. Leia o valor de um arquivo ou de um prompt, para não ficar no histórico do shell:Create the secret in each account, with different values. Read the value from a file or a prompt, so it does not stay in shell history:
    aws secretsmanager create-secret --name meu-app/develop/db \
          --secret-string file://db-develop.json --profile dev
      # db-develop.json: {"username":"app","password":"..."}  (delete the file afterwards)
  2. Dar permissão de leitura à task execution role (não à role de deploy). É ela que o ECS usa para buscar o segredo ao iniciar o container. Acrescente à role, em cada conta, uma política como esta (kms:Decrypt só se o segredo usar uma chave KMS própria):Give read permission to the task execution role (not to the deploy role). It is what ECS uses to fetch the secret when starting the container. Add a policy like this to the role, in each account (kms:Decrypt only if the secret uses a customer managed KMS key):
    {
        "Version": "2012-10-17",
        "Statement": [{
          "Effect": "Allow",
          "Action": "secretsmanager:GetSecretValue",
          "Resource": "arn:aws:secretsmanager:<REGION>:<ACCOUNT_ID>:secret:meu-app/develop/db-*"
        }]
      }
  3. Referenciar o segredo no .aws/task-definition.json, em containerDefinitions[].secrets. O ECS injeta o valor como variável de ambiente; o sufixo :password:: escolhe uma chave do JSON:Reference the secret in .aws/task-definition.json, under containerDefinitions[].secrets. ECS injects the value as an environment variable; the :password:: suffix picks one key of the JSON:
    "secrets": [
        {
          "name": "DB_PASSWORD",
          "valueFrom": "arn:aws:secretsmanager:<REGION>:<ACCOUNT_ID>:secret:meu-app/develop/db-AbCdEf:password::"
        }
      ]
  4. Cuidado com o arquivo único. O task-definition.json é o mesmo nos dois ambientes, mas o ARN do segredo tem o ID da conta e o nome do ambiente. Duas saídas: manter um arquivo por ambiente (.aws/task-definition.develop.json e .production.json) ou nomear o segredo igual nas duas contas e montar o ARN com o ID da conta da variável. O exemplo do workflow usa um arquivo só; adapte o caminho se escolher um por ambiente.Watch out for the single file. task-definition.json is the same in both environments, but the secret ARN contains the account ID and the environment name. Two ways out: keep one file per environment (.aws/task-definition.develop.json and .production.json) or name the secret the same in both accounts and build the ARN with the account ID from a variable. The workflow example uses a single file; adapt the path if you choose one per environment.
  5. Deploy. O workflow não muda. A role de deploy só precisa do iam:PassRole sobre a execution role (já está na política da etapa 2) e não precisa de nenhuma permissão no Secrets Manager.Deploy. The workflow does not change. The deploy role only needs iam:PassRole on the execution role (already in the step 2 policy) and needs no Secrets Manager permission.
O ECS busca o valor quando o container inicia. Se você atualizar ou rotacionar o segredo depois, as tasks em execução não recebem o valor novo: é preciso subir tasks novas (por exemplo, um novo deploy ou “Force new deployment” no serviço). Além disso, a aplicação, os logs e as ferramentas de depuração do container enxergam as variáveis de ambiente.ECS fetches the value when the container starts. If you update or rotate the secret later, running tasks do not get the new value: new tasks must be launched (for example a new deploy or “Force new deployment” on the service). Also, the application, logs and debugging tools in the container can see the environment variables.

Quando você usa secrets do GitHubWhen you do use GitHub secrets

  • Crie pela interface (Settings → Secrets and variables → Actions, ou dentro do environment) ou pela CLI: gh secret set NOME --env develop (sem --env, vai para o repositório). O valor não pode ser lido de volta depois.Create them in the UI (Settings → Secrets and variables → Actions, or inside the environment) or from the CLI: gh secret set NAME --env develop (without --env it goes to the repository). The value cannot be read back afterwards.
  • Prefira environment secrets a repository secrets: só chegam a jobs que usam aquele environment e respeitam as regras dele (branch permitida, revisores).Prefer environment secrets over repository secrets: they only reach jobs that use that environment and respect its rules (allowed branch, reviewers).
  • O workflow só lê um secret que você cita de forma explícita (${{ secrets.NOME }}). Passe-o por env: ou with:, nunca escrito direto no comando.A workflow only reads a secret you cite explicitly (${{ secrets.NAME }}). Pass it through env: or with:, never written directly in the command.
  • Secrets não são passados a workflows disparados de forks (exceto o GITHUB_TOKEN), nem a jobs do Dependabot, nem automaticamente a reusable workflows.Secrets are not passed to workflows triggered from forks (except GITHUB_TOKEN), nor to Dependabot jobs, nor automatically to reusable workflows.

5. Configuração por ambiente no Dockerfile5. 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:

BUILD · no GitHub ActionsBUILD · in GitHub ActionsEnvironment variablesEnvironment variablesAPP_ENV, PUBLIC_API_URLAPP_ENV, PUBLIC_API_URLdocker builddocker build--build-arg--build-argImagem no ECRImage in ECRvalores gravadosvalues baked inValor público, fica dentro da imagem.Public value, lives inside the image.RUNTIME · no ECS, quando a task iniciaRUNTIME · in ECS, when the task startsSecrets ManagerSecrets Managerna conta do ambientein the environment’s accountTask execution roleTask execution rolelê o segredoreads the secretSegredo: nunca entra na imagem nem passa pelo pipeline.Secret: never enters the image or goes through the pipeline.ContainerContainerem execuçãorunning
Config pública entra no build; segredo entra só na hora de rodar, vindo do Secrets Manager.Public config enters at build time; secrets enter only at run time, from Secrets Manager.
TipoKindExemploExampleEntra quando e comoEnters when and how
Pública, conhecida no buildPublic, known at build timeURL 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 bundleBuild. 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 timeNível de log, nome da fila, flagsLog level, queue name, flagsRuntime. Campo environment da task definition.Runtime. The task definition’s environment field.
SegredoSecretSenha do banco, chave de API, tokenDatabase password, API key, tokenRuntime. 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"]
Segredo no build é vazamento. Valores de 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.
Nota pessoal, não verificada nas fontes: reconstruir a imagem por ambiente significa que o que você testou em dev não é exatamente o artefato que roda em prod. A alternativa comum é construir uma vez e injetar tudo em runtime. Esta nota segue o seu pedido (imagem por ambiente) e mantém no build só valores públicos, o que reduz esse risco.Personal note, not verified in the sources: rebuilding the image per environment means what you tested in dev is not exactly the artifact that runs in prod. The common alternative is to build once and inject everything at run time. This note follows your request (an image per environment) and keeps only public values at build time, which reduces that risk.

6. O repositório de imagens (ECR)6. 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

  1. Na conta dev, abra Amazon ECR → Private registry → Repositories → Create repository.In the dev account, open Amazon ECR → Private registry → Repositories → Create repository.
  2. Em Repository name, informe meu-app. Ative Tag immutability: uma tag, uma vez enviada, não pode ser sobrescrita.Under Repository name, enter meu-app. Turn on Tag immutability: once pushed, a tag cannot be overwritten.
  3. 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.
  4. 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
Mantenha uma contagem folgada. Se a imagem da versão em execução expirar, uma task nova (autoscaling, reinício) não consegue baixá-la. Trinta imagens deixam bastante margem para voltar atrás.Keep a generous count. If the running version’s image expires, a new task (autoscaling, restart) cannot pull it. Thirty images leave plenty of room to roll back.

7. Como o ECS troca o container7. 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).minimumHealthyPercent is 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).maximumPercent is the upper limit of running tasks during the swap (rounded down).
Padrão: sobe a nova, depois derruba a antigaDefault: start the new one, then stop the old oneminimumHealthyPercent 100 · maximumPercent 200v1v1antesbeforev1v1v2v2duranteduringv2v2depoisafterSem indisponibilidade; por alguns instantes roda o dobro de tasks.No downtime; for a few moments twice the tasks run.Para a antiga, depois sobe a novaStop the old one, then start the new oneminimumHealthyPercent 0 · maximumPercent 100v1v1antesbefore––duranteduringv2v2depoisafterHá uma janela sem container, mas nunca duas versões ao mesmo tempo.There is a window with no container, but never two versions at once.
v1 é a task atual, v2 a nova. O ECS para a antiga e descarta o container dela; você não roda docker stop nem docker rm.v1 is the current task, v2 the new one. ECS stops the old one and discards its container; you never run docker stop or docker rm.

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.

A documentação avisa: ao definir esses percentuais, garanta que o scheduler consiga parar ou iniciar pelo menos uma task. Com 1 task desejada, 100/100 trava o deploy (nem sobe a nova, nem para a antiga) e o serviço registra um evento de erro.The docs warn: when you set these percentages, make sure the scheduler can stop or start at least one task. With 1 desired task, 100/100 stalls the deploy (it can neither start the new one nor stop the old one) and the service logs an error event.

Configurar, passo a passoConfiguring, step by step

  1. 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.
  2. Defina Minimum healthy percent e Maximum percent conforme o desenho acima (a primeira opção é a recomendada).Set Minimum healthy percent and Maximum percent as drawn above (the first option is the recommended one).
  3. 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.
  4. 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.

8. O workflow completo8. The full workflow

Um único arquivo atende os dois ambientes: o nome do environment sai do nome da branch, e as --build-arg vêm das variáveis do environment.One file serves both environments: the environment name comes from the branch name, and the --build-args come from the environment variables.

.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
Complemento recomendado: proteja develop e main com uma regra de branch (Settings → Rules → Rulesets) que exija pull request e a passagem dos testes. Assim ninguém dá push direto e todo deploy passa por revisão. Crie um workflow de testes separado, disparado por pull_request, sem environment: ele não consegue assumir as roles de deploy, porque o sub de um PR (repo:ORG/REPO:pull_request) não bate com nenhuma trust policy.Recommended addition: protect develop and main with a branch rule (Settings → Rules → Rulesets) requiring a pull request and passing tests. That way nobody pushes directly and every deploy goes through review. Create a separate tests workflow triggered by pull_request, without an environment: it cannot assume the deploy roles, because a PR’s sub (repo:ORG/REPO:pull_request) matches no trust policy.

O que cada parte fazWhat each part does

  • on.push.branches. Só develop e main disparam. Uma branch de feature não faz deploy.on.push.branches. Only develop and main trigger. A feature branch does not deploy.
  • environment:. A expressão escolhe production para main e develop para o resto. É isso que seleciona as variáveis (vars.*), a role e as regras de aprovação.environment:. The expression picks production for main and develop otherwise. 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 7.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 7.
  • 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.
As actions não são certificadas pelo GitHub e os exemplos usam tags (@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.

9. Primeiro deploy, passo a passo9. First deploy, step by step

  1. Confira o checklist. Repositório ECR e serviço ECS em cada conta; as sete variáveis em cada environment (as cinco da etapa 3 mais APP_ENV e PUBLIC_API_URL); ecr:DescribeImages na política da role.Check the list. ECR repository and ECS service in each account; the seven variables in each environment (the five from step 3 plus APP_ENV and PUBLIC_API_URL); ecr:DescribeImages in the role policy.
  2. Commit na develop. Mergeie um PR (ou faça um commit) e abra a aba Actions. O job mostra o environment develop.Commit to develop. Merge a PR (or commit) and open the Actions tab. The job shows the develop environment.
  3. Acompanhe os passos. Credenciais, login no ECR, build e push, render, deploy. Se o de credenciais falhar, veja os problemas comuns no fim da página.Follow the steps. Credentials, ECR login, build and push, render, deploy. If credentials fail, see the common problems at the end of the page.
  4. 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}'
  5. 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)
  6. Veja quem fez a chamada. No CloudTrail da conta dev, procure o evento AssumeRoleWithWebIdentity: o nome da sessão é gh-<id da execução>, o que liga a ação da AWS à execução exata do workflow.See who made the call. In the dev account’s CloudTrail, look for the AssumeRoleWithWebIdentity event: the session name is gh-<run id>, which ties the AWS action to the exact workflow run.
  7. 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).
  8. Promova para produção. Abra um PR de develop para main e 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 from develop to main and 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.
Se algo der errado no meio, o container que estava no ar continua rodando: o ECS só para a versão antiga depois que a nova ficou saudável (configuração padrão). Com o circuit breaker, ele volta sozinho para a revisão anterior.If something goes wrong midway, the container that was running keeps running: ECS stops the old version only after the new one is healthy (default configuration). With the circuit breaker, it returns to the previous revision on its own.

10. Alternativa: Docker puro numa EC210. 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.

GitHub ActionsGitHub ActionsSSM Run CommandSSM Run CommandAWS-RunShellScriptAWS-RunShellScriptInstância EC2 (SSM Agent + Docker)EC2 instance (SSM Agent + Docker)1 docker pull a imagem nova1 docker pull the new image2 docker stop app2 docker stop app3 docker rm app3 docker rm app4 docker run a imagem nova4 docker run the new imagea role de deploy só precisa de ssm:SendCommandthe deploy role only needs ssm:SendCommandno documento e na instânciaon the document and the instance
Sem ECS, o workflow manda a instância trocar o container, na ordem: baixar, parar, remover, rodar.Without ECS, the workflow tells the instance to swap the container, in order: pull, stop, remove, run.

Preparar a instância, uma vez por contaPreparing the instance, once per account

  1. 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 exemplo AmazonEC2ContainerRegistryReadOnly). Em EC2 → Instances → Actions → Security → Modify IAM role.The instance needs an instance profile with the managed policy AmazonSSMManagedInstanceCore (for the SSM Agent) and read permission on ECR (for example AmazonEC2ContainerRegistryReadOnly). Under EC2 → Instances → Actions → Security → Modify IAM role.
  2. 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.
  3. Salve o script abaixo em /opt/meu-app/deploy.sh (chmod +x).Save the script below at /opt/meu-app/deploy.sh (chmod +x).
  4. Na role de deploy (OIDC), troque as permissões do ECS por ssm:SendCommand (nos ARNs do documento AWS-RunShellScript e da instância) e ssm:GetCommandInvocation. Crie a variável EC2_INSTANCE_ID em cada environment.On the deploy role (OIDC), replace the ECS permissions with ssm:SendCommand (on the ARNs of the AWS-RunShellScript document and the instance) and ssm:GetCommandInvocation. Create the EC2_INSTANCE_ID variable 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"
Aqui há sempre uma janela sem container (entre o 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.

Como fica em produção realWhat it looks like in real production

O fluxo das etapas 1 a 9 é o mesmo em produção. O que muda é quem cria e quem pode mexer em cada peça. Esta lista mistura o que a AWS e o GitHub recomendam com prática comum de times; o que não vem das fontes oficiais está marcado.The flow in steps 1 to 9 is the same in production. What changes is who creates each piece and who may touch it. This list mixes what AWS and GitHub recommend with common team practice; what does not come from the official sources is marked.

  • Infraestrutura como código. Provedor, roles e políticas ficam em Terraform (ou CloudFormation), revisados por PR, e não criados no console. Assim a trust policy tem histórico e revisão, e recriar uma conta é repetir o código. Veja a nota de Terraform.Infrastructure as code. Provider, roles and policies live in Terraform (or CloudFormation), reviewed through PRs, not created in the console. That gives the trust policy history and review, and rebuilding an account is rerunning the code. See the Terraform note.
  • Um provedor por conta, muitas roles. A AWS recomenda um provedor por IdP externo. Cada repositório ou serviço ganha a sua própria role, com a sua trust policy e permissões só para o que ele faz. Uma role “de deploy para tudo” é o oposto disso.One provider per account, many roles. AWS recommends one provider per external IdP. Each repository or service gets its own role, with its own trust policy and permissions for only what it does. One “deploy everything” role is the opposite.
  • A proteção de produção mora no GitHub, não na AWS. Quando o job usa environment: production, o sub é repo:ORG/REPO:environment:production e não contém a branch. Quem decide que só main chega lá é a regra Deployment branches do environment, junto de Required reviewers. Sem essas regras, qualquer branch cujo workflow declare esse environment obtém a role de produção.Production protection lives in GitHub, not in AWS. When the job uses environment: production, sub is repo:ORG/REPO:environment:production and does not contain the branch. What makes sure only main gets there is the environment’s Deployment branches rule, together with Required reviewers. Without these rules, any branch whose workflow declares that environment gets the production role.
  • Quem pode editar workflows e environments. Quem consegue alterar o workflow ou as regras do environment controla o que a role faz. Em produção: CODEOWNERS sobre .github/workflows/, proteção de branch em main, e poucos administradores no repositório.Who can edit workflows and environments. Whoever can change the workflow or the environment rules controls what the role does. In production: CODEOWNERS on .github/workflows/, branch protection on main, and few repository admins.
  • Restringir por workflow. Como a AWS só lê sub e aud, o jeito de amarrar a role a um arquivo de workflow (por exemplo um workflow reutilizável central) é pedir ao GitHub que inclua job_workflow_ref no formato do sub, pela customização do assunto do OIDC (API do repositório ou da organização). Depois a trust policy compara o sub novo. Confira na documentação do GitHub o formato exato antes, e lembre que mudar o sub quebra as trust policies existentes.Restricting by workflow. Since AWS only reads sub and aud, the way to tie a role to a workflow file (for example a central reusable workflow) is to ask GitHub to include job_workflow_ref in the sub format, through the OIDC subject customization (repository or organization API). The trust policy then compares the new sub. Check GitHub’s docs for the exact format first, and remember that changing sub breaks existing trust policies.
  • Permissões mínimas e nada de chaves. A política da etapa 2 já usa ARNs específicos onde a ação permite. Em produção, revise com o IAM Access Analyzer e considere uma SCP na Organization negando iam:CreateAccessKey e iam:CreateUser na conta de produção, para que ninguém reintroduza chaves de longa duração. (Prática comum, não está no artigo.)Least privilege and no keys. The policy in step 2 already uses specific ARNs where the action allows it. In production, review it with IAM Access Analyzer and consider an SCP in the Organization denying iam:CreateAccessKey and iam:CreateUser in the production account, so nobody reintroduces long-lived keys. (Common practice, not in the article.)
  • Auditoria. Cada troca de token vira um evento AssumeRoleWithWebIdentity no CloudTrail, com o sub e a role. Use role-session-name com o número da execução para ligar a sessão ao run do GitHub, e crie um alerta (EventBridge ou CloudWatch) para trocas de produção fora do horário esperado ou com sub inesperado.Auditing. Every token exchange becomes an AssumeRoleWithWebIdentity event in CloudTrail, with the sub and the role. Use role-session-name with the run number to link the session to the GitHub run, and add an alert (EventBridge or CloudWatch) for production exchanges outside expected hours or with an unexpected sub.
  • Dependências do pipeline. O token só vale para quem roda dentro do job, então uma action de terceiros comprometida enxerga a role. Fixe actions por SHA do commit (não por tag móvel), mantenha poucas e atualize com revisão (Dependabot). (Prática comum, não está no artigo.)Pipeline dependencies. The token works for whoever runs inside the job, so a compromised third-party action sees the role. Pin actions to a commit SHA (not a moving tag), keep few of them and update with review (Dependabot). (Common practice, not in the article.)
  • Recuperação. Se a role for abusada, você pode negar novas sessões e revogar as ativas pelo console do IAM (“Revoke active sessions”) e depois corrigir a trust policy. Como não há chave, não há o que rotacionar.Recovery. If the role is abused, you can deny new sessions and revoke active ones from the IAM console (“Revoke active sessions”) and then fix the trust policy. Since there is no key, there is nothing to rotate.

Exemplo em Terraform do provedor e da role de produção (adapte nomes e ARNs; em versões recentes do provider AWS os thumbprints são opcionais, confira a sua):Terraform example of the provider and the production role (adapt names and ARNs; in recent AWS provider versions thumbprints are optional, check yours):

resource "aws_iam_openid_connect_provider" "github" {
    url            = "https://token.actions.githubusercontent.com"
    client_id_list = ["sts.amazonaws.com"]
  }

  data "aws_iam_policy_document" "trust" {
    statement {
      actions = ["sts:AssumeRoleWithWebIdentity"]
      principals {
        type        = "Federated"
        identifiers = [aws_iam_openid_connect_provider.github.arn]
      }
      condition {
        test     = "StringEquals"
        variable = "token.actions.githubusercontent.com:aud"
        values   = ["sts.amazonaws.com"]
      }
      condition {
        test     = "StringEquals"
        variable = "token.actions.githubusercontent.com:sub"
        values   = ["repo:minha-org/meu-repo:environment:production"]
      }
    }
  }

  resource "aws_iam_role" "deploy_production" {
    name               = "github-deploy-production"
    assume_role_policy = data.aws_iam_policy_document.trust.json
    max_session_duration = 3600
  }
Nota pessoal, não verificada nas fontes: o exemplo em Terraform e as práticas marcadas como “comuns” (SCP, alertas, fixar actions por SHA) vêm de prática geral, não do artigo da AWS. O artigo cobre o provedor, a trust policy com aud e sub, permissões mínimas, um provedor por IdP e auditoria com CloudTrail.Personal note, not verified in the sources: the Terraform example and the practices marked as “common” (SCP, alerts, pinning actions by SHA) come from general practice, not from the AWS article. The article covers the provider, the trust policy with aud and sub, least privilege, one provider per IdP and auditing with CloudTrail.

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 do build e do deployCommon 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:DescribeImages está 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 that ecr:DescribeImages is 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 minimumHealthyPercent ou maximumPercent.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). Adjust minimumHealthyPercent or maximumPercent.
  • 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 in AWS_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.

OIDC, roles e permissõesOIDC, roles and permissions

  • “Credentials could not be loaded” ou “Could not assume role”. Falta permissions: id-token: write no workflow ou o AWS_ROLE_ARN está vazio: confira se o job usa o environment que contém a variável (variáveis de environment só existem para jobs que o declaram).“Credentials could not be loaded” or “Could not assume role”. The workflow lacks permissions: id-token: write or AWS_ROLE_ARN is empty: check that the job uses the environment that holds the variable (environment variables exist only for jobs that declare it).
  • Deploy de dev caiu na conta errada. Confira o ARN em AWS_ROLE_ARN de cada environment: o ID da conta no ARN é o que decide o destino. Os dois environments com ARNs trocados é o erro mais caro desta configuração.A dev deploy landed in the wrong account. Check the ARN in each environment’s AWS_ROLE_ARN: the account ID in the ARN decides the destination. Swapped ARNs between the two environments is the costliest mistake in this setup.
  • AccessDenied em iam:PassRole ou ecs:RegisterTaskDefinition. A política da role de deploy não cobre os ARNs das roles de task da mesma conta (os ARNs têm o ID da conta).AccessDenied on iam:PassRole or ecs:RegisterTaskDefinition. The deploy role’s policy does not cover the task role ARNs of the same account (the ARNs contain the account ID).
  • A task não sobe com “ResourceInitializationError ... unable to pull secrets”. A task execution role não tem secretsmanager:GetSecretValue sobre o segredo (ou o ARN no valueFrom está errado). Não é a role de deploy que precisa dessa permissão.The task fails with “ResourceInitializationError ... unable to pull secrets”. The task execution role lacks secretsmanager:GetSecretValue on the secret (or the ARN in valueFrom is wrong). It is not the deploy role that needs this permission.
  • “Not authorized to perform sts:AssumeRoleWithWebIdentity”. O sub da trust policy não bate com o do token: confira se o job referencia o environment, o nome exato dele e o formato imutável.“Not authorized to perform sts:AssumeRoleWithWebIdentity”. The trust policy’s sub does not match the token’s: check that the job references the environment, its exact name, and the immutable format.
  • Nunca use só curinga no sub. O IAM rejeita uma trust policy do GitHub sem a condição, ou com valor que seja só *.Never use only a wildcard in sub. IAM rejects a GitHub trust policy without the condition, or whose value is only *.
  • Evite ForAllValues: em statements Allow: pode liberar acesso quando a claim está ausente ou escrita errada. Use StringEquals ou StringLike.Avoid ForAllValues: in Allow statements: it can grant access when a claim is absent or misspelled. Use StringEquals or StringLike.
  • Cuidado com gatilhos pull_request_target. Jobs do Dependabot têm event_name igual a dynamic; se a trust policy deve valer só para Actions, restrinja os eventos esperados.Be careful with pull_request_target triggers. Dependabot jobs have event_name set to dynamic; if the trust policy should cover only Actions, restrict the expected events.
  • A AWS não suporta claims OIDC personalizadas; o GitHub permite personalizar o formato do sub, mas crie antes a condição correspondente na AWS.AWS does not support custom OIDC claims; GitHub lets you customise the sub format, but create the matching condition in AWS first.
  • A sessão dura 1 hora por padrão (role-duration-seconds, de 15 min a 12 h). Deploys longos precisam de uma duração maior.The session lasts 1 hour by default (role-duration-seconds, from 15 min to 12 h). Long deploys need a longer duration.
Nota pessoal, não verificada nas fontes: esta combinação (OIDC, build com variáveis por environment, ECR com tags imutáveis e troca no ECS) não aparece num único exemplo oficial; ela junta as páginas listadas abaixo. Os comandos 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.
Nota pessoal, não verificada nas fontes: o exemplo do GitHub para ECS usa chaves de acesso em secrets. A troca delas por OIDC (esta página) segue a documentação de OIDC do GitHub e das actions da AWS, mas a combinação completa não aparece num único exemplo oficial.Personal note, not verified in the sources: GitHub’s own ECS example uses access keys in secrets. Replacing them with OIDC (this page) follows the GitHub and AWS-action OIDC docs, but the full combination does not appear in a single official example.
Fontes oficiais · AWS verificada em 2026-10-26; Docker em 2026-10-24; GitHub em 2026-10-21Official sources · AWS verified on 2026-10-26; Docker on 2026-10-24; GitHub on 2026-10-21
100%