Terraform #

In the cloud native era, serverless computing has redefined how we deploy and run applications. By eliminating the need to maintain physical servers or virtual machines, serverless offers incredible deployment speed and highly efficient automatic scaling. However, there’s one important reality often overlooked by development teams: even though physical servers disappear from our sight, the infrastructure itself still exists — and becomes even more fragmented.

When we build production-scale serverless applications on Google Cloud Platform (GCP), our system doesn’t consist of just one piece of code. In the field, a complete serverless architecture typically includes dozens of Cloud Run services, several Cloud Functions, Pub/Sub queue channels, cron schedulers, secrets in Secret Manager, Eventarc triggers, Workflows orchestrators, and hundreds of Identity and Access Management (IAM) access policies. Configuring this entire web of infrastructure manually through the Google Cloud Console (GUI) is a high-risk action triggering human error, inconsistency across work environments (dev, staging, prod), and a lack of audit trails for changes.

To address these challenges, Terraform comes in as the industry’s leading declarative Infrastructure as Code (IaC) tool. By writing our serverless infrastructure definitions into declarative HashiCorp Configuration Language (HCL) code files, Terraform lets us provision, update, and manage the entire GCP serverless ecosystem as a single source of truth trackable by Git, reviewable through Pull Requests, and automatically deployable through CI/CD pipelines.


Infrastructure as Code Architecture for the Serverless Ecosystem #

Before diving into HCL code writing details, we need to understand Terraform’s internal architecture concepts and how it interacts with Google Cloud APIs securely and reliably.

1. Declarative System and Dependency Graph #

Terraform works with a Declarative model. That means we write the desired state of the infrastructure we want (e.g., “create a Cloud Run service named backend with 1GB memory”), and Terraform analyzes the difference between the current real cloud state and our desired state.

Terraform builds an internal Dependency Graph mapping dependencies between resources. For example, if an Eventarc trigger needs a Pub/Sub topic, and the Pub/Sub topic needs a service account, Terraform automatically knows the resource creation order without manual instruction from us.

2. Detailed Terraform Command Lifecycle #

Terraform’s work cycle is managed through four main sequential commands:

  • terraform init: Initializes the working directory. At this stage, Terraform reads configuration code, downloads Google Provider plugins from the HashiCorp Registry, and prepares the state storage backend plugin. The .terraform.lock.hcl dependency lock file is also created to record provider version checksums, guaranteeing build consistency on both developer laptops and CI/CD servers.
  • terraform plan: Performs comparative analysis. Terraform reads the current infrastructure state using GCP APIs, compares it with the state file, then generates an action plan list. We can use the -out=tfplan option to save this action plan into an encrypted physical file, ensuring only that approved plan gets executed in the next step.
  • terraform apply: Executes the plan instructions to the GCP API. Before applying changes, Terraform uses backend locking (e.g., GCS lock) to hold write access from other users. After successful execution, the state file is updated and output variables are printed to the terminal.
  • terraform destroy: Used to delete all resources registered in the state file. This command must be run with the highest level of caution in production, but is very useful for deleting temporary sandbox environments to save operational costs.

3. State File: An Isolated Single Source of Truth #

Terraform stores the real infrastructure state map in a special file called the State File (terraform.tfstate). The state file acts as Terraform’s internal memory to detect manual changes (configuration drift) in the GCP console.

  • State Storage in GCS (Remote Backend): In production environments, storing the state file on a developer’s local computer is strictly forbidden (strict anti-pattern). We must store the state file securely in Google Cloud Storage (GCS) with encryption, object versioning, and state locking enabled. The locking feature guarantees that if two developers (or CI/CD pipelines) run deployment processes simultaneously, Terraform uses the GCS metadata lock mechanism to block the second execution until the first completes, preventing state file corruption.

Terraform Provisioning Flow Diagram (IaC Architecture) #

Here’s an architecture visualization of how HCL code is evaluated by the Terraform CLI, matched against the remote state file in a GCS bucket, and deployed to GCP APIs to automatically create our various serverless resources.

