Deploying Python FastAPI Application On GCP Cloud Run With Terraform
Terraform on Google Cloud V1.6 — Cloud Run
This is the 7th blog post on the series Terraform on Google Cloud, and in this blog, I will talk about Cloud Run, a managed GCP service to run your stateless, containerized workloads.
Before heading into writing Terraform code for Cloud Run, let me talk more about Cloud Run, why they are so good, and where using them makes more sense.
What is Cloud Run?
Cloud Run is a serverless compute platform provided by Google Cloud. It allows you to run containerized applications in a fully managed environment.
A Cloud Run service provides you with the infrastructure required to run a reliable HTTPS endpoint, given that your code listens on a TCP port and handles HTTP requests.
Cloud Run also supports event-driven architectures, allowing you to trigger containerized functions based on various event sources like HTTP requests, Pub/Sub messages, Cloud Storage events, and more.
Two ways to run your code on Cloud Run
Your code can either run continuously as a service or as a job.
- Cloud Run services – Used to run code that responds to web requests, or events
- Cloud Run jobs – Used to run code that performs work (a job) and quits when the work is done
Why we love Cloud Run?
You can deploy code written in any programming language on Cloud Run if you can build a container image from it.
If you do not want to build your own Docker image, and if you’re using Go, Node.js, Python, Java, .NET Core, or Ruby, you can use the source-based deployment option that builds the container for you.
Here are some best features of Cloud Run:
- Unique HTTPS endpoint for every service – Every service is given an HTTPS endpoint on a unique subdomain of the *.run.app domain. You can configure custom domains as well
- Fast request-based auto scaling – service can rapidly scale out to one thousand instances
- Built-in traffic management – you can deploy multiple revisions of your service, route traffic to latest and can easily rollback
- Private and public services – service can be reachable from the internet, or you can restrict access using IAP, IAM and Ingress
Where to use Cloud Run and where not to?
Good use cases:
- Websites and web applications
- APIs and microservices
- Streaming data processing
Not ideal for:
- Long-running processes that need persistent state
- Applications that require local file storage
Let’s now deploy Cloud Run on GCP with Terraform
Below are the steps we’ll follow:
- Write a basic Python FastAPI app
- Dockerize the Python application
- Create an Artifact Registry for Docker images
- Push the Docker image to Artifact Registry
- Enable Cloud Run API with Terraform
- Write Terraform configuration for Cloud Run
- Deploy the app on Cloud Run
Prerequisites
I assume that you already have a Google Cloud Platform (GCP) project created and that you have the owner or editor role assigned to that project.
You also need gcloud CLI installed on your machine.
For the purposes of this demonstration, we will use basic roles, although it is recommended to use a predefined role that is suitable for your specific use case or create a custom role with the necessary permissions.
Getting started
Since we already have a well-structured Terraform project from the previous parts, we’ll add Cloud Run resources to our existing setup.
Create the application structure:
# Create application directory structure
mkdir -p app/src
touch app/src/main.py app/src/requirements.txt
touch app/Dockerfile
Let’s create the basic FastAPI Python application
Create the FastAPI application
Paste the below code to app/src/main.py
:
import os
from fastapi import FastAPI
app = FastAPI(title="Terraform GCP API", version="1.0.0")
# Get configuration from environment variables
PROJECT_ID = os.environ.get('PROJECT_ID', 'your-project-id')
ENVIRONMENT = os.environ.get('ENVIRONMENT', 'dev')
@app.get("/")
def read_root():
return {
"message": "Hello from Cloud Run!",
"project_id": PROJECT_ID,
"environment": ENVIRONMENT
}
@app.get("/health")
def health_check():
return {"status": "healthy"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
return {
"item_id": item_id,
"q": q,
"environment": ENVIRONMENT
}
if __name__ == "__main__":
import uvicorn
port = int(os.environ.get("PORT", 8080))
uvicorn.run(app, host="0.0.0.0", port=port)
Define Python dependencies
Paste below Python dependencies into app/src/requirements.txt
:
fastapi==0.104.1
uvicorn[standard]==0.24.0
Create the Dockerfile
Paste the below code in app/Dockerfile
:
# Use an official Python runtime as the base image
FROM python:3.11-slim
# Set the working directory in the container
WORKDIR /app
# Copy requirements first for better Docker layer caching
COPY src/requirements.txt .
# Install the Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy the application code to the container
COPY src/ .
# Expose the port on which the application will run
EXPOSE 8080
# Set the command to run the application when the container starts
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
Create Artifact Registry with Terraform
Instead of using gcloud commands, let’s create everything with Terraform so we have infrastructure as code.
First, let’s understand what APIs we need and why.
Step 1: Enable required APIs
Create a new file cloud-run.tf
and add this code:
# We need to enable APIs before we can use any GCP services
# This is like turning on the services in Google Cloud Console
resource "google_project_service" "artifact_registry_api" {
service = "artifactregistry.googleapis.com"
# This prevents destroying dependent services when we destroy this resource
disable_dependent_services = true
# Keep the API enabled even if we destroy this Terraform resource
disable_on_destroy = false
}
resource "google_project_service" "cloud_run_api" {
service = "run.googleapis.com"
disable_dependent_services = true
disable_on_destroy = false
}
What’s happening here?
google_project_service
is a Terraform resource that enables APIs in your GCP project- We need Artifact Registry API to store our Docker images
- We need Cloud Run API to deploy our containerized application
disable_on_destroy = false
means even if we delete this Terraform config, the APIs stay enabled
Step 2: Create Artifact Registry repository
Add this code to your cloud-run.tf
:
# Artifact Registry is like a secure warehouse for your Docker images
# Think of it as Docker Hub but private and integrated with GCP
resource "google_artifact_registry_repository" "docker_repo" {
location = var.region
repository_id = "docker-repo"
description = "Docker repository for our applications"
format = "DOCKER"
# This creates an implicit dependency - Terraform will create the API first
depends_on = [google_project_service.artifact_registry_api]
}
What’s happening here?
google_artifact_registry_repository
creates a private Docker registryformat = "DOCKER"
tells GCP this will store Docker images (not Maven jars or npm packages)depends_on
is an explicit dependency – Terraform waits for the API to be enabled first- Without
depends_on
, Terraform would try to create the repository and API at the same time, which would fail
Step 3: Create a service account for Cloud Run
Add this to cloud-run.tf
:
# Service accounts are like robot users that applications use to authenticate
# Instead of hardcoding credentials, we give our app an identity
resource "google_service_account" "cloud_run_sa" {
account_id = "cloud-run-sa"
display_name = "Cloud Run Service Account"
description = "Service account for Cloud Run services"
}
# Grant the service account permission to write logs
# Without this, we won't see any logs from our application
resource "google_project_iam_member" "cloud_run_logging" {
role = "roles/logging.logWriter"
member = "serviceAccount:${google_service_account.cloud_run_sa.email}"
}
What’s happening here?
- Every Cloud Run service needs a service account to run
- Service accounts follow the principle of least privilege
roles/logging.logWriter
allows our app to send logs to Cloud Logging- We use
${google_service_account.cloud_run_sa.email}
to reference the email from the service account we created above
Build and push the Docker image
Before we can deploy to Cloud Run, we need to build and push our Docker image.
# Deploy the infrastructure first to create Artifact Registry
terraform fmt -recursive
terraform validate
terraform plan -out=tfplan
terraform apply tfplan
# Get the registry URL - this is where we'll push our image
REGISTRY_URL="${var.region}-docker.pkg.dev/${var.project_id}/docker-repo"
# Authenticate Docker with our private registry
gcloud auth configure-docker ${var.region}-docker.pkg.dev
# Build the Docker image
cd app
docker build -t ${REGISTRY_URL}/fastapi-app:latest .
# Push the image to our private registry
docker push ${REGISTRY_URL}/fastapi-app:latest
cd ..
Create the Cloud Run service
Now let’s create the actual Cloud Run service. Add this to your cloud-run.tf
:
# Cloud Run service is the main resource that runs our containerized application
# It's like a managed Kubernetes deployment but simpler
resource "google_cloud_run_service" "api_service" {
name = "fastapi-app"
location = var.region
template {
# Metadata contains configuration about the service
metadata {
annotations = {
# These annotations control scaling behavior
"autoscaling.knative.dev/minScale" = "0" # Scale to zero when no traffic
"autoscaling.knative.dev/maxScale" = "10" # Maximum 10 instances
"run.googleapis.com/client-name" = "terraform"
}
}
spec {
# Tell Cloud Run which service account our container should use
service_account_name = google_service_account.cloud_run_sa.email
containers {
# This is the Docker image we built and pushed earlier
image = "${var.region}-docker.pkg.dev/${var.project_id}/docker-repo/fastapi-app:latest"
# Environment variables that our FastAPI app can read
env {
name = "PROJECT_ID"
value = var.project_id
}
env {
name = "ENVIRONMENT"
value = var.environment
}
env {
name = "PORT"
value = "8080"
}
# Resource limits prevent runaway costs
resources {
limits = {
cpu = "1000m" # 1 CPU core
memory = "512Mi" # 512 MB RAM
}
}
# Health check endpoint
liveness_probe {
http_get {
path = "/health"
port = 8080
}
}
}
}
}
# Traffic configuration - 100% traffic goes to latest revision
traffic {
percent = 100
latest_revision = true
}
# Wait for the Cloud Run API to be enabled
depends_on = [google_project_service.cloud_run_api]
}
What’s happening here?
google_cloud_run_service
is the main resource that creates our web servicetemplate
defines how our containers should run (like a Kubernetes Pod template)service_account_name
tells Cloud Run which identity our app should useenv
blocks set environment variables that our Python app can read withos.environ.get()
resources.limits
prevent our app from using too much CPU/memory and running up costsliveness_probe
tells Cloud Run how to check if our app is healthytraffic
controls which revision of our app gets traffic (useful for blue-green deployments)
Make the service publicly accessible
By default, Cloud Run services are private. Let’s make ours public for this demo:
# IAM policy that allows anyone on the internet to call our service
# In production, you'd want to restrict this to specific users or service accounts
resource "google_cloud_run_service_iam_member" "public_access" {
service = google_cloud_run_service.api_service.name
location = google_cloud_run_service.api_service.location
role = "roles/run.invoker"
member = "allUsers"
}
What’s happening here?
google_cloud_run_service_iam_member
grants permissions to invoke (call) our serviceroles/run.invoker
is the minimum permission needed to send HTTP requests to Cloud Runmember = "allUsers"
means anyone on the internet can access our service- In production, you’d use
serviceAccount:name@project.iam.gserviceaccount.com
instead
Add outputs to see our service URL
Add this to your outputs.tf
:
# Output the URL where our service is running
output "cloud_run_service_url" {
value = google_cloud_run_service.api_service.status[0].url
description = "URL of the deployed Cloud Run service"
}
# Output the registry URL for future image pushes
output "docker_registry_url" {
value = "${var.region}-docker.pkg.dev/${var.project_id}/docker-repo"
description = "URL of the Docker registry"
}
What’s happening here?
google_cloud_run_service.api_service.status[0].url
gets the HTTPS URL that Google assigned to our service- This URL will look like
https://fastapi-app-xyz123-uc.a.run.app
- The
[0]
is needed becausestatus
is an array (list) in Terraform
Deploy the Cloud Run resources
# Apply our Terraform configuration
terraform apply -auto-approve
In the end, you will see the Terraform output:
cloud_run_service_url = "https://fastapi-app-xyz123-uc.a.run.app"
You can also get the URL using this gcloud command:
gcloud run services describe fastapi-app --region=us-central1 --format='value(status.url)'
Test your deployment
Copy the service URL from the output and test it:
# Get the URL from Terraform output
SERVICE_URL=$(terraform output -raw cloud_run_service_url)
# Test the endpoints
curl $SERVICE_URL/
curl $SERVICE_URL/health
curl $SERVICE_URL/items/123?q=test
You should see responses like:
{
"message": "Hello from Cloud Run!",
"project_id": "your-project-id",
"environment": "dev"
}
Understanding Terraform dependencies
In this tutorial, we used both implicit and explicit dependencies:
Implicit dependencies (Terraform figures these out automatically):
- Cloud Run service references the service account email
- IAM member references the service name
- Terraform knows to create the service account before the service
Explicit dependencies (we tell Terraform with depends_on
):
- Repository depends on Artifact Registry API
- Cloud Run service depends on Cloud Run API
- We use these when Terraform can’t figure out the dependency automatically
Why dependencies matter: Without proper dependencies, Terraform might try to create the Cloud Run service before the API is enabled, which would fail.
What we accomplished
In this blog, we successfully:
✅ Created a FastAPI application with health checks and environment configuration
✅ Set up Artifact Registry with Terraform instead of gcloud commands
✅ Deployed Cloud Run service with proper scaling and resource limits
✅ Used service accounts for secure authentication
✅ Made the service publicly accessible with IAM policies
✅ Learned about Terraform dependencies and how they work
The beauty of using Terraform is that everything is defined as code. If we need to create the same setup in another project or environment, we just change the variables and run terraform apply
.
Thanks for reading. See you at the next one!
This is all for this blog post. If you found it useful, do clap, comment, follow, and subscribe. See you at the next one.
Connect with me:
- LinkedIn: https://www.linkedin.com/in/akhilesh-mishra-0ab886124/
- Twitter: https://x.com/livingdevops
Tags: #GoogleCloud #CloudRun #Terraform #FastAPI #Containerization #DevOps #Infrastructure