Skip to content

openteams-ai/terraform-hrafnar-gcp-deploy

Repository files navigation

Terraform GCP Hrafnar Deployment Module

A Terraform module for deploying the Hrafnar AI application on Google Cloud Platform with Cloud Run, Cloud SQL PostgreSQL, and optional Cloudflare DNS integration.

Features

  • 🚀 Cloud Run Deployment: Scalable serverless deployment for the Hrafnar application
  • 🔐 Secure Secret Management: AI API keys and database credentials stored in Google Secret Manager
  • 🗄️ Managed PostgreSQL: Cloud SQL PostgreSQL with automated backups and private networking
  • 📡 Cloudflare Integration: Optional DNS management with automatic TLS certificates
  • 🔧 MCP Server Support: Integration with Model Context Protocol servers
  • 🛡️ VPC Security: Private networking with Cloud NAT for outbound connectivity
  • 📊 Monitoring Ready: Built-in support for Google Cloud Monitoring and Logging

Architecture

The module deploys:

  • Hrafnar Application: Main Python/HTMX application on Cloud Run
  • PostgreSQL Database: Private Cloud SQL instance with automated backups
  • VPC Network: Private subnet with Cloud NAT for secure networking
  • Secret Manager: Secure storage for API keys and database credentials
  • DNS Records (optional): Cloudflare-managed DNS with automatic TLS

Quick Start

Basic Deployment

module "hrafnar_deploy" {
  source = "openteams-ai/hrafnar-gcp-deploy/gcp"

  project_id   = "my-gcp-project"
  name_prefix  = "prod-hrafnar"
  app_image    = "gcr.io/my-project/hrafnar:latest"

  ai_api_keys = {
    OPENAI_API_KEY = "sk-..."
  }
}

Project Structure

.
├── .github/
│   └── workflows/
│       └── terraform.yml    # CI/CD pipeline
├── examples/               # Usage examples
│   ├── dev/               # Development environment
│   └── prod/              # Production environment
├── test/                  # Terratest suite
├── cloud-run.tf           # Hrafnar application deployment
├── database.tf            # Cloud SQL PostgreSQL
├── dns.tf                 # Cloudflare DNS records
├── iam.tf                 # Service accounts and permissions
├── locals.tf              # Local values and computed resources
├── networking.tf          # VPC, subnets, and Cloud NAT
├── outputs.tf             # Module outputs
├── secrets.tf             # Secret Manager configuration
├── variables.tf           # Input variables
├── versions.tf            # Provider version constraints
└── README.md             # This documentation

Module Documentation

The following section contains auto-generated documentation for this Terraform module using terraform-docs:

Usage

Basic Usage (hrafnar app only)

module "hrafnar_gcp_deploy" {
  source  = "openteams-ai/hrafnar-gcp-deploy/gcp"
  version = "~> 1.0"

  project_id   = "my-gcp-project"
  region       = "us-central1"
  name_prefix  = "acme-hrafnar"

  # Hrafnar application configuration
  app_image = "gcr.io/my-project/hrafnar:latest"

  # AI API keys (stored securely in Secret Manager)
  ai_api_keys = {
    OPENAI_API_KEY    = "sk-..."
    ANTHROPIC_API_KEY = "sk-ant-..."
  }
}

With Cloudflare DNS and React Frontend

module "hrafnar_gcp_deploy" {
  source  = "openteams-ai/hrafnar-gcp-deploy/gcp"
  version = "~> 1.0"

  project_id   = "my-gcp-project"
  region       = "us-central1"
  name_prefix  = "acme-hrafnar"

  # Hrafnar application
  app_image = "gcr.io/my-project/hrafnar:latest"

  # Optional React frontend
  enable_react_frontend = true
  react_image          = "gcr.io/my-project/hrafnar-ui:latest"

  # Cloudflare DNS configuration
  enable_cloudflare_dns = true
  cloudflare_api_token  = "your-cloudflare-token"
  cloudflare_zone_id    = "your-zone-id"
  base_domain          = "example.com"
  api_subdomain        = "api"
  ui_subdomain         = "app"

  # AI configuration
  ai_api_keys = {
    OPENAI_API_KEY    = "sk-..."
    ANTHROPIC_API_KEY = "sk-ant-..."
  }

  # MCP servers
  mcp_servers = {
    filesystem = {
      url         = "https://mcp-fs.example.com"
      description = "Filesystem MCP server"
    }
  }
}

Requirements

Name Version
terraform >= 1.5
cloudflare ~> 5.0
google ~> 7.0
random ~> 3.0

Providers

Name Version
cloudflare 5.9.0
google 7.1.0
random 3.7.2

Modules

No modules.

Resources