flowchart TD
    Local["Developer / CI/CD (Terraform CLI)"] -->|"Apply HCL Configuration"| TFEngine["Terraform Engine"]
    TFEngine -->|"Read State File"| GCSState["Google Cloud Storage Bucket (Remote State)"]
    TFEngine -->|"Provision Resources via GCP API"| GCP["Google Cloud Platform (GCP)"]
    
    subgraph GCP Resources
        CRService["google_cloud_run_service"]
        GCFFunction["google_cloudfunctions_function"]
        PSTopic["google_pubsub_topic"]
        ETrigger["google_eventarc_trigger"]
        IAM["google_project_iam_member"]
    end
    
    GCP --> CRService
    GCP --> GCFFunction
    GCP --> PSTopic
    GCP --> ETrigger
    GCP --> IAM

    style TFEngine stroke:#0288d1,stroke-width:2px

Main Serverless Resource Declarations #

Writing IaC configuration for serverless requires HCL parameter accuracy. Here are the functional details of the main parameters used:

  • google_cloud_run_service: Used to provision serverless containers. Important parameters include limits (memory and CPU allocation), autoscaling (setting minScale to reduce cold starts and maxScale to limit costs), and container_concurrency to set the request concurrency limit per instance.
  • google_pubsub_topic and google_pubsub_subscription: Used to build message queues. The ack_deadline_seconds configuration sets the message processing tolerance time limit before redelivery, while the push_config parameter is used to channel events directly to a Cloud Run HTTP endpoint with encrypted OIDC authentication.
  • google_workflows_workflow: Compiles and uploads YAML orchestration code using the source_contents argument into the serverless engine.
  • google_eventarc_trigger: Sets event source matching criteria (matching_criteria) and defines the target destination container in the destination parameter.

Least Privilege-Based Access Management (IAM) #

Security is the most crucial yet challenging aspect of serverless architecture. Because every serverless service runs isolated in the cloud network, they need special identities to access other resources (e.g., a Cloud Run service calling a Cloud SQL database or reading files in Cloud Storage).

  • DON’T use the default Google Compute Engine or App Engine service account to run your serverless services in production, because those accounts have overly broad project editor access rights.
  • DO: Create a custom Service Account for each serverless service using Terraform. Grant access with the Least Privilege principle using precise IAM bindings.

Crucial IAM Binding Difference: Member vs. Binding vs. Policy #

Terraform offers three resources for managing IAM permissions:

  1. google_project_iam_policy (Very Dangerous): Overwrites all IAM policies at the project level with the defined list. All other manually created permissions are immediately removed.
  2. google_project_iam_binding (Dangerous): Takes full control of one specific role. If we define a binding for the roles/storage.admin role, all other users or service accounts holding that role but not written in the Terraform file immediately lose access.
  3. google_project_iam_member (Highly Recommended): Adds one member to one role additively without disturbing other existing members’ access rights. This is the safest choice for avoiding accidentally removing other admin team access.

Multi-Environment Terraform Project Folder Structure #

To keep code clean and avoid the risk of accidentally destroying production infrastructure while experimenting in development environments, we must structure Terraform directories modularly and separately by environment.

Why Choose Folder Structure Over Terraform Workspaces? #

Terraform provides a built-in feature called Workspaces for managing multi-environment in one code directory. However, for enterprise production levels, using Folder Structure-based isolation is far safer because:

  • State Backend Separation: Each environment folder has its own fully isolated state backend configuration in a different GCS bucket. State damage in the dev environment will never propagate to the prod environment.
  • Separate Credentials: We can restrict CI/CD service account access so the dev service account has no write access at all to the prod folder, enforcing absolute security isolation.
  • More Readable Dynamic Variables: Storing environment-specific variable values in terraform.tfvars files in each folder is much easier to audit than tracking workspace state stored in Terraform memory.

Implementation Code Example: Complete HCL Configuration #

Here’s a complete Terraform code example (main.tf at the environment level) configuring a GCS remote backend, provisioning a Cloud Run service, creating a Pub/Sub topic, configuring an Eventarc trigger, and setting IAM access rights securely.

# ==========================================================================
# 1. PROVIDER & REMOTE BACKEND CONFIGURATION
# ==========================================================================
terraform {
  required_version = ">= 1.6.0"
  
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 5.10.0"
    }
  }

  # ✓ CORRECT: Storing the state file in Google Cloud Storage with lock feature
  backend "gcs" {
    bucket = "my-company-terraform-states"
    prefix = "serverless-app/production"
  }
}

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

# ==========================================================================
# 2. SERVICE ACCOUNT & IAM PRIVILEGES (Least Privilege)
# ==========================================================================

