DEV Community

sam-nash
sam-nash

Posted on

Automating Google Cloud Resource Management with GitHub Actions and Workload Identity Federation

In this blog post, we’ll demonstrate how to leverage GitHub Actions (GHA) and Google Workload Identity Federation (WIF) to securely authenticate and create resources on Google Cloud Platform (GCP) using Terraform.

We’ll use two GitHub repositories:

Main Repo: Contains the Terraform code for provisioning GCP resources.
Workflow Repo: Hosts the GitHub Actions workflow, triggered by events in the Main Repo, to execute Terraform commands.

We will use two Google Cloud Projects.

Host Project as a landing zone to which GitHub Actions will authenticate using WIF.
Target Google Project where the required resources will be created.

Let’s dive into the setup!

Step 1: Setting Up Google Workload Identity Federation
1.1 - On your host project create a Google Cloud Service Account
Navigate to the Google Cloud Console.
Go to IAM & Admin > Service Accounts.
Create a new service account with the necessary roles for managing your GCP resources.
1.2 - Configure Workload Identity Federation
Go to IAM & Admin > Workload Identity Federation.
Create a Workload Identity Pool and a Provider linked to your GitHub repository.
Follow Google’s official WIF setup guide for detailed instructions. More in a separate Blog Post

Step 2: Permissions
2.1 - Ensure that the tragte gcp project has a terraform state bucket (ex: project_id-tfstate)
2.2 - Assign relevant roles(roles/editor &
roles/storage.admin) to the Service Account of the Host Project to perform relevant actions on the Target Google Project

  gcloud projects add-iam-policy-binding gcp_project_name \
    --member="serviceAccount:service-acct-name@host_gcp_project.iam.gserviceaccount.com" \
    --role="roles/editor"

  gcloud projects add-iam-policy-binding gcp_project_name \
    --member="serviceAccount:service-acct-name@host_gcp_project.iam.gserviceaccount.com" \
    --role="roles/storage.admin"
Enter fullscreen mode Exit fullscreen mode

Step 3: Configure the Main Repo
This repository (e.g., org_name/google_cloud) will contain your Terraform code to manage GCP resources.

Example main.tf:

provider "google" {
  project = var.project
  region  = var.region
}

