Fearless AWS

Share this post

Automating Infrastructure as Code

fearlessaws.substack.com

Automating Infrastructure as Code

Setting up a full continuous deployment pipeline

Tim Myers
Oct 4, 2022
3
Share this post

Automating Infrastructure as Code

fearlessaws.substack.com
Original photograph by me, outpainted with the help of DALL-E

In the last post, we bootstrapped our first Infrastructure as Code for our AWS account. The deployment of that IaC was still done manually however. In this post, we’ll look at how to set up continuous deployment (CD) of our IaC using Github Actions (GHA).

Thanks for reading Fearless AWS! Subscribe for free to receive new posts and support my work.

Refining our Access Control

Before we can fully automate things to let GHA apply our IaC changes, we need to make some changes and additions to our AWS access control resources that we have so far.

Creating an IAM role

The first step will to be to create an IAM role specifically for managing infrastructure. It’s often considered a best practice to create IAM roles with the permissions necessary for certain functions, and then have other IAM entities (like the user we previously set up for ourselves) assume these roles in order to take actions, as opposed to directly attaching permissions to them.

Here are the resources for our IAM role:

This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Show hidden characters
import * as aws from '@pulumi/aws';
const defaultTags = { Creator: 'pulumi' };
const awsAccountId = pulumi.output(aws.getCallerIdentity()).accountId;
const infrastructureRole = new aws.iam.Role('infrastructure', {
name: 'infrastructure',
assumeRolePolicy: {
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: 'sts:AssumeRole',
Principal: {
AWS: pulumi.interpolate`${awsAccountId}`,
}
}],
},
tags: defaultTags,
})
new aws.iam.RolePolicyAttachment('infrastructure-admin', {
role: infrastructureRole.name,
policyArn: aws.iam.getPolicyOutput({ name: 'SystemAdministrator'}).arn,
});
new aws.iam.RolePolicyAttachment('infrastructure-iam', {
role: infrastructureRole.name,
policyArn: aws.iam.getPolicyOutput({ name: 'IAMFullAccess' }).arn,
});
view raw index.ts hosted with ❤ by GitHub

The IAM role is currently configured so that any IAM entity from the same AWS account is allowed to assume it. We attach the built in SystemAdministrator policy, which is a reasonable starting point for a role that will manage AWS resources. We also attach the built in IAMFullAccess policy, since this is not included in the other policy, and we are managing our IAM resources here.

Using the IAM role

Now that we have a role for managing infrastructure, let’s modify our IAM user so that we can take advantage of it, and decrease the permissions attached directly to it:

This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Show hidden characters
import * as aws from '@pulumi/aws';
const defaultTags = { Creator: 'pulumi' };
const timUser = new aws.iam.User('tim.myers', {
name: 'tim.myers',
tags: defaultTags,
});
new aws.iam.UserPolicyAttachment('tim.myers-readonly', {
user: timUser.name,
policyArn: aws.iam.getPolicyOutput({ name: 'ReadOnlyAccess'}).arn,
});
new aws.iam.UserPolicyAttachment('tim.myers-password', {
user: timUser.name,
policyArn: aws.iam.getPolicyOutput({ name: 'IAMUserChangePassword'}).arn,
});
ew aws.iam.UserPolicy('tim.myers-infrastructure-role', {
user: timUser.name,
policy: {
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: 'sts:AssumeRole',
Resource: infrastructureRole.arn,
}]
},
});
view raw index.ts hosted with ❤ by GitHub

We’ve replaced the AdministratorAccess policy that was previously attached with a much lesser ReadOnlyAccess policy that will still let us navigate and view data in the AWS web console, but won’t let us make any changes. We’ve also added a new policy that lets our user assume the infrastructure IAM role.

With this done, we should also change our IaC configuration to use our new infrastructure role. With pulumi, this is done by configuring the stack:

This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Show hidden characters
config:
aws:region: us-west-2
aws:assumeRole:
roleArn: arn:aws:iam::<AWS Account ID>:role/infrastructure
view raw Pulumi.main.yaml hosted with ❤ by GitHub

