Deploying Google Cloud Functions using GitHub Actions and Workload Identity Authentication

In this article, we will cover how to create a Workload Identity that works with GitHub Actions while deploying a simple Google Cloud Functions.

GitHub has recently introduced the OIDC tokens as part of GitHub Actions Workflow, so now you can authenticate from GitHub Actions to Google Cloud using Workload Identity Federation, and no longer use JSON service account keys.


Prerequisites

  • A GitHub account with a repository created
  • A Google Cloud account with Billing enabled
  • The Google Cloud CLI installed and authenticated

Create a Hello World function

For the purposes of testing, we will create a basic HTTP triggered function that prompts the user with "Hello World".

1. Using your favorite editor, create an index.js with this helloWorld function:

/**
 * Responds to any HTTP request.
 *
 * @param {!express:Request} req HTTP request context.
 * @param {!express:Response} res HTTP response context.
 */
exports.helloWorld = (req, res) => {
  let message = req.query.message || req.body.message || "Hello World!";
  res.status(200).send(message);
};

2. Commit and push the code to your GitHub repository.

git add index.js
git commit -m "Adding helloWorld function"
git push

Create a Google Cloud Project

1. If you don't have one already, create a new project on the Google Cloud Platform.

New Project on Google Cloud Platform

2. Take a note of the Project ID, in this example github-actions-example-342323.

3. Visit the Navigation Menu, then Billing to make sure there's a billing account linked to the project.

Linking a Billing Account on Google Cloud Platform

Create a Workload Identity on Google Cloud

While most of the configuration can be made on Google Cloud Console, from now on, we will only use the gcloud command to make the chances needed.

1. To make things easier, we will set temporary variables in our terminal session:

For SERVICE_ACCOUNT, WORKLOAD_IDENTITY_POOL and WORKLOAD_IDENTITY_PROVIDER, you can use the same values as I did, or come up with your own.
export GITHUB_REPO=$YOUR_GITHUB_REPO$ (e.g. user/project)
export PROJECT_ID=$YOUR_PROJECT_ID$
export SERVICE_ACCOUNT=github-actions-service-account
export WORKLOAD_IDENTITY_POOL=gh-pool
export WORKLOAD_IDENTITY_PROVIDER=gh-provider

2. Now, using the gcloud command, set the project:

gcloud config set project $PROJECT_ID

3. Enable the APIs for Cloud Functions, Cloud Build and IAM Credential:

gcloud services enable \
(out)   cloudfunctions.googleapis.com \
(out)   cloudbuild.googleapis.com \
(out)   iamcredentials.googleapis.com

4. Create a Service Account that will be used by GitHub Actions:

gcloud iam service-accounts create $SERVICE_ACCOUNT \
(out)   --display-name="GitHub Actions Service Account"

5. Bind the Service Account to the Roles in the Services it must interact:

gcloud projects add-iam-policy-binding $PROJECT_ID \
(out)   --member="serviceAccount:$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com" \
(out)   --role="roles/cloudfunctions.developer"
gcloud projects add-iam-policy-binding $PROJECT_ID \
(out)   --member="serviceAccount:$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com" \
(out)   --role="roles/iam.serviceAccountUser"

6. Create a Workload Identity Pool for GitHub:

gcloud iam workload-identity-pools create $WORKLOAD_IDENTITY_POOL \
(out)   --location="global" \
(out)   --display-name="GitHub pool"

7. Create a Workload Identity Provider for GitHub:

gcloud iam workload-identity-pools providers create-oidc $WORKLOAD_IDENTITY_PROVIDER \
(out)   --location="global" \
(out)   --workload-identity-pool=$WORKLOAD_IDENTITY_POOL \
(out)   --display-name="GitHub provider" \
(out)   --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository" \
(out)   --issuer-uri="https://token.actions.githubusercontent.com"

8. Retrieve the Workload Identity Pool ID:

