Cloud Functions Series2: Deploy Cloud Functions from GitHub Actions

Masato Naka
7 min readApr 14, 2024

--

Introduction

Welcome back to the next installment of our Cloud Functions series! In our previous episode, we delved into the creation of Cloud Functions triggered by Google Cloud Storage (GCS) events. Building upon that foundation, today we’re taking our deployment process to the next level.

In this post, we’ll tackle two crucial aspects of deploying Cloud Functions effectively. Firstly, we’ll guide you through the essential Google Cloud Platform (GCP) resources required for seamless deployment. Understanding these resources is key to ensuring the smooth functioning of your Cloud Functions.

Secondly, we’ll dive into the world of GitHub Actions. By setting up automated workflows with GitHub Actions, you can streamline the deployment process, making it more efficient and reliable. We’ll provide detailed explanations and code snippets to guide you through every step of the GitHub Actions setup.

So, whether you’re a newcomer looking to deploy your first Cloud Function or a seasoned developer seeking to optimize your deployment workflow, this post has something for you. Let’s embark on this journey to deploy Cloud Functions from GitHub Actions!

GCP resources

Service Accounts

When deploying Cloud Functions using GitHub Actions, you’ll need to set up multiple service accounts:

  1. Service Account for GitHub Actions: This account is used by GitHub Actions to interact with GCP resources.
  1. Service Account for Cloud Functions: we already created in the last episode.
  2. Service Account for Cloud Build (Optional): This is necessary only if you use a dedicated service account for Cloud Build.

Permissions for Service Account for GitHub Actions

The GitHub Actions workflow to deploy Cloud Functions needs permissions to access and manage relevant resources. Here are the roles that need to be attached to the Service Account:

  • roles/resourcemanager.projectIamAdmin for GitHub Actions workload identity (Setup GitHub Actions and Terraform for a new GCP project)
  • roles/cloudfunctions.admin to deploy and invoke Cloud Functions
  • roles/cloudbuild.builds.builder to build docker image and deploy Cloud Functions using Cloud Build.
  • roles/iam.serviceAccountUser to allow GitHub Actions service account to deploy Cloud Functions with the dedicated service account

You can grant those permissions with the following Terraform codes:

// This is already created in the last episode.
resource "google_service_account" "cloud_function_sa" {
account_id = "cloud-function-sa"
display_name = "cloud-function-sa"
description = "For Cloud Function cloud-function-sa"
}

resource "google_service_account" "github-actions" {
account_id = "github-actions"
display_name = "github-actions"
description = "service account used in github actions"
}

resource "google_project_iam_member" "github-actions" {
project = var.project
for_each = {
for role in [
"roles/resourcemanager.projectIamAdmin", # GitHub Actions identity
"roles/cloudfunctions.admin", # To deploy Cloud Functions (cloudfunctions.*, run.*, eventarc.*, etc.)
"roles/cloudbuild.builds.builder", # To submit cloud build jobs (build docker image & deploy Cloud Functions)
]: role => role
}
role = each.value
member = google_service_account.github-actions.member
}


resource "google_service_account_iam_binding" "cloud-functions-sa" {
service_account_id = google_service_account.cloud_function_sa.name
role = "roles/iam.serviceAccountUser"
members = [
google_service_account.github-actions.member
]
}

User-defined Service Account for Cloud Build (Optional)

By default, Cloud Build jobs are executed with the default service account <project number>@cloudbuild.gserviceaccount.com with Cloud Build Service Account role (ref: Cloud Build service account)

For the Principle of Least Privileges, you can optionally use a user-defined service account for Cloud Build. (ref: Configure user-specified service accounts)

You need to grant the following permissions to the service account:

GitHub Actions

