Mastering Serverless Infrastructure: Deploying Google Cloud Run Services with Terraform

Provisioning cloud infrastructure using code has become the standard for modern DevOps practices, but managing serverless container workloads adds a layer of complexity that traditional Infrastructure as Code (IaC) tools must handle with precision. Terraform provides the declarative framework necessary to define, review, and deploy Google Cloud Run services in a repeatable manner. By codifying your Cloud Run configuration, teams can move away from console-based, manual deployments that are prone to drift and error, toward a version-controlled workflow that supports automated testing and peer review. This approach is particularly critical in production environments where reliability, security, and scalability are paramount.

Google Cloud Run offers a platform for running containers in response to HTTP requests, handling all the underlying complexity of scaling, patching, and load balancing. When combined with Terraform, organizations can achieve a fully codified, reproducible deployment pipeline. Whether you are deploying a simple microservice or a complex application requiring private network connectivity, secret management, and traffic splitting, Terraform provides the resources and data sources needed to orchestrate these components seamlessly. This article explores the technical implementation of deploying Cloud Run services using Terraform, covering basic setup, advanced security patterns, VPC connectivity, and integration with Google Cloud SQL and Functions.

Foundational Architecture and Provider Configuration

Before defining any Cloud Run resources, the Terraform environment must be properly initialized with the correct Google provider version. The stability and feature set of the Google provider are critical to the success of the deployment. For recent deployments, pinning the provider version ensures compatibility with the latest Cloud Run APIs while maintaining backward compatibility with existing infrastructure.

The initialization process begins with the definition of the Terraform block in your main.tf file. This block specifies the required providers and their versions. Pinning the version is a best practice that prevents unexpected breaking changes during terraform apply.

```hcl
terraform {
requiredproviders {
google = {
source = "hashicorp/google"
version = "~> 5.28.0"
}
}
required
version = ">= 1.0"
}

provider "google" {
project = var.project_id
region = var.region
}
```

In the configuration above, the google provider is pinned to version 5.28.0 or higher. This version ensures access to the latest features of the Cloud Run API. The provider block sets the default project and region, which can be overridden in individual resources if necessary. Variables such as var.project_id and var.region should be defined in a variables.tf file to keep the code environment-agnostic. This modular approach allows the same Terraform code to be deployed to development, staging, and production environments by simply changing variable values.

Deploying Basic Cloud Run Services

The core resource for deploying a Cloud Run service is google_cloud_run_service (for first-gen) or google_cloud_run_v2_service (for second-gen). The second-gen API offers improved performance and features, making it the preferred choice for new deployments. A basic deployment involves defining the container image, resource limits, environment variables, and scaling parameters.

The following example demonstrates a basic Cloud Run service using the second-gen resource. It includes specific resource limits to prevent resource exhaustion and defines the container port on which the application listens.

```hcl
resource "googlecloudrunv2service" "api" {
name = "api-service"
location = var.region
project = var.project_id

template {
containers {
image = "us-central1-docker.pkg.dev/my-project/my-repo/api:latest"

  # Resource limits
  resources {
    limits = {
      cpu    = "1"
      memory = "512Mi"
    }
  }

  # Environment variables
  env {
    name  = "ENV"
    value = "production"
  }
  env {
    name  = "LOG_LEVEL"
    value = "info"
  }

  # Container port
  ports {
    container_port = 8080
  }
}

# Scaling configuration
scaling {
  min_instance_count = 1
  max_instance_count = 10
}

# Service account for the container
service_account = google_service_account.api_runner.email

}
}
```

Key considerations in this configuration include:

  • Resource Limits: Explicitly defining CPU and memory limits is crucial. In the example, the CPU is limited to 1 core (equivalent to 1000m) and memory to 512Mi. Exceeding these limits will result in the container being throttled or terminated.
  • Scaling Parameters: min_instance_count is set to 1 to keep at least one instance warm, reducing cold start latency for the first request. max_instance_count caps the scaling behavior at 10 instances to control costs and resource availability.
  • Service Account: The service_account field references a dedicated service account. This is a security best practice that isolates permissions for the running application.

Security and Service Account Management

A common mistake in Cloud Run deployments is using the default compute service account or embedding secrets directly in the configuration. Terraform allows for the creation of dedicated service accounts and the retrieval of secrets from Google Secret Manager, ensuring that sensitive data is never committed to version control.

Creating a dedicated service account involves two resources: the service account itself and the IAM policies that grant it necessary permissions.

```hcl
resource "googleserviceaccount" "apirunner" {
account
id = "api-runner"
displayname = "API Service Cloud Run SA"
project = var.project
id
}

Grant permissions the service needs

resource "googleprojectiammember" "apirunnersql" {
project = var.project
id
role = "roles/cloudsql.client"
member = "serviceAccount:${googleserviceaccount.api_runner.email}"
}
```

For handling secrets, such as database passwords, Terraform can retrieve them from Google Secret Manager using the google_secret_manager_secret_version data source. This requires that the secret already exists in the project and that the Terraform runner has permission to read it.

hcl data "google_secret_manager_secret_version" "db_pass" { secret = "db-password" }

The secret_data attribute of this data source contains the decoded value of the secret. This value can then be interpolated into environment variables within the Cloud Run service definition. It is critical to ensure that the service account used by Terraform has the roles/secretmanager.secretAccessor role to read these secrets during the planning and application phases.

Integrating with Cloud SQL and VPC Connectivity

Connecting a Cloud Run service to a Google Cloud SQL instance is a common production requirement. Cloud Run services run in a shared network environment by default, which does not have direct access to the private IP addresses of Cloud SQL instances. To bridge this gap, Terraform can manage the VPC connection or rely on the Cloud Run service's ability to reach Cloud SQL if the service account has the correct permissions and the Cloud SQL instance is configured to allow private IP access.

A more robust approach involves defining the Cloud SQL instance in Terraform and referencing its private IP address in the Cloud Run environment variables.

```hcl
resource "googlesqldatabaseinstance" "default" {
name = "example-instance"
database
version = "POSTGRES_15"
region = "us-central1"

settings {
tier = "db-f1-micro"
}
}
```

The Cloud Run service configuration then references this instance:

```hcl
resource "googlecloudrun_service" "default" {
name = "example-service"
location = "us-central1"

template {
spec {
containers {
image = "gcr.io/your-project-id/example-image"
resources {
limits {
cpu = "1000m"
memory = "512Mi"
}
}
env {
name = "DATABASEURL"
value = "postgres://username:${data.google
secretmanagersecretversion.dbpass.secretdata}@${googlesqldatabaseinstance.default.privateip}/dbname"
}
}
service
accountname = googleservice_account.default.email
}
}
}
```

It is important to note that if the Cloud SQL instance is behind a VPC, the Cloud Run service may require a VPC connector to access it. Terraform can manage these connectors, ensuring that the network topology is fully defined and auditable. Best practices dictate using VPC connectors when accessing private resources to avoid exposing databases to the public internet.

Traffic Management and IAM Policies

Traffic splitting is a powerful feature of Cloud Run that allows gradual rollouts. By defining multiple revisions and setting traffic percentages, teams can minimize the risk of deploying buggy code.

```hcl
resource "googlecloudrun_service" "default" {
name = "example-service"
location = "us-central1"

template {
spec {
containers {
image = "gcr.io/your-project-id/example-image"
}
}
}

traffic {
percent = 100
latest_revision = true
}

autogeneraterevisionname = true
}
```

For services that require public access, IAM policies must be configured to allow invocations. A common pattern for public APIs is to grant the roles/run.invoker role to allUsers.

