Step-by-Step Guide: Deploying Google Cloud Functions Using Terraform

Synchronize your Google Cloud Function code with Terraform

Guillaume Vincent
8 min readJul 8, 2024
Photo by Shahadat Rahman on Unsplash

Have you developed Google Cloud Functions and are you thinking about deploying them? By reading this article, you will know how to do it with Terraform. You will be able to update your cloud functions after a code change.

This will be done through a small project to demonstrate it. We will develop a small simple cloud function for this purpose. Then we will create the Terraform code to deploy and update it.

The Terraform Project Structure

├── src
│ ├── README.md
│ ├── index.js
│ ├── package-lock.json
│ └── package.json
└── terraform
├── README.md
├── main.tf
├── outputs.tf
├── providers.tf
└── variables.tf

The src directory contains the source code of the cloud function :

  • index.js is the file where the cloud function code is defined
  • package-lock.json is automatically generated for any operations where npm modifies either the node_modules tree, or package.json. It describes the exact tree that was generated, such that subsequent installs are able to generate identical trees, regardless of the intermediate dependency updates
  • package.jsoncontains human-readable metadata about the project (like the project name and description) as well as functional metadata like the package version number and a list of dependencies required by the application

The terraform directory contains the code to deploy the cloud function :

  • backend.tf defines the terraform backend to store the state and to version the infrastructure
  • main.tf defines the Terraform resource to handle the cloud function deployment and update
  • outputs.tf returns the attributes relative to the cloud function deployment
  • variables.tf defines the inputs of the Terraform code

Create The Cloud Function With Node.js

The cloud function used here takes an environment variable called VERSION and returns an HTML response with that value:

/**
* Responds to any HTTP request.
*
* @param {!express:Request} req HTTP request context.
* @param {!express:Response} res HTTP response context.
*/
const functions = require('@google-cloud/functions-framework');functions.http('main', (req, res) => {
if (!process.env.VERSION) {
throw new Error('VERSION is not defined in the environment variable');
}
res.status(200).send('<h1>Cloud Function version ' + process.env.VERSION + '</h1>');
});

To define the function, we use a package called @google-cloud/functions-framework. This dependency also allows testing the cloud function locally before the final deployment. We install it in the package.json file

{
"name": "cloud-function-terraform-example",
"version": "1.0.0",
"description": "Google Cloud Function terraform example",
"scripts": {
"start": "functions-framework --target=main --port 8080"
},
"author": "Guillaume Vincent",
"dependencies": {
"@google-cloud/functions-framework": "^3.1.2"
}
}

To facilitate the test, a start command is added to the package.json to launch it locally

Before tackling the deployment with Terraform, we will test the cloud function locally. The first thing to do is to install the dependencies via npm:

Installation of the Node.JS dependencies with npm

After the installation, the package-lock.json is generated. Now we can test the function locally with this command:

Deploy and test the function locally with functions-framework

Executing curl on the function URL returns the expected output:

Calling the local cloud function

Enable Needed Google Services

In the next example, you will see how to deploy the cloud function 2nd generation with Terraform. This new generation is based on the CloudRun Google service. It needs to enable a few service APIs on your Google project. These operations are done with gcloud command because it is not the role of the Terraform code exposed here to manage your project.

First of all, select your project:

$ gcloud config set project <YOUR_PROJECT_ID>

Next, enable the following services needed by 2nd gen functions:

$ gcloud services enable cloudfunctions.googleapis.com
$ gcloud services enable run.googleapis.com
$ gcloud services enable artifactregistry.googleapis.com
$ gcloud services enable cloudbuild.googleapis.com

Implement The Terraform Code

data.archive_file.this is automatically launched at terraform apply to create an archive of the cloud function source code. It exposes an SHA checksum attribute to google_storage_bucket_object.this. When the cloud function code changes, the checksum also changes. This triggers the update of google_storage_bucket_object and thus the cloud function resource google_cloudfunctions2_function.this.

The data.archive_file.this uses the exclude argument helps to specify files to ignore when creating the archive. This helps to remove non-needed files in the cloud function and reduces its size. Particularly useful, because if the total size of files is too large you cannot use the live editor. In addition, there is a max deployment size per function.

The cloud function release update is smooth and managed by the Google Function Service itself. That means there is no downtime when updating a cloud function. When the new version is released, the traffic is automatically routed to the latest if all_traffic_on_latest_revision attribute is set to true.

resource "google_cloudfunctions2_function" "this" {
name = var.name
location = var.location
description = var.description
project = var.project
labels = var.labels
  build_config {
runtime = var.runtime
entry_point = var.entry_point
source {
storage_source {
bucket = google_storage_bucket.this.id
object = google_storage_bucket_object.this.name
}
}
}
service_config {
min_instance_count = var.min_instance_count
max_instance_count = var.max_instance_count
timeout_seconds = var.timeout_seconds
environment_variables = var.environment_variables
ingress_settings = var.ingress_settings
all_traffic_on_latest_revision = var.all_traffic_on_latest_revision
}
}
data "archive_file" "this" {
type = "zip"
output_path = "/tmp/${var.name}.zip"
source_dir = "${path.module}/../src"
excludes = var.excludes
}
resource "google_storage_bucket" "this" {
name = var.bucket_name
project = var.project
location = var.bucket_location
force_destroy = true
uniform_bucket_level_access = true
storage_class = var.bucket_storage_class
versioning {
enabled = var.bucket_versioning
}
}
resource "google_storage_bucket_object" "this" {
name = "${var.name}.${data.archive_file.this.output_sha}.zip"
bucket = google_storage_bucket.this.id
source = data.archive_file.this.output_path
}