Although there’s a provided GitHub Actions deploy-cloud-functions, unfortunately, gen2 is not supported by google-github-actions/deploy-cloud-functions as of Apr 2024. (Ref: https://github.com/google-github-actions/deploy-cloud-functions/issues/304)

Here’s an example of a GitHub Actions workflow to deploy Cloud Functions:

name: deploy-cloud-functions
on:
pull_request:
paths:
- script_cloud_functions.py
- .github/workflows/deploy-cloud-functions.yml
push:
branches:
- main
paths:
- script_cloud_functions.py
- .github/workflows/deploy-cloud-functions.yml

env:
PROJECT_ID: <project_id>
REGION: <region>
BUCKET_NAME: <test-bucket>
SERVICE_ACCOUNT: <service-account-name>@<project>.iam.gserviceaccount.com
SERVICE_ACCOUNT_FOR_GITHUB_ACTIONS: github-actions@<project>.iam.gserviceaccount.com
WORKLOAD_IDENTITY_PROVIDER_GITHUB_ACTIONS: projects/<project_number>/locations/global/workloadIdentityPools/github/providers/github-provider

jobs:
deploy:
if: github.event_name == 'pull_request'|| github.event_name == 'push'
runs-on: 'ubuntu-latest'
permissions:
contents: 'read'
id-token: 'write'

steps:
- uses: 'actions/checkout@v3'

- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@v2'
with:
workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER_GITHUB_ACTIONS }}
service_account: ${{ env.SERVICE_ACCOUNT_FOR_GITHUB_ACTIONS }}
token_format: 'access_token'

- name: Set up Cloud SDK
uses: 'google-github-actions/setup-gcloud@v2'

- name: Set upload config
id: set-conf
run: |
echo "TRIGGER_EVENT_FILTERS_PATH_PATTERN=resourceName=/projects/_/buckets/${BUCKET_NAME}/objects/<PATH>/*" >> $GITHUB_OUTPUT
if [[ "${{ github.event_name }}" = "pull_request" ]]; then
echo "CLOUD_FUNCTIONS_NAME=cloud-functions-dev" >> $GITHUB_OUTPUT
echo "SLACK_WEBHOOK_URL=${{ secrets.SLACK_WEBHOOK_URL_DEV }}" >> $GITHUB_OUTPUT
else
echo "CLOUD_FUNCTIONS_NAME=cloud-functions-prod" >> $GITHUB_OUTPUT
echo "SLACK_WEBHOOK_URL=${{ secrets.SLACK_WEBHOOK_URL_PROD }}" >> $GITHUB_OUTPUT
fi

- name: Deploy Cloud Functions
run: |
gcloud functions deploy "${{ steps.set-conf.outputs.CLOUD_FUNCTIONS_NAME }}" \
--gen2 \
--runtime python312 \
--set-build-env-vars GOOGLE_FUNCTION_SOURCE=script_cloud_functions.py \
--timeout 60s \
--project $PROJECT_ID \
--region $REGION \
--service-account $SERVICE_ACCOUNT \
--source . \
--entry-point main \
--trigger-event-filters type=google.cloud.audit.log.v1.written \
--trigger-event-filters serviceName=storage.googleapis.com \
--trigger-event-filters methodName=storage.objects.create \
--trigger-event-filters-path-pattern "${{ steps.set-conf.outputs.TRIGGER_EVENT_FILTERS_PATH_PATTERN }}"

- name: Send success message to Slack
run: |
curl -X POST -H 'Content-type: application/json' --data '{"text":"Successfully deployed Cloud Functions `${{ steps.set-conf.outputs.CLOUD_FUNCTIONS_NAME }}` in <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|GitHub Actions>"}' ${{ secrets.SLACK_WEBHOOK_URL }}

- name: Send error message to Slack
if: failure()
run: |
curl -X POST -H 'Content-type: application/json' --data '{"text":"<!here> Failed to deploy Cloud Functions `${{ steps.set-conf.outputs.CLOUD_FUNCTIONS_NAME }}`. Please check <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|GitHub Actions>"}' ${{ secrets.SLACK_WEBHOOK_URL }}

- name: 'Get Cloud Functions URL'
id: get-cloud-functions-url
run: |
echo "URL=$(gcloud functions describe ${{ steps.set-conf.outputs.CLOUD_FUNCTIONS_NAME }} --project $PROJECT_ID --region $REGION --format json | jq -r .url)" >> $GITHUB_OUTPUT

- name: 'Authenticate to Google Cloud'
id: 'auth-id-token'
uses: 'google-github-actions/auth@v2'
with:
workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER_GITHUB_ACTIONS }}
service_account: ${{ env.SERVICE_ACCOUNT_FOR_GITHUB_ACTIONS }}
id_token_audience: ${{ steps.get-cloud-functions-url.outputs.URL }}
token_format: 'id_token'