Name Type
cloudflare_dns_record.app resource
google_artifact_registry_repository.docker_images resource
google_artifact_registry_repository.quay_remote resource
google_artifact_registry_repository_iam_member.cloud_run_docker_images_reader resource
google_artifact_registry_repository_iam_member.cloud_run_quay_reader resource
google_cloud_run_domain_mapping.main_app resource
google_cloud_run_service.main_app resource
google_cloud_run_service_iam_member.main_app_public resource
google_compute_firewall.allow_health_checks resource
google_compute_firewall.allow_internal resource
google_compute_global_address.private_ip_address resource
google_compute_network.main resource
google_compute_router.main resource
google_compute_router_nat.main resource
google_compute_subnetwork.private resource
google_project_iam_member.app_cloudsql_client resource
google_project_iam_member.app_cloudsql_instanceuser resource
google_project_iam_member.app_logging_writer resource
google_project_iam_member.app_monitoring_writer resource
google_project_service.required_apis resource
google_redis_instance.valkey resource
google_secret_manager_secret.ai_api_keys resource
google_secret_manager_secret.config_files resource
google_secret_manager_secret.db_connection resource
google_secret_manager_secret.db_password resource
google_secret_manager_secret.mcp_api_keys resource
google_secret_manager_secret.storage_hmac_access_id resource
google_secret_manager_secret.storage_hmac_secret resource
google_secret_manager_secret.valkey_auth resource
google_secret_manager_secret.valkey_connection resource
google_secret_manager_secret_iam_member.app_ai_api_keys resource
google_secret_manager_secret_iam_member.app_config_files resource
google_secret_manager_secret_iam_member.app_db_connection resource
google_secret_manager_secret_iam_member.app_db_password resource
google_secret_manager_secret_iam_member.app_mcp_api_keys resource
google_secret_manager_secret_iam_member.app_storage_hmac_access_id resource
google_secret_manager_secret_iam_member.app_storage_hmac_secret resource
google_secret_manager_secret_iam_member.app_valkey_auth resource
google_secret_manager_secret_iam_member.app_valkey_connection resource
google_secret_manager_secret_version.ai_api_keys resource
google_secret_manager_secret_version.config_files resource
google_secret_manager_secret_version.db_connection resource
google_secret_manager_secret_version.db_password resource
google_secret_manager_secret_version.mcp_api_keys resource
google_secret_manager_secret_version.storage_hmac_access_id resource
google_secret_manager_secret_version.storage_hmac_secret resource
google_secret_manager_secret_version.valkey_auth resource
google_secret_manager_secret_version.valkey_connection resource
google_service_account.app resource
google_service_networking_connection.private_vpc_connection resource
google_sql_database.main resource
google_sql_database_instance.main resource
google_sql_user.main resource
google_storage_bucket.hrafner_storage resource
google_storage_bucket_iam_member.dev_storage_access resource
google_storage_bucket_iam_member.hrafner_app_storage_access resource
google_storage_bucket_object.storage_folders resource
google_storage_hmac_key.hrafner_storage_hmac resource
random_id.network_suffix resource
random_password.db_password resource
google_project.current data source

Inputs

