In Part 1, we did the basic setup and started creating/importing repositories.
In Part 2, we migrated the state to a remote storage and made it so the Terraform could be ran anywhere.
For Part 3, let's add a deployment process so you can properly gate and version control your changes. This is pretty overkill for your personal GitHub, but it's crucial when you're working in production/enterprise environments.
Credentials
In the previous two posts, we've relied on CLI's installed on your machine to make things work. The AWS CLI provided credentials to your AWS account and the GitHub CLI handled the same for your GitHub account. Obviously, we can't rely on that when we're running on machines that are not owned by us. Wouldn't really want to anyways.
So we'll need to create and save the appropriate credentials for both services.
Note: you can probably do this through Terraform using ephemeral resources, but that's a bit heavy for all of this.
GitHub PAT
While logged into GitHub in your browser, go to Settings > Developer Settings > Personal Access Tokens > Fine-grained Personal Access Tokens (or just click this link) and create a new token.
You will only need this token to have Administration Read and Write permissions. Which, I use the word only in square quotes a bit. I strongly recommend setting this with a short expiration date so you have to rotate it regularly and it expires when you're not using it. It's a burden worth having when something has admin rights.
Once you have that token, you'll go to the Repository Settings > Secrets and Variables > Actions > New Repository Secret and save the token with the name GH_PAT. This will be used later when we setup the workflow.
We're set there, now let's work on the AWS credentials.
AWS Credentials
Honestly, I was going to try and write this all up, but there's a lot to it and I can't really beat the AWS Docs on the topic. My contribution here will be that you should give the service account the AmazonEC2FullAccess role. This will allow it to read and write to the Terraform State we created in Part 2.
Another note that I will add is that I found the Trusted Entity page a little different than the current docs, so here's how I set mine up. It's pretty straightforward, but I thought it worth calling out.
You can be more fine-grained than that, but for this example we'll be a little more permissive. Especially considering the use case.
Once you have the role created, you'll want to copy the ARN of the role and add it to the Actions Secrets as AWS_ROLE_ARN. While this isn't strictly necessary, I like to keep things like that stored as a secret. It makes it easier to swap out down the road and if you have multiple people working on a project/multiple ARNs for roles, you can make sure that copy pasta is scoped correctly or it breaks.
GitHub Actions Workflow
Structuring
GitHub Actions Workflows live in the .github/workflows path. You can create that with:
# Makes the directory
mkdir -p .github/workflows
# Creates the deploy workflow
touch .github/workflows/deploy.yml
I also like to move all of my *.tf files into a directory called .tf so it cleans up the root of the repository.
# Makes the directory
mkdir .tf
# Moves all of the files
mv *.tf ./.tf
It's worth running a terraform init and a terraform plan in that directory to make sure that everything is still good. Just a bit of a sanity check before you start pushing things up.
The Actual Workflow
(Note: as I'm writing this, GitHub Actions is having "service degradation" so I'm somewhat air-coding. I'll come back and update if I find out a missed anything.)
The full workflow lives here as a reference, but I want to call out a few sections and what they do and why they're there.
Credentials Handling
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
Here we tell the whole workflow to use the PAT we created as its token. This helps us limit the token and makes it possible to adjust scope as we need.
# Configure AWS Credentials
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v5.1.1
with:
aws-region: us-east-1
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
role-session-name: ${{ github.actor }}-github-actions
This step makes sure that we've defined the role that the actor will use. It's possible that we may have other roles we want different pipelines to use, so that's where having that stored as a secret works for us.
Terraform Plan
jobs:
terraform-plan:
name: 'Terraform Plan'
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event_name == 'pull_request'
outputs:
tfplanExitCode: ${{ steps.tf-plan.outputs.exitcode }}
defaults:
run:
shell: bash
working-directory: ./.tf/
[...]
This is quite the action here, but I'll explain what it does in bullets.
- Check out the repo and all of its code.
- Configures the AWS credentials so the action runner can authenticate with AWS.
- Installs Terraform on the runner.
- Initializes our Terraform configuration
- Checks the formatting of our Terraform configuration.
- This isn't necessary , but it does make sure you're formatted. It's like linting your other code.
- Runs the
terraform planand outputs the plan itself as a.tfplanfile. - That
.tfplanfile is then used in the following steps to output the plan as a comment on the Pull Request. This is so, so helpful when you have someone reviewing your code. They can quickly look and see what changes Terraform is making without having to "interpret" the git comparison.
All of this runs when a Pull Request is created. I cannot stress this enough that, especially if you are working on a team, you should ALWAYS ENABLE BRANCH PROTECTION ON THE DEFAULT BRANCH SO NOTHING GOES TO THAT BRANCH WITHOUT A CODE REVIEW/PULL REQUEST.
Treat your default branch like an absolute treasure.
It is.
Terraform Apply
terraform-apply:
name: 'Terraform Apply'
if: github.ref == 'refs/heads/main' && needs.terraform-plan.outputs.tfplanExitCode == 2
runs-on: ubuntu-latest
needs: [terraform-plan]
defaults:
run:
shell: bash
working-directory: ./.tf/
[...]
You guessed it, it does all of the same things the plan does but it actually makes the changes. This job only runs if:
- This is a merge or a push to the branch (in our case
main) - The
terraform-planstep succeeds.
In Closing
At this point, I would hope that everything is in good working order. You can build from here and add things like creating storage buckets for docker containers for each repo, creating each repo it's own roles or credentials, or just adding BRANCH PROTECTION AS A DEFAULT to all of your repositories.
You can now also work across multiple people with the same experience and have a clean, baked in way to validate infrastructure changes before they are made.




Top comments (0)