# Creating a dedicated Service Account for the Cloud Run Backend
resource "google_service_account" "run_backend_sa" {
  account_id   = "cr-backend-production-sa"
  display_name = "Service Account untuk Cloud Run Backend Production"
}

# Granting access so Cloud Run can read Secret Manager
resource "google_project_iam_member" "secret_accessor" {
  project = var.project_id
  role    = "roles/secretmanager.secretAccessor"
  member  = "serviceAccount:${google_service_account.run_backend_sa.email}"
}

# ==========================================================================
# 3. PUB/SUB TOPIC & SUBSCRIPTION PROVISIONING
# ==========================================================================
resource "google_pubsub_topic" "order_topic" {
  name = "production-order-events-topic"
  
  labels = {
    environment = "production"
    owner       = "checkout-team"
  }
}

# ==========================================================================
# 4. CLOUD RUN SERVICE CONFIGURATION (Production Ready)
# ==========================================================================
resource "google_cloud_run_service" "backend_app" {
  name     = "backend-api-production"
  location = var.region

  # Enabling automatic revision splitting to the latest revision
  traffic {
    percent         = 100
    latest_revision = true
  }

  template {
    spec {
      service_account_name = google_service_account.run_backend_sa.email
      
      containers {
        image = var.container_image_url
        
        resources {
          limits = {
            cpu    = "2000m" # 2 vCPU
            memory = "2Gi"   # 2 GB RAM
          }
        }

        # Injecting environment variables
        env {
          name  = "APP_ENV"
          value = "production"
        }

        env {
          name  = "DATABASE_NAME"
          value = "production_db"
        }
      }
    }

    metadata {
      annotations = {
        # ✓ CORRECT: Configuring autoscaling & concurrency limits precisely
        "autoscaling.knative.dev/minScale" = "1" 
        "autoscaling.knative.dev/maxScale" = "30"
        "container.googleapis.com/concurrency" = "80"
      }
    }
  }
}

# ==========================================================================
# 5. EVENTARC TRIGGER CONFIGURATION
# ==========================================================================

# Service Account to allow Eventarc to perform event routing
resource "google_service_account" "eventarc_trigger_sa" {
  account_id   = "eventarc-order-trigger-sa"
  display_name = "Service Account Eventarc Order Trigger"
}

# Role to allow the Eventarc Service Account to call Cloud Run APIs
resource "google_cloud_run_service_iam_member" "eventarc_invoker" {
  service  = google_cloud_run_service.backend_app.name
  location = google_cloud_run_service.backend_app.location
  role     = "roles/run.invoker"
  member   = "serviceAccount:${google_service_account.eventarc_trigger_sa.email}"
}

# Eventarc Trigger to deliver messages from the Pub/Sub order topic
resource "google_eventarc_trigger" "pubsub_trigger" {
  name     = "pubsub-order-event-trigger"
  location = var.region

  destination {
    cloud_run_service {
      service = google_cloud_run_service.backend_app.name
      region  = google_cloud_run_service.backend_app.location
      path    = "/webhooks/order" # Target HTTP backend endpoint
    }
  }

  matching_criteria {
    attribute = "type"
    value     = "google.cloud.pubsub.topic.v1.messagePublished"
  }

  # Filtering based on the origin topic
  transport {
    pubsub {
      topic = google_pubsub_topic.order_topic.id
    }
  }

  service_account = google_service_account.eventarc_trigger_sa.email

  depends_on = [
    google_cloud_run_service_iam_member.eventarc_invoker
  ]
}

CI/CD Pipeline Integration (Terraform + GitOps) #

Deploying infrastructure manually from the terminal on a developer’s local laptop risks state file corruption from Terraform CLI version inconsistencies or insecure credentials.

  • DO: Integrate the Terraform provisioning cycle into a GitOps-based CI/CD pipeline (like GitHub Actions) automatically.

Here’s an example GitHub Actions workflow script (.github/workflows/terraform.yml) for validating and provisioning our infrastructure:

name: "Terraform GitOps Pipeline"

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

permissions:
  contents: read
  pull-requests: write

