Deploy to AWS with Terraform within a GitHub Action
This is a guide on how to use a GitHub action to deploy with Terraform in AWS and OIDC.
If you are lazy to read, you can watch the youtube video instead.
Today we are going to create a GitHub action to deploy with Terraform in AWS.
Architecture
What we want to achieve is to be able to deploy whatever you we want to AWS, using Terraform within a GitHub Action for the deployment.
Also, instead of creating an AWS API Keys and Secret Keys and storing them in GitHub secrets, we can make it more secure by using the GitHub OIDC (OpenID Connect) Provider and only allowing these credentials to be run from our GitHub Action in this specific repository.
To explain the authentification part, nothing better than an image :
- GitHub Action is going to request a JWT (Java Web Token) to the GitHub OIDC Provider
- GitHub OIDC Provider will issue the signed JWT to our GitHub Action.
- GitHub Action with our signed JWT is going to request a temporary access token from the IAM Identity Provider in AWS.
- IAM Identity Provider is going to verify the signed JWT with the GitHub OIDC Provider and verify if the role we want to assume can be used by the Identity Provider.
- IAM Identity Provider is going to issue the temporary access token from the role to the GitHub Action.
After this authentification, our GitHub action environment will get access to AWS following the policies we have attached to the role we assume.
GitHub repository
Create yourself a GitHub repository, note somewhere your username + repository name as you will need it later.
For me it’s going to be : KasteM34/github-oidc-terraform
Create the AWS IAM Identity Provider
We need to create an IAM Identity Provider in AWS.
Go to IAM » Identity Providers.
Click Add Provider on the right side.
Provider type : OpenID Connect.
For the provider URL:
For the Audience:
- sts.amazonaws.com
Click on “Add Provider”
Create a bucket
We need to create an S3 bucket to store our Terraform states.
Let’s go in S3 » Create bucket
(choose your unique bucket name)
Bucket name: github-oidc-terraform-tfstate AWS Region: choose one
Click on “Create bucket”
Create your AWS IAM Role and policies
Go to IAM » Roles » Create Role
Step 1: Select trusted entity
Select trusted entity: choose “Custom trust policy”
{
"Version": "2008-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::YOUR_ACCOUNT_NUMBER:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:YOUR_GITHUB_USERNAME/YOUR_REPO_NAME:*"
}
}
}
]
}
NOTE: This policy is a trusted entity, so to allow the usage of this role through our Identity Provider, which we created before.
You will need to modify:
- YOUR_ACCOUNT_NUMBER
- YOUR_GITHUB_USERNAME
- YOUR_REPO_NAME
You can set optionally the specific branch, the GitHub action will be allowed.
In my example today, “repo:YOUR_GITHUB_USERNAME/YOUR_REPO_NAME:*”, I have set a *, as I am using pull requests from different branches to do a Terraform plan, and the main branch to do the apply.
Once you are ready, click on “Next”
Step 2: Add permissions
We need to create a new policy, so our Role can access the s3 bucket where the terraform states are going to be located, then we will attach this policy.
Up right: Create Policy.
Click on “JSON”
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::YOUR_BUCKET/*",
"arn:aws:s3:::YOUR_BUCKET"
]
}
]
}
NOTE: Replace YOUR_BUCKET, by your bucket name.
then : Next > Next > Next > Choose a name you want for the Policy > Create Policy
I am going to create another policy! and this policy is about what I want to deploy in AWS.
In my case, I just want to deploy some parameters in AWS SSM (Systems manager) for the sake of deploying something, if you want to deploy something else, you will need to adapt to your use case.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ssm:PutParameter",
"ssm:LabelParameterVersion",
"ssm:DeleteParameter",
"ssm:UnlabelParameterVersion",
"ssm:DescribeParameters",
"ssm:GetParameterHistory",
"ssm:GetParametersByPath",
"ssm:GetParameters",
"ssm:GetParameter",
"ssm:DeleteParameters"
],
"Resource": "*"
}
]
}
I’m going to name it : SSM-Parameters-access
Let’s go back to our role, and attach both policies we created!
As we are back into our Role configuration, let’s not forget to hit the refresh button on the left of “Create policy”.
I’m able to find both policies, the one for the bucket s3 and the one for SSM Parameter store.
Select both, click on Next.
Step 3: Name, review, and create
Add a name, check your trusted entities policy, check you have the correct policy attach.
Click on “Create role”
Terraform code
Let’s upload some Terraform code on our GitHub repository :
main.tf
resource "aws_ssm_parameter" "foo" {
name = "foo"
type = "String"
value = "bar"
}
providers.tf
terraform {
backend "s3" {}
}
Our Terraform code is ready, this is only deploying a simple parameter in AWS Systems Manager.
GitHub Secrets
Before creating the GitHub Actions, let’s add some Secrets inside our repository.
Click on the Settings.
Left side » Secrets » Actions
Click on: New repository secret
Here are the secrets you need to create :
- AWS_BUCKET_NAME : YOUR BUCKET NAME
- AWS_BUCKET_KEY_NAME : NAME OF THE PATH FOR TERRAFORM STATE
- AWS_REGION : YOUR REGION
- AWS_ROLE : ARN OF YOUR ROLE
For me, it looks like this :
- AWS_BUCKET_NAME : github-oidc-terraform-tfstate
- AWS_BUCKET_KEY_NAME : github-oidc-terraform.tfstate
- AWS_REGION : ca-central-1
- AWS_ROLE : arn:aws:iam::x000x758xxxx:role/github-oidc-terraform
GitHub Action
Let’s create our GitHub Action, for this, we need to create a file in our GitHub repository :
.github/workflows/main.yml
name: "Terraform action"
on:
push:
branches:
- main
pull_request:
permissions:
id-token: write # This is required for aws oidc connection
contents: read # This is required for actions/checkout
pull-requests: write # This is required for gh bot to comment PR
env:
TF_LOG: INFO
AWS_REGION: ${{ secrets.AWS_REGION }}
jobs:
deploy:
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: .
steps:
- name: Git checkout
uses: actions/checkout@v3
- name: Configure AWS credentials from AWS account
uses: aws-actions/configure-aws-credentials@v1
with:
role-to-assume: ${{ secrets.AWS_ROLE }}
aws-region: ${{ secrets.AWS_REGION }}
role-session-name: GitHub-OIDC-TERRAFORM
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.2.5
- name: Terraform fmt
id: fmt
run: terraform fmt -check
continue-on-error: true
- name: Terraform Init
id: init
env:
AWS_BUCKET_NAME: ${{ secrets.AWS_BUCKET_NAME }}
AWS_BUCKET_KEY_NAME: ${{ secrets.AWS_BUCKET_KEY_NAME }}
run: terraform init -backend-config="bucket=${AWS_BUCKET_NAME}" -backend-config="key=${AWS_BUCKET_KEY_NAME}" -backend-config="region=${AWS_REGION}"
- name: Terraform Validate
id: validate
run: terraform validate -no-color
- name: Terraform Plan
id: plan
run: terraform plan -no-color
if: github.event_name == 'pull_request'
continue-on-error: true
- uses: actions/github-script@v6
if: github.event_name == 'pull_request'
env:
PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
#### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
#### Terraform Validation 🤖\`${{ steps.validate.outcome }}\`
<details><summary>Validation Output</summary>
\`\`\`\n
${{ steps.validate.outputs.stdout }}
\`\`\`
</details>
#### Terraform Plan 📖\`${{ steps.plan.outcome }}\`
<details><summary>Show Plan</summary>
\`\`\`\n
${process.env.PLAN}
\`\`\`
</details>
*Pushed by: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
- name: Terraform Plan Status
if: steps.plan.outcome == 'failure'
run: exit 1
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve -input=false
Push this code in your repository.
Create a pull request
Now, let’s say I want to change something in my infrastructure which is only a single AWS SSM Parameter.
Let’s go in the main.tf
Change a parameter and create a Pull Request from this change.
I’m going to use the GitHub Editor directly for this task.
Create Pull request.
Inside your pull request, you will need to wait a bit until the checks (GitHub actions) have finished running.
Then, a comment is going to be posted by github-actions!
Our Terraform Plan is working! Wonderful!
If I’m going to accept the change, I just have to merge the Pull Request, so when my code is pushed to the main branch, it’s going to be “terraform apply”
Congrats everyone!
Reference links
GitHub repository: github.com/KasteM34/github-oidc-terraform
AWS OIDC documentation: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html
GitHub Action OIDC AWS Documentation : github.com/KasteM34/lambda-pull-cloudflare-ips
Hashicorp Terraform GitHub Action Documentation: https://learn.hashicorp.com/tutorials/terraform/github-actions
Hashicorp Terraform learning platform : https://learn.hashicorp.com/terraform