Name Description Type Default Required
ai_api_keys Map of AI API keys where key is the environment variable name (e.g., OPENAI_API_KEY, ANTHROPIC_API_KEY) and value is the actual API key (stored in Secret Manager) map(string) {} no
app_command Command to run the container list(string)
[
"hrafnar",
"serve"
]
no
app_config_files Configuration files to mount as volumes from Secret Manager. Key is the config name, value contains file content and mount path.
map(object({
content = string # File content to store in Secret Manager
mount_path = string # Path where file will be mounted in container (e.g., "/etc/config/app.yaml")
}))
{} no
app_cpu CPU allocation for the hrafnar application string "1000m" no
app_env_vars Environment variables for the hrafnar application map(string) {} no
app_image Container image for the hrafnar application (without tag) string n/a yes
app_image_sha Container image SHA (takes precedence over tag if provided) string "" no
app_image_tag Container image tag string "latest" no
app_max_instances Maximum number of instances for the hrafnar application number 10 no
app_memory Memory allocation for the hrafnar application string "512Mi" no
app_min_instances Minimum number of instances for the hrafnar application number 0 no
app_port Port the application listens on number 8080 no
base_domain Base domain name managed by Cloudflare (e.g., 'example.com'). A subdomain will be created under this domain for application access string "" no
cloudflare_zone_id Cloudflare zone ID for DNS records (required if enable_cloudflare_dns is true) string "" no
database_backup_enabled Enable automated database backups bool true no
database_backup_retention_days Number of days to retain database backups number 7 no
database_disk_autoresize_limit Maximum disk size in GB for database autoresize number 100 no
database_disk_size Database disk size in GB number 20 no
database_log_retention_days Number of days to retain database transaction logs number 7 no
database_ssl_mode SSL mode for database connections string "ENCRYPTED_ONLY" no
database_tier Database instance tier string "db-f1-micro" no
enable_artifact_registry Enable Artifact Registry remote repository for quay.io bool false no
enable_cloudflare_dns Enable Cloudflare DNS management bool false no
enable_database Enable Cloud SQL database deployment bool true no
enable_monitoring Enable Google Cloud Monitoring and Logging bool true no
enable_nat_gateway Enable Cloud NAT for outbound internet access bool true no
enable_storage Enable Cloud Storage bucket for the application bool false no
enable_valkey Enable Google Cloud Memorystore for Redis (Valkey-compatible) deployment bool false no
hrafnar_subdomain Subdomain for hrafnar application access (e.g., 'hrafnar' for hrafnar.example.com) string "hrafnar" no
labels Labels to apply to all resources map(string) {} no
log_level Log level for applications string "INFO" no
mcp_servers MCP server configurations
map(object({
url = string
api_key = optional(string)
description = string
}))
{} no
name_prefix Prefix for resource naming string n/a yes
private_subnet_cidr CIDR block for the private subnet string "10.0.0.0/24" no
project_id The GCP project ID where resources will be created string n/a yes
region The GCP region for resources string "us-central1" no
storage_app_role IAM role for the application to access the storage bucket string "roles/storage.objectAdmin" no
storage_cors_config CORS configuration for the storage bucket
list(object({
origin = list(string)
method = list(string)
response_header = list(string)
max_age_seconds = number
}))
[
{
"max_age_seconds": 3600,
"method": [
"GET",
"HEAD",
"PUT",
"POST",
"DELETE"
],
"origin": [
""
],
"response_header": [
"
"
]
}
]
no
storage_create_hmac_key Create HMAC key for S3-compatible access to the storage bucket bool false no
storage_dev_access_members List of IAM members to grant development access to the storage bucket (e.g., 'user:[email protected]') list(string) [] no
storage_dev_role IAM role for development access to the storage bucket string "roles/storage.objectAdmin" no
storage_enable_dev_access Enable external access to the storage bucket for development bool false no
storage_folders List of folders to create in the storage bucket list(string)
[
"files",
"thumbnails"
]
no
storage_force_destroy Force destroy the storage bucket even if it contains objects bool false no
storage_lifecycle_rules Lifecycle rules for the storage bucket
list(object({
condition = object({
age = optional(number)
num_newer_versions = optional(number)
matches_prefix = optional(list(string))
matches_storage_class = optional(list(string))
})
action = object({
type = string
storage_class = optional(string)
})
}))
[
{
"action": {
"type": "Delete"
},
"condition": {
"age": 30,
"matches_prefix": [
"thumbnails/"
]
}
},
{
"action": {
"type": "Delete"
},
"condition": {
"num_newer_versions": 5
}
}
]
no
storage_public_access_prevention Public access prevention setting for the storage bucket string "enforced" no
storage_versioning_enabled Enable versioning for the storage bucket bool true no
valkey_auth_enabled Whether AUTH is enabled for the Redis instance bool true no
valkey_maintenance_policy Maintenance policy for Redis instance
object({
weekly_maintenance_window = object({
day = string # MONDAY, TUESDAY, etc.
start_time = object({
hours = number # 0-23
minutes = number # 0-59
seconds = number # 0-59
nanos = number # 0-999999999
})
})
})
{
"weekly_maintenance_window": {
"day": "SUNDAY",
"start_time": {
"hours": 3,
"minutes": 0,
"nanos": 0,
"seconds": 0
}
}
}
no
valkey_memory_size_gb Redis instance memory size in GB number 1 no
valkey_redis_configs Redis configuration parameters map(string)
{
"maxmemory-policy": "allkeys-lru"
}
no
valkey_redis_version Redis version for the instance string "REDIS_7_0" no
valkey_tier Service tier of the Redis instance (BASIC or STANDARD_HA) string "BASIC" no
valkey_transit_encryption_mode TLS mode for Redis instance string "SERVER_AUTHENTICATION" no
vpc_cidr CIDR block for the VPC string "10.0.0.0/16" no

Outputs

