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.
- Push. O merge de um PR em
develop(oumain) registra um eventopushe dispara o workflow.Push. Merging a PR intodevelop(ormain) records apushevent and triggers the workflow. - Build. O job escolhe o environment pelo nome da branch e constrói a imagem passando como
--build-argas variáveis desse environment.Build. The job picks the environment from the branch name and builds the image passing that environment’s variables as--build-arg. - Credenciais. O GitHub emite um token OIDC; a AWS o troca por credenciais temporárias da role da conta do ambiente (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).
- Registro. A imagem sobe para o ECR da mesma conta, com o SHA do commit como tag.Registry. The image is pushed to the ECR of the same account, tagged with the commit SHA.
- Troca. O workflow registra uma task definition nova apontando para a imagem nova e manda o serviço ECS usá-la. O ECS sobe o container novo, espera ele ficar saudável e para o antigo.Swap. The workflow registers a new task definition pointing to the new image and tells the ECS service to use it. ECS starts the new container, waits for it to be healthy and stops the old one.
- Verificação. O job espera o serviço estabilizar. Se a versão nova não ficar saudável, o job falha e o ECS pode desfazer sozinho.Check. The job waits for the service to stabilise. If the new version does not become healthy, the job fails and ECS can roll back on its own.
docker stop e docker rm. O serviço faz a troca (rolling update): a documentação diz que o scheduler “substitui as tasks em execução por tasks novas”. A task antiga é parada e o container dela é descartado junto. Se você usa Docker puro numa EC2, o mesmo passo é manual: veja a alternativa com EC2 (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.
- O job pede um token ao GitHub (precisa de
id-token: write).The job asks GitHub for a token (it needsid-token: write). - A action
aws-actions/configure-aws-credentialsenvia o token à AWS (sts:AssumeRoleWithWebIdentity).Theaws-actions/configure-aws-credentialsaction sends the token to AWS (sts:AssumeRoleWithWebIdentity). - 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). - 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
| TermoTerm | O que éWhat it is |
|---|---|
token.actions.githubusercontent.com | O 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.com | Dois 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. |
| STS | AWS 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). |
| JWT | JSON 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 · sub | Claims 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). |
| JWKS | JSON 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:AssumeRoleWithWebIdentity | A 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
- 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 comAssumeRoleWithWebIdentity, indicando o ARN da role.The GitHub runner asks the issuer for the token (the action does it for you when the job hasid-token: write) and sends it to STS withAssumeRoleWithWebIdentity, naming the role ARN. - O STS lê o
isse procura, na conta da role, um provedor OIDC com essa URL. Se não houver, falha.STS readsissand looks, in the role’s account, for an OIDC provider with that URL. If there is none, it fails. - Busca
https://token.actions.githubusercontent.com/.well-known/openid-configuration, de onde tira ojwks_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 fetcheshttps://token.actions.githubusercontent.com/.well-known/openid-configuration, takes thejwks_urifrom 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. - Verifica a assinatura com a chave pública certa e confere a validade do token (
iate expiração).It verifies the signature with the right public key and checks the token’s validity (iatand expiry). - Confere a audiência: o
auddo token precisa estar na lista de Audience do provedor.It checks the audience: the token’saudmust be in the provider’s Audience list. - Avalia a trust policy da role:
Principal.Federatedé este provedor? As condições em:aude:subbatem? Só então emite as credenciais temporárias.It evaluates the role’s trust policy: isPrincipal.Federatedthis provider? Do the conditions on:audand:submatch? 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/jwksA 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
}É 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.
| PartePart | De 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 claims | Padrõ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 ARN | AWS (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 audience | Convençã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:sub | AWS: é 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.
develop→ conta dev. Deploy automático, sem aprovação.develop→ dev account. Automatic deploy, no approval.main→ conta prod. O environmentproductionpode exigir um revisor antes de o job continuar.main→ prod account. Theproductionenvironment can require a reviewer before the job continues.- Mais ambientes? Para um terceiro (por exemplo
staging), crie outra branch, outro environment, outra conta (ou o mesmo par de contas com outro cluster) e acrescente a branch aoon.push.branchese ao mapeamento doenvironment:no workflow. Nada mais muda.More environments? For a third one (for examplestaging), create another branch, another environment, another account (or the same pair of accounts with another cluster) and add the branch toon.push.branchesand to theenvironment:mapping in the workflow. Nothing else changes.
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.
| OndeWhere | O 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 account | Um 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 repository | Um 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 machine | AWS 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.
| PapelRole | Quem é aquiWho it is here | O que fazWhat it does |
|---|---|---|
| Identity provider (emissor)Identity provider (issuer) | GitHub Actionstoken.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 job | Carrega 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.come 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 thests.amazonaws.comaudience, 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 onsub, any GitHub repository would pass.
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
- Entre na conta (portal do Identity Center, escolha a conta
deve a role de administrador). Abra IAM → Identity providers → Add provider.Sign in to the account (Identity Center portal, pick thedevaccount and the administrator role). Open IAM → Identity providers → Add provider. - Em Provider type, escolha OpenID Connect.Under Provider type, choose OpenID Connect.
- Provider URL ·
https://token.actions.githubusercontent.com - Audience ·
sts.amazonaws.com - 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 ARNarn:aws:iam::<ACCOUNT_ID>:oidc-provider/token.actions.githubusercontent.com. - Troque para a conta
prode repita os passos 1 a 5.Switch to theprodaccount 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
doneSe 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 job | sub |
|---|---|
| referencia um environmentreferences an environment | repo: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 |
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.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:
- 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 asgithub-deploy-develop-policy. - 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. - Anexe a política criada no passo 1, dê o nome
github-deploy-develope crie a role.Attach the policy created in step 1, name the rolegithub-deploy-developand create it. - Abra a role → aba Trust relationships → Edit trust policy e ajuste a condição
subpararepo:<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 thesubcondition torepo:<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. - Copie o ARN da role (algo como
arn:aws:iam::<ID_CONTA_DEV>:role/github-deploy-develop). Ele vai para a variávelAWS_ROLE_ARNdo environmentdevelop(etapa 3).Copy the role’s ARN (something likearn:aws:iam::<ID_CONTA_DEV>:role/github-deploy-develop). It goes into theAWS_ROLE_ARNvariable of thedevelopenvironment (step 3). - Na conta prod, repita tudo com os ARNs da prod, o nome
github-deploy-productioneenvironment:productionnosub.In the prod account, repeat everything with the prod ARNs, the namegithub-deploy-productionandenvironment:productioninsub.
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 files3. 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:
- Settings → Environments → New environment, nome
develop(exatamente igual ao usado nosubda trust policy), Configure environment.Settings → Environments → New environment, namedevelop(exactly the same as used in the trust policy’ssub), Configure environment. - 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. - 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.
- Volte e crie o environment
production: branch permitidamain, Required reviewers marcado com quem aprova, e as mesmas cinco variáveis com os valores da conta prod.Go back and create theproductionenvironment: allowed branchmain, 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 branchdevelop; emproduction, só amain. É o que impede outra branch de obter acesso à role.Deployment branches. Ondevelop, allow only thedevelopbranch; onproduction, onlymain. 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 onproduction(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 contextovars.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 thevarscontext.
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ávelVariable | Em developIn develop | Em productionIn production |
|---|---|---|
APP_ENVAPP_ENV | developmentdevelopment | productionproduction |
PUBLIC_API_URLPUBLIC_API_URL | https://api-dev.exemplo.comhttps://api-dev.example.com | https://api.exemplo.comhttps://api.example.com |
- No repositório: Settings → Environments → develop → Environment variables → Add variable.In the repository: Settings → Environments → develop → Environment variables → Add variable.
- Crie
APP_ENVePUBLIC_API_URLcom os valores de dev.CreateAPP_ENVandPUBLIC_API_URLwith the dev values. - Repita em production com os valores de prod.Repeat in production with the prod values.
gh variable set APP_ENV --env develop --body "development"
gh variable set PUBLIC_API_URL --env develop --body "https://api-dev.example.com"
gh variable set APP_ENV --env production --body "production"
gh variable set PUBLIC_API_URL --env production --body "https://api.example.com"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 is | Onde guardarWhere to keep it | Por quêWhy |
|---|---|---|
| Credencial da AWS para o deployAWS credential for the deploy | Em 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 names | GitHub · Environment variables | Nã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 runtime | AWS · Secrets Manager (ou SSM Parameter Store SecureString), na conta de cada ambiente(or SSM Parameter Store SecureString), in each environment’s account | Quem 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.
Ligando o segredo ao serviço, passo a passoWiring the secret to the service, step by step
- 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) - 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:Decryptsó 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:Decryptonly 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-*" }] } - Referenciar o segredo no
.aws/task-definition.json, emcontainerDefinitions[].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, undercontainerDefinitions[].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::" } ] - 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.jsone.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.jsonis 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.jsonand.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. - Deploy. O workflow não muda. A role de deploy só precisa do
iam:PassRolesobre 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 needsiam:PassRoleon the execution role (already in the step 2 policy) and needs no Secrets Manager permission.
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--envit 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 porenv:ouwith:, nunca escrito direto no comando.A workflow only reads a secret you cite explicitly (${{ secrets.NAME }}). Pass it throughenv:orwith:, 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 (exceptGITHUB_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:
| TipoKind | ExemploExample | Entra quando e comoEnters when and how |
|---|---|---|
| Pública, conhecida no buildPublic, known at build time | URL pública da API, nome do ambiente, ID de analytics, variáveis embutidas no bundle do front-endPublic API URL, environment name, analytics ID, variables embedded in the front-end bundle | Build. GitHub Environment variables → --build-arg → ARG/ENV no Dockerfile.Build. GitHub Environment variables → --build-arg → ARG/ENV in the Dockerfile. |
| Não secreta, só necessária ao rodarNon-secret, needed only at run time | Nível de log, nome da fila, flagsLog level, queue name, flags | Runtime. Campo environment da task definition.Runtime. The task definition’s environment field. |
| SegredoSecret | Senha do banco, chave de API, tokenDatabase password, API key, token | Runtime. Secrets Manager → campo secrets da task definition. Nunca no build.Runtime. Secrets Manager → the task definition’s secrets field. Never at build time. |
Dockerfile de exemploExample Dockerfile
Um app Node com build em duas etapas, só para mostrar onde ficam os ARG. Adapte ao seu stack; a ideia vale para qualquer linguagem.A Node app with a two-stage build, only to show where the ARGs go. Adapt it to your stack; the idea holds for any language.
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
ARG APP_ENV
ARG PUBLIC_API_URL
RUN APP_ENV=$APP_ENV PUBLIC_API_URL=$PUBLIC_API_URL npm run build
FROM node:22-alpine
WORKDIR /app
ARG APP_ENV
ENV NODE_ENV=production APP_ENV=$APP_ENV
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]ARG e ENV ficam gravados nos metadados e nas camadas da imagem, e qualquer pessoa com permissão de baixá-la os lê (por exemplo com docker history). A documentação do Docker recomenda secret mounts (RUN --mount=type=secret) para segredos de build, que não ficam na imagem. Para a senha do banco, use o caminho de runtime da tabela.A secret at build time is a leak. ARG and ENV values are recorded in the image metadata and layers, and anyone allowed to pull it can read them (for example with docker history). Docker’s documentation recommends secret mounts (RUN --mount=type=secret) for build secrets, which do not stay in the image. For the database password, use the runtime path from the table.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
- Na conta dev, abra Amazon ECR → Private registry → Repositories → Create repository.In the dev account, open Amazon ECR → Private registry → Repositories → Create repository.
- Em Repository name, informe
meu-app. Ative Tag immutability: uma tag, uma vez enviada, não pode ser sobrescrita.Under Repository name, entermeu-app. Turn on Tag immutability: once pushed, a tag cannot be overwritten. - Ative Scan on push para analisar vulnerabilidades a cada envio e crie o repositório.Turn on Scan on push to scan for vulnerabilities on every push, and create the repository.
- Repita na conta prod, com o mesmo nome.Repeat in the prod account, with the same name.
for P in dev prod; do
aws ecr create-repository --repository-name $APP \
--image-tag-mutability IMMUTABLE \
--image-scanning-configuration scanOnPush=true \
--region $REGION --profile $P
doneLimpeza 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 prod7. 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).minimumHealthyPercentis the lower limit of healthy tasks during the swap, as a % of the desired count (rounded up).maximumPercenté o limite superior de tasks em execução durante a troca (arredondado para baixo).maximumPercentis the upper limit of running tasks during the swap (rounded down).
Com 1 task desejada, o padrão (100/200) sobe a nova antes de parar a antiga: sem indisponibilidade, desde que a nova passe no health check. Com 0/100 o ECS é forçado a parar a antiga antes: é o equivalente literal de “parar, excluir e criar de novo”, ao custo de uma janela fora do ar.With 1 desired task, the default (100/200) starts the new one before stopping the old one: no downtime, as long as the new one passes the health check. With 0/100 ECS is forced to stop the old one first: the literal equivalent of “stop, delete and create again”, at the cost of a window offline.
Configurar, passo a passoConfiguring, step by step
- No console: ECS → Clusters → seu cluster → seu serviço → Update service, seção de opções de deployment.In the console: ECS → Clusters → your cluster → your service → Update service, deployment options section.
- Defina
Minimum healthy percenteMaximum percentconforme o desenho acima (a primeira opção é a recomendada).SetMinimum healthy percentandMaximum percentas drawn above (the first option is the recommended one). - Ative o deployment circuit breaker com rollback: se as tasks novas não conseguirem iniciar, o ECS marca o deploy como falho e volta para a revisão anterior. A documentação o indica para parar um deploy quando as tasks não sobem.Turn on the deployment circuit breaker with rollback: if the new tasks cannot start, ECS marks the deploy as failed and returns to the previous revision. The docs recommend it for stopping a deployment when tasks cannot start.
- Repita no serviço da outra conta (em prod, mantenha o padrão 100/200 para não ter janela fora do ar).Repeat on the other account’s service (in prod, keep the default 100/200 to avoid an offline window).
aws ecs update-service --cluster meu-cluster --service meu-servico \
--deployment-configuration "minimumHealthyPercent=100,maximumPercent=200,deploymentCircuitBreaker={enable=true,rollback=true}" \
--profile devO 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: truedevelop 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ódevelopemaindisparam. Uma branch de feature não faz deploy.on.push.branches. Onlydevelopandmaintrigger. A feature branch does not deploy.environment:. A expressão escolheproductionparamainedeveloppara o resto. É isso que seleciona as variáveis (vars.*), a role e as regras de aprovação.environment:. The expression picksproductionformainanddevelopotherwise. It is what selects the variables (vars.*), the role and the approval rules.- Build. As variáveis do environment viram
--build-arg. A imagem nasce com os valores do ambiente certo.Build. The environment’s variables become--build-arg. The image is born with the right environment’s values. - Checagem de existência. Com tags imutáveis, enviar de novo a mesma tag dá erro. Se você reexecutar um job do mesmo commit, o workflow pula o build e reaproveita a imagem. Consequência: se mudar uma variável do environment e reexecutar o mesmo commit, a imagem não é refeita; faça um commit novo.Existence check. With immutable tags, pushing the same tag again is an error. If you re-run a job for the same commit, the workflow skips the build and reuses the image. Consequence: if you change an environment variable and re-run the same commit, the image is not rebuilt; make a new commit.
- Render + deploy. A primeira ação grava a imagem nova na task definition; a segunda registra a revisão e atualiza o serviço. A partir daí, o ECS faz a troca descrita na etapa 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.
@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
- 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_ENVePUBLIC_API_URL);ecr:DescribeImagesna política da role.Check the list. ECR repository and ECS service in each account; the seven variables in each environment (the five from step 3 plusAPP_ENVandPUBLIC_API_URL);ecr:DescribeImagesin the role policy. - Commit na
develop. Mergeie um PR (ou faça um commit) e abra a aba Actions. O job mostra o environmentdevelop.Commit todevelop. Merge a PR (or commit) and open the Actions tab. The job shows thedevelopenvironment. - Acompanhe os passos. Credenciais, login no ECR, build e push, render, deploy. Se o de credenciais falhar, veja os problemas comuns 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.
- Confirme a imagem no ECR da conta dev:Confirm the image in the dev account’s ECR:
aws ecr describe-images --repository-name $APP --profile dev \ --query 'sort_by(imageDetails,&imagePushedAt)[-1].{tags:imageTags,pushed:imagePushedAt}' - Veja a troca acontecer. Durante o deploy, o serviço lista duas revisões; quando termina, só a nova:Watch the swap happen. During the deploy the service lists two revisions; when it ends, only the new one:
aws ecs describe-services --cluster meu-cluster --services meu-servico --profile dev \ --query 'services[0].deployments[].[status,rolloutState,runningCount,taskDefinition]' # PRIMARY IN_PROGRESS ... new task definition # ACTIVE COMPLETED ... old task definition (disappears when done) - 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 theAssumeRoleWithWebIdentityevent: the session name isgh-<run id>, which ties the AWS action to the exact workflow run. - Confirme o container novo. Em ECS → serviço → Tasks, a task antiga aparece como parada e a nova em execução. A aplicação deve responder com a configuração de dev (por exemplo a URL de API de dev).Confirm the new container. Under ECS → service → Tasks, the old task shows as stopped and the new one running. The application must answer with the dev configuration (for example the dev API URL).
- Promova para produção. Abra um PR de
developparamaine mergeie. O job fica em “Waiting” até um revisor aprovar em Review deployments; depois constrói outra imagem, com os valores de prod, no ECR da conta prod.Promote to production. Open a PR fromdeveloptomainand merge it. The job sits in “Waiting” until a reviewer approves under Review deployments; it then builds another image, with the prod values, in the prod account’s ECR.
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.
Preparar a instância, uma vez por contaPreparing the instance, once per account
- A instância precisa de um instance profile com a política gerenciada
AmazonSSMManagedInstanceCore(para o SSM Agent) e permissão de leitura no ECR (por exemploAmazonEC2ContainerRegistryReadOnly). Em EC2 → Instances → Actions → Security → Modify IAM role.The instance needs an instance profile with the managed policyAmazonSSMManagedInstanceCore(for the SSM Agent) and read permission on ECR (for exampleAmazonEC2ContainerRegistryReadOnly). Under EC2 → Instances → Actions → Security → Modify IAM role. - Confirme em Systems Manager → Fleet Manager que a instância aparece como gerenciada. Docker e AWS CLI instalados nela.Confirm under Systems Manager → Fleet Manager that the instance shows as managed. Docker and the AWS CLI installed on it.
- Salve o script abaixo em
/opt/meu-app/deploy.sh(chmod +x).Save the script below at/opt/meu-app/deploy.sh(chmod +x). - Na role de deploy (OIDC), troque as permissões do ECS por
ssm:SendCommand(nos ARNs do documentoAWS-RunShellScripte da instância) essm:GetCommandInvocation. Crie a variávelEC2_INSTANCE_IDem cada environment.On the deploy role (OIDC), replace the ECS permissions withssm:SendCommand(on the ARNs of theAWS-RunShellScriptdocument and the instance) andssm:GetCommandInvocation. Create theEC2_INSTANCE_IDvariable in each environment.
#!/usr/bin/env bash
# /opt/meu-app/deploy.sh <image>
set -euo pipefail
IMAGE="$1"
REGISTRY="${IMAGE%%/*}"
REGION="$(echo "$REGISTRY" | cut -d. -f4)"
aws ecr get-login-password --region "$REGION" \
| docker login --username AWS --password-stdin "$REGISTRY"
docker pull "$IMAGE" # download first: shortens the downtime
docker stop app || true # stop the running container
docker rm app || true # delete it
docker run -d --name app --restart unless-stopped -p 80:3000 "$IMAGE"No workflow, remova os passos “Render” e “Deploy to Amazon ECS” e acrescente este:In the workflow, remove the “Render” and “Deploy to Amazon ECS” steps and add this one:
- name: Replace the container on the instance
env:
IMAGE: ${{ steps.build-image.outputs.image }}
INSTANCE_ID: ${{ vars.EC2_INSTANCE_ID }}
run: |
COMMAND_ID=$(aws ssm send-command \
--document-name AWS-RunShellScript \
--instance-ids "$INSTANCE_ID" \
--parameters commands="bash /opt/meu-app/deploy.sh $IMAGE" \
--query Command.CommandId --output text)
aws ssm wait command-executed \
--command-id "$COMMAND_ID" --instance-id "$INSTANCE_ID"docker stop e o docker run) e nenhum rollback automático: se a versão nova não subir, você precisa reexecutar o deploy de um commit anterior. É o que o ECS resolve para você. Segredos: leia-os do Secrets Manager no próprio script, com a role da instância; não os passe pelo workflow.Here there is always a window with no container (between docker stop and docker run) and no automatic rollback: if the new version does not start, you must redeploy an earlier commit. This is what ECS solves for you. Secrets: read them from Secrets Manager in the script itself, with the instance role; do not pass them through the workflow.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, osubérepo:ORG/REPO:environment:productione não contém a branch. Quem decide que sómainchega 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 usesenvironment: production,subisrepo:ORG/REPO:environment:productionand does not contain the branch. What makes sure onlymaingets 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:
CODEOWNERSsobre.github/workflows/, proteção de branch emmain, 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:CODEOWNERSon.github/workflows/, branch protection onmain, and few repository admins. - Restringir por workflow. Como a AWS só lê
subeaud, o jeito de amarrar a role a um arquivo de workflow (por exemplo um workflow reutilizável central) é pedir ao GitHub que incluajob_workflow_refno formato dosub, pela customização do assunto do OIDC (API do repositório ou da organização). Depois a trust policy compara osubnovo. Confira na documentação do GitHub o formato exato antes, e lembre que mudar osubquebra as trust policies existentes.Restricting by workflow. Since AWS only readssubandaud, the way to tie a role to a workflow file (for example a central reusable workflow) is to ask GitHub to includejob_workflow_refin thesubformat, through the OIDC subject customization (repository or organization API). The trust policy then compares the newsub. Check GitHub’s docs for the exact format first, and remember that changingsubbreaks 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:CreateAccessKeyeiam:CreateUserna 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 denyingiam:CreateAccessKeyandiam:CreateUserin the production account, so nobody reintroduces long-lived keys. (Common practice, not in the article.) - Auditoria. Cada troca de token vira um evento
AssumeRoleWithWebIdentityno CloudTrail, com osube a role. Userole-session-namecom 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 comsubinesperado.Auditing. Every token exchange becomes anAssumeRoleWithWebIdentityevent in CloudTrail, with thesuband the role. Userole-session-namewith 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 unexpectedsub. - 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
}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:DescribeImagesestá na política (sem ela a checagem falha e o workflow tenta construir de novo).“tag invalid: The image tag ... already exists” on push. The repository has immutable tags and the tag already exists. The workflow check avoids it; if it appears, check thatecr:DescribeImagesis in the policy (without it the check fails and the workflow tries to build again). - A task nova não sobe:
CannotPullContainerError. A task execution role não pode baixar do ECR, ou a task está numa sub-rede privada sem caminho até o ECR (NAT ou VPC endpoints). Confira também se a imagem existe com aquela tag.The new task does not start:CannotPullContainerError. The task execution role cannot pull from ECR, or the task is in a private subnet with no path to ECR (NAT or VPC endpoints). Also check that the image exists with that tag. - O deploy fica parado e o serviço registra “unable to stop or start tasks”. Os percentuais do serviço não deixam o scheduler iniciar nem parar nenhuma task (por exemplo 100/100 com 1 task). Ajuste
minimumHealthyPercentoumaximumPercent.The deploy stalls and the service logs “unable to stop or start tasks”. The service percentages do not let the scheduler start or stop any task (for example 100/100 with 1 task). AdjustminimumHealthyPercentormaximumPercent. - A aplicação em dev usa a URL de prod (ou o contrário). Confira as variáveis do environment e o ARN em
AWS_ROLE_ARN: o ID da conta no ARN decide o destino. ARNs trocados entre os dois environments é o erro mais caro desta configuração.The application in dev uses the prod URL (or the reverse). Check the environment variables and the ARN inAWS_ROLE_ARN: the account ID in the ARN decides the destination. Swapped ARNs between the two environments is the costliest mistake in this setup. - Mudei uma variável e o deploy continua com o valor antigo. Reexecutar o mesmo commit reaproveita a imagem. Faça um commit novo (pode ser vazio:
git commit --allow-empty).I changed a variable and the deploy still has the old value. Re-running the same commit reuses the image. Make a new commit (it can be empty:git commit --allow-empty). - O segredo mudou e a app não viu. O ECS lê o segredo quando o container inicia. Faça um novo deploy ou “Force new deployment” no serviço.The secret changed and the app did not see it. ECS reads the secret when the container starts. Run a new deploy or “Force new deployment” on the service.
OIDC, roles e permissõesOIDC, roles and permissions
- “Credentials could not be loaded” ou “Could not assume role”. Falta
permissions: id-token: writeno workflow ou oAWS_ROLE_ARNestá 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 lackspermissions: id-token: writeorAWS_ROLE_ARNis 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_ARNde 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’sAWS_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:PassRoleouecs: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 oniam:PassRoleorecs: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:GetSecretValuesobre o segredo (ou o ARN novalueFromestá 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 lackssecretsmanager:GetSecretValueon the secret (or the ARN invalueFromis wrong). It is not the deploy role that needs this permission. - “Not authorized to perform sts:AssumeRoleWithWebIdentity”. O
subda 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’ssubdoes 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 insub. IAM rejects a GitHub trust policy without the condition, or whose value is only*. - Evite
ForAllValues:em statementsAllow: pode liberar acesso quando a claim está ausente ou escrita errada. UseStringEqualsouStringLike.AvoidForAllValues:inAllowstatements: it can grant access when a claim is absent or misspelled. UseStringEqualsorStringLike. - Cuidado com gatilhos
pull_request_target. Jobs do Dependabot têmevent_nameigual adynamic; se a trust policy deve valer só para Actions, restrinja os eventos esperados.Be careful withpull_request_targettriggers. Dependabot jobs haveevent_nameset todynamic; 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 thesubformat, 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.
aws ecr create-repository e aws ecs update-service foram escritos pela referência da CLI e não foram executados aqui: confira os parâmetros com --help na sua versão.Personal note, not verified in the sources: this combination (OIDC, build with per-environment variables, ECR with immutable tags and an ECS swap) does not appear in a single official example; it joins the pages listed below. The aws ecr create-repository and aws ecs update-service commands were written from the CLI reference and were not run here: check the parameters with --help on your version.- AWS Security Blog · Use IAM roles to connect GitHub Actions to actions in AWS
- AWS Docs · Create an OpenID Connect (OIDC) identity provider in IAM (issuer, JWKS, audience, thumbprints)
- AWS Docs · Available keys for AWS OIDC federation (aud, sub, amr, email, oaud)
- AWS Docs · Deploy Amazon ECS services by replacing tasks (rolling update, minimumHealthyPercent, maximumPercent)
- AWS Docs · How the Amazon ECS deployment circuit breaker detects failures
- AWS Docs · Automate the cleanup of images by using lifecycle policies in Amazon ECR
- AWS Docs · Preventing image tags from being overwritten in Amazon ECR
- AWS Docs · AWS Systems Manager Run Command
- Docker Docs · Build secrets with GitHub Actions
- GitHub Docs · Configuring OpenID Connect in Amazon Web Services
- GitHub Docs · Store information in variables
- GitHub Docs · Deploying to Amazon Elastic Container Service
- aws-actions/configure-aws-credentials · amazon-ecr-login · amazon-ecs-render-task-definition · amazon-ecs-deploy-task-definition
- GitHub Docs · OpenID Connect reference (sub claim formats)
- GitHub Docs · Managing environments for deployment
- GitHub Docs · Workflow syntax (push branches, concurrency, permissions, environment)
- GitHub Docs · Secrets · Using secrets in GitHub Actions · Store information in variables
- GitHub Docs · REST API endpoints for deployment branch policies · gh variable set
- AWS Docs · Pass Secrets Manager secrets through Amazon ECS environment variables
- AWS Docs · Amazon ECS task execution IAM role
- AWS Docs · Creating and configuring an organization
- AWS Docs · Create an OpenID Connect (OIDC) identity provider in IAM
- AWS Docs · Create a role for OpenID Connect federation (GitHub section)
- aws-actions/configure-aws-credentials
- aws-actions/amazon-ecr-login
- aws-actions/amazon-ecs-deploy-task-definition