jobs:
  terraform:
    name: "Terraform Job"
    runs-on: "ubuntu-latest"
    
    steps:
      - name: Checkout Code
        uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.6.0"

      # Authenticating to Google Cloud using Workload Identity Federation
      - name: Authenticate to Google Cloud
        uses: google-github-actions/auth@v2
        with:
          credentials_json: ${{ secrets.GCP_SA_KEY }}

      - name: Terraform Format Check
        run: terraform fmt -check -recursive

      - name: Terraform Init
        run: terraform init
        working-directory: ./environments/prod

      - name: Terraform Validate
        run: terraform validate
        working-directory: ./environments/prod

      - name: Terraform Plan
        id: plan
        if: github.event_name == 'pull_request'
        run: terraform plan -no-color
        working-directory: ./environments/prod

      # Applying changes only when a Pull Request is merged to the main branch
      - name: Terraform Apply
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: terraform apply -auto-approve
        working-directory: ./environments/prod

Best Practices for Managing Serverless IaC #

Applying the following architecture principles is highly recommended to guarantee the long-term stability and security of our serverless Terraform code:

1. Retrieve Sensitive Credentials from Secret Manager Dynamically #

Writing database passwords directly (hardcoded) in variables.tf or .tfvars files is a high-level security hole that often leaks into public Git repositories.

  • DO: Register those secrets manually in the GCP Secret Manager console. In Terraform, retrieve the secret using a data query block:
data "google_secret_manager_secret_version" "db_password" {
  secret = "production-database-password"
}

# Use this query data in the Cloud Run environment variable configuration
# ${data.google_secret_manager_secret_version.db_password.secret_data}

2. Handle Container Image Versions with the Right Lifecycle #

Every time our application CI/CD pipeline builds a new container image (e.g., gcr.io/my-project/api:v1.2.0), deploying it with Terraform can leave the Terraform state file behind if the container is deployed directly by a separate CD pipeline.

  • DO: If we deploy new container images independently using Cloud Build, configure a lifecycle block on the Terraform google_cloud_run_service resource to ignore container image property changes, so the next terraform apply doesn’t accidentally revert the image version to an old one:
lifecycle {
  ignore_changes = [
    template[0].spec[0].containers[0].image,
  ]
}

3. Apply Resource Labeling for Cost Management #

Ballooning cloud usage costs are often hard to trace to their source if we deploy dozens of serverless resources randomly.

  • DO: Always add a labels block (or metadata.labels on Cloud Run) configuration to every resource we create in Terraform. Use industry-standard labels like environment = "production", cost-center = "marketing-team", and project = "checkout-system". These labels automatically integrate into GCP Billing reports, making it easy for finance teams to audit per-team usage costs in detail.

4. Handling Stuck State Locks (Force Unlock) #

Sometimes, if our CI/CD pipeline dies suddenly mid-terraform apply due to a runner server failure, the GCS backend detects the state remains locked, permanently blocking the next deployment process.

  • DO: Don’t panic and don’t try to delete the state file. Copy the Lock ID printed in the terminal error, verify no other deployment process is actively running on the backend, then run the terraform force-unlock <LOCK_ID> command in your local terminal to safely unlock the GCS state.

5. Periodic Manual Change Detection (Scheduled Drift Detection) #

Even with GitOps, team members sometimes have to make emergency manual modifications in the Google Cloud console while handling production issues. These manual changes (drift) must be redefined into code immediately so they aren’t removed in the next deployment.

  • DO: Set up a daily cron job in our GitHub Actions pipeline running the terraform plan -detailed-exitcode command during quiet hours. The -detailed-exitcode option returns status code 2 if drift is detected. The pipeline can then send automatic notifications to the developer team’s Slack to reconcile the code immediately.

6. Version Compatibility Constraints #

Automatic Google provider version upgrades by terraform init can introduce unexpected breaking changes that break our HCL build scripts.

  • DO: Always pin the Google provider version range using the ~> operator in the required_providers block (e.g., ~> 5.10.0). Also pin the Terraform CLI executable version using the required_version argument to guarantee all team developers use the same engine version.

Summary #

  • Terraform is a declarative IaC tool acting as the single source of truth for all serverless infrastructure on GCP.
  • Must use a GCS remote backend with versioning and object locking enabled to secure the state file from synchronization corruption.
  • Design custom service accounts dedicated to each serverless service to enforce the least privilege security principle.
  • Separate Terraform directories into modules (reusable blueprints) and environments (isolated dev/prod instances).
  • Use GitOps CI/CD cycles to validate, plan, and apply infrastructure changes from the main Git branch.
  • Apply ignore_changes on container images if new image deployments are managed by external CD pipelines outside Terraform.
← Previous: Eventarc   Next: NeonDB →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact