6 min read

Deploy to AWS with Terraform within a GitHub Action

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

image

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 :

image
  1. GitHub Action is going to request a JWT (Java Web Token) to the GitHub OIDC Provider
  2. GitHub OIDC Provider will issue the signed JWT to our GitHub Action.
  3. GitHub Action with our signed JWT is going to request a temporary access token from the IAM Identity Provider in AWS.
  4. 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.
  5. 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.

https://media.cloudscalr.com/images/github-oidc-terraform/image1.png

Click Add Provider on the right side.

Provider type : OpenID Connect.

For the provider URL:

  1. Use https://token.actions.githubusercontent.com

For the Audience:

  1. sts.amazonaws.com
https://media.cloudscalr.com/images/github-oidc-terraform/image2.png

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:

  1. YOUR_ACCOUNT_NUMBER
  2. YOUR_GITHUB_USERNAME
  3. 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”.

https://media.cloudscalr.com/images/github-oidc-terraform/image3.png

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.

https://media.cloudscalr.com/images/github-oidc-terraform/image4.png

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!

https://media.cloudscalr.com/images/github-oidc-terraform/image5.png

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!

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