With this set, when pulumi is ran, it will automatically first assume our infrastructure role before taking any actions.

Setting up Github Actions

Authenticating Github to AWS

The next step to automating our infrastructure is to set up some resource that will allow GHA to authenticate to our AWS account. Github has documentation on this process, but we’ll go ahead and look at the resources necessary:

This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Show hidden characters
import * as aws from '@pulumi/aws';
const defaultTags = { Creator: 'pulumi' };
const githubOIDC = new aws.iam.OpenIdConnectProvider('github', {
url: 'https://token.actions.githubusercontent.com',
thumbprintLists: ['15e29108718111e59b3dad31954647e3c344a231'],
clientIdLists: ['sts.amazonaws.com'],
tags: defaultTags,
});
const githubRole = new aws.iam.Role('github', {
name: 'github',
assumeRolePolicy: {
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: 'sts:AssumeRoleWithWebIdentity',
Principal: {
Federated: githubOIDC.arn,
},
Condition: {
StringLike: {
'token.actions.githubusercontent.com:sub': 'repo:timmyers/fearless:*',
},
'ForAllValues:StringEquals': {
'token.actions.githubusercontent.com:iss': 'https://token.actions.githubusercontent.com',
'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com',
},
},
}],
},
tags: defaultTags,
});
new aws.iam.RolePolicy('github', {
role: githubRole.name,
name: 'github',
policy: {
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: 'sts:AssumeRole',
Resource: infrastructureRole.arn,
}],
},
});
view raw index.ts hosted with ❤ by GitHub

First we create an OIDC provider. This lets AWS know how to check that authentication claims it receives from Github are valid. Next is a new IAM role that GitHub actions will assume, and its principal is set to the OIDC provider. In the condition, the iss and aud fields are common boilerplate, while the sub field allows for filtering of things like which GitHub repos are allowed. For more detail, see this section of the docs.

Finally, we add a policy to the role that allows it to assume our infrastructure role. When a GHA authenticates with our AWS account, it will first assume the github role, and then may do anything that role is allowed to. In this case, it will be to apply our IaC, which we have now configured to require assuming the infrastructure role first.

Applying IaC in a GHA workflow

With all the IAM changes and additions we created throughout this post, we’re now finally ready to actually create a GHA workflow that can apply our IaC:

This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Show hidden characters
name: pulumi
'on':
push:
branches:
- main
pull_request:
types:
- opened
- synchronize
- reopened
- labeled
permissions:
id-token: write
contents: read
pull-requests: write
jobs:
run-pulumi:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: haya14busa/action-cond@v1
id: pulumi-op
with:
cond: ${{ github.event_name == 'pull_request' }}
if_true: preview
if_false: up
- uses: aws-actions/configure-aws-credentials@v1
with:
role-to-assume: arn:aws:iam::<AWS Account ID>:role/github
aws-region: us-west-2
- run: yarn install
- uses: pulumi/actions@v3
with:
command: ${{ steps.pulumi-op.outputs.value }}
comment-on-pr: true
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
view raw pulumi.yml hosted with ❤ by GitHub

The workflow runs on pushes to the main branch (to apply changes) and on pull requests (to preview changes). It sets the permissions necessary to get a token for authenticating to AWS, as well as to edit pull requests with a comment of the change introduced by it. In the job itself, note that we assume our github role, which will be successful thanks to all the configuration we set up earlier, and then run the appropriate pulumi commands.

Summary

It took a bit of work to get there, but we’ve managed to fully automate our infrastructure! We can now use a GitOps flow to open a pull request with changes to our IaC, where we will see the actions that would be taken as a comment on the PR. Once we are happy with the code diffs and infrastructure preview, we can merge and the changes will automatically be applied to our AWS account! In the process we’ve also made some incremental improvements to our access control, decreasing the permissions attached directly to our user. Hooray!

Share this post

Automating Infrastructure as Code

fearlessaws.substack.com
Comments
TopNewCommunity

No posts

Ready for more?

© 2023 Tim Myers
Privacy ∙ Terms ∙ Collection notice
Start WritingGet the app
Substack is the home for great writing