Name Description
ai_api_key_secret_names Names of the Secret Manager secrets for AI API keys
app_domain Full domain name for application access
common_labels Common labels applied to all resources
database_connection_name Connection name for the Cloud SQL database instance
database_connection_secret_name Name of the Secret Manager secret for database connection string
database_instance_name Name of the Cloud SQL database instance
database_password_secret_name Name of the Secret Manager secret for database password
database_private_ip Private IP address of the Cloud SQL database instance
hrafnar_app_service_account_email Email of the hrafnar application service account
hrafnar_app_service_name Name of the hrafnar Cloud Run service
hrafnar_app_url URL of the hrafnar application
private_subnet_id ID of the private subnet
private_subnet_name Name of the private subnet
resource_prefix Prefix used for naming resources
storage_bucket_name Name of the Cloud Storage bucket
storage_bucket_self_link Self link of the Cloud Storage bucket
storage_bucket_url URL of the Cloud Storage bucket
storage_hmac_access_id_secret_name Name of the Secret Manager secret containing the storage HMAC access ID
storage_hmac_secret_secret_name Name of the Secret Manager secret containing the storage HMAC secret key
valkey_auth_secret_name Name of the Secret Manager secret containing the Valkey auth string
valkey_connection_secret_name Name of the Secret Manager secret containing the Valkey connection URL
valkey_host Host IP of the Valkey/Redis instance
valkey_instance_name Name of the Valkey/Redis instance
valkey_memory_size_gb Memory size of the Valkey/Redis instance in GB
valkey_port Port of the Valkey/Redis instance
valkey_tier Service tier of the Valkey/Redis instance
vpc_id ID of the VPC network
vpc_name Name of the VPC network

Documentation Maintenance

This README uses terraform-docs to automatically generate and maintain module documentation. The content between <!-- BEGIN_TF_DOCS --> and <!-- END_TF_DOCS --> is automatically generated.

How to Update Documentation

  1. Auto-generate: Run make docs to update the terraform-docs section
  2. Manual content: Edit sections outside the terraform-docs markers
  3. Configuration: Modify .terraform-docs.yml to customize the generated content

terraform-docs Workflow

  • The make docs command uses Docker to run terraform-docs
  • It reads your Terraform files (main.tf, variables.tf, outputs.tf, etc.)
  • Generates documentation in Markdown format
  • Injects the content between the <!-- BEGIN_TF_DOCS --> and <!-- END_TF_DOCS --> markers
  • Preserves all custom content outside these markers

Important: Never manually edit content between the terraform-docs markers as it will be overwritten.

Examples

See the examples/ directory for complete usage examples:

  • Development: Minimal configuration for development environments
  • Production: Full-featured production deployment with React frontend and Cloudflare DNS

Security Considerations

  • Secret Management: All sensitive data (API keys, database passwords) is stored in Google Secret Manager
  • Network Security: Database runs in a private subnet with no public IP
  • Access Control: Fine-grained IAM permissions for service accounts
  • TLS Encryption: Database connections require TLS, Cloudflare provides automatic HTTPS

Testing

The module includes comprehensive tests using Terratest:

Test Coverage

  1. Hrafnar Module Functionality: Tests variable validation and core functionality
  2. Cloudflare Integration: Validates DNS configuration when enabled
  3. GCP Resources: Tests Cloud Run, Cloud SQL, and networking components
  4. Configuration Scenarios: Tests minimal and full-featured deployments

Running Tests

Unit Tests (No Infrastructure)

# Run validation tests (no cloud resources)
cd test && go test -v -run TestTerraformValidation
cd test && go test -v -run TestExamplesValidation
cd test && go test -v -run TestHrafnarModuleFunctionality

Integration Tests (Real Infrastructure)

Integration tests deploy actual infrastructure to test end-to-end functionality.

Required Environment Variables:

export TF_VAR_project_id="your-gcp-project-id"
export TF_VAR_app_image="gcr.io/your-project/hrafnar:latest"
export TF_VAR_openai_api_key="sk-your-openai-key"

# Optional (for Cloudflare DNS testing):
export TF_VAR_cloudflare_api_token="your-cloudflare-token"
export TF_VAR_cloudflare_zone_id="your-zone-id"
export TF_VAR_base_domain="yourdomain.com"

Run Integration Tests:

# Test development environment deployment
cd test && go test -v -run TestDevEnvironmentDeployment -timeout 30m

# Test production environment deployment
cd test && go test -v -run TestProdEnvironmentDeployment -timeout 30m

# Test minimal configuration
cd test && go test -v -run TestMinimalDeployment -timeout 30m

# Run all integration tests
cd test && go test -v -run Integration -timeout 45m

Important Notes:

  • Integration tests will create and destroy real GCP resources
  • Tests use unique prefixes to avoid naming conflicts
  • Resources are automatically cleaned up after each test
  • Ensure you have appropriate GCP permissions and billing enabled

Makefile Commands

Command Description
make help Display available make targets with descriptions
make init Initialize OpenTofu and install pre-commit hooks
make fmt Format all Terraform files
make validate Validate Terraform configuration
make lint Run all linting checks
make test Run the full test suite
make docs Generate documentation with terraform-docs
make clean Clean up temporary files and directories

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •