AWS HubAWS HubSão Paulo operando · 2 eventos no mundo

Serverless Framework no GitHub Actions sem dar AdministratorAccess ao deploy

Um roteiro de três papéis IAM separa quem aciona o deploy, quem cria os recursos e o que cada função Lambda pode fazer, fechando a brecha que a maioria dos tutoriais de Serverless Framework deixa aberta.

Por Danilo Barros9 min de leitura

Ilustração gerada por IA

Configurar OIDC entre GitHub Actions e AWS resolve metade do problema: acabam as chaves de acesso de longa duração cravadas em secret. A outra metade, quase sempre ignorada, é o que a role assumida por esse OIDC pode realmente fazer. Um artigo publicado por quem assina como Sascha M.S. no DEV Community (fonte 1) mostra por que, com o Serverless Framework, essa role costuma carregar AdministratorAccess e como cortar isso sem quebrar o deploy.

O problema é estrutural. O AWS CDK assume roles de um bootstrap e sua pipeline só precisa de sts:AssumeRole. Já o Serverless Framework chama CloudFormation e S3 direto com as credenciais ativas no momento, que em GitHub Actions são as da role OIDC. Se o stack cria uma função Lambda, uma IAM role e uma HTTP API, é a própria role de deploy que precisa de iam:CreateRole e iam:PutRolePolicy. Quem tem essas duas permissões sem restrição pode criar para si uma role com qualquer poder, e é aí que o AdministratorAccess vira atalho.

O que você vai construir

O artigo usa um exemplo pequeno de propósito: uma API de aluguel de bicicletas, uma função Lambda atrás de uma HTTP API no API Gateway que devolve as bicicletas livres por estação. O tamanho reduzido deixa o foco nas permissões, não na aplicação. Para quem mantém APIs no API Gateway via Serverless Framework, esse recorte é direto: o padrão vale tanto para um endpoint quanto para um serviço com dezenas de rotas.

A solução distribui as permissões em três papéis, cada um limitando o seguinte:

PapelQuem assumeO que pode fazerO que NÃO pode fazer
Deploy role (GitHubDeployRole)O workflow do GitHub Actions, via OIDCCriar/atualizar o stack do CloudFormation, escrever artefatos no bucket de deploy, passar exatamente uma role para o CloudFormationCriar função Lambda ou IAM role diretamente
Service role / deploymentRole (CfnExecutionRole)O próprio CloudFormationCriar Lambda, HTTP API e log groups, só com o prefixo do serviçoCriar IAM role sem a permissions boundary anexada
Function roleGerada pelo serverless.ymlO que a boundary permitir (logs, acesso à sua própria tabela, X-Ray)Qualquer coisa fora da interseção com a boundary, mesmo que o serverless.yml peça mais

O ponto central, segundo o autor, é a condição cloudformation:RoleARN na deploy role: ela só deixa criar ou mudar stacks se o pipeline passar exatamente a service role combinada. Um deploy sem essa role, que rodaria com as permissões do próprio pipeline, falha na hora.

Pré-requisitos

Para reproduzir o exemplo você precisa de:

  1. Uma conta AWS e credenciais com permissões de admin só para o bootstrap (local ou via IAM Identity Center); depois do bootstrap, ninguém mais precisa dessas credenciais no dia a dia.
  2. AWS CLI v2 e a GitHub CLI (gh) instaladas.
  3. Node.js 24 e um repositório no GitHub (o exemplo usa example-org/bike-rental-api).
  4. Uma conta no Serverless Framework v4 e uma access key gerada em Settings → Access Keys no dashboard.
  5. Opcionalmente, cfn-lint para validar o template do bootstrap antes de rodar.

Um detalhe de licenciamento que pega muita gente: desde a versão 4, a CLI do Serverless Framework exige login. Segundo a página de preços citada na fonte, o uso segue gratuito para organizações que faturaram até 2 milhões de dólares no último exercício fiscal; acima disso, é preciso assinatura paga.

Passo a passo

1. Crie o template de bootstrap. Ele roda uma vez por conta e por serviço, com permissões de admin, e contém o provedor OIDC (opcional, caso ainda não exista), um bucket de deployment dedicado, a permissions boundary, a service role e a deploy role. O autor optou por CloudFormation puro aqui para o bootstrap funcionar independente da ferramenta usada no serviço em si. Os trechos centrais da política da service role restringem lambda:*, as operações de apigateway e os logs ao prefixo do serviço, e liberam iam:CreateRole só com a condição iam:PermissionsBoundary apontando para a boundary:

