How To Deploy Google Cloud Functions With Terraform

Guillaume Vincent
Guillaume Vincent
The cover image of this article showing an IDE with the code of a function. This is a reference to Google Cloud Function that
Photo by Luca Bravo / Unsplash
Table of Contents
Table of Contents

Synchronize your Google Cloud Function code with Terraform

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
Presentation of the project structure
  • 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.json contains 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 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

Write the cloud function code

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

Test the cloud function in local

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:

Terminal screenshot showing the installation of cloud function dependencies with npm install command
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:

Deployment of the cloud function with function-framework and specifying the VERSION environment variable to 1
Deploy and test the function locally with functions-framework

Executing curl on the function URL returns the expected output:

Calling the cloud function deployed locally on 8080 port with curl command
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
}
main.tf
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"
    }
  }
}
providers.tf
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
}
outputs.tf
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"
  ]
}
variables.tf

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:

$ terraform apply -var=environment_variables='{"VERSION": 2}' -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 πŸ‘‡

GitHub - guivin/google-cloud-function-with-terraform-example: Terraform example with Google Cloud Function gen2
Terraform example with Google Cloud Function gen2. Contribute to guivin/google-cloud-function-with-terraform-example development by creating an account on GitHub.


Great! Next, complete checkout for full access to Getbetterdevops
Welcome back! You've successfully signed in
You've successfully subscribed to Getbetterdevops
Success! Your account is fully activated, you now have access to all content
Success! Your billing info has been updated
Your billing was not updated