Stackvora.tech

GCP Cloud Run: deploy a containerized API

Google Cloud Run provides a fully managed platform for stateless containers that are invocable via web requests or Pub/Sub events. It offers autoscaling…

Google Cloud Run provides a fully managed platform for stateless containers that are invocable via web requests or Pub/Sub events. It offers autoscaling from zero to thousands of instances based on traffic, eliminating server management overhead. This article details the process of containerizing a simple API, deploying it to Cloud Run, and configuring essential settings for production use.

Our focus will be on an HTTP-triggered service, common for RESTful APIs and webhooks. We'll use a Python Flask application as the example, but the principles apply broadly to any language or framework that can be containerized.

Containerizing Your API with Docker

Before deployment, your application must be packaged into a Docker container. This ensures a consistent runtime environment across development and deployment stages. Let's assume a basic Flask application named main.py:

# main.py
from flask import Flask, jsonify, request
import os

app = Flask(__name__)

@app.route('/')
def hello_world():
    return jsonify(message='Hello from Cloud Run API!')

@app.route('/status', methods=['GET'])
def get_status():
    return jsonify(status='OK', version=os.environ.get('APP_VERSION', '1.0.0'))

@app.route('/echo', methods=['POST'])
def echo():
    data = request.json
    return jsonify(received_data=data)

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=int(os.environ.get('PORT', 8080)))

Dockerfile Creation

A Dockerfile defines the steps to build your container image. For our Flask app, a multi-stage build is a good practice for smaller image sizes.

# Dockerfile
# Stage 1: Build dependencies
FROM python:3.9-slim-buster as builder

WORKDIR /app

# Install production dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Stage 2: Final image
FROM python:3.9-slim-buster

WORKDIR /app

# Copy only the installed dependencies from the builder stage
COPY --from=builder /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages
COPY --from=builder /usr/local/bin/gunicorn /usr/local/bin/gunicorn

# Copy application code
COPY . .

# Cloud Run expects your application to listen on the PORT environment variable.
# It defaults to 8080 if not specified.
ENV PORT 8080

# Expose the port (optional, for documentation and local testing)
EXPOSE 8080

# Command to run the application using Gunicorn for production
# Gunicorn is recommended over Flask's built-in server for production.
CMD ["gunicorn", "--bind", "0.0.0.0:$(PORT)", "main:app"]

Ensure you have a requirements.txt file:

# requirements.txt
Flask==2.2.3
gunicorn==20.1.0

To test locally, build and run the container:

docker build -t cloud-run-api .
docker run -p 8080:8080 cloud-run-api

Then access http://localhost:8080 in your browser or with curl.

Building and Pushing the Docker Image to Artifact Registry

Cloud Run deploys images from Google's container registries. Artifact Registry is the recommended choice, succeeding Container Registry (GCR). First, enable the necessary APIs:

gcloud services enable artifactregistry.googleapis.com cloudbuild.googleapis.com run.googleapis.com

Create an Artifact Registry Repository

Create a Docker repository in your desired region. For Cloud Run deployments, it's generally best to co-locate your image repository in the same region as your Cloud Run service to minimize latency and egress costs.

gcloud artifacts repositories create cloud-run-repos --repository-format=docker \
    --location=europe-west1 --description="Docker repository for Cloud Run services"

Replace europe-west1 with your preferred region.

Build and Push the Image

Use gcloud builds submit to build your Docker image and push it to Artifact Registry. This leverages Cloud Build, Google's CI/CD service, for the build process.

PROJECT_ID=$(gcloud config get-value project)
REPO_LOCATION="europe-west1" # Must match your Artifact Registry repo location
IMAGE_NAME="api"
IMAGE_TAG="${REPO_LOCATION}-docker.pkg.dev/${PROJECT_ID}/cloud-run-repos/${IMAGE_NAME}:v1.0.0"

gcloud builds submit --tag "${IMAGE_TAG}" .

This command builds the Dockerfile in the current directory and tags it with a specific version (v1.0.0). The tag format REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY_NAME/IMAGE_NAME:TAG is crucial for Artifact Registry.

Deploying to Cloud Run

Once your image is in Artifact Registry, you can deploy it to Cloud Run. The deployment process creates a new Cloud Run service or updates an existing one.

# Ensure you've set your default project and region for gcloud
# gcloud config set project your-gcp-project-id
# gcloud config set run/region europe-west1

SERVICE_NAME="api-service"
REGION="europe-west1" # Must match your Cloud Run service region
ALLOW_UNAUTHENTICATED="--allow-unauthenticated" # Or omit for authenticated access

gcloud run deploy "${SERVICE_NAME}" \
    --image "${IMAGE_TAG}" \
    --region "${REGION}" \
    "${ALLOW_UNAUTHENTICATED}" \
    --platform managed \
    --port 8080 \
    --memory 512Mi \
    --cpu 1 \
    --max-instances 10 \
    --min-instances 0 \
    --set-env-vars "APP_VERSION=v1.0.0" \
    --service-account "cloud-run-service-account@${PROJECT_ID}.iam.gserviceaccount.com" # Replace with a dedicated SA