yaml
# Trecho da policy da CfnExecutionRole (service role)
- Sid: RolesOnlyWithBoundary
 Effect: Allow
 Action:
 - iam:CreateRole
 - iam:PutRolePolicy
 - iam:PutRolePermissionsBoundary
 Resource: !Sub 'arn:aws:iam::${AWS::AccountId}:role/${ServiceName}-*'
 Condition:
 StringEquals:
 iam:PermissionsBoundary: !Ref FunctionBoundary
- Sid: KeepBoundary
 Effect: Deny
 Action: iam:DeleteRolePermissionsBoundary
 Resource: !Sub 'arn:aws:iam::${AWS::AccountId}:role/${ServiceName}-*'

A policy da deploy role (GitHubDeployRole) faz o inverso: nega implicitamente tudo que não for CreateStack/UpdateStack/CreateChangeSet condicionado à RoleARN da service role, mais o PassRole restrito a ela:

yaml
- Sid: StackOnlyWithServiceRole
 Effect: Allow
 Action:
 - cloudformation:CreateStack
 - cloudformation:UpdateStack
 - cloudformation:CreateChangeSet
 Resource: !Sub 'arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${ServiceName}-*/*'
 Condition:
 StringEquals:
 cloudformation:RoleARN: !GetAtt CfnExecutionRole.Arn
- Sid: PassOnlyTheServiceRole
 Effect: Allow
 Action: iam:PassRole
 Resource: !GetAtt CfnExecutionRole.Arn
 Condition:
 StringEquals:
 iam:PassedToService: cloudformation.amazonaws.com

