Agent skill
gcp-coding
Language-agnostic GCP best practices and common pitfalls. Use when writing code that integrates with GCP services (Cloud Run, Secret Manager, Cloud Storage, Pub/Sub, etc.) to avoid anti-patterns and follow Google Cloud's recommended approaches.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/gcp-coding
SKILL.md
GCP Coding Best Practices
This skill provides language-agnostic guidance for integrating with Google Cloud Platform services. Apply these patterns regardless of your programming language (Python, Node.js, Go, Java, etc.).
🔴 Critical Anti-Pattern: Runtime Secret Manager API Calls
❌ NEVER DO THIS:
Anti-pattern: Store secret NAMES as environment variables, then make API calls to fetch secret VALUES at runtime.
# Python example (applies to all languages)
# Environment variable stores the secret NAME
SECRET_NAME = os.getenv("GITHUB_API_KEY_SECRET") # "gh-app-key-prod"
# Application makes runtime API call to Secret Manager
secret_client = secretmanager.SecretManagerServiceClient()
response = secret_client.access_secret_version(
request={"name": f"projects/{project}/secrets/{SECRET_NAME}/versions/latest"}
)
secret_value = response.payload.data.decode("UTF-8")
Why this is bad:
- Adds network latency (especially on cold starts)
- Requires error handling for API failures
- Creates runtime dependencies on Secret Manager availability
- Increases code complexity unnecessarily
- Slower than native integration
- Requires Secret Manager client library dependency
✅ CORRECT APPROACH: Use Cloud Run Native Secret Integration
Best practice: Let Cloud Run inject secret VALUES directly as environment variables.
Terraform/Infrastructure:
# Cloud Run service configuration
env {
name = "GITHUB_API_KEY" # Direct value, not a reference
value_source {
secret_key_ref {
secret = google_secret_manager_secret.github_api_key.id
version = "latest" # or pin to specific version
}
}
}
Application code (any language):
# Python
api_key = os.environ["GITHUB_API_KEY"]
# Node.js
const apiKey = process.env.GITHUB_API_KEY;
# Go
apiKey := os.Getenv("GITHUB_API_KEY")
# Java
String apiKey = System.getenv("GITHUB_API_KEY");
Benefits:
- Zero network latency (secrets pre-injected before container starts)
- No API client library needed
- Simpler code (direct environment variable reads)
- Fail-fast validation (Cloud Run verifies access during deployment)
- Supports automatic secret rotation (for volume mounts)
- Works the same in all languages
Secret Manager: Environment Variables vs Volume Mounts
Cloud Run supports two methods for injecting secrets. Choose based on your use case:
Method 1: Environment Variables (Most Common)
When to use:
- Static configuration values (API keys, database URLs)
- Secrets that don't change frequently
- When you need simple
os.environaccess
Terraform:
env {
name = "DATABASE_URL"
value_source {
secret_key_ref {
secret = google_secret_manager_secret.db_url.id
version = "3" # Pin to specific version for predictability
}
}
}
Behavior:
- Secret value fetched ONCE at container startup
- Cached for the lifetime of the container instance
- Requires container restart to pick up new secret versions
- Best practice: Pin to specific version (not "latest")
Method 2: Volume Mounts (For Rotation)
When to use:
- Credentials that rotate frequently (OAuth tokens, TLS certs)
- Large secrets (multi-line certificates, JSON key files)
- When you need automatic rotation without redeployment
Terraform:
volumes {
name = "secrets"
secret {
secret = google_secret_manager_secret.tls_cert.id
items {
version = "latest" # Use "latest" for auto-rotation
path = "tls.crt"
}
}
}
# Mount in container
volume_mounts {
name = "secrets"
mount_path = "/secrets"
}
Application code:
# Python: Read file every time you need it (gets latest value)
with open("/secrets/tls.crt") as f:
cert = f.read()
# Node.js
const cert = fs.readFileSync('/secrets/tls.crt', 'utf8');
# Go
cert, err := os.ReadFile("/secrets/tls.crt")
Behavior:
- Cloud Run fetches CURRENT secret value on every file read
- Automatically picks up new versions without redeployment
- No startup validation (errors occur when reading file)
- Best practice: Use "latest" version for automatic rotation
IAM Permissions for Secrets
Your Cloud Run service account needs roles/secretmanager.secretAccessor:
# Project-level (grants access to all secrets)
resource "google_project_iam_member" "secret_accessor" {
project = var.project_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.app.email}"
}
# Secret-level (grants access to specific secret)
resource "google_secret_manager_secret_iam_member" "app_access" {
secret_id = google_secret_manager_secret.api_key.id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.app.email}"
}
Cloud Run validates IAM permissions at deployment time. If the service account lacks access, deployment fails (fail-fast).
Cloud Run Best Practices
1. Use Dedicated Service Accounts (Least Privilege)
❌ Bad: Using default Compute Engine service account (overly permissive)
# Default service account has Editor role on project
service_account = "[email protected]"
✅ Good: Create service account with only required permissions
resource "google_service_account" "app" {
account_id = "my-app-service"
display_name = "My App Service Account"
}
# Grant only what's needed
resource "google_project_iam_member" "secret_accessor" {
project = var.project_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.app.email}"
}
# Use in Cloud Run
resource "google_cloud_run_v2_service" "app" {
template {
service_account = google_service_account.app.email
}
}
2. Set Resource Limits
Always specify CPU and memory limits to control costs:
resources {
limits = {
cpu = "1" # 1 vCPU
memory = "512Mi" # 512 MB
}
cpu_idle = true # Only allocate CPU during requests
}
3. Configure Proper Health Checks
startup_probe {
http_get {
path = "/health"
}
initial_delay_seconds = 10
timeout_seconds = 3
period_seconds = 5
failure_threshold = 3
}
liveness_probe {
http_get {
path = "/health"
}
period_seconds = 10
failure_threshold = 3
}
4. Control Scaling
scaling {
min_instance_count = 0 # Scale to zero when idle
max_instance_count = 10 # Prevent runaway costs
}
GCP Project ID: Don't Hardcode
❌ Avoid Hardcoding Project IDs
# Bad: Hardcoded project ID
PROJECT_ID = "my-project-prod-12345"
✅ Use Environment Variables or Metadata
Option 1: Environment variable (simplest)
# Terraform
env {
name = "GCP_PROJECT_ID"
value = var.project_id
}
# Application
project_id = os.environ["GCP_PROJECT_ID"]
Option 2: Metadata service (no env var needed)
# Python
import google.auth
_, project_id = google.auth.default()
# Node.js
const {auth} = require('google-auth-library');
const client = await auth.getClient();
const projectId = await auth.getProjectId();
# Go
import "google.golang.org/api/option"
projectID, err := metadata.ProjectID()
Cloud Storage: Use Signed URLs for Public Access
❌ Don't Make Buckets Public
# Bad: Public bucket (security risk)
resource "google_storage_bucket_iam_member" "public" {
bucket = google_storage_bucket.app.name
role = "roles/storage.objectViewer"
member = "allUsers" # Everyone can read!
}
✅ Use Signed URLs for Temporary Access
# Python
from google.cloud import storage
def generate_signed_url(bucket_name: str, blob_name: str) -> str:
"""Generate a temporary signed URL (valid for 1 hour)."""
client = storage.Client()
bucket = client.bucket(bucket_name)
blob = bucket.blob(blob_name)
url = blob.generate_signed_url(
version="v4",
expiration=datetime.timedelta(hours=1),
method="GET",
)
return url
// Node.js
const {Storage} = require('@google-cloud/storage');
const storage = new Storage();
async function generateSignedUrl(bucketName, fileName) {
const [url] = await storage
.bucket(bucketName)
.file(fileName)
.getSignedUrl({
version: 'v4',
action: 'read',
expires: Date.now() + 60 * 60 * 1000, // 1 hour
});
return url;
}
Pub/Sub: Acknowledge Messages Properly
❌ Don't Forget to Acknowledge
# Bad: Message never acknowledged (will be redelivered repeatedly)
def process_message(message):
data = json.loads(message.data)
process_data(data)
# Missing: message.ack()
✅ Always Acknowledge or Nack
# Good: Explicit acknowledgment
def process_message(message):
try:
data = json.loads(message.data)
process_data(data)
message.ack() # Success: remove from queue
except Exception as e:
logger.error(f"Failed to process message: {e}")
message.nack() # Failure: requeue for retry
Cloud Run Pub/Sub integration handles this automatically:
- Returns 200-299: Message acknowledged
- Returns 400-499: Message discarded (won't retry)
- Returns 500-599: Message nacked (will retry)
Firestore: Use Batched Writes
❌ Don't Write Documents One-by-One
# Bad: N separate writes (slow, expensive)
for item in items:
db.collection('users').document(item.id).set(item.to_dict())
✅ Use Batched Writes (500 ops/batch)
# Good: Single batch write
from google.cloud import firestore
db = firestore.Client()
batch = db.batch()
for item in items:
doc_ref = db.collection('users').document(item.id)
batch.set(doc_ref, item.to_dict())
batch.commit() # All writes in single transaction
Cloud Logging: Structured Logs
❌ Don't Use Plain Strings
# Bad: Unstructured logs (hard to query)
print(f"User {user_id} performed action {action}")
✅ Use Structured Logging (JSON)
# Good: Structured logs (easy to query in Cloud Logging)
import logging
import json
logger = logging.getLogger(__name__)
logger.info("User action performed", extra={
"json_fields": {
"user_id": user_id,
"action": action,
"timestamp": datetime.utcnow().isoformat(),
"metadata": {"source": "webhook"}
}
})
Query in Cloud Logging:
jsonPayload.user_id="12345"
jsonPayload.action="pr_opened"
Common Pitfalls Summary
| Anti-Pattern | Why Bad | Correct Approach |
|---|---|---|
| Runtime Secret Manager API calls | Adds latency, complexity, network deps | Cloud Run native secret injection |
| Hardcoded project IDs | Not portable across environments | Environment vars or metadata service |
| Public Cloud Storage buckets | Security risk | Signed URLs for temporary access |
| Default service accounts | Over-permissive (security risk) | Dedicated service accounts (least privilege) |
| Unstructured logs | Hard to query/filter | Structured JSON logging |
| One-by-one Firestore writes | Slow, expensive | Batched writes |
| Missing Pub/Sub acknowledgments | Infinite redelivery | Always ack() or nack() |
| No resource limits | Runaway costs | Explicit CPU/memory limits |
Testing Locally
Use Emulators for Local Development
# Firestore emulator
gcloud emulators firestore start
# Pub/Sub emulator
gcloud emulators pubsub start
# Set environment variables to use emulator
export FIRESTORE_EMULATOR_HOST=localhost:8080
export PUBSUB_EMULATOR_HOST=localhost:8085
Mock GCP Services in Tests
# Python with pytest
from unittest.mock import patch, MagicMock
def test_secret_access():
with patch.dict('os.environ', {'DATABASE_URL': 'postgres://test'}):
# Your test code that reads DATABASE_URL
assert get_database_url() == 'postgres://test'
Deployment Verification Checklist
Before deploying to production:
- Service account has minimal required permissions (not Editor/Owner)
- Secrets use Cloud Run native integration (not runtime API calls)
- Resource limits set (CPU, memory)
- Health checks configured
- Max instances set (cost control)
- Structured logging implemented
- IAM permissions validated (
gcloud run services describe) - Environment variables verified (no hardcoded secrets)
- Error handling for GCP API calls
- Emulators used for local testing
Additional Resources
- Secret Manager Best Practices: https://cloud.google.com/run/docs/configuring/services/secrets
- IAM Best Practices: https://cloud.google.com/iam/docs/best-practices-service-accounts
- Cloud Run Best Practices: https://cloud.google.com/run/docs/tips/general
- Structured Logging: https://cloud.google.com/logging/docs/structured-logging
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?