How to create IAM roles for deploying your AWS Serverless app
Getting IAM permissions right is one of the hardest parts about building serverless applications on AWS.
Many official tutorials and blog posts cop out of giving you the full details on how to set up IAM, preferring something vague like “ensure you use least-privilege permissions when creating this role”. Or worse, they give you a wide open wildcard or admin-level example policy with a “don’t use this in production” warning. Not very helpful either way.
(To be fair to these authors, production-hardened IAM is difficult and often context-dependent, and it would probably double the length of their article just to explain it.)
I find that the high-level concept of least-privilege is pretty simple to grasp for most engineers—grant an actor in your system just the amount of permissions it needs to do its job and no more. Simple to understand but difficult and time-consuming to implement well.
In the context of serverless applications, there are two general categories of action to which IAM permissions can be applied. I call them run-time and deploy-time. Run-time actions are those that your running application performs, e.g. the AWS services that a Lambda function calls on to when it is invoked. Deploy-time actions are those which deploy the resources and their associated configuration to an AWS account, e.g. creating a DynamoDB table, S3 bucket, Lambda function, etc.
In this article, I’m focusing solely on deploy-time actions, specifically those which deploy resources to AWS via CloudFormation, or an abstraction over CloudFormation (Serverless Framework, CDK, SAM, etc).
In this article, you’ll learn:
- What IAM roles you need to create, along with canned role definitions you can use as your own starting point.
- How each IAM role interacts within the deployment process.
- Why creating a separate role for CloudFormation ensures your CD deployments are more secure.
- Recommended practices to follow when defining your role policy statements.
- How to use your IAM deployment roles in a multi-account AWS environment.
- How to assume your IAM deployer role from a CodeBuild project.
- How to assume your IAM deployer role from GitHub Actions (or any third-party CI/CD provider).
- Using dedicated IAM role for running post-deployment tests.
- Useful tools and further reading you can use to dig deeper.
The two IAM roles you need
Before we talk about specific permissions, let’s look at the two IAM roles you will need to create and how they work together: DeployerRole
and CloudFormationExecutionRole
. Both of these roles must be created inside the account which is the target of the deployment.
Both of these roles should be created within a standalone CloudFormation stack which is independent of (and a prerequisite to) your application workload stacks.
Now let’s walk through each role.
DeployerRole
This role is assumed by the principal who is initiating the deployment process. This will typically be a CI/CD service such as a CodeBuild container or GitHub Actions workflow runner. The CI/CD script will execute a CLI command which will run under the context of this role when making any calls to the AWS API, e.g. sls deploy
for the Serverless Framework or sam deploy
for the AWS SAM CLI.
These deployment framework CLIs make AWS API calls for the following actions:
- Fetching configuration data required to synthesise a new CloudFormation template (e.g. from SSM Parameter Store or other CloudFormation stack exports)
- Creating an S3 bucket for storing deployment state artifacts and metadata within it
- Validating a CloudFormation template that it has just synthesised
- Deploying the CloudFormation template
Once it is ready to deploy the CloudFormation template, the deployment framework can tell the CloudFormation service to assume a specific role—the CloudFormationExecutionRole
–when provisioning the resources defined with the template.
In the Serverless Framework, this can be done via the following setting:
# serverless.yml
provider:
iam:
deploymentRole:
Fn::Sub: 'arn:aws:iam::${AWS::AccountId}:role/MyApp-CloudFormationExecutionRole'
Here’s an example of the policies inside the DeployerRole
for a pipeline that uses the Serverless Framework to deploy the app resources:
# `DeployerRole` policies
Policies:
- PolicyName: DelegateToCloudFormationRole
PolicyDocument:
Version: '2012-10-17'
Statement:
- Action:
- iam:PassRole
Resource:
- !GetAtt CloudFormationExecutionRole.Arn
Effect: Allow
- PolicyName: ServerlessFrameworkCli
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: ValidateCloudFormation
Effect: Allow
Action:
- cloudformation:ValidateTemplate
Resource: '*'
- Sid: ExecuteCloudFormation
Effect: Allow
Action:
- cloudformation:CreateChangeSet
- cloudformation:CreateStack
- cloudformation:DeleteChangeSet
- cloudformation:DeleteStack
- cloudformation:DescribeChangeSet
- cloudformation:DescribeStackEvents
- cloudformation:DescribeStackResource
- cloudformation:DescribeStackResources
- cloudformation:DescribeStacks
- cloudformation:ExecuteChangeSet
- cloudformation:ListStackResources
- cloudformation:SetStackPolicy
- cloudformation:UpdateStack
- cloudformation:UpdateTerminationProtection
- cloudformation:GetTemplate
Resource:
- !Sub 'arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${AppId}-*/*'
- Sid: ReadLambda
Effect: Allow
Action:
- lambda:Get*
- lambda:List*
Resource:
- '*'
- Sid: ManageSlsDeploymentBucket
Effect: Allow
Action:
- s3:CreateBucket
- s3:DeleteBucket
- s3:ListBucket
- s3:PutObject
- s3:GetObject
- s3:DeleteObject
- s3:GetBucketPolicy
- s3:PutBucketPolicy
- s3:DeleteBucketPolicy
- s3:PutBucketAcl
- s3:GetEncryptionConfiguration
- s3:PutEncryptionConfiguration
Resource:
- !Sub 'arn:aws:s3:::${AppId}-*'
- Sid: ListS3
Effect: Allow
Action:
- s3:List*
Resource: '*'
A few things to note here:
- the
iam:PassRole
permission is used to allow the role to delegate toCloudFormationExecutionRole
- the
ServerlessFrameworkCli
inline policy defines statements for the different operations the CLI (and its plugins) might need to make - I’ve used an
${AppId}-*
prefix on theResource
values for the CloudFormation stacks and S3 bucket.AppId
is supplied as a parameter to the CloudFormation template and should be set to the unique name of your application. This naming convention ensures that only application-level stacks (which should all start with this prefix) can be modified by the app and any baseline landing zone stacks (e.g. for auditing) won’t be accessible to this role. Note thatAppId
should not contain the deployment stage within it (dev, staging, prod, etc) because certain deployment frameworks (e.g. the Serverless Framework) append this stage to the very end of the stack and resource names it autogenerates (e.g.myapp-restapi-dev
instead ofmyapp-dev-restapi
).
CloudFormationExecutionRole
The CloudFormationExecutionRole
is where the permissions for deploying the application-specific resources are defined.
You could get away without creating the CloudFormationExecutionRole
and instead have CloudFormation assume the DeployerRole
and define all your permissions within it. However, the deployment itself requires high-privilege permissions which could also be highly destructive (creating, updating and deleting DynamoDB tables and S3 buckets, and data within them).
By separating these out into another role, we ensure they can only be executed by the CloudFormation service, which is inherently a more secure environment than a CodeBuild or GitHub Actions container. This is done by way of the AssumePolicyDocument
which defines which principals are allowed to assume this role:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: cloudformation.amazonaws.com
Action: sts:AssumeRole
Determining deploy-time permissions
So now we need to decide what permissions to grant the CloudFormationExecutionRole
. These permissions will vary depending on your application architecture and there will inevitably be some trial and error involved in getting this permission set right:
- Deploy IAM role
- Deploy stack
- If stack deploy fails, check error message in CloudFormation and update role definition with new permissions
- Go back to 1
Since most serverless apps use a few common services, I’ve included a good starting point below for the policies that you can attach to the CloudFormationExecutionRole
. I’ve grouped each target service into its own policy (e.g. DeployLambdaFunctions
) which should make it easier for you to remove what you don’t need.
# `CloudFormationExecutionRole` policies
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs
Policies:
- PolicyName: DeployLambdaFunctions
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- lambda:Get*
- lambda:List*
- lambda:CreateFunction
- lambda:DeleteFunction
- lambda:CreateFunction
- lambda:DeleteFunction
- lambda:UpdateFunctionConfiguration
- lambda:UpdateFunctionCode
- lambda:PublishVersion
- lambda:CreateAlias
- lambda:DeleteAlias
- lambda:UpdateAlias
- lambda:AddPermission
- lambda:RemovePermission
- lambda:InvokeFunction
Resource:
- !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${AppId}-*'
- PolicyName: DeployLogGroups
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:Get*
- logs:Describe*
- logs:List*
- logs:DeleteLogGroup
- logs:PutResourcePolicy
- logs:DeleteResourcePolicy
- logs:PutRetentionPolicy
- logs:DeleteRetentionPolicy
- logs:TagLogGroup
- logs:UntagLogGroup
Resource:
- !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${AppId}-*'
- !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/http-api/${AppId}-*'
- Effect: Allow
Action:
- logs:Describe*
Resource:
- !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:*'
- Effect: Allow
Action:
- logs:CreateLogDelivery
- logs:DeleteLogDelivery
- logs:DescribeResourcePolicies
- logs:DescribeLogGroups
Resource:
- '*'
- PolicyName: DeployAppBuckets
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: AllBucketPermissions
Effect: Allow
Action:
- s3:ListAllMyBuckets
- s3:ListBucket
Resource: '*'
- Sid: WriteAppBuckets
Effect: Allow
Action:
- s3:Get*
- s3:List*
- s3:CreateBucket
- s3:DeleteBucket
- s3:PutObject
- s3:DeleteObject
- s3:PutBucketPolicy
- s3:DeleteBucketPolicy
- s3:PutEncryptionConfiguration
Resource:
- !Sub 'arn:aws:s3:::${AppId}-*'
- PolicyName: DeployCloudFront
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- cloudfront:Get*
- cloudfront:List*
- cloudfront:CreateDistribution
- cloudfront:UpdateDistribution
- cloudfront:DeleteDistribution
- cloudfront:TagResource
- cloudfront:UntagResource
Resource:
- !Sub 'arn:aws:cloudfront::${AWS::AccountId}:*/*'
- Sid: DeployCloudFrontOriginAccessIdentity
Effect: Allow
Action:
- cloudfront:CreateCloudFrontOriginAccessIdentity
- cloudfront:UpdateCloudFrontOriginAccessIdentity
- cloudfront:GetCloudFrontOriginAccessIdentity
- cloudfront:GetCloudFrontOriginAccessIdentityConfig
- cloudfront:DeleteCloudFrontOriginAccessIdentity
- cloudfront:ListCloudFrontOriginAccessIdentities
Resource: '*'
- PolicyName: DeployLambdaExecutionRoles
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- iam:Get*
- iam:List*
- iam:PassRole
- iam:CreateRole
- iam:DeleteRole
- iam:AttachRolePolicy
- iam:DeleteRolePolicy
- iam:PutRolePolicy
- iam:TagRole
- iam:UntagRole
Resource:
- !Sub 'arn:aws:iam::${AWS::AccountId}:role/${AppId}-*'
- PolicyName: DeployAPIGateway
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- apigateway:GET
- apigateway:POST
- apigateway:PUT
- apigateway:PATCH
- apigateway:DELETE
Resource:
- !Sub 'arn:aws:apigateway:${AWS::Region}::/apis'
- !Sub 'arn:aws:apigateway:${AWS::Region}::/apis/*'
- PolicyName: DeployEventBridge
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- events:Describe*
- events:Get*
- events:List*
- events:CreateEventBus
- events:DeleteEventBus
- events:PutRule
- events:DeleteRule
- events:PutTargets
- events:RemoveTargets
- events:TagResource
- events:UntagResource
Resource:
- !Sub 'arn:aws:events:${AWS::Region}:${AWS::AccountId}:event-bus/${AppId}-*'
- !Sub 'arn:aws:events:${AWS::Region}:${AWS::AccountId}:rule/${AppId}-*'
- PolicyName: DeploySNSTopics
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- sns:Get*
- sns:Describe*
- sns:CreateTopic
- sns:DeleteTopic
- sns:SetTopicAttributes
- sns:Subscribe
- sns:Unsubscribe
- sns:TagResource
Resource:
- !Sub 'arn:aws:sns:${AWS::Region}:${AWS::AccountId}:${AppId}-*'
- PolicyName: DeployDynamoDB
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:CreateTable
- dynamodb:CreateTableReplica
- dynamodb:CreateGlobalTable
- dynamodb:DeleteTable
- dynamodb:DeleteGlobalTable
- dynamodb:DeleteTableReplica
- dynamodb:Describe*
- dynamodb:List*
- dynamodb:Get*
- dynamodb:TagResource
- dynamodb:UntagResource
- dynamodb:UpdateContinuousBackups
- dynamodb:UpdateGlobalTable
- dynamodb:UpdateGlobalTableSettings
- dynamodb:UpdateTable
- dynamodb:UpdateTableReplicaAutoScaling
- dynamodb:UpdateTimeToLive
Resource:
- !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${AppId}-*'
- !Sub 'arn:aws:dynamodb::${AWS::AccountId}:global-table/${AppId}-*'
- PolicyName: DeploySQS
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- sqs:CreateQueue
- sqs:DeleteQueue
- sqs:SetQueueAttributes
- sqs:AddPermission
- sqs:RemovePermission
- sqs:TagQueue
- sqs:UntagQueue
- sqs:Get*
- sqs:List*
Resource:
- !Sub 'arn:aws:sqs:${AWS::Region}:${AWS::AccountId}:${AppId}-*'
- PolicyName: DeploySSMParameterStore
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- ssm:GetParameter*
- ssm:DescribeParameters
- ssm:DeleteParameter*
- ssm:PutParameter
Resource:
- !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${AppId}'
- !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${AppId}/*'
Guidelines on permission definitions
Here are a few guidelines to follow when specifying the CloudFormationExecutionRole
permissions:
- Use a resource prefix pattern where possible (e.g. a short name for your application) to constrain the resources to which the permissions are granted. This works well for most AWS services where the resource ARN is predictable and can be controlled by IaC configuration. However, this isn’t possible with some AWS resources whose ARNs are completely dynamic (e.g. CloudFront distributions and AppSync API endpoints).
- Certain actions aren’t specific to a resource (e.g.
cloudformation:ValidateTemplate
) and so you must setResource: '*'
when declaring these. This feels dirty but is totally fine and does not mean you’re being overly permissive. - Always grant the equivalent Update and Remove/Delete actions to the Create actions. While your stack may deploy successfully the first time (when every action is a create), it may fail whenever an update is required or a resource is removed from the stack and requires deletion. For example, if granting
lambda:CreateFunction
, ensure you also grantlambda:UpdateFunction*
andlambda:DeleteFunction
for the same resource pattern.
Handling cross-account deployments
AWS recommends that you maintain separate accounts for different environments for an application/workload (dev, staging, prod, etc). If you have a CI/CD pipeline that deploys across these stages, how do you handle this?
Firstly, all the above recommendations about creating the two IAM roles remain. These roles should be created in every target account.
Tip: Use OrgFormation to centrally manage the definitions of these IAM roles via Infrastructure-as-Code, and apply them to all target accounts within your AWS Organization via a single CLI command.
In addition to the deployment target accounts, AWS recommends creating a shared “Tools” account to hold resources such as CodePipeline, CodeBuild and any other resources required to support delivery of releases.
The diagram below gives an overview of how each IAM entity is linked inside a GitHub Actions workflow:
Trusting the Tools account from the target accounts
The first step in setting up cross-account deployment, which applies no matter if you’re using CodeBuild or a third party provider, is to instruct the DeployerRole
s in the target accounts to allow an IAM principal within the Tools account to assume its role.
This is done by setting the following AssumeRolePolicyDocument
for the DeployerRole
:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
AWS:
# pick one of the following
- !Sub 'arn:aws:iam::${ToolsAccountId}:role/${AppId}-CodeBuildRole'
- !Sub 'arn:aws:iam::${ToolsAccountId}:user/${AppId}-GitHubActionsUser'
Action: sts:AssumeRole
The value you choose for the the principal here will depend upon on whether you’re using CodeBuild or GitHub Actions.
Assuming IAM deployer role from CodeBuild
If you are using a CodeBuild project to deploy your serverless app, this project will be configured to run using a dedicated IAM role defined in the Tools account. In order to assume the DeployerRole
in the target accounts, your CodeBuild role needs to be granted access via the sts:AssumeRole
action.
The following policy statement can be applied inline in your CodeBuild role to grant it access to the DeployerRole in the dev, staging and prod accounts.
Statement:
- Sid: assumeCrossAccountRoles
Action:
- sts:AssumeRole
Resource:
- !Sub 'arn:aws:iam::${DevAccountId}:role/${AppId}-DeployerRole'
- !Sub 'arn:aws:iam::${StagingAccountId}:role/${AppId}-DeployerRole'
- !Sub 'arn:aws:iam::${ProdAccountId}:role/${AppId}-DeployerRole'
Effect: Allow
Assuming IAM deployer role from CodeBuild from GitHub Actions or third party Continuous Deployment provider
If you’re using GitHub Actions for your Continuous Deployment, the approach is slightly different. IAM roles need to be assumed by an IAM principal. A principal can be an AWS service or an IAM user. For the CodeBuild example, we already had a principal capable of assuming the role (CodeBuild itself), but we don’t for third party services. Therefore, the current method AWS recommend for connecting to it is to create a dedicated IAM user (see here for more info).
In a multi-account AWS environment, this IAM user should be created in the Tools account. This means that the CD provider only needs to store one set of credentials, not a set per target account.
The IAM user should have the same policy statement attached to it as specified above for the CodeBuild role. This policy could be attached inline within the IAM user or alternatively (my preferred approach) define it as a standalone managed policy as follows:
GithubActionsCrossAccountPolicy:
Type: 'AWS::IAM::ManagedPolicy'
Properties:
ManagedPolicyName: !Sub '${AppId}-GithubActionsCrossAccountPolicy'
Description: Policy for user to assume the GitHub Actions cross account role in target accounts.
Path: '/managed/'
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- sts:AssumeRole
Resource:
- !Sub 'arn:aws:iam::${DevAccountId}:role/${AppId}-DeployerRole'
- !Sub 'arn:aws:iam::${StagingAccountId}:role/${AppId}-DeployerRole'
- !Sub 'arn:aws:iam::${ProdAccountId}:role/${AppId}-DeployerRole'
- Effect: Allow
Action:
- sts:TagSession
Resource: '*'
The managed policy approach means that the policy definition provisioning can be managed independently of the IAM user creation.
Once the IAM user and policy are set up, the IAM user credentials can be stored inside GitHub Actions encrypted secrets and the user can be used in the workflow.
The following GHA workflow uses a composite steps action named sls-deploy
(defined beneath it) to deploy a Serverless Framework service through three environments:
# Main Workflow File
name: main-svc-RestApi
# Triggers the workflow on commits to main branch
on:
push:
branches:
- main
env:
SERVICE_FOLDER: services/rest-api
NODEJS_VERSION: 14.x
jobs:
deploy-test:
name: MainPipeline-RestAPI
runs-on: ubuntu-latest
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: eu-west-1
DEV_ACCOUNT_ID: ${{ secrets.DEV_ACCOUNT_ID }}
STAGING_ACCOUNT_ID: ${{ secrets.STAGING_ACCOUNT_ID }}
PROD_ACCOUNT_ID: ${{ secrets.PROD_ACCOUNT_ID }}
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ env.NODEJS_VERSION }}
uses: actions/setup-node@v1
with:
node-version: ${{ env.NODEJS_VERSION }}
# Other steps here include npm install, linting, unit tests, etc
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: 'Deploying to stage: dev'
uses: './.github/actions/sls-deploy'
with:
service-folder: ${{ env.SERVICE_FOLDER }}
stage: dev
aws-account-id: ${{ env.DEV_ACCOUNT_ID }}
# Post-deployment test steps go here
- name: 'Deploying to stage: staging'
uses: './.github/actions/sls-deploy'
with:
service-folder: ${{ env.SERVICE_FOLDER }}
stage: staging
aws-account-id: ${{ env.STAGING_ACCOUNT_ID }}
# Post-deployment test steps go here
- name: 'Deploying to stage: prod'
uses: './.github/actions/sls-deploy'
with:
service-folder: ${{ env.SERVICE_FOLDER }}
stage: prod
aws-account-id: ${{ env.PROD_ACCOUNT_ID }}
# Post-deployment test steps go here
# `.github/actions/sls-deploy/action.yml` file
name: sls-deploy
description: Deploys a Serverless Framework service
inputs:
service-folder:
description: Folder containing serverless.yml to be deployed
required: true
stage:
description: Stage to be deployed to
required: true
aws-account-id:
description: AWS Account Id of target environment
required: true
app-id:
description: Name of the app
required: true
default: slslaunchpad
runs:
using: 'composite'
steps:
- name: 'Deploy: ${{ inputs.service-folder }} [${{ inputs.stage }}]'
working-directory: ${{ inputs.service-folder }}
shell: bash
run: |
CREDS=`aws sts assume-role --role-arn arn:aws:iam::${{ inputs.aws-account-id }}:role/${{ inputs.app-id }}-DeployerRole --role-session-name=gha_deployer`
export AWS_ACCESS_KEY_ID=`echo $CREDS | jq -r '.Credentials.AccessKeyId'`
export AWS_SECRET_ACCESS_KEY=`echo $CREDS | jq -r '.Credentials.SecretAccessKey'`
export AWS_SESSION_TOKEN=`echo $CREDS | jq -r '.Credentials.SessionToken'`
export STAGE=${{inputs.stage}}
export SLS_DEBUG="*"
npm run deploy
There are two key parts of this workflow to note:
- The “Configure AWS credentials” step. This is where the GHA runner authenticates with AWS using the IAM user credentials stored in GitHub secrets.
- The last few lines of the
sls-deploy/action.yml
file where theaws sts assume-role
command assumes theDeployerRole
role inside the target account. The subsequent call tonpm run deploy
is where your deployment framework’s deploy command is executed.
If you would rather not store long-lived IAM user credentials as secrets inside GitHub Actions, check out actions2aws which uses some extra infrastructure inside your AWS account to avoid the need to do this.
Running post-deployment tests under a dedicated role
After you have performed the deployment within your pipeline, you will probably wish to run some tests, be they integration, E2E or smoke tests. If these tests need to call onto the AWS API (e.g. to seed/read data in DynamoDB or publish an event to EventBridge), you can create a separate IAM role with its own least-privilege permission set. Then use the same technique as with the DeployerRole
to have your build step assume this role before running the tests. This approach reduces the risk of your tests deleting or modifying something they shouldn’t.
Need help with production-proofing your IAM setup?
If all this sounds like a lot to do and you'd like some help with getting it set up for a new project, you might be interested in my Serverless Launchpad service.
I can build you a secure, multi-account AWS org, with code structure, dev environments and delivery pipelines all pre-installed, allowing your team to focus on building features for your app.
Further reading & useful tools
- AWS IAM Service Authorization Reference – Official AWS docs documenting granular IAM permissions, principals and resources for every AWS service. Keep this close at hand when defining your deployment policies.
- Effective IAM book by Stephen Kuenzli. “If you’re struggling to deliver effective AWS security policies, this guide will help you understand why it’s hard and how both you and your organization can use IAM well.”
- cfnflip — quickly flip your CloudFormation template (or IAM policy/role) that you copied off some website from JSON to YAML (or vice versa if you really must), maintaining the
!Ref
,!Sub
, etc, intrinsic functions. - actions2aws — AWS IAM roles for GitHub Actions workflows without storing long-lived credentials
- OrgFormation — Superb IaC tool for managing AWS Organization-level landing zone configuration. Basically all the stuff that your application stacks can assume is already in place within your AWS account.
Other articles you might enjoy:
Free Email Course
How to transition your team to a serverless-first mindset
In this 5-day email course, you’ll learn:
- Lesson 1: Why serverless is inevitable
- Lesson 2: How to identify a candidate project for your first serverless application
- Lesson 3: How to compose the building blocks that AWS provides
- Lesson 4: Common mistakes to avoid when building your first serverless application
- Lesson 5: How to break ground on your first serverless project