Explanation of Deployment Flags:

  • --image "${IMAGE_TAG}": Specifies the Docker image to deploy.
  • --region "${REGION}": The GCP region where the service will run (e.g., europe-west1, us-central1).
  • --allow-unauthenticated: Makes the service publicly accessible without requiring Google authentication. Omit this for internal services.
  • --platform managed: Explicitly selects the fully managed Cloud Run environment.
  • --port 8080: The port your container listens on. Cloud Run injects the PORT environment variable, so your application should respect it (as our Flask example does). If your app listens on a different port, specify it here.
  • --memory 512Mi: Allocates 512 MiB of memory per container instance. Default is 256 MiB. Max is 16 GiB.
  • --cpu 1: Allocates 1 vCPU per container instance. Default is 1 vCPU. Max is 8 vCPUs.
  • --max-instances 10: Defines the maximum number of container instances Cloud Run can scale up to. Default is 100. Setting a lower limit helps control costs and resource consumption.
  • --min-instances 0: Allows the service to scale down to zero instances when idle, incurring no charges. This is a key cost-saving feature of Cloud Run. For latency-sensitive applications, set --min-instances to 1 or more to keep instances warm.
  • --set-env-vars "APP_VERSION=v1.0.0": Sets environment variables accessible within your container. Multiple --set-env-vars flags can be used. For sensitive data, use Secret Manager.
  • --service-account "...": Specifies the service account the Cloud Run service will use to interact with other GCP services. It's best practice to create a dedicated service account with the least privilege necessary. By default, Cloud Run uses the Compute Engine default service account.

After deployment, the command output will provide the service URL (e.g., https://api-service-abcxyz-ew.a.run.app). You can then test your API:

curl -X GET https://api-service-abcxyz-ew.a.run.app/status
curl -X POST -H "Content-Type: application/json" -d '{"key": "value"}' https://api-service-abcxyz-ew.a.run.app/echo

Configuring Custom Domains and TLS

Cloud Run provides a default .run.app URL, but for production, you'll likely want a custom domain (e.g., api.yourdomain.com). Cloud Run handles TLS certificate provisioning and renewal automatically.

  1. Map your custom domain:
    gcloud run domains add api.yourdomain.com --service "${SERVICE_NAME}" --region "${REGION}"
            

    This command will output DNS records (A and AAAA records) you need to configure in your domain registrar or DNS provider.

  2. Update DNS records:

    Go to your domain provider's DNS management console and add the specified A and AAAA records. It might take some time for DNS changes to propagate (up to 24-48 hours, though usually faster).

  3. Verify and access:

    Once DNS propagation is complete, Cloud Run will automatically provision an SSL certificate. You can then access your service at https://api.yourdomain.com.

Managing Revisions and Rollbacks

Each deployment to Cloud Run creates a new "revision" of your service. Cloud Run keeps previous revisions available, allowing for quick rollbacks or traffic splitting.

Traffic Management:

By default, 100% of traffic is routed to the latest revision. You can split traffic between revisions for A/B testing or canary deployments:

gcloud run services update "${SERVICE_NAME}" \
    --region "${REGION}" \
    --traffic "LATEST=90,api-service-00001-fuc=10" # LATEST gets 90%, old revision gets 10%

To roll back to a previous stable revision (e.g., api-service-00001-fuc):

gcloud run services update "${SERVICE_NAME}" \
    --region "${REGION}" \
    --to-revisions "api-service-00001-fuc" # Sends 100% traffic to this revision

Common Pitfalls

  • Container not listening on 0.0.0.0:PORT: Cloud Run injects the PORT environment variable. Your application must listen on 0.0.0.0 and the value of PORT. Not doing so (e.g., listening on 127.0.0.1 or a hardcoded port) will lead to connection refused errors.
  • Exceeding memory limits: If your application consumes more memory than allocated (e.g., 512Mi), Cloud Run will terminate the instance and restart it, leading to 500 errors. Monitor memory usage in Cloud Monitoring.
  • Cold starts for min-instances=0: While cost-effective, scaling to zero means the first request after an idle period will experience a "cold start" delay as a new container instance spins up. For low-latency requirements, consider setting --min-instances to 1 or higher.
  • Incorrect IAM permissions: The service account used by your Cloud Run service needs appropriate permissions (e.g., to access Cloud Storage buckets, Secret Manager secrets). Errors like "Permission denied" often point to IAM misconfigurations.
  • Using HTTP for sensitive data: Always ensure your Cloud Run service is accessed over HTTPS. Cloud Run automatically provides TLS for both .run.app and custom domains.
  • Large Docker images: Keep your Docker images as small as possible. Use multi-stage builds, smaller base images (e.g., python:3.9-slim-buster), and ensure unnecessary development dependencies are not included. This speeds up deployments and cold start times.

Back to the knowledge base · Ask the AI assistant