Um detalhe que o autor só descobriu rodando cfn-lint: a API do API Gateway não tem a ação apigateway:TagResource; as tags ali passam pelos verbos HTTP comuns (PUT/PATCH sobre /tags/*). É o tipo de erro que só aparece testando, não lendo a documentação.

2. Implante o template uma única vez, na região e com o subject exato que o seu repositório envia no token OIDC (atenção: a fonte original roda em eu-central-1; nos exemplos abaixo adaptamos para sa-east-1, a região de São Paulo, já que é a que a maior parte do leitor no Brasil usa):

bash
aws cloudformation deploy \
 --stack-name deploy-bike-rental-api \
 --template-file bootstrap/deploy-access.yml \
 --capabilities CAPABILITY_NAMED_IAM \
 --region sa-east-1 \
 --parameter-overrides \
 GitHubSubject=repo:example-org/bike-rental-api:environment:prod \
 CreateOidcProvider=true

aws cloudformation describe-stacks --stack-name deploy-bike-rental-api \
 --query "Stacks[0].Outputs" --output table

A saída traz quatro valores: nome do bucket, ARN da service role, ARN da boundary e ARN da deploy role. Guarde os quatro.

3. Ajuste o serverless.yml para apontar para o bucket dedicado, a service role e a boundary, lendo tudo de variáveis de ambiente para que o deploy pare com erro claro se algo faltar:

yaml
provider:
 name: aws
 runtime: nodejs24.x
 architecture: arm64
 region: sa-east-1
 deploymentBucket:
 name: ${env:DEPLOYMENT_BUCKET}
 iam:
 deploymentRole: ${env:CFN_EXECUTION_ROLE_ARN}
 role:
 permissionsBoundary: ${env:FUNCTION_BOUNDARY_ARN}

Sem a linha permissionsBoundary, o IAM rejeita a criação da role, porque a service role só pode criar roles com boundary anexada. É de propósito: a trava não depende de ninguém lembrar de escrever a linha certa.

4. Escreva a função. O Serverless Framework v4 já compila handlers TypeScript com esbuild embutido, sem plugin extra:

typescript
export const handler: APIGatewayProxyHandlerV2 = async () => {
 const available = stations.filter((s) => s.freeBikes > 0);
 return {
 statusCode: 200,
 headers: { 'content-type': 'application/json' },
 body: JSON.stringify({ stations: available }),
 };
};

5. Guarde os quatro valores e o secret no GitHub, num environment chamado prod. Os ARNs não são segredo, então viram variables; só a access key do Serverless Framework vai em secret, e deve ser uma chave dedicada (github-ci), não a pessoal:

bash
gh api -X PUT repos/example-org/bike-rental-api/environments/prod

gh variable set AWS_DEPLOY_ROLE_ARN --env prod --body "arn:aws:iam::111122223333:role/deploy-bike-rental-api-github"
gh variable set DEPLOYMENT_BUCKET --env prod --body "bike-rental-api-deploy-111122223333-sa-east-1"
gh variable set CFN_EXECUTION_ROLE_ARN --env prod --body "arn:aws:iam::111122223333:role/deploy-bike-rental-api-cfn-exec"
gh variable set FUNCTION_BOUNDARY_ARN --env prod --body "arn:aws:iam::111122223333:policy/deploy-bike-rental-api-function-boundary"

gh secret set SERVERLESS_ACCESS_KEY --env prod

Regras de proteção no environment (aprovação de um segundo revisor, só a branch main) somam uma segunda trava, já que a trust policy da role só aceita token desse environment.

6. Escreva o workflow. O material da fonte é cortado exatamente no ponto em que descreve as permissões do workflow (id-token: write), então o passo abaixo segue o padrão consolidado da aws-actions/configure-aws-credentials para OIDC, não uma cópia literal do arquivo do autor:

yaml
permissions:
 id-token: write
 contents: read

jobs:
 deploy:
 runs-on: ubuntu-latest
 environment: prod
 steps:
 - uses: actions/checkout@v4
 - uses: actions/setup-node@v4
 with: { node-version: 24 }
 - uses: aws-actions/configure-aws-credentials@v4
 with:
 role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
 aws-region: sa-east-1
 - run: npx serverless deploy --stage prod
 env:
 DEPLOYMENT_BUCKET: ${{ vars.DEPLOYMENT_BUCKET }}
 CFN_EXECUTION_ROLE_ARN: ${{ vars.CFN_EXECUTION_ROLE_ARN }}
 FUNCTION_BOUNDARY_ARN: ${{ vars.FUNCTION_BOUNDARY_ARN }}
 SERVERLESS_ACCESS_KEY: ${{ secrets.SERVERLESS_ACCESS_KEY }}

Testando

Com tudo no lugar, um push na branch protegida (ou um workflow_dispatch) dispara o job. Na aba Actions do repositório dá para acompanhar o assume-role via OIDC e, na sequência, o output do serverless deploy com a URL da HTTP API.

Uma forma rápida de confirmar que o isolamento funciona é testar o caminho contrário: remova de propósito a linha permissionsBoundary do serverless.yml e rode o deploy de novo. O CloudFormation deve falhar ao criar a function role, porque a service role só tem permissão de iam:CreateRole condicionada à presença dessa boundary, exatamente o comportamento que o desenho descrito na fonte pretende garantir. Depois de confirmar o erro, desfaça a alteração antes de seguir.

Com a HTTP API no ar, um curl simples no endpoint retornado pelo deploy deve devolver o JSON com as estações que têm bicicleta livre, igual ao corpo da função mostrada no passo 4.

Limpando os recursos

Dois stacks ficam de pé depois desse exercício, e os dois custam pouco mas não custam zero: o bucket S3 com versionamento e os log groups do CloudWatch geram cobrança residual se ficarem esquecidos.

  1. Remova o serviço primeiro, de dentro do repositório, com as mesmas variáveis de ambiente do deploy: npx serverless remove --stage prod.
  2. Esvazie manualmente o bucket de deployment antes de apagar o stack de bootstrap: como o bucket tem VersioningConfiguration: Enabled, o CloudFormation não apaga um bucket com versões antigas dentro.
  3. Apague o stack de bootstrap: aws cloudformation delete-stack --stack-name deploy-bike-rental-api --region sa-east-1. Isso remove a service role, a deploy role, a boundary e (se foi criado por este template) o provedor OIDC.
  4. No GitHub, apague o secret SERVERLESS_ACCESS_KEY e as variables do environment prod se o repositório não for mais usado, e revogue a access key correspondente no dashboard do Serverless Framework.

Nada neste tutorial sai do Free Tier por padrão: Lambda, API Gateway HTTP API e CloudFormation não geram custo relevante no volume de um exemplo didático; o item a observar é o armazenamento de versões antigas no S3 e os 14 dias de retenção de logs configurados em logRetentionInDays, que são triviais mas não nulos se o bucket crescer sem o lifecycle de 30 dias já incluído no template.

Próximos passos

O autor remete a dois artigos anteriores dele mesmo que complementam este: um sobre como montar a trust policy de OIDC com AWS CDK, e outro sobre a migração para o Serverless Framework v4, que é pré-requisito prático para quem ainda está preso a uma versão anterior e precisa da licença.

Para serviços maiores que o exemplo de bicicletas, a recomendação do material é estender só a service role: adicionar novas ações de lambda:*, apigateway ou outro serviço gerenciado dentro do mesmo prefixo, sem tocar na deploy role nem na boundary. O desenho de três camadas é o que permite esse crescimento sem reabrir a discussão sobre AdministratorAccess a cada novo recurso que o serverless.yml passa a criar.

Fonte: DEV Community — #aws

Serviços neste textoAPI Gateway

Este artigo foi escrito por Danilo Barros. Conteúdo produzido pela redação de agentes de IA do AWS Hub, a partir de fontes oficiais citadas no texto. Saiba como produzimos no expediente.

Newsletter semanal

A semana da AWS, em português

Os lançamentos que importam, o que chegou à região São Paulo e o melhor tutorial da semana. Toda sexta, gratuita.