Infrastructure as Code: Deploying Hugo to Google Cloud Run with Terraform

When I decided to move my blog to Google Cloud Platform (GCP), I didn’t just want to manually click around the console. I wanted a reproducible, version-controlled infrastructure—commonly known as Infrastructure as Code (IaC).
In this post, I’ll walk you through how I set up a robust deployment pipeline for this Hugo site using Terraform and Google Cloud Run.
Why Terraform?
Terraform allows you to define your cloud resources in declarative configuration files. This offers several huge benefits:
- Consistency: No more “it works on my machine” or forgetting which checkbox you clicked in the console.
- Version Control: Your infrastructure lives in Git alongside your code. You can track changes, rollback, and review infrastructure updates just like software features.
- Automation: Deploying a whole new environment (staging, production) becomes a single command.
The Architecture
For this blog, the infrastructure is simple yet powerful:
- Google Artifact Registry: Stores the Docker container images for the blog.
- Google Cloud Run: a serverless platform that runs the stateless container.
- Terraform: Orchestrates the creation of these resources and the deployment process.
The Setup
1. Variables and Configuration
To keep secrets safe and the code reusable, I use variables. I strictly avoid hardcoding project IDs or sensitive info in the code.
variables.tf:
variable "project_id" {
description = "The Google Cloud project ID."
type = string
}
variable "region" {
description = "The GCP region (e.g., europe-north1)."
type = string
}
variable "service_name" {
description = "The name of the Cloud Run service."
type = string
}
variable "repository_id" {
description = "The ID of the Artifact Registry repository."
type = string
}
I configure these using a terraform.tfvars file (which is git-ignored!), but here is what the example looks like:
terraform.tfvars.example:
project_id = "your-project-id"
region = "europe-north1"
service_name = "blog-app"
repository_id = "blog-repo"
2. Artifact Registry
Container Registry (GCR) is deprecated, so I use the modern Artifact Registry. Terraform handles enabling the API and creating the repository with lifecycle policies to save costs.
# Enable API
resource "google_project_service" "artifact_registry" {
service = "artifactregistry.googleapis.com"
}
# Create Repository
resource "google_artifact_registry_repository" "default" {
location = var.region
repository_id = var.repository_id
description = "Docker repository for ${var.service_name}"
format = "DOCKER"
# Keep only the last 5 tagged versions to save storage
cleanup_policies {
id = "keep-last-5-versions"
action = "KEEP"
most_recent_versions {
package_name_prefixes = ["${var.service_name}"]
keep_count = 5
}
}
}
3. The Build & Deploy Flow
This is the cool part. Typically, you might use a separate CI/CD tool (like Cloud Build triggers) to build your image. But for simplicity, I verified a flow where Terraform triggers a local gcloud build command.
This null_resource acts as a bridge. It calculates a timestamp to tag the image uniquely every time you deploy, ensuring Cloud Run always picks up the new version.
locals {
timestamp = formatdate("YYYYMMDDhhmmss", timestamp())
image_path = "${var.region}-docker.pkg.dev/${var.project_id}/${var.repository_id}/${var.service_name}:${local.timestamp}"
}
resource "null_resource" "build_image" {
provisioner "local-exec" {
# Builds the image locally and pushes to Artifact Registry
command = "gcloud builds submit --tag ${local.image_path} .."
}
triggers = {
always_run = "${timestamp()}"
}
}
4. Cloud Run Service
Finally, the google_cloud_run_service resource defines the actual running application.
resource "google_cloud_run_service" "default" {
name = var.service_name
location = var.region
template {
spec {
containers {
image = local.image_path # Uses the dynamic image tag from above
}
}
}
traffic {
percent = 100
latest_revision = true
}
depends_on = [null_resource.build_image]
}
We also explicitly allow public access so the world can see the blog:
resource "google_cloud_run_service_iam_member" "public_access" {
service = google_cloud_run_service.default.name
location = google_cloud_run_service.default.location
role = "roles/run.invoker"
member = "allUsers"
}
How Deployment Works
With this setup, deployment is incredibly streamlined:
- Code: I write a new blog post (like this one!) in Markdown.
- Plan: I run
terraform planto preview changes. Terraform sees that thealways_runtrigger has changed (due to the timestamp) and plans to rebuild the image. - Apply: I run
terraform apply.- Terraform runs
gcloud builds submit, which packages my Hugo site into a Docker container and pushes it to Artifact Registry. - Terraform updates the Cloud Run service to point to this new image tag.
- Cloud Run spins up a new revision and migrates traffic to it.
- Terraform runs
All of this happens in minutes, with zero downtime, and fully automated resource management.
Conclusion
Using Terraform for a personal blog might seem like overkill, but the peace of mind and strict control it gives over your cloud resources is invaluable. Plus, it’s a great way to practice IaC principles that scale up to enterprise systems.