resource "google_storage_bucket" "bucket" {
  name     = "${var.project}-bucket"
  location = var.region
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Add a Workflow to the Main Repo
Create a GitHub Actions workflow (e.g., .github/workflows/dispatch.yml) that triggers events in the Workflow Repo. Example triggers include push, pull request, or tag creation.

Step 5: Configure the Workflow Repo
In the Workflow Repo, create a Terraform-specific GitHub Actions workflow(lets call this Terraform Workflow) (e.g., .github/workflows/terraform_plan_apply_gcp.yml) to perform Terraform operations such as plan and apply.
Example Trigger:
This workflow listens to repository_dispatch events:

  repository_dispatch:
    types: [terraform_plan, terraform_apply]
Enter fullscreen mode Exit fullscreen mode

How the Main Repo Workflow Operates
Scenario 1: Push with a Tag
A developer pushes Terraform code to a feature branch and adds a tag (e.g., gcp_project_TFPLAN_01).
The dispatch.yml workflow triggers when a tag matching a specific pattern is pushed (e.g., '[a-z]+-[a-z]+TFPLAN[0-9]+').
The workflow determines the Terraform action (e.g., plan) and triggers the Workflow Repo's Terraform workflow via a GitHub API repository_dispatch event.

Scenario 2: Pull Request Creation
When a pull request is opened from a feature branch to develop, the workflow sends a repository_dispatch event with details such as:
event_type: terraform_plan
GCP project name & PR metadata (e.g., PR number, status=opened, merged=false).

Scenario 3: Pull Request Merge
When a pull request is merged, the workflow sends a repository_dispatch event with:
event_type: terraform_apply
GCP project name & PR metadata (e.g., PR number, status=closed, merged=true).

# This GitHub Actions workflow is designed to trigger a Terraform workflow based on specific events.

name: Trigger Terraform Workflow

on:
  push:
    tags:
      - '[a-z]+-[a-z]+_PLAN_[0-9]+'
    # branches:
    #   - 'feature/*'
  pull_request:
    types: [opened, synchronize, closed]
    branches:
      - develop

jobs:
  trigger:
    runs-on: ubuntu-latest
    if: >
      github.event_name == 'push' || 
      (github.event_name == 'pull_request' && 
        (github.event.action == 'opened' || 
          github.event.action == 'synchronize' || 
          (github.event.action == 'closed' && github.event.pull_request.merged == true)))

    steps:
      - name: Checkout repository
        uses: actions/checkout@v2

      # Debug step to print the GITHUB_REF
      - name: Retrieve GitHub Data
        if: github.event_name == 'push' || github.event_name == 'pull_request' 
        run: |
          echo "GITHUB_REF=${GITHUB_REF}"
          TAG_NAME=${GITHUB_REF#refs/tags/}
          echo "TAG_NAME=$TAG_NAME" >> $GITHUB_ENV

      - name: Print PR Information
        if: github.event_name == 'pull_request'
        run: |
          # Get the latest TAG Name from the source branch
          git fetch --tags

          # Get latest tag
          TAG_NAME=$(git tag --sort=-creatordate | head -n 1)

          # Output the tag
          if [ -z "$TAG_NAME" ]; then
            echo "No tags found on the source branch: $SOURCE_BRANCH"
          else
            echo "The latest tag on the source branch ($SOURCE_BRANCH) is: $TAG_NAME"
            echo "TAG_NAME=$TAG_NAME" >> $GITHUB_ENV
          fi
          echo "PR Number: ${{ github.event.pull_request.number }}"
          echo "PR Action: ${{ github.event.action }}"
          echo "PR Merged: ${{ github.event.pull_request.merged }}"

      - name: Extract Information from Tag or PR
        id: extract_info
        run: |
          GCP_PROJECT=$(echo $TAG_NAME | cut -d'_' -f1)
          echo "GCP_PROJECT=$GCP_PROJECT" >> $GITHUB_ENV
          echo "The Tag Name is: $TAG_NAME"
          echo "The Target GCP Project is: $GCP_PROJECT"
          if [[ "${{ github.event_name }}" == "push" ]]; then
            ACTION="plan"
          elif [[ "${{ github.event_name }}" == "pull_request" ]]; then
            if [[ "${{ github.event.action }}" == "closed" && "${{ github.event.pull_request.merged }}" == "true" ]]; then
              ACTION="apply"
            else
              ACTION="plan"
            fi
          fi
          echo "ACTION=$ACTION" >> $GITHUB_ENV
          echo "The Terraform Action is: $ACTION"

      - name: Trigger Terraform Workflow for Push Commits
        if: github.event_name == 'push'
        run: |
          echo "This was triggered as a result of the Event: ${{ github.event_name }} commits"
          curl -X POST \
            -H "Accept: application/vnd.github.v3+json" \
            -H "Authorization: Bearer ${{ secrets.GH_DISPATCH_PAT }}" \
            https://api.github.com/repos/${{ github.repository }}/dispatches \
            -d "{\"event_type\":\"terraform_${{ env.ACTION }}\", \"client_payload\": {\"repository\": \"${{ github.repository }}\", \"project_name\": \"${{ env.GCP_PROJECT }}\", \"tag_name\": \"${{ env.TAG_NAME }}\"}}"

      - name: Trigger Terraform Workflow for PR
        if: github.event_name == 'pull_request'
        run: |
          echo "This was triggered as a result of PR Number: ${{ github.event.pull_request.number }} being ${{ github.event.action }}"
          curl -X POST \
            -H "Accept: application/vnd.github.v3+json" \
            -H "Authorization: Bearer ${{ secrets.GH_DISPATCH_PAT }}" \
            https://api.github.com/repos/${{ github.repository }}/dispatches \
            -d "{\"event_type\":\"terraform_${{ env.ACTION }}\", \"client_payload\": {\"repository\": \"${{ github.repository }}\", \"pr_number\": \"${{ github.event.pull_request.number }}\", \"pr_event\": \"${{ github.event.action }}\", \"pr_merged\": \"${{ github.event.pull_request.merged  }}\", \"project_name\": \"${{ env.GCP_PROJECT }}\", \"action\": \"${{ env.ACTION }}\"}}"
Enter fullscreen mode Exit fullscreen mode

How the Terraform Workflow Operates
This workflow is hosted in the Workflow Repo and is triggered by repository dispatch events. Below, we break down each step with detailed explanations and corresponding code.

Step 1: Define Workflow Triggers
The workflow listens for certain event types:

repository_dispatch: Triggered by the Main Repo for Terraform plan and apply actions. More on repository dispatch can be found on the Official GitHub Documentation.

name: Terraform CI/CD

on:
  workflow_dispatch:
  repository_dispatch:
    types: [terraform_plan, terraform_apply]
Enter fullscreen mode Exit fullscreen mode

Step 2: Set Up the Terraform Job
Define a Terraform job that runs on ubuntu-latest with relevant permissions for GitHub Actions to interact with the repository and pull requests.

jobs:
  terraform:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
      pull-requests: write
      repository-projects: write
Enter fullscreen mode Exit fullscreen mode

Step 3: Set Environment Variables
Capture the payload data from the triggering event(the main workflow) and set them as environment variables for subsequent steps.

- name: Set ENV variables
  run: |
    echo "TARGET_GCP_PROJECT=${{ github.event.client_payload.project_name }}" >> $GITHUB_ENV
    echo "CLOUDSDK_CORE_PROJECT=${{ github.event.client_payload.project_name }}" >> $GITHUB_ENV
    echo "GH_REPOSITORY=${{ github.event.client_payload.repository }}" >> $GITHUB_ENV
    GH_REPO_NAME=$(echo "${{ github.event.client_payload.repository }}" | cut -d'/' -f2)
    echo "GH_REPO_NAME=${GH_REPO_NAME}" >> $GITHUB_ENV       
    echo "GH_PR_NUMBER=${{ github.event.client_payload.pr_number }}" >> $GITHUB_ENV
    echo "GH_PR_EVENT=${{ github.event.client_payload.pr_event }}" >> $GITHUB_ENV
    echo "GH_PR_MERGED=${{ github.event.client_payload.pr_merged }}" >> $GITHUB_ENV
Enter fullscreen mode Exit fullscreen mode

Step 4: Checkout the Code
Clone the Main Repo containing the Terraform code.

- name: Checkout code
  uses: actions/checkout@v2
  with:
    repository: ${{ env.GH_REPOSITORY }}
    token: ${{ secrets.GITHUB_TOKEN }}
Enter fullscreen mode Exit fullscreen mode

Step 5: Set Up Terraform
Set up Terraform to run commands such as init, plan, and apply.

- name: Set up Terraform
  uses: hashicorp/setup-terraform@v3

Enter fullscreen mode Exit fullscreen mode

Step 6: Authenticate to Google Cloud
Use Workload Identity Federation to securely authenticate with Google Cloud.

- name: Authenticate to Google Cloud
  id: authenticate
  uses: google-github-actions/auth@v2
  with:
    create_credentials_file: true
    workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
    service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
Enter fullscreen mode Exit fullscreen mode

Step 7: Initialize Terraform
Set the GCP project and initialize Terraform with the remote backend configuration.

- name: Terraform Init
  id: init
  run: terraform init -backend-config="bucket=$TARGET_GCP_PROJECT-tfstate"
  working-directory: ${{ env.TF_WORKING_DIR }}
Enter fullscreen mode Exit fullscreen mode

Step 8: Validate Terraform Code
Ensure the Terraform configuration is correctly formatted and syntactically valid.

- name: Terraform Format
  id: fmt
  run: terraform fmt
  working-directory: ${{ env.TF_WORKING_DIR }}

- name: Terraform Validate
  id: validate
  run: terraform validate
  working-directory: ${{ env.TF_WORKING_DIR }}
Enter fullscreen mode Exit fullscreen mode

Step 9: Generate Terraform Plan
Generate a plan and output the details for review.

- name: Terraform Plan
  id: plan
  run: terraform plan -var-file="$TARGET_GCP_PROJECT.tfvars" -out=tfplan
  working-directory: ${{ env.TF_WORKING_DIR }}

- run: terraform show -no-color tfplan
  id: show
  working-directory: ${{ env.TF_WORKING_DIR }}
## We will use the output of terraform show to write the plan as a comment to the pull request
Enter fullscreen mode Exit fullscreen mode

Step 10: Comment on Pull Requests
If triggered by a pull request, post the plan as a comment for review.

- name: PR Comment
  uses: actions/github-script@v7
  if: github.event.action == 'terraform_plan' && ( env.GH_PR_EVENT == 'opened' || env.GH_PR_EVENT == 'synchronize' )
  env:
    PLAN: "terraform\n${{ steps.show.outputs.stdout }}"
  with:
    github-token: ${{ secrets.GH_PAT }}
    script: |
      const { data: comments } = await github.rest.issues.listComments({
        owner: context.repo.owner,
        repo: process.env.GH_REPO_NAME,
        issue_number: process.env.GH_PR_NUMBER,
      })
      const botComment = comments.find(comment => comment.body.includes('Terraform Format and Style'))
      const output = `#### Terraform Plan\n\`\`\`\n${process.env.PLAN}\n\`\`\``

      if (botComment) {
        github.rest.issues.updateComment({
          owner: context.repo.owner,
          repo: process.env.GH_REPO_NAME,
          comment_id: botComment.id,
          body: output
        })
      } else {
        github.rest.issues.createComment({
          owner: context.repo.owner,
          repo: process.env.GH_REPO_NAME,
          issue_number: process.env.GH_PR_NUMBER,
          body: output
        })
      }
Enter fullscreen mode Exit fullscreen mode

Step 11: Apply the Terraform Plan
If the event is terraform_apply, apply the plan to create resources.

- name: Terraform Apply
  if: github.event.action == 'terraform_apply'
  id: apply
  run: terraform apply -auto-approve tfplan
  working-directory: ${{ env.TF_WORKING_DIR }}
Enter fullscreen mode Exit fullscreen mode

Conclusion
By integrating GitHub Actions and Google Workload Identity Federation, you can establish a secure, automated CI/CD pipeline for managing GCP resources using Terraform. This approach ensures that Terraform plans are reviewed, validated, and applied only after thorough approval, enhancing both security and operational efficiency.

Top comments (0)