Replace the bucket getbetterdevops-terraform-states by your own. This is used to store the terraform states and versions remotely

terraform {
backend "gcs" {
bucket = "getbetterdevops-terraform-states"
prefix = "google-cloud-function-with-terraform-example"
}
  required_providers {
archive = {
source = "hashicorp/archive"
version = "~> 2.2.0"
}
google = {
source = "hashicorp/google"
version = "~> 4.44.1"
}
}
}
output "id" {
description = "An identifier for the resource with format `projects/{{project}}/locations/{{location}}/functions/{{name}}`"
value = google_cloudfunctions2_function.this.id
}
output "environment" {
description = "The environment the function is hosted on"
value = google_cloudfunctions2_function.this.environment
}
output "state" {
description = "Describes the current state of the function"
value = google_cloudfunctions2_function.this.state
}
output "update_time" {
description = "The last update timestamp of a Cloud Function"
value = google_cloudfunctions2_function.this.update_time
}
output "uri" {
description = "The uri to reach the function"
value = google_cloudfunctions2_function.this.service_config[0].uri
}
variable "name" {
description = "A user-defined name of the function."
type = string
default = "example-managed-by-terraform"
}
variable "location" {
description = "The location of this cloud function."
type = string
default = "europe-west1"
}
variable "description" {
description = "User-provided description of a function."
type = string
default = "Cloud function example managed by Terraform"
}
variable "project" {
description = "The ID of the project in which the resource belongs. If it is not provided, the provider project is used."
type = string
}
variable "labels" {
description = "A set of key/value label pairs associated with this Cloud Function."
type = map(string)
default = {}
}
variable "runtime" {
description = "The runtime in which to run the function. Required when deploying a new function, optional when updating an existing function."
type = string
default = "nodejs16"
}
variable "entry_point" {
description = "The name of the function (as defined in source code) that will be executed. Defaults to the resource name suffix, if not specified. For backward compatibility, if function with given name is not found, then the system will try to use function named \"function\". For Node.js this is name of a function exported by the module specified in source_location."
type = string
default = "main"
}
variable "min_instance_count" {
description = "(Optional) The limit on the minimum number of function instances that may coexist at a given time."
type = number
default = 1
}
variable "max_instance_count" {
description = "(Optional) The limit on the maximum number of function instances that may coexist at a given time."
type = number
default = 10
}
variable "timeout_seconds" {
description = "(Optional) The function execution timeout. Execution is considered failed and can be terminated if the function is not completed at the end of the timeout period. Defaults to 60 seconds."
type = number
default = 60
}
variable "environment_variables" {
description = "(Optional) Environment variables that shall be available during function execution."
type = map(string)
}
variable "ingress_settings" {
description = "(Optional) Available ingress settings. Defaults to \"ALLOW_ALL\" if unspecified. Default value is ALLOW_ALL. Possible values are ALLOW_ALL, ALLOW_INTERNAL_ONLY, and ALLOW_INTERNAL_AND_GCLB."
type = string
default = "ALLOW_ALL"
}
variable "all_traffic_on_latest_revision" {
description = "(Optional) Whether 100% of traffic is routed to the latest revision. Defaults to true."
type = bool
default = true
}
variable "bucket_name" {
description = "The bucket name where the cloud function code will be stored"
type = string
}
variable "bucket_location" {
description = "The bucket location where the cloud function code will be stored"
type = string
default = "EU"
}
variable "bucket_versioning" {
description = "Enable the versioning on the bucket where the cloud function code will be stored"
type = bool
default = true
}
variable "bucket_storage_class" {
description = "The bucket storage class where the cloud function code will be stored"
type = string
default = "STANDARD"
}
variable "excludes" {
description = "Files to exclude from the cloud function src directory"
type = list(string)
default = [
"node_modules",
"README.md"
]
}

Deploy The Google Cloud Function With Terraform

Initial cloud function deployment

Replace the following command with your own project ID and desired bucket name. This bucket will be used to store the function archive artifact:

$ terraform apply -var=environment_variables='{"VERSION": 1}' -var=project=<YOUR_PROJECT_ID> -var=bucket_name=<YOUR_BUCKET_NAME>

Once applied, the function URI is returned through the outputs:

FUNCTION_URI=$(terraform output --json | jq -r '.uri.value')

The cloud function is exposed publicly with authentication. With the following curl command and gcloud, you can call the cloud function URI:

$ curl -m 70 -X POST $FUNCTION_URI -H "Authorization: bearer $(gcloud auth print-identity-token)"
<h1>Cloud Function version 1</h1>

Update the cloud function

Change the VERSION environment variable and re-apply the terraform code:

-var=project=getbetterdevops-lab -var=bucket_name=getbetterdevops-cloud-functions

When recalling the cloud function URI, the output has changed. The function has been successfully applied!

$ curl -m 70 -X POST $FUNCTION_URI -H "Authorization: bearer $(gcloud auth print-identity-token)" -H "Content-Type: application/json"
<h1>Cloud Function version 2</h1>

Conclusion

To deploy a Google Cloud Function, there are 3 possible solutions:

  1. Deploy your functions with the gcloud command
  2. Use Google Cloud Source Repositories which is a git repository managed in Google Cloud Platform. Either you push your code directly into Cloud Source or you can mirror a GitHub repository with it
  3. Use a Google Storage Bucket with the code of the function compressed in an archive. This is what was done here by automating the release with Terraform

The 3rd solution has the merit of being very quick to implement. Terraform is a must in the DevOps ecosystem. Once the Terraform code is modularized, you can deploy new cloud functions very quickly.

Find the code presented in this article here 👇

--

--

Guillaume Vincent
Guillaume Vincent

No responses yet