Deploying GCP Cloud Function to rotate the service account key stored in Secret Manager
Terraform on Google Cloud V1.4 — Cloud Functions
In DevOps, secrets are like keys that unlock important resources and sensitive data. If these secrets are not securely managed, it can lead to unauthorized access, data breaches, and financial losses.
That’s why DevOps teams need to prioritize protecting sensitive information, such as API keys, passwords, and cryptographic keys.
In the last blog post of my blog series, Terraform on Google Cloud, I talked about leveraging GCP Secret Manager to store the service account key that can be accessed securely by GKE, CI workflows and applications.
In this blog I will talk about rotating the service account key stored in Secret Manager using Cloud Functions (Gen 1) and how to deploy the Cloud Function with Terraform.
What are Cloud Functions?
Cloud Functions is an event-driven serverless compute platform. Cloud Functions allows you to write your code without worrying about provisioning resources or scaling to handle changing requirements.
Cloud Functions allow developers to write and deploy lightweight code snippets or functions that are executed in response to events or triggers, such as HTTP requests, Pub/Sub messages, or changes in storage buckets.
Why Cloud Functions?
- Serverless
- Event-driven
- Scalable
- Cheap
- Easily integrated with other GCP services
When to use Cloud Functions
- When you need to respond to events or triggers, such as HTTP requests, database changes, or file uploads
- When you need to process small amounts of data, and perform quick functions such as rotating the secrets on a schedule
- When you need cost-optimized stateless operations
When to avoid Cloud Functions
- When you need stateful operations
- When your tasks require longer execution times – use Cloud Run instead
- When your application requires complex networking configurations
Best Practices for using Cloud Functions
- Keep it small
- Use environment variables
- Implement logging and error handling
- Optimize function performance
- Monitor and manage costs
Deploying Cloud Function – below steps will be required for this
Write Python code to rotate the service account key, and define dependencies
Before we start, let’s create a directory functions
in the root path and key-rotation
directory inside functions – functions/key-rotation
. Create main.py
and requirements.txt
under it.
main.py
import os
import json
import base64
from google.cloud import secretmanager_v1 as secretmanager
from google.cloud import iam_admin_v1
from google.oauth2 import service_account
def rotate_service_account_key(event, context):
"""Cloud Function to rotate service account keys stored in Secret Manager"""
# Get configuration from environment variables
project_id = os.environ.get('PROJECT_ID')
secret_id = os.environ.get('SECRET_ID', 'artifact-registry-sa-key')
service_account_email = os.environ.get('SERVICE_ACCOUNT_EMAIL')
if not all([project_id, service_account_email]):
print("Missing required environment variables")
return "Error: Missing configuration"
try:
# Create clients
secret_client = secretmanager.SecretManagerServiceClient()
iam_client = iam_admin_v1.IAMClient()
print(f"Starting key rotation for service account: {service_account_email}")
# Create new service account key
service_account_path = f"projects/{project_id}/serviceAccounts/{service_account_email}"
new_key_request = iam_admin_v1.CreateServiceAccountKeyRequest(
name=service_account_path,
private_key_type=iam_admin_v1.ServiceAccountPrivateKeyType.TYPE_GOOGLE_CREDENTIALS_FILE
)
new_key = iam_client.create_service_account_key(request=new_key_request)
new_key_data = base64.b64decode(new_key.private_key_data).decode('utf-8')
print("New service account key created successfully")
# Store new key in Secret Manager
secret_name = f"projects/{project_id}/secrets/{secret_id}"
# Add new secret version
secret_client.add_secret_version(
request={
"parent": secret_name,
"payload": {
"data": new_key_data.encode("UTF-8")
}
}
)
print("New key stored in Secret Manager")
# Get all versions and disable old ones (keep latest 2 versions)
versions = secret_client.list_secret_versions(
request={"parent": secret_name}
)
enabled_versions = []
for version in versions:
if version.state == secretmanager.SecretVersion.State.ENABLED:
enabled_versions.append(version)
# Sort by create time (newest first)
enabled_versions.sort(key=lambda x: x.create_time, reverse=True)
# Disable old versions (keep latest 2)
for version in enabled_versions[2:]:
secret_client.disable_secret_version(
request={"name": version.name}
)
print(f"Disabled old secret version: {version.name}")
# Delete old service account keys (keep only the new one)
keys = iam_client.list_service_account_keys(
request={"name": service_account_path}
)
for key in keys.keys:
# Skip the key we just created
if key.name != new_key.name and key.key_type == iam_admin_v1.ServiceAccountKey.KeyType.USER_MANAGED:
try:
iam_client.delete_service_account_key(
request={"name": key.name}
)
print(f"Deleted old service account key: {key.name}")
except Exception as e:
print(f"Failed to delete key {key.name}: {e}")
print("Service account key rotation completed successfully")
return "Service account key rotation completed."
except Exception as e:
print(f"Error during key rotation: {e}")
return f"Error: {e}"
requirements.txt
# Client library for interacting with Google Cloud Secret Manager
google-cloud-secret-manager==2.16.4
# Library for authenticating requests to Google APIs
google-auth==2.23.4
# IAM client for managing service account keys
google-cloud-iam==2.12.0
Create a file key-rotation.tf on root dir and paste below code in that one
Zip the code files
data "archive_file" "key_rotation" {
type = "zip"
output_path = "${path.module}/functions/key-rotation.zip"
dynamic "source" {
for_each = [
"${path.module}/functions/key-rotation/main.py",
"${path.module}/functions/key-rotation/requirements.txt"
]
content {
content = file(source.value)
filename = basename(source.value)
}
}
}
Create a storage bucket and storage object with zipped code file
resource "google_storage_bucket" "cf_bucket" {
project = var.project_id
name = "${var.project_id}-cf-archive"
uniform_bucket_level_access = true
location = var.region
public_access_prevention = "enforced"
versioning {
enabled = true
}
labels = {
environment = var.environment
purpose = "cloud-functions"
managed-by = "terraform"
}
}
# Bucket object to hold the code for Cloud Function
resource "google_storage_bucket_object" "cf_bucket_object" {
name = "key-rotation-${data.archive_file.key_rotation.output_md5}.zip"
bucket = google_storage_bucket.cf_bucket.name
source = data.archive_file.key_rotation.output_path
}
Create a Pub/Sub topic for the Cloud Function
# Pub/Sub topic to trigger Cloud Function
resource "google_pubsub_topic" "secret_rotation" {
name = "secret-rotation-topic"
message_retention_duration = "86400s" # 24 hours
project = var.project_id
labels = {
environment = var.environment
purpose = "secret-rotation"
managed-by = "terraform"
}
}
Create service account with access to Secret Manager and service account key creation
# Service account to run the Cloud Function
resource "google_service_account" "key_rotation_cf_sa" {
project = var.project_id
account_id = "key-rotation-cf-sa"
display_name = "Key Rotation Cloud Function Service Account"
description = "Service account for the key rotation Cloud Function"
}
# Grant necessary permissions to the service account
resource "google_project_iam_member" "key_rotation_permissions" {
for_each = toset([
"roles/secretmanager.admin",
"roles/iam.serviceAccountKeyAdmin",
"roles/logging.logWriter",
"roles/monitoring.metricWriter"
])
project = var.project_id
role = each.value
member = "serviceAccount:${google_service_account.key_rotation_cf_sa.email}"
}
# Allow the Cloud Function to be invoked by Pub/Sub
resource "google_cloudfunctions_function_iam_member" "invoker" {
project = var.project_id
region = var.region
cloud_function = google_cloudfunctions_function.key_rotation_cf.name
role = "roles/cloudfunctions.invoker"
member = "serviceAccount:service-${data.google_project.current.number}@gcp-sa-pubsub.iam.gserviceaccount.com"
}
# Get current project information
data "google_project" "current" {
project_id = var.project_id
}
Important Note: The original blog had IAM role assignment errors. The correct way to assign IAM roles to Cloud Functions is through google_project_iam_member
for project-level permissions, not google_cloudfunctions_function_iam_member
for function-specific roles like secretmanager.admin
and iam.serviceAccountKeyAdmin
.
Create the Cloud Function with Pub/Sub trigger
# Cloud Function (Gen 1)
resource "google_cloudfunctions_function" "key_rotation_cf" {
name = "key-rotation-function"
project = var.project_id
region = var.region
runtime = "python39"
entry_point = "rotate_service_account_key"
available_memory_mb = 512
timeout = 540
max_instances = 10
source_archive_bucket = google_storage_bucket.cf_bucket.name
source_archive_object = google_storage_bucket_object.cf_bucket_object.name
service_account_email = google_service_account.key_rotation_cf_sa.email
event_trigger {
event_type = "google.pubsub.topic.publish"
resource = google_pubsub_topic.secret_rotation.name
failure_policy {
retry = true
}
}
environment_variables = {
PROJECT_ID = var.project_id
SECRET_ID = "artifact-registry-sa-key"
SERVICE_ACCOUNT_EMAIL = google_service_account.artifact_registry_sa.email
}
labels = {
environment = var.environment
purpose = "secret-rotation"
managed-by = "terraform"
}
depends_on = [
google_project_iam_member.key_rotation_permissions,
google_storage_bucket_object.cf_bucket_object
]
}
Create a Cloud Scheduler with Pub/Sub as target
# Create a scheduler (cron job)
resource "google_cloud_scheduler_job" "secret_rotation" {
name = "secret-rotation-job"
project = var.project_id
region = var.region
description = "Triggers Cloud Function to rotate service account keys"
schedule = "0 2 1 * *" # At 02:00 on day-of-month 1 (1st of every month)
time_zone = "UTC"
pubsub_target {
topic_name = google_pubsub_topic.secret_rotation.id
data = base64encode(jsonencode({
message = "trigger service account key rotation",
timestamp = timestamp()
}))
}
retry_config {
retry_count = 3
}
depends_on = [
google_pubsub_topic.secret_rotation,
google_cloudfunctions_function.key_rotation_cf
]
}
Add required variables
Add these variables to your variables.tf
:
variable "key_rotation_schedule" {
type = string
description = "Cron schedule for key rotation (default: 1st of every month at 2 AM UTC)"
default = "0 2 1 * *"
}
variable "cf_timeout" {
type = number
description = "Cloud Function timeout in seconds"
default = 540
validation {
condition = var.cf_timeout >= 60 && var.cf_timeout <= 540
error_message = "Cloud Function timeout must be between 60 and 540 seconds."
}
}
Add outputs
Add these outputs to your outputs.tf
:
# Cloud Function outputs
output "key_rotation_function_name" {
value = google_cloudfunctions_function.key_rotation_cf.name
description = "Name of the key rotation Cloud Function"
}
output "key_rotation_function_url" {
value = google_cloudfunctions_function.key_rotation_cf.https_trigger_url
description = "HTTPS trigger URL for the Cloud Function (if applicable)"
}
output "secret_rotation_topic" {
value = google_pubsub_topic.secret_rotation.name
description = "Name of the Pub/Sub topic for secret rotation"
}
output "scheduler_job_name" {
value = google_cloud_scheduler_job.secret_rotation.name
description = "Name of the Cloud Scheduler job"
}
output "next_rotation_schedule" {
value = var.key_rotation_schedule
description = "Cron schedule for the next key rotation"
}
That’s it. Now it will rotate the key on the 1st of every month at 2 AM UTC.
Deploy the Cloud Function
Just go ahead with deploying everything by running Terraform commands or a CI workflow – I talked about both approaches in my previous blogs.
Local deployment
# Format and validate
terraform fmt -recursive
terraform validate
# Plan and apply
terraform plan -out=tfplan
terraform apply tfplan
Test your Cloud Function
After deployment, you can test the function manually:
# Trigger the function manually via Pub/Sub
gcloud pubsub topics publish secret-rotation-topic --message="manual test"
# Check function logs
gcloud functions logs read key-rotation-function --region=us-central1
# Check the scheduler job
gcloud scheduler jobs list --location=us-central1
# Describe the scheduler job
gcloud scheduler jobs describe secret-rotation-job --location=us-central1
Monitor your function
# View function details
gcloud functions describe key-rotation-function --region=us-central1
# Check recent executions
gcloud functions logs read key-rotation-function --region=us-central1 --limit=50
# Test the scheduler
gcloud scheduler jobs run secret-rotation-job --location=us-central1
Troubleshooting Common Issues
IAM Permission Errors
If you encounter the errors mentioned in the feedback:
Error 400: Role roles/secretmanager.admin is not supported for this resource.
Error 400: Role roles/iam.serviceAccountKeyAdmin is not supported for this resource.
The issue is that these roles should be assigned at the project level using google_project_iam_member
, not at the function level using google_cloudfunctions_function_iam_member
. The corrected code above addresses this issue.
Function Timeout Issues
If your function times out, increase the timeout value:
resource "google_cloudfunctions_function" "key_rotation_cf" {
# ... other configuration
timeout = 540 # Maximum allowed for Gen 1 functions
}
Secret Manager Access Issues
Ensure your service account has the correct permissions:
# Check current IAM policy
gcloud projects get-iam-policy YOUR_PROJECT_ID
# Verify service account permissions
gcloud iam service-accounts get-iam-policy key-rotation-cf-sa@YOUR_PROJECT_ID.iam.gserviceaccount.com
Cloud Functions Generation 1 vs Generation 2
In this blog, we used Cloud Functions Generation 1, which is the original Cloud Functions runtime. However, Google Cloud has evolved and now offers Cloud Functions Generation 2, also known as Cloud Run Functions.
Key Differences:
Cloud Functions Gen 1:
- Based on the original Cloud Functions runtime
- Limited to 540 seconds maximum execution time
- Fewer configuration options
- Event-driven with specific trigger types
Cloud Functions Gen 2 (Cloud Run Functions):
- Built on Cloud Run infrastructure
- Support for up to 60 minutes execution time
- More configuration flexibility
- Better performance and scaling
- Support for more trigger types and HTTP/2
What’s Next?
In the next blog post, I will show you how to migrate this same key rotation function to Cloud Functions Generation 2 (Cloud Run Functions) and take advantage of the improved performance, longer execution times, and better scaling capabilities.
We’ll also explore:
- Advanced scheduling options
- Better error handling and retry mechanisms
- Integration with Cloud Monitoring and Alerting
- Cost optimization strategies
What we accomplished
In this blog, we successfully:
✅ Deployed Cloud Functions Gen 1 for automated key rotation
✅ Fixed common IAM permission issues with proper role assignments
✅ Set up automated scheduling with Cloud Scheduler
✅ Implemented proper error handling and logging
✅ Created a complete key rotation workflow that runs monthly
✅ Integrated with Secret Manager from our previous setup
Thank you for reading. See you in the next one where we’ll explore Cloud Functions Gen 2!
Connect with me:
- LinkedIn: https://www.linkedin.com/in/akhilesh-mishra-0ab886124/
- Twitter: https://x.com/livingdevops
Tags: #GoogleCloud #CloudFunctions #Terraform #SecretManager #DevOps #Infrastructure #Automation #Security