WORKLOAD_IDENTITY_POOL_ID=$(gcloud iam workload-identity-pools \
(out)   describe $WORKLOAD_IDENTITY_POOL \
(out)   --location="global" \
(out)   --format="value(name)")

9. Allow authentications from the Workload Identity Provider originating from your repository:

gcloud iam service-accounts add-iam-policy-binding \
(out)   $SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com \
(out)   --role="roles/iam.workloadIdentityUser" \
(out)   --member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${GITHUB_REPO}"

10. Finally, extract the Workload Identity Provider resource name:

WORKLOAD_IDENTITY_PROVIDER_LOCATION=$(gcloud iam workload-identity-pools providers \
(out)   describe $WORKLOAD_IDENTITY_PROVIDER \
(out)   --location="global" \
(out)   --workload-identity-pool=$WORKLOAD_IDENTITY_POOL \
(out)   --format="value(name)")

Create a GitHub Actions Workflow

We will create a simple workflow that can be triggered by new changes on the main branch, pull requests or manual dispatch. This workflow will create a new Google Cloud Function called helloWorld (if one doesn't exist already) and will run a simple curl test on the function URL.

When omitting some attributes under google-github-actions/deploy-cloud-functions@v0, it assumes the Cloud Function name is the same as the entry point, in our index.js file note export.helloWorld. Also, it defaults the region to us-central1, and event_trigger_type to HTTP. Read more about how to change these inputs here.

1. Head over to your GitHub Project, go to Actions, then click on set up a workflow yourself:

Landing page for new GitHub Actions

2. Before we edit the main.yml file, let's recover some variables from our terminal session:

echo $WORKLOAD_IDENTITY_PROVIDER_LOCATION
(out)(e.g.) projects/21710762087/locations/global/workloadIdentityPools/gh-pool/providers/gh-provider
echo $SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com
(out)(e.g.) github-actions-service-account@github-actions-example-342323.iam.gserviceaccount.com

3. Now, using the values from above, replace them in the file below and submit the changes on GitHub by clicking on the Start commit button.

# This is a basic workflow to help you get started with Actions
name: CD

# Controls when the workflow will run
on:
  # Triggers the workflow on push or pull request events but only for the main branch
  push:
    branches: [main]
  pull_request:
    branches: [main]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  # This workflow contains a single job called "deploy"
  deploy:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest

    # Add "id-token" with the intended permissions.
    permissions:
      contents: "read"
      id-token: "write"

    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
      - uses: actions/checkout@v2

      - id: "auth"
        name: "Authenticate to Google Cloud"
        uses: "google-github-actions/auth@v0"
        with:
          # Replace with your Workload Identity Provider Location
          workload_identity_provider: "$WORKLOAD_IDENTITY_PROVIDER_LOCATION"
          # Replace with your GitHub Service Account
          service_account: "$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com"

      - id: "deploy"
        uses: "google-github-actions/deploy-cloud-functions@v0"
        with:
          # Name of the Cloud Function, same as the entry point name
          name: "helloWorld"
          # Runtime to use for the function
          runtime: "nodejs16"

      # Example of using the output
      - id: "test"
        run: 'curl "${{ steps.deploy.outputs.url }}"'

4. Almost immediately after submitting you can see a new workflow run under the Actions tab. If everything goes well your function will be deployed:

GitHub Actions run for the "deploy" workflow

That's it! Now, any new changes in your repository will trigger a new deploy action.


Tip: Allowing anyone to access a Google Cloud Function

You may have noticed that if the Cloud Function was deploy for the first time using google-github-actions/deploy-cloud-functions@v0, it will give you an 403 Forbidden when you try to access its URL.

If it's your intention to make it public, and available to everyone with access to your function URL, you can simply fix it by adding allUsers as cloudfunctions.invoker to your function:

gcloud functions add-iam-policy-binding helloWorld \
(out)  --member="allUsers" \
(out)  --role="roles/cloudfunctions.invoker"

Ta-da!

Google Cloud Function "helloWorld" deployed via GitHub Actions

References