CI/CD

Deploy from GitHub to AWS, without stored keys.

A push to develop or main deploys to the matching AWS environment, using OpenID Connect instead of long-lived access keys.

Como funcionaHow it 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.

O desenhoThe layout

  • Branch develop → ambiente develop na AWS.Branch develop → develop environment on AWS.
  • Branch main → ambiente production na AWS.Branch main → production environment on AWS.
  • Um GitHub Environment por ambiente, cada um com sua role IAM e suas variáveis.One GitHub Environment per environment, each with its own IAM role and variables.

O exemplo faz deploy de uma imagem Docker no Amazon ECR e atualiza um serviço do Amazon ECS, o mesmo caminho das notas 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.

1. Provedor OIDC na AWS1. OIDC provider in AWS

Faça uma vez por conta: IAM → Identity providers → Add provider → OpenID Connect.Do it once per account: IAM → Identity providers → Add provider → OpenID Connect.

  • Provider URL · https://token.actions.githubusercontent.com
  • Audience · sts.amazonaws.com

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).

aws iam create-open-id-connect-provider --url https://token.actions.githubusercontent.com --client-id-list sts.amazonaws.com

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: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>"
      }
    ]
  }

3. Environments no GitHub3. GitHub Environments

No repositório: Settings → Environments → New environment. Crie develop e production.In the repository: Settings → Environments → New environment. Create develop and production.

  • 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.

4. O workflow4. The workflow

Um único arquivo atende os dois ambientes: o nome do environment sai do nome da branch. Arquivos no repositório:One file serves both environments: the environment name comes from the branch name. Files in the repository:

.github/workflows/deploy.yml
  .aws/task-definition.json     # aws ecs register-task-definition --generate-cli-skeleton
name: 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 }}
          run: |
            docker build -t "$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" .
            docker push "$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
            echo "image=$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> "$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
          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
  • permissions: id-token: write permite pedir o token OIDC e não dá acesso de escrita a nenhum recurso; contents: read é para o checkout.permissions: id-token: write lets the job request the OIDC token and grants no write access to any resource; contents: read is for the checkout.
  • concurrency faz dois pushes na mesma branch não implantarem ao mesmo tempo.concurrency keeps two pushes to the same branch from deploying at the same time.
  • A imagem é marcada com o SHA do commit, então cada deploy é rastreável e reversível.The image is tagged with the commit SHA, so each deploy is traceable and reversible.
  • role-session-name com o ID da execução faz o CloudTrail mostrar qual execução do workflow fez cada chamada.role-session-name with the run ID makes CloudTrail show which workflow run made each call.
  • wait-for-service-stability: true faz o job esperar o serviço estabilizar; se o deploy falhar, o job falha.wait-for-service-stability: true makes the job wait for the service to stabilise; if the deploy fails, the job fails.
Estas actions não são certificadas pelo GitHub. A documentação do GitHub recomenda fixar actions por SHA de commit; os exemplos usam tags (@v6, @v2, @v1) por legibilidade. Em produção, fixe por SHA.These actions are not certified by GitHub. GitHub’s docs recommend pinning actions to a commit SHA; the examples use tags (@v6, @v2, @v1) for readability. In production, pin to a SHA.

Cuidados e problemas comunsSafety and common problems

  • “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: 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.