Cloud Functions Series2: Deploy Cloud Functions from GitHub Actions
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:
- Service Account for GitHub Actions: This account is used by GitHub Actions to interact with GCP resources.
- Service Account for Cloud Functions: we already created in the last episode.
- 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 Functionsroles/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:
- To store images or artifacts in Artifact Registry, grant the Artifact Registry Create-on-push Writer (
roles/artifactregistry.createOnPushWriter
) role to the service account. - To store images in Container Registry, grant the Storage Admin (
roles/storage.admin
) role to the service account. - To store artifacts in Cloud Storage, grant the Storage Object Admin (
roles/storage.objectAdmin
) role 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:
- 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 themain
branch targeting the same files. It deploys tocloud-functions-dev
for pull requests andcloud-functions-prod
for pushes to themain
branch. - Environment Variables: Several environment variables are defined for configuration, including
PROJECT_ID
,REGION
,BUCKET_NAME
,SERVICE_ACCOUNT
,SERVICE_ACCOUNT_FOR_GITHUB_ACTIONS
, andWORKLOAD_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. - The entrypoint script is specified by
GOOGLE_FUNCTION_SOURCE
during the build process. If you’re usingmain.py
, no need of--set-build-env-vars GOOGLE_FUNCTION_SOURCE=script_cloud_functions.py
- 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. - 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.