```hcl
resource "googlecloudrunserviceiampolicy" "public" {
location = google
cloudrunservice.default.location
project = googlecloudrunservice.default.project
service = google
cloudrunservice.default.name

policy_data = jsonencode({
bindings = [
{
role = "roles/run.invoker"
members = ["allUsers"]
},
]
})
}
```

This configuration opens the service to all users. For internal services, the members list should be restricted to specific service accounts or groups.

Deploying Cloud Run Functions with Terraform

Terraform also supports the deployment of Cloud Run Functions, which are ideal for event-driven tasks and simple HTTP endpoints. When deploying functions with Terraform, the source code must be uploaded to a Cloud Storage bucket. This requirement differs from the container-based approach, where an image registry is used.

The process involves:
1. Zipping the function source code.
2. Uploading the zip file to a source_archive_bucket.
3. Specifying the source_archive_object name in the Terraform configuration.

Cloud Run functions copy the source file to a regional bucket with a name format like gcf-v2-sources-PROJECT_NUMBER-REGION. This automated handling simplifies the deployment pipeline, but it requires that the Cloud Storage bucket exists and is accessible by the Terraform service account.

Operational Best Practices and Advanced Patterns

Moving beyond basic deployment, several operational best practices enhance the reliability and security of Cloud Run services managed by Terraform.

  • Cold Starts: Cold starts add noticeable delay. Setting min_instance_count to a value greater than zero keeps instances warm, reducing latency for the first request.
  • Secret Management: Use Secret Manager for all sensitive configuration. Never put secrets directly in plaintext environment variables or Terraform variables.
  • Service Accounts: Create dedicated service accounts for each Cloud Run service. Do not use the default compute service account.
  • VPC Connectivity: Use VPC connectors when accessing private resources. Do not expose databases to the public internet.
  • Health Checks: Configure health check probes so Cloud Run can detect and restart unhealthy containers.
  • Concurrency Limits: Set appropriate concurrency limits based on your application's capabilities to prevent resource exhaustion.
  • Traffic Splitting: Use traffic splitting for gradual rollouts instead of deploying directly to 100% of traffic.

The execution workflow in Terraform follows a standard lifecycle:

bash terraform init terraform plan terraform apply -auto-approve

The terraform plan command is essential for verifying the changes before they are applied. It provides a detailed preview of the infrastructure changes, allowing operators to catch configuration errors early.

Comparison of Cloud Run Resources

The following table compares the key attributes of the first-gen and second-gen Cloud Run resources in Terraform, as well as the Functions resource.

Feature googlecloudrun_service googlecloudrunv2service googlecloudrun_function
API Generation First-gen Second-gen Second-gen (Functions)
Workload Type Container Container Function Source Code
Image/Source Container Image URI Container Image URI Cloud Storage Zip Object
Scaling Control Limited Advanced (Min/Max) Automatic
Traffic Splitting Supported Supported Not Standard
VPC Connectivity Supported Supported Supported
Recommended Use Legacy New Deployments Simple HTTP/Events

Conclusion

Deploying Google Cloud Run services with Terraform offers a robust, scalable, and secure approach to managing serverless infrastructure. By codifying the deployment process, organizations gain the ability to automate releases, enforce security policies, and maintain consistency across environments. The integration of Terraform with Google's secret management, VPC services, and IAM policies ensures that production workloads are not only highly available but also compliant with security best practices.

The evolution from basic container deployments to advanced patterns involving traffic splitting, VPC connectivity, and private database access demonstrates the flexibility of the Cloud Run platform. As applications grow in complexity, the ability to manage these components declaratively becomes a strategic advantage. Teams should start with basic deployments, validate their configurations in non-production environments, and gradually incorporate advanced features such as VPC connectors and traffic management. Ultimately, the combination of Cloud Run's serverless capabilities and Terraform's infrastructure management strength provides a comprehensive solution for modern cloud-native applications.

Sources

  1. One Uptime Blog
  2. Dev.to
  3. Google Cloud Documentation

Related Posts