- name: Test invocation
id: test-invocation
run: |
curl ${{ steps.get-cloud-functions-url.outputs.URL }} \
--fail-with-body \
-H "Authorization: Bearer ${{ steps.auth-id-token.outputs.id_token }}" \
-X POST \
-H "Content-Type: application/json" \
-H "ce-id: 123451234512345" \
-H "ce-specversion: 1.0" \
-H "ce-time: 2020-01-02T12:34:56.789Z" \
-H "ce-type: google.cloud.audit.log.v1.written" \
-H "ce-source: //cloudaudit.googleapis.com/projects/PROJECT_ID/logs/data_access" \
-d '{
"protoPayload": {
"resourceName": "projects/_/buckets/$BUCKET_NAME/objects/<PATH>/test_obj.txt",
"serviceName": "storage.googleapis.com",
"methodName": "storage.objects.create"
}
}'

- name: Send success message to Slack
run: |
curl -X POST -H 'Content-type: application/json' --data '{"text":"Test invocation succeeded for `${{ steps.set-conf.outputs.CLOUD_FUNCTIONS_NAME }}` in <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|GitHub Actions>"}' ${{ secrets.SLACK_WEBHOOK_URL }}

- name: Send error message to Slack
if: failure() && steps.test-invocation.outcome == 'failure'
run: |
curl -X POST -H 'Content-type: application/json' --data '{"text":"<!here> Test invocation failed for `${{ steps.set-conf.outputs.CLOUD_FUNCTIONS_NAME }}`. Please check <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|GitHub Actions>"}' ${{ secrets.SLACK_WEBHOOK_URL }}

This GitHub Actions workflow automates the deployment of Cloud Functions to Google Cloud Platform (GCP). Below is an overview of the key components and functionalities of the workflow:

  1. Trigger Events: The workflow is triggered on pull requests targeting files script_cloud_functions.py and .github/workflows/deploy-cloud-functions.yml, as well as on pushes to the main branch targeting the same files. It deploys to cloud-functions-dev for pull requests and cloud-functions-prod for pushes to the main branch.
  2. Environment Variables: Several environment variables are defined for configuration, including PROJECT_ID, REGION, BUCKET_NAME, SERVICE_ACCOUNT, SERVICE_ACCOUNT_FOR_GITHUB_ACTIONS, and WORKLOAD_IDENTITY_PROVIDER_GITHUB_ACTIONS. These variables are used throughout the workflow to specify GCP project details, region, service accounts, and workload identity provider for GitHub Actions.
  3. The entrypoint script is specified by GOOGLE_FUNCTION_SOURCE during the build process. If you’re using main.py, no need of--set-build-env-vars GOOGLE_FUNCTION_SOURCE=script_cloud_functions.py
  4. Testing Invocation: After deployment, the workflow tests the invocation of the deployed Cloud Functions by making a sample HTTP POST request using curl. The test checks if the Cloud Functions respond correctly to the provided event. The cloud functions are triggered by an events of a new Cloud Storage object on the specified storage <test-bucket> that matches filter pattern. In this case, only the objects under <PATH> trigger the Cloud Functions.
  5. Slack Notifications: Upon successful deployment or failure, the workflow sends messages to Slack using webhook URLs stored as secrets. These notifications inform team members about the deployment status and provide links to GitHub Actions runs for further investigation.

Summary

In this post, we’ve delved into the automation of Cloud Functions deployment on Google Cloud Platform (GCP) using GitHub Actions, which is a crucial part of fast and reliable development. We highlighted the critical role of Service Accounts and the specific permissions required for seamless deployment, ensuring secure access to GCP resources. Additionally, we provided a detailed example of a custom GitHub Actions Workflow tailored for Cloud Functions deployment. By automating the deployment process, developers can enhance efficiency, reliability, and scalability while maintaining control and visibility over their Cloud Functions deployments on GCP.

References

  1. Cloud Functions Series1: Trigger Cloud Functions by Cloud Storage upload events(2nd gen)
  2. https://github.com/google-github-actions/deploy-cloud-functions
  3. https://zenn.dev/nbstsh/scraps/fdda1240d41fa8

--

--

Masato Naka

An SRE, mainly working on Kubernetes. CKA (Feb 2021). His Interests include Cloud-Native application development, and machine learning.