From 96cc88843eca4f5b397213ba27a6883879a10ece Mon Sep 17 00:00:00 2001 From: Jye Cusch Date: Thu, 25 Sep 2025 10:36:52 +1000 Subject: [PATCH 01/10] ci: add terraform linting, validation and sec scanning --- .github/workflows/test.yml | 68 ++++++++++++++++++++++++++++++++++++++ .gitignore | 3 ++ .tflint.hcl | 64 +++++++++++++++++++++++++++++++++++ .trivyignore | 12 +++++++ DEVELOPERS.md | 16 +++++++++ Makefile | 47 ++++++++++++++++++++++++++ 6 files changed, 210 insertions(+) create mode 100644 .github/workflows/test.yml create mode 100644 .tflint.hcl create mode 100644 .trivyignore create mode 100644 DEVELOPERS.md create mode 100644 Makefile diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..7f93968 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,68 @@ +name: "Test" + +on: + pull_request: + paths: + - "**/*.tf" + - "**/*.tfvars" + - ".github/workflows/test.yml" + workflow_dispatch: + +jobs: + terraform-test: + name: Test Terraform + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: "~1.8.0" + + - name: Install TFLint + uses: terraform-linters/setup-tflint@v4 + with: + tflint_version: latest + + - name: Install Trivy + run: | + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin + + - name: Run all tests + run: make test + + + - name: Summary + if: always() + run: | + echo "## Terraform Lint Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Modules Checked" >> $GITHUB_STEP_SUMMARY + modules=$(find . -name "*.tf" -type f | xargs dirname | sort -u) + echo "The following Terraform modules were validated:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + for module in $modules; do + echo "- \`$module\`" >> $GITHUB_STEP_SUMMARY + done + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Checks Performed" >> $GITHUB_STEP_SUMMARY + echo "- ✅ Terraform Format Check" >> $GITHUB_STEP_SUMMARY + echo "- ✅ Terraform Validation" >> $GITHUB_STEP_SUMMARY + echo "- ✅ TFLint Analysis" >> $GITHUB_STEP_SUMMARY + echo "- ✅ Security Scan (Trivy)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Local Development" >> $GITHUB_STEP_SUMMARY + echo "Run the same checks locally with:" >> $GITHUB_STEP_SUMMARY + echo '```bash' >> $GITHUB_STEP_SUMMARY + echo "make test # Run all tests" >> $GITHUB_STEP_SUMMARY + echo "make format # Auto-format files" >> $GITHUB_STEP_SUMMARY + echo "make scan # Run security scan" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index a2d9c02..8d8bafd 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +.terraform/ +.terraform.lock.hcl + # Dependency directories (remove the comment below to include it) # vendor/ diff --git a/.tflint.hcl b/.tflint.hcl new file mode 100644 index 0000000..9ee3ef0 --- /dev/null +++ b/.tflint.hcl @@ -0,0 +1,64 @@ +config { + # Set minimum Terraform version to support provider functions + terraform_version = "~> 1.8.0" +} + +plugin "terraform" { + enabled = true + preset = "recommended" + version = "0.9.1" + source = "github.com/terraform-linters/tflint-ruleset-terraform" +} + +plugin "google" { + enabled = true +} + +rule "terraform_comment_syntax" { + enabled = true +} + +rule "terraform_deprecated_index" { + enabled = true +} + +rule "terraform_deprecated_interpolation" { + enabled = false # Disable to allow provider function syntax +} + +rule "terraform_documented_outputs" { + enabled = true +} + +rule "terraform_documented_variables" { + enabled = true +} + +rule "terraform_naming_convention" { + enabled = true + format = "snake_case" +} + +rule "terraform_required_providers" { + enabled = true +} + +rule "terraform_required_version" { + enabled = true +} + +rule "terraform_standard_module_structure" { + enabled = true +} + +rule "terraform_typed_variables" { + enabled = true +} + +rule "terraform_unused_declarations" { + enabled = true +} + +rule "terraform_unused_required_providers" { + enabled = true +} \ No newline at end of file diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 0000000..d336173 --- /dev/null +++ b/.trivyignore @@ -0,0 +1,12 @@ +# Trivy ignore file for Suga GCP Plugins +# https://aquasecurity.github.io/trivy/latest/docs/configuration/filtering/ + +# Example: Ignore specific checks that may not apply to plugin modules +# AVD-GCP-0011 # Uncomment to ignore specific GCP checks +# AVD-GCP-0001 # Uncomment to ignore public access warnings if intentional + +# Ignore directories +.git/ +.terraform/ +node_modules/ +security-reports/ \ No newline at end of file diff --git a/DEVELOPERS.md b/DEVELOPERS.md new file mode 100644 index 0000000..f8034aa --- /dev/null +++ b/DEVELOPERS.md @@ -0,0 +1,16 @@ +# Developer Guide + +## Prerequisites + +- [Terraform](https://www.terraform.io/downloads) >= 1.5.0 +- [Docker](https://www.docker.com/get-started) (for tflint and trivy) + +## Usage + +```bash +make format # Format all Terraform files +make test # Run all tests (format-check, validate, lint, scan) +make clean # Clean up temp files +``` + +Tools run in Docker containers - no local installation needed. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..84b2852 --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +.PHONY: help format lint clean +.DEFAULT_GOAL := help + +## TODO: include MEDIUM severity in security scanning. +TRIVY_SEVERITY := HIGH,CRITICAL + +help: ## Show available commands + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-10s %s\n", $$1, $$2}' + +trivy-severity: ## Output the Trivy severity levels for use in scripts + @echo $(TRIVY_SEVERITY) + +format: ## Format all Terraform files + @find . -name "*.tf" -type f | xargs dirname | sort -u | xargs -I {} terraform fmt {} + +format-check: ## Check formatting of all Terraform files + @echo "Checking format..." + @find . -name "*.tf" -type f | xargs dirname | sort -u | while read dir; do \ + terraform fmt -check=true -diff=true "$$dir" || exit 1; \ + done + +validate: ## Validate all Terraform files + @echo "Validating..." + @find . -name "*.tf" -type f | xargs dirname | sort -u | while read dir; do \ + echo " $$dir"; \ + cd "$$dir" && terraform init -backend=false -get=false -upgrade=false >/dev/null && terraform validate && cd - >/dev/null || exit 1; \ + done + +lint: ## Lint using tflint + @echo "Running tflint..." + @find . -name "*.tf" -type f | xargs dirname | sort -u | while read dir; do \ + echo " $$dir"; \ + docker run --rm -v "$$(pwd)/$$dir:/data" -t ghcr.io/terraform-linters/tflint --format=compact --minimum-failure-severity=error; \ + done + +scan: ## Run security scan using Trivy + @echo "Running security scan..." + @docker run --rm -v "$$(pwd):/work" -w /work ghcr.io/aquasecurity/trivy:latest config . --format=table --quiet --exit-code 1 --severity $(TRIVY_SEVERITY) + +test: format-check validate lint scan ## Run all tests: format-check, validate, lint, and scan + @echo "All tests passed!" + +clean: ## Clean up .terraform directories and temp files + @find . -type d -name ".terraform" -exec rm -rf {} + 2>/dev/null || true + @find . -name "*.tfplan" -delete 2>/dev/null || true + @find . -name "*.tfstate*" -delete 2>/dev/null || true + @find . -name ".terraform.lock.hcl" -delete 2>/dev/null || true \ No newline at end of file From e318827ad56e4e30c8e5ee6d539a675a9892e8b8 Mon Sep 17 00:00:00 2001 From: Jye Cusch Date: Thu, 25 Sep 2025 10:37:51 +1000 Subject: [PATCH 02/10] chore: reformat .tf files --- cdn/module/main.tf | 90 +++++++++++++++--------------- cdn/module/variables.tf | 10 ++-- cloudrun/module/main.tf | 18 +++--- cloudrun/module/outputs.tf | 16 +++--- cloudrun/module/variables.tf | 70 +++++++++++------------ serviceaccount/module/main.tf | 4 +- serviceaccount/module/outputs.tf | 6 +- serviceaccount/module/variables.tf | 4 +- storage/module/main.tf | 36 ++++++------ storage/module/outputs.tf | 16 +++--- storage/module/variables.tf | 4 +- 11 files changed, 137 insertions(+), 137 deletions(-) diff --git a/cdn/module/main.tf b/cdn/module/main.tf index 50c4905..a4fae4e 100644 --- a/cdn/module/main.tf +++ b/cdn/module/main.tf @@ -39,15 +39,15 @@ resource "google_project_service" "required_services" { service = each.key project = var.project_id # Leave API enabled on destroy - disable_on_destroy = false + disable_on_destroy = false disable_dependent_services = false } resource "random_string" "cdn_prefix" { length = 8 special = false - lower = true - upper = false + lower = true + upper = false } # Create Network Endpoint Groups for services @@ -63,7 +63,7 @@ resource "google_compute_region_network_endpoint_group" "service_negs" { service = each.value.id } - depends_on = [ google_project_service.required_services ] + depends_on = [google_project_service.required_services] } # Create Backend Services for services @@ -72,9 +72,9 @@ resource "google_compute_backend_service" "service_backends" { project = var.project_id - name = "${provider::corefunc::str_kebab(each.key)}-service-bs" - protocol = "HTTPS" - enable_cdn = false + name = "${provider::corefunc::str_kebab(each.key)}-service-bs" + protocol = "HTTPS" + enable_cdn = false backend { group = google_compute_region_network_endpoint_group.service_negs[each.key].self_link @@ -83,7 +83,7 @@ resource "google_compute_backend_service" "service_backends" { # Create a Global IP Address for the CDN resource "google_compute_global_address" "cdn_ip" { - name = "cdn-ip-${random_string.cdn_prefix.result}" + name = "cdn-ip-${random_string.cdn_prefix.result}" project = var.project_id } @@ -120,14 +120,14 @@ resource "google_compute_region_network_endpoint_group" "external_negs" { name = "${each.key}-external-neg" network_endpoint_type = "INTERNET_FQDN_PORT" - region = var.region + region = var.region connection { host = each.value.domain_name port = 443 } - depends_on = [ google_project_service.required_services ] + depends_on = [google_project_service.required_services] } resource "google_compute_backend_service" "external_backends" { @@ -135,9 +135,9 @@ resource "google_compute_backend_service" "external_backends" { project = var.project_id - name = "${provider::corefunc::str_kebab(each.key)}-external-bs" - protocol = "HTTPS" - enable_cdn = false + name = "${provider::corefunc::str_kebab(each.key)}-external-bs" + protocol = "HTTPS" + enable_cdn = false backend { group = google_compute_region_network_endpoint_group.external_negs[each.key].self_link @@ -146,17 +146,17 @@ resource "google_compute_backend_service" "external_backends" { locals { // Return correct backend_service depending on type of the default origin - default_origin_backend_service = (contains(keys(local.cloud_storage_origins), local.default_origin) ? - google_compute_backend_bucket.bucket_backends[local.default_origin].self_link : - (contains(keys(local.cloud_run_origins), local.default_origin) ? - google_compute_backend_service.service_backends[local.default_origin].self_link : - google_compute_backend_service.external_backends[local.default_origin].self_link)) + default_origin_backend_service = (contains(keys(local.cloud_storage_origins), local.default_origin) ? + google_compute_backend_bucket.bucket_backends[local.default_origin].self_link : + (contains(keys(local.cloud_run_origins), local.default_origin) ? + google_compute_backend_service.service_backends[local.default_origin].self_link : + google_compute_backend_service.external_backends[local.default_origin].self_link)) } # Create a URL Map for routing requests resource "google_compute_url_map" "https_url_map" { name = "https-site-url-map-${random_string.cdn_prefix.result}" - project = var.project_id + project = var.project_id default_service = local.default_origin_backend_service host_rule { @@ -173,13 +173,13 @@ resource "google_compute_url_map" "https_url_map" { content { service = google_compute_backend_service.service_backends[path_rule.key].self_link - paths = [ + paths = [ // The route path provided by the user may or may not start with a slash but will always end with a slash. // Ensure /${path}/* and /${path}/ are both supported regardless of what the base path is. startswith("${path_rule.value.base_path}${path_rule.value.path}", "/") ? "${path_rule.value.base_path}${path_rule.value.path}*" : "/${path_rule.value.base_path}${path_rule.value.path}*", // Ensure /${path}/* - startswith("${path_rule.value.base_path}${path_rule.value.path}", "/") ? "${path_rule.value.base_path}${path_rule.value.path}" : "/${path_rule.value.base_path}${path_rule.value.path}" // Ensure /${path}/ + startswith("${path_rule.value.base_path}${path_rule.value.path}", "/") ? "${path_rule.value.base_path}${path_rule.value.path}" : "/${path_rule.value.base_path}${path_rule.value.path}" // Ensure /${path}/ ] - + route_action { url_rewrite { path_prefix_rewrite = "/" @@ -194,13 +194,13 @@ resource "google_compute_url_map" "https_url_map" { content { service = google_compute_backend_bucket.bucket_backends[path_rule.key].self_link - paths = [ + paths = [ // The route path provided by the user may or may not start with a slash but will always end with a slash. // Ensure /${path}/* and /${path}/ are both supported regardless of what the base path is. startswith("${path_rule.value.base_path}${path_rule.value.path}", "/") ? "${path_rule.value.base_path}${path_rule.value.path}*" : "/${path_rule.value.base_path}${path_rule.value.path}*", // Ensure /${path}/* - startswith("${path_rule.value.base_path}${path_rule.value.path}", "/") ? "${path_rule.value.base_path}${path_rule.value.path}" : "/${path_rule.value.base_path}${path_rule.value.path}" // Ensure /${path}/ + startswith("${path_rule.value.base_path}${path_rule.value.path}", "/") ? "${path_rule.value.base_path}${path_rule.value.path}" : "/${path_rule.value.base_path}${path_rule.value.path}" // Ensure /${path}/ ] - + route_action { url_rewrite { path_prefix_rewrite = "/" @@ -214,13 +214,13 @@ resource "google_compute_url_map" "https_url_map" { content { service = google_compute_backend_service.external_backends[path_rule.key].self_link - paths = [ + paths = [ // The route path provided by the user may or may not start with a slash but will always end with a slash. // Ensure /${path}/* and /${path}/ are both supported regardless of what the base path is. startswith("${path_rule.value.base_path}${path_rule.value.path}", "/") ? "${path_rule.value.base_path}${path_rule.value.path}*" : "/${path_rule.value.base_path}${path_rule.value.path}*", // Ensure /${path}/* - startswith("${path_rule.value.base_path}${path_rule.value.path}", "/") ? "${path_rule.value.base_path}${path_rule.value.path}" : "/${path_rule.value.base_path}${path_rule.value.path}" // Ensure /${path}/ + startswith("${path_rule.value.base_path}${path_rule.value.path}", "/") ? "${path_rule.value.base_path}${path_rule.value.path}" : "/${path_rule.value.base_path}${path_rule.value.path}" // Ensure /${path}/ ] - + route_action { url_rewrite { path_prefix_rewrite = "/" @@ -233,10 +233,10 @@ resource "google_compute_url_map" "https_url_map" { # Lookup the Managed Zone for the CDN Domain data "google_dns_managed_zone" "cdn_zone" { - name = var.cdn_domain.zone_name + name = var.cdn_domain.zone_name project = var.project_id - depends_on = [ google_project_service.required_services ] + depends_on = [google_project_service.required_services] } # Create DNS Records for the CDN @@ -245,8 +245,8 @@ resource "google_dns_record_set" "cdn_dns_record" { managed_zone = data.google_dns_managed_zone.cdn_zone.name type = "A" rrdatas = [google_compute_global_address.cdn_ip.address] - ttl = var.cdn_domain.domain_ttl - project = var.project_id + ttl = var.cdn_domain.domain_ttl + project = var.project_id } resource "google_dns_record_set" "www_cdn_dns_record" { @@ -254,8 +254,8 @@ resource "google_dns_record_set" "www_cdn_dns_record" { managed_zone = data.google_dns_managed_zone.cdn_zone.name type = "A" rrdatas = [google_compute_global_address.cdn_ip.address] - ttl = var.cdn_domain.domain_ttl - project = var.project_id + ttl = var.cdn_domain.domain_ttl + project = var.project_id } resource "google_certificate_manager_certificate" "cdn_cert" { @@ -268,7 +268,7 @@ resource "google_certificate_manager_certificate" "cdn_cert" { domains = [var.cdn_domain.domain_name] } - depends_on = [ google_project_service.required_services ] + depends_on = [google_project_service.required_services] } resource "google_certificate_manager_certificate_map" "cdn_cert_map" { @@ -276,24 +276,24 @@ resource "google_certificate_manager_certificate_map" "cdn_cert_map" { name = "cert-map-${random_string.cdn_prefix.result}" description = "CDN Certificate Map" - depends_on = [ google_project_service.required_services ] + depends_on = [google_project_service.required_services] } resource "google_certificate_manager_certificate_map_entry" "cdn_cert_map_entry" { - name = "cdn-cert-map-entry-${random_string.cdn_prefix.result}" - description = "CDN Certificate Map Entry" - map = google_certificate_manager_certificate_map.cdn_cert_map.name + name = "cdn-cert-map-entry-${random_string.cdn_prefix.result}" + description = "CDN Certificate Map Entry" + map = google_certificate_manager_certificate_map.cdn_cert_map.name certificates = [google_certificate_manager_certificate.cdn_cert.id] - matcher = "PRIMARY" - project = var.project_id + matcher = "PRIMARY" + project = var.project_id } # Create a Target HTTPS Proxy resource "google_compute_target_https_proxy" "https_proxy" { - name = "https-proxy-${random_string.cdn_prefix.result}" + name = "https-proxy-${random_string.cdn_prefix.result}" certificate_map = "//certificatemanager.googleapis.com/${google_certificate_manager_certificate_map.cdn_cert_map.id}" - url_map = google_compute_url_map.https_url_map.self_link - project = var.project_id + url_map = google_compute_url_map.https_url_map.self_link + project = var.project_id } # Create a Global Forwarding Rule @@ -303,5 +303,5 @@ resource "google_compute_global_forwarding_rule" "https_forwarding_rule" { ip_protocol = "TCP" port_range = "443" target = google_compute_target_https_proxy.https_proxy.self_link - project = var.project_id + project = var.project_id } \ No newline at end of file diff --git a/cdn/module/variables.tf b/cdn/module/variables.tf index 42df35e..4502d82 100644 --- a/cdn/module/variables.tf +++ b/cdn/module/variables.tf @@ -3,12 +3,12 @@ variable "suga" { name = string stack_id = string origins = map(object({ - path = string - base_path = string - type = string + path = string + base_path = string + type = string domain_name = string - id = string - resources = map(string) + id = string + resources = map(string) })) }) } diff --git a/cloudrun/module/main.tf b/cloudrun/module/main.tf index 1302ec5..b14b77a 100644 --- a/cloudrun/module/main.tf +++ b/cloudrun/module/main.tf @@ -22,7 +22,7 @@ resource "google_project_service" "required_services" { service = each.key project = var.project_id # Leave API enabled on destroy - disable_on_destroy = false + disable_on_destroy = false disable_dependent_services = false } @@ -33,12 +33,12 @@ resource "google_artifact_registry_repository" "service-image-repo" { description = "service images for suga stack ${var.suga.name}" format = "DOCKER" - depends_on = [ google_project_service.required_services ] + depends_on = [google_project_service.required_services] } locals { artifact_registry_url = "${var.region}-docker.pkg.dev/${var.project_id}/${google_artifact_registry_repository.service-image-repo.name}" - service_image_url = "${local.artifact_registry_url}/${var.suga.name}" + service_image_url = "${local.artifact_registry_url}/${var.suga.name}" } # Tag the provided docker image with the repository url @@ -81,10 +81,10 @@ resource "random_password" "event_token" { resource "google_cloud_run_v2_service" "service" { name = replace(var.suga.name, "_", "-") - location = var.region - project = var.project_id - launch_stage = "GA" - ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" + location = var.region + project = var.project_id + launch_stage = "GA" + ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" deletion_protection = false @@ -93,7 +93,7 @@ resource "google_cloud_run_v2_service" "service" { min_instance_count = var.min_instances max_instance_count = var.max_instances } - + containers { image = "${local.service_image_url}@${docker_registry_image.push.sha256_digest}" resources { @@ -108,7 +108,7 @@ resource "google_cloud_run_v2_service" "service" { } env { - name = "SUGA_GUEST_PORT" + name = "SUGA_GUEST_PORT" value = 8080 } diff --git a/cloudrun/module/outputs.tf b/cloudrun/module/outputs.tf index 87147b3..e684c98 100644 --- a/cloudrun/module/outputs.tf +++ b/cloudrun/module/outputs.tf @@ -1,12 +1,12 @@ output "suga" { - value = { - id = google_cloud_run_v2_service.service.name - domain_name = google_cloud_run_v2_service.service.uri - exports = { - resources = { - "google_cloud_run_v2_service" = google_cloud_run_v2_service.service.name - } - } + value = { + id = google_cloud_run_v2_service.service.name + domain_name = google_cloud_run_v2_service.service.uri + exports = { + resources = { + "google_cloud_run_v2_service" = google_cloud_run_v2_service.service.name + } } + } } diff --git a/cloudrun/module/variables.tf b/cloudrun/module/variables.tf index 0f36f0e..cf55c99 100644 --- a/cloudrun/module/variables.tf +++ b/cloudrun/module/variables.tf @@ -1,9 +1,9 @@ variable "suga" { type = object({ - name = string - stack_id = string - image_id = string - env = map(string) + name = string + stack_id = string + image_id = string + env = map(string) identities = map(object({ exports = map(string) })) @@ -11,65 +11,65 @@ variable "suga" { } variable "environment" { - type = map(string) - description = "Environment variables to set on the lambda function" - default = {} + type = map(string) + description = "Environment variables to set on the lambda function" + default = {} } # TODO: review defaults variable "memory_mb" { - description = "The amount of memory to allocate to the CloudRun service in MB" - type = number - default = 512 + description = "The amount of memory to allocate to the CloudRun service in MB" + type = number + default = 512 } variable "cpus" { - description = "The amount of cpus to allocate to the CloudRun service" - type = number - default = 1 + description = "The amount of cpus to allocate to the CloudRun service" + type = number + default = 1 } variable "gpus" { - description = "The amount of gpus to allocate to the CloudRun service" - type = number - default = 0 + description = "The amount of gpus to allocate to the CloudRun service" + type = number + default = 0 } variable "min_instances" { - description = "The minimum number of instances to run" - type = number - default = 0 + description = "The minimum number of instances to run" + type = number + default = 0 } variable "max_instances" { - description = "The maximum number of instances to run" - type = number - default = 10 + description = "The maximum number of instances to run" + type = number + default = 10 } variable "container_concurrency" { - description = "The number of concurrent requests the CloudRun service can handle" - type = number - default = 80 + description = "The number of concurrent requests the CloudRun service can handle" + type = number + default = 80 } variable "timeout_seconds" { - description = "The timeout for the CloudRun service in seconds" - type = number - default = 10 + description = "The timeout for the CloudRun service in seconds" + type = number + default = 10 } variable "project_id" { - description = "The project ID to deploy the CloudRun service to" - type = string + description = "The project ID to deploy the CloudRun service to" + type = string } variable "region" { - description = "The region to deploy the CloudRun service to" - type = string + description = "The region to deploy the CloudRun service to" + type = string } variable "container_port" { - description = "The port to expose the CloudRun service to" - type = number - default = 9001 + description = "The port to expose the CloudRun service to" + type = number + default = 9001 } \ No newline at end of file diff --git a/serviceaccount/module/main.tf b/serviceaccount/module/main.tf index af92c3e..18f72fa 100644 --- a/serviceaccount/module/main.tf +++ b/serviceaccount/module/main.tf @@ -12,7 +12,7 @@ resource "google_project_service" "required_services" { service = each.key project = var.project_id # Leave API enabled on destroy - disable_on_destroy = false + disable_on_destroy = false disable_dependent_services = false } @@ -27,5 +27,5 @@ resource "google_service_account" "service_account" { display_name = "${var.suga.name} service account" description = "Service account which runs the ${var.suga.name}" - depends_on = [ google_project_service.required_services ] + depends_on = [google_project_service.required_services] } \ No newline at end of file diff --git a/serviceaccount/module/outputs.tf b/serviceaccount/module/outputs.tf index b0b2000..51de3bf 100644 --- a/serviceaccount/module/outputs.tf +++ b/serviceaccount/module/outputs.tf @@ -1,10 +1,10 @@ output "suga" { value = { exports = { - "gcp_service_account" = google_service_account.service_account.email - "gcp_service_account:id" = google_service_account.service_account.id + "gcp_service_account" = google_service_account.service_account.email + "gcp_service_account:id" = google_service_account.service_account.id "gcp_service_account:email" = google_service_account.service_account.email - "gcp_service_account:name" = google_service_account.service_account.display_name + "gcp_service_account:name" = google_service_account.service_account.display_name } } } diff --git a/serviceaccount/module/variables.tf b/serviceaccount/module/variables.tf index 9c8d967..309fd58 100644 --- a/serviceaccount/module/variables.tf +++ b/serviceaccount/module/variables.tf @@ -1,12 +1,12 @@ variable "suga" { type = object({ - name = string + name = string stack_id = string }) } variable "trusted_actions" { - type = list(string) + type = list(string) default = [ "monitoring.timeSeries.create", "resourcemanager.projects.get", diff --git a/storage/module/main.tf b/storage/module/main.tf index 964fb7e..60909e4 100644 --- a/storage/module/main.tf +++ b/storage/module/main.tf @@ -9,7 +9,7 @@ locals { locals { suga_bucket_name = provider::corefunc::str_kebab(var.suga.name) - bucket_name = "${local.suga_bucket_name}-${var.suga.stack_id}" + bucket_name = "${local.suga_bucket_name}-${var.suga.stack_id}" } # Enable the required services @@ -19,7 +19,7 @@ resource "google_project_service" "required_services" { service = each.key project = var.project_id # Leave API enabled on destroy - disable_on_destroy = false + disable_on_destroy = false disable_dependent_services = false } @@ -30,57 +30,57 @@ resource "google_storage_bucket" "bucket" { project = var.project_id storage_class = var.storage_class - depends_on = [ google_project_service.required_services ] + depends_on = [google_project_service.required_services] } locals { - read_actions = ["storage.objects.get", "storage.objects.list"] - write_actions = ["storage.objects.create", "storage.objects.delete"] + read_actions = ["storage.objects.get", "storage.objects.list"] + write_actions = ["storage.objects.create", "storage.objects.delete"] delete_actions = ["storage.objects.delete"] } resource "google_project_iam_custom_role" "bucket_access_role" { for_each = var.suga.services - role_id = "BucketAccess_${substr("${var.suga.name}_${each.key}", 0, 40)}_${var.suga.stack_id}" + role_id = "BucketAccess_${substr("${var.suga.name}_${each.key}", 0, 40)}_${var.suga.stack_id}" project = var.project_id title = "${each.key} Bucket Access For ${var.suga.name}" description = "Custom role that allows access to the ${var.suga.name} bucket" permissions = distinct(concat( - ["storage.buckets.list", "storage.buckets.get"], // Base roles required for finding buckets - contains(each.value.actions, "read") ? local.read_actions : [], - contains(each.value.actions, "write") ? local.write_actions : [], - contains(each.value.actions, "delete") ? local.delete_actions : [] + ["storage.buckets.list", "storage.buckets.get"], // Base roles required for finding buckets + contains(each.value.actions, "read") ? local.read_actions : [], + contains(each.value.actions, "write") ? local.write_actions : [], + contains(each.value.actions, "delete") ? local.delete_actions : [] ) ) - depends_on = [ google_project_service.required_services ] + depends_on = [google_project_service.required_services] } resource "google_project_iam_member" "iam_access" { for_each = var.suga.services project = var.project_id - role = google_project_iam_custom_role.bucket_access_role[each.key].name - member = "serviceAccount:${each.value.identities["gcp:iam:role"].exports["gcp_service_account:email"]}" + role = google_project_iam_custom_role.bucket_access_role[each.key].name + member = "serviceAccount:${each.value.identities["gcp:iam:role"].exports["gcp_service_account:email"]}" } locals { relative_content_path = "${path.root}/../../../${var.suga.content_path}" - content_files = var.suga.content_path != "" ? fileset(local.relative_content_path, "**/*") : [] + content_files = var.suga.content_path != "" ? fileset(local.relative_content_path, "**/*") : [] } # Upload each file to GCP cloud storage (only if files exist) resource "google_storage_bucket_object" "files" { for_each = toset(local.content_files) - + bucket = google_storage_bucket.bucket.name - name = each.value + name = each.value source = "${local.relative_content_path}/${each.value}" - + detect_md5hash = filemd5("${local.relative_content_path}/${each.value}") - + content_type = lookup({ "html" = "text/html" "css" = "text/css" diff --git a/storage/module/outputs.tf b/storage/module/outputs.tf index 8974ef8..6164a53 100644 --- a/storage/module/outputs.tf +++ b/storage/module/outputs.tf @@ -1,11 +1,11 @@ output "suga" { - value = { - id = google_storage_bucket.bucket.id - domain_name = google_storage_bucket.bucket.url - exports = { - resources = { - "google_storage_bucket" = google_storage_bucket.bucket.id - } - } + value = { + id = google_storage_bucket.bucket.id + domain_name = google_storage_bucket.bucket.url + exports = { + resources = { + "google_storage_bucket" = google_storage_bucket.bucket.id + } } + } } \ No newline at end of file diff --git a/storage/module/variables.tf b/storage/module/variables.tf index 6a664db..bd185e7 100644 --- a/storage/module/variables.tf +++ b/storage/module/variables.tf @@ -1,7 +1,7 @@ variable "suga" { type = object({ - name = string - stack_id = string + name = string + stack_id = string content_path = string services = map(object({ actions = list(string) From 1e2fea227156b67bdd5f001e2f5e27f449f27e6b Mon Sep 17 00:00:00 2001 From: Jye Cusch Date: Thu, 25 Sep 2025 10:52:43 +1000 Subject: [PATCH 03/10] multi-path origins wip --- cdn/module/main.tf | 60 +++++++++++++++++++++++++++++++---------- cdn/module/variables.tf | 6 +++-- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/cdn/module/main.tf b/cdn/module/main.tf index a4fae4e..65ab3e3 100644 --- a/cdn/module/main.tf +++ b/cdn/module/main.tf @@ -8,12 +8,28 @@ locals { "compute.googleapis.com" ] - root_origins = { - for k, v in var.suga.origins : k => v - if v.path == "/" - } + # Flatten origins to create individual path entries + origin_paths = flatten([ + for origin_name, origin in var.suga.origins : [ + for route in origin.routes : { + origin_name = origin_name + path = route.path + base_path = route.base_path + type = origin.type + domain_name = origin.domain_name + id = origin.id + resources = origin.resources + } + ] + ]) - default_origin = length(local.root_origins) > 0 ? keys(local.root_origins)[0] : keys(var.suga.origins)[0] + # Find default origin (path == "/") + default_origin_paths = [ + for entry in local.origin_paths : entry + if entry.path == "/" + ] + + default_origin = length(local.default_origin_paths) > 0 ? local.default_origin_paths[0].origin_name : keys(var.suga.origins)[0] cloud_storage_origins = { for k, v in var.suga.origins : k => v @@ -29,6 +45,22 @@ locals { for k, v in var.suga.origins : k => v if !contains(keys(v.resources), "google_storage_bucket") && !contains(keys(v.resources), "google_cloud_run_v2_service") } + + # Group path rules by origin type for routing + cloud_run_path_rules = { + for i, entry in local.origin_paths : "${entry.origin_name}-${i}" => entry + if contains(keys(entry.resources), "google_cloud_run_v2_service") + } + + cloud_storage_path_rules = { + for i, entry in local.origin_paths : "${entry.origin_name}-${i}" => entry + if contains(keys(entry.resources), "google_storage_bucket") + } + + other_path_rules = { + for i, entry in local.origin_paths : "${entry.origin_name}-${i}" => entry + if !contains(keys(entry.resources), "google_storage_bucket") && !contains(keys(entry.resources), "google_cloud_run_v2_service") + } } @@ -169,12 +201,12 @@ resource "google_compute_url_map" "https_url_map" { default_service = local.default_origin_backend_service dynamic "path_rule" { - for_each = local.cloud_run_origins + for_each = local.cloud_run_path_rules content { - service = google_compute_backend_service.service_backends[path_rule.key].self_link + service = google_compute_backend_service.service_backends[path_rule.value.origin_name].self_link paths = [ - // The route path provided by the user may or may not start with a slash but will always end with a slash. + // The route path provided by the user may or may not start with a slash but will always end with a slash. // Ensure /${path}/* and /${path}/ are both supported regardless of what the base path is. startswith("${path_rule.value.base_path}${path_rule.value.path}", "/") ? "${path_rule.value.base_path}${path_rule.value.path}*" : "/${path_rule.value.base_path}${path_rule.value.path}*", // Ensure /${path}/* startswith("${path_rule.value.base_path}${path_rule.value.path}", "/") ? "${path_rule.value.base_path}${path_rule.value.path}" : "/${path_rule.value.base_path}${path_rule.value.path}" // Ensure /${path}/ @@ -190,12 +222,12 @@ resource "google_compute_url_map" "https_url_map" { } dynamic "path_rule" { - for_each = local.cloud_storage_origins + for_each = local.cloud_storage_path_rules content { - service = google_compute_backend_bucket.bucket_backends[path_rule.key].self_link + service = google_compute_backend_bucket.bucket_backends[path_rule.value.origin_name].self_link paths = [ - // The route path provided by the user may or may not start with a slash but will always end with a slash. + // The route path provided by the user may or may not start with a slash but will always end with a slash. // Ensure /${path}/* and /${path}/ are both supported regardless of what the base path is. startswith("${path_rule.value.base_path}${path_rule.value.path}", "/") ? "${path_rule.value.base_path}${path_rule.value.path}*" : "/${path_rule.value.base_path}${path_rule.value.path}*", // Ensure /${path}/* startswith("${path_rule.value.base_path}${path_rule.value.path}", "/") ? "${path_rule.value.base_path}${path_rule.value.path}" : "/${path_rule.value.base_path}${path_rule.value.path}" // Ensure /${path}/ @@ -210,12 +242,12 @@ resource "google_compute_url_map" "https_url_map" { } dynamic "path_rule" { - for_each = local.other_origins + for_each = local.other_path_rules content { - service = google_compute_backend_service.external_backends[path_rule.key].self_link + service = google_compute_backend_service.external_backends[path_rule.value.origin_name].self_link paths = [ - // The route path provided by the user may or may not start with a slash but will always end with a slash. + // The route path provided by the user may or may not start with a slash but will always end with a slash. // Ensure /${path}/* and /${path}/ are both supported regardless of what the base path is. startswith("${path_rule.value.base_path}${path_rule.value.path}", "/") ? "${path_rule.value.base_path}${path_rule.value.path}*" : "/${path_rule.value.base_path}${path_rule.value.path}*", // Ensure /${path}/* startswith("${path_rule.value.base_path}${path_rule.value.path}", "/") ? "${path_rule.value.base_path}${path_rule.value.path}" : "/${path_rule.value.base_path}${path_rule.value.path}" // Ensure /${path}/ diff --git a/cdn/module/variables.tf b/cdn/module/variables.tf index 4502d82..18e19fa 100644 --- a/cdn/module/variables.tf +++ b/cdn/module/variables.tf @@ -3,8 +3,10 @@ variable "suga" { name = string stack_id = string origins = map(object({ - path = string - base_path = string + routes = list(object({ + path = string + base_path = string + })) type = string domain_name = string id = string From 0e3171a95c8d2b1ec38bac1bbcdf570f281760a3 Mon Sep 17 00:00:00 2001 From: Ryan Cartwright Date: Wed, 24 Sep 2025 16:10:30 +1000 Subject: [PATCH 04/10] fix(cdn): separate cdn domain variables into individual parts --- cdn/manifest.yaml | 11 +++++++++++ cdn/module/main.tf | 16 ++++++++-------- cdn/module/variables.tf | 21 ++++++++++++++------- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/cdn/manifest.yaml b/cdn/manifest.yaml index 3e6a561..2f2e6ef 100644 --- a/cdn/manifest.yaml +++ b/cdn/manifest.yaml @@ -20,5 +20,16 @@ inputs: type: object required: true description: 'CDN domain configuration (e.g. `{"name": "cdn.example.com", "ssl": true}`)' + domain_name: + type: string + required: true + description: "New A records will be created in the hosted zone to establish this domain name for the CDN" + dns_zone_name: + type: string + required: true + description: "The name of the existing Cloud DNS zone that you would like your domain to be configured in" + domain_ttl: + type: number + description: "The time to live (TTL) for the A record created (in seconds). Defaults to 300 seconds" outputs: diff --git a/cdn/module/main.tf b/cdn/module/main.tf index 65ab3e3..f28a122 100644 --- a/cdn/module/main.tf +++ b/cdn/module/main.tf @@ -265,7 +265,7 @@ resource "google_compute_url_map" "https_url_map" { # Lookup the Managed Zone for the CDN Domain data "google_dns_managed_zone" "cdn_zone" { - name = var.cdn_domain.zone_name + name = var.dns_zone_name project = var.project_id depends_on = [google_project_service.required_services] @@ -273,21 +273,21 @@ data "google_dns_managed_zone" "cdn_zone" { # Create DNS Records for the CDN resource "google_dns_record_set" "cdn_dns_record" { - name = endswith(var.cdn_domain.domain_name, ".") ? var.cdn_domain.domain_name : "${var.cdn_domain.domain_name}." + name = endswith(var.domain_name, ".") ? var.domain_name : "${var.domain_name}." managed_zone = data.google_dns_managed_zone.cdn_zone.name type = "A" rrdatas = [google_compute_global_address.cdn_ip.address] - ttl = var.cdn_domain.domain_ttl - project = var.project_id + ttl = var.domain_ttl + project = var.project_id } resource "google_dns_record_set" "www_cdn_dns_record" { - name = endswith(var.cdn_domain.domain_name, ".") ? "www.${var.cdn_domain.domain_name}" : "www.${var.cdn_domain.domain_name}." + name = endswith(var.domain_name, ".") ? "www.${var.domain_name}" : "www.${var.domain_name}." managed_zone = data.google_dns_managed_zone.cdn_zone.name type = "A" rrdatas = [google_compute_global_address.cdn_ip.address] - ttl = var.cdn_domain.domain_ttl - project = var.project_id + ttl = var.domain_ttl + project = var.project_id } resource "google_certificate_manager_certificate" "cdn_cert" { @@ -297,7 +297,7 @@ resource "google_certificate_manager_certificate" "cdn_cert" { scope = "DEFAULT" managed { - domains = [var.cdn_domain.domain_name] + domains = [var.domain_name] } depends_on = [google_project_service.required_services] diff --git a/cdn/module/variables.tf b/cdn/module/variables.tf index 18e19fa..1f7cd0f 100644 --- a/cdn/module/variables.tf +++ b/cdn/module/variables.tf @@ -25,11 +25,18 @@ variable "region" { type = string } -variable "cdn_domain" { - description = "The CDN domain configuration." - type = object({ - domain_name = string - zone_name = string - domain_ttl = optional(number, 300) - }) +variable "domain_name" { + type = string + description = "New A records will be created in the hosted zone to establish this domain name for the CDN" } + +variable "dns_zone_name" { + type = string + description = "The name of the existing Cloud DNS zone that you would like your domain to be configured in" +} + +variable "domain_ttl" { + type = string + description = "The time to live (TTL) for the A record created (in seconds). Defaults to 300 seconds" + default = 300 +} \ No newline at end of file From cb2fd48a7910f1fc6a4c4ad8b30d69656397d6f6 Mon Sep 17 00:00:00 2001 From: Ryan Cartwright Date: Wed, 24 Sep 2025 16:10:58 +1000 Subject: [PATCH 05/10] feat(cloudrun): add configurable ingress for cloudrun services --- cloudrun/manifest.yaml | 3 +++ cloudrun/module/main.tf | 8 ++++---- cloudrun/module/variables.tf | 6 ++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/cloudrun/manifest.yaml b/cloudrun/manifest.yaml index 815d2de..f97c2ec 100644 --- a/cloudrun/manifest.yaml +++ b/cloudrun/manifest.yaml @@ -47,5 +47,8 @@ inputs: container_port: type: number description: "Container port number (e.g. `9001`)" + ingress: + type: string + description: "The ingress for this Service. Possible values are INGRESS_TRAFFIC_ALL, INGRESS_TRAFFIC_INTERNAL_ONLY, or INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" outputs: diff --git a/cloudrun/module/main.tf b/cloudrun/module/main.tf index b14b77a..7f8c755 100644 --- a/cloudrun/module/main.tf +++ b/cloudrun/module/main.tf @@ -81,10 +81,10 @@ resource "random_password" "event_token" { resource "google_cloud_run_v2_service" "service" { name = replace(var.suga.name, "_", "-") - location = var.region - project = var.project_id - launch_stage = "GA" - ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" + location = var.region + project = var.project_id + launch_stage = "GA" + ingress = var.ingress deletion_protection = false diff --git a/cloudrun/module/variables.tf b/cloudrun/module/variables.tf index cf55c99..2c7ee84 100644 --- a/cloudrun/module/variables.tf +++ b/cloudrun/module/variables.tf @@ -72,4 +72,10 @@ variable "container_port" { description = "The port to expose the CloudRun service to" type = number default = 9001 +} + +variable "ingress" { + description = "The ingress for this Service. Possible values are INGRESS_TRAFFIC_ALL, INGRESS_TRAFFIC_INTERNAL_ONLY, or INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" + type = string + default = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" } \ No newline at end of file From 47953aacc2e37ea148c7d9b60fd4a33c49a3c0bb Mon Sep 17 00:00:00 2001 From: Ryan Cartwright Date: Wed, 24 Sep 2025 16:11:10 +1000 Subject: [PATCH 06/10] docs: add READMEs for each of the modules --- cdn/README.md | 64 ++++++++++++++++++++++++ cloudrun/README.md | 81 +++++++++++++++++++++++++++++++ serviceaccount/README.md | 102 +++++++++++++++++++++++++++++++++++++++ storage/README.md | 100 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 347 insertions(+) create mode 100644 cdn/README.md create mode 100644 cloudrun/README.md create mode 100644 serviceaccount/README.md create mode 100644 storage/README.md diff --git a/cdn/README.md b/cdn/README.md new file mode 100644 index 0000000..6fd4fbc --- /dev/null +++ b/cdn/README.md @@ -0,0 +1,64 @@ +# GCP CDN Plugin + +Creates a Google Cloud CDN distribution with load balancing, SSL certificates, and DNS configuration for global content delivery. + +## Overview + +This plugin provisions a complete CDN solution using Google Cloud CDN with: + +- Global load balancing with Cloud Load Balancer +- Automatic SSL certificate provisioning via Certificate Manager +- DNS record management in Cloud DNS +- Support for multiple origin types (Cloud Run, Cloud Storage, external) +- Path-based routing capabilities + +## Required Inputs + +| Parameter | Type | Description | +| --------------- | ------ | --------------------------------------------------- | +| `project_id` | string | Google Cloud Project ID (e.g. `my-project-123`) | +| `region` | string | Google Cloud region (e.g. `us-central1`) | +| `domain_name` | string | Domain name for the CDN (A records will be created) | +| `dns_zone_name` | string | Name of the existing Cloud DNS zone | + +## Optional Inputs + +| Parameter | Type | Description | +| ------------ | ------ | ----------------------------------------------- | +| `domain_ttl` | number | TTL for DNS A records in seconds (default: 300) | + +## Prerequisites + +- Existing Cloud DNS zone in your project +- Enabled APIs: Certificate Manager, DNS, Compute Engine + +## Usage Example + +**Note:** This example shows platform file syntax. You can configure this plugin directly in the Suga Platform Builder UI without writing YAML. + +```yaml +entrypoints: + default: + plugin: "gcp-cdn" + properties: + project_id: "my-project-123" + region: "us-central1" + domain_name: "cdn.example.com" + dns_zone_name: "example-com-zone" + domain_ttl: 300 +``` + +## Features + +- **Global Distribution**: Leverages Google's global network for content delivery +- **Automatic SSL**: Provisions and manages SSL certificates automatically +- **Multi-Origin Support**: Routes traffic to Cloud Run services, Cloud Storage buckets, or external origins +- **DNS Integration**: Automatically configures DNS records in your existing zone +- **Load Balancing**: Built-in load balancing with health checking + +## References + +- [Cloud CDN Documentation](https://cloud.google.com/cdn/docs) +- [Cloud Load Balancing Documentation](https://cloud.google.com/load-balancing/docs) +- [Certificate Manager Documentation](https://cloud.google.com/certificate-manager/docs) +- [Cloud DNS Documentation](https://cloud.google.com/dns/docs) diff --git a/cloudrun/README.md b/cloudrun/README.md new file mode 100644 index 0000000..953eb92 --- /dev/null +++ b/cloudrun/README.md @@ -0,0 +1,81 @@ +# GCP Cloud Run Plugin + +Deploys containerized applications to Google Cloud Run with automatic scaling, traffic management, and service account integration. + +## Overview + +This plugin provisions Cloud Run services with: +- Automatic scaling based on traffic +- Configurable CPU, memory, and concurrency limits +- Environment variable management +- Service account integration for secure resource access +- Flexible ingress controls +- Request timeout configuration + +## Required Inputs + +| Parameter | Type | Description | +|-----------|------|-------------| +| `project_id` | string | Google Cloud Project ID (e.g. `my-project-123`) | +| `region` | string | Google Cloud region for service deployment (e.g. `us-central1`) | + +## Optional Inputs + +| Parameter | Type | Description | +|-----------|------|-------------| +| `environment` | map(string) | Environment variables (e.g. `{"NODE_ENV": "production", "API_KEY": "secret"}`) | +| `memory_mb` | number | Memory allocation in MB (e.g. `512`) | +| `cpus` | number | CPU allocation (e.g. `1`) | +| `gpus` | number | GPU allocation (e.g. `0`) | +| `min_instances` | number | Minimum instances to keep running (e.g. `0`) | +| `max_instances` | number | Maximum instances that can be created (e.g. `10`) | +| `container_concurrency` | number | Maximum concurrent requests per instance (e.g. `80`) | +| `timeout_seconds` | number | Maximum request timeout in seconds (e.g. `10`) | +| `container_port` | number | Container port number (e.g. `9001`) | +| `ingress` | string | Traffic ingress setting (`INGRESS_TRAFFIC_ALL`, `INGRESS_TRAFFIC_INTERNAL_ONLY`, `INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER`) | + +## Prerequisites + +- Container image pushed to Google Container Registry or Artifact Registry +- Service account with appropriate IAM permissions (managed by `gcp-service-account` plugin) + +## Usage Example + +**Note:** This example shows platform file syntax. You can configure this plugin directly in the Suga Platform Builder UI without writing YAML. + +```yaml +services: + api: + plugin: "gcp-cloudrun" + identities: + - plugin: "gcp-service-account" + properties: + project_id: "my-project-123" + region: "us-central1" + environment: + NODE_ENV: "production" + DATABASE_URL: "postgresql://..." + memory_mb: 1024 + cpus: 2 + min_instances: 1 + max_instances: 100 + container_concurrency: 80 + timeout_seconds: 300 + container_port: 8080 + ingress: "INGRESS_TRAFFIC_ALL" +``` + +## Features + +- **Serverless**: Pay only for the resources you use +- **Auto-scaling**: Automatically scales from zero to handle traffic spikes +- **Security**: Integrated with GCP IAM and service accounts +- **Flexibility**: Support for various runtime configurations and constraints +- **Traffic Management**: Multiple ingress options for different security requirements + +## References + +- [Cloud Run Documentation](https://cloud.google.com/run/docs) +- [Cloud Run Pricing](https://cloud.google.com/run/pricing) +- [Cloud Run Quotas and Limits](https://cloud.google.com/run/quotas) +- [Container Runtime Contract](https://cloud.google.com/run/docs/container-contract) \ No newline at end of file diff --git a/serviceaccount/README.md b/serviceaccount/README.md new file mode 100644 index 0000000..5b2c083 --- /dev/null +++ b/serviceaccount/README.md @@ -0,0 +1,102 @@ +# GCP Service Account Plugin + +Creates and manages Google Cloud IAM service accounts with customizable permissions for secure resource access. + +## Overview + +This plugin provisions IAM service accounts with: +- Fine-grained permission management via trusted actions +- Automatic key generation and rotation support +- Integration with other GCP services +- Principle of least privilege enforcement +- Support for custom IAM roles and predefined roles + +## Required Inputs + +| Parameter | Type | Description | +|-----------|------|-------------| +| `project_id` | string | Google Cloud Project ID (e.g. `my-project-123`) | + +## Optional Inputs + +| Parameter | Type | Description | +|-----------|------|-------------| +| `trusted_actions` | list(string) | IAM actions/roles this service account can perform (e.g. `["storage.objects.get", "storage.objects.create", "cloudsql.instances.connect"]`) | + +## Prerequisites + +- Sufficient IAM permissions to create service accounts and assign roles +- Enabled APIs for the services the account will access + +## Usage Examples + +**Note:** This example shows platform file syntax. You can configure this plugin directly in the Suga Platform Builder UI without writing YAML. + +### Basic Storage Access +```yaml +services: + storage-worker: + plugin: "gcp-cloudrun" + identities: + - plugin: "gcp-service-account" + properties: + project_id: "my-project-123" + trusted_actions: + - "storage.objects.get" + - "storage.objects.create" + - "storage.objects.delete" +``` + +### Full Storage and Cloud SQL Access +```yaml +services: + backend: + plugin: "gcp-cloudrun" + identities: + - plugin: "gcp-service-account" + properties: + project_id: "my-project-123" + trusted_actions: + - "roles/storage.admin" + - "roles/cloudsql.client" + - "secretmanager.versions.access" +``` + +### Read-Only Monitoring Access +```yaml +services: + monitoring: + plugin: "gcp-cloudrun" + identities: + - plugin: "gcp-service-account" + properties: + project_id: "my-project-123" + trusted_actions: + - "roles/monitoring.viewer" + - "roles/logging.viewer" +``` + +## Common IAM Actions + +| Action/Role | Description | +|-------------|-------------| +| `storage.objects.get` | Read objects from Cloud Storage | +| `storage.objects.create` | Create objects in Cloud Storage | +| `roles/storage.admin` | Full Storage access | +| `roles/cloudsql.client` | Connect to Cloud SQL instances | +| `secretmanager.versions.access` | Access Secret Manager secrets | +| `roles/monitoring.viewer` | View monitoring data | + +## Security Best Practices + +- Use the minimum required permissions +- Regularly audit and rotate service account keys +- Use workload identity when possible for GKE workloads +- Avoid downloading service account keys when not necessary + +## References + +- [IAM Service Accounts Documentation](https://cloud.google.com/iam/docs/service-accounts) +- [Understanding Service Accounts](https://cloud.google.com/iam/docs/understanding-service-accounts) +- [IAM Permissions Reference](https://cloud.google.com/iam/docs/permissions-reference) +- [Service Account Best Practices](https://cloud.google.com/iam/docs/best-practices-service-accounts) \ No newline at end of file diff --git a/storage/README.md b/storage/README.md new file mode 100644 index 0000000..4faaedd --- /dev/null +++ b/storage/README.md @@ -0,0 +1,100 @@ +# GCP Storage Plugin + +Provisions Google Cloud Storage buckets with configurable storage classes, access controls, and automatic file upload capabilities. + +## Overview + +This plugin creates Cloud Storage buckets with: +- Multiple storage class options for cost optimization +- Configurable access controls and permissions +- Automatic file upload from local directories +- Integration with other GCP services +- Lifecycle management support +- Regional or multi-regional deployment options + +## Required Inputs + +| Parameter | Type | Description | +|-----------|------|-------------| +| `project_id` | string | Google Cloud Project ID (e.g. `my-project-123`) | +| `region` | string | Google Cloud region for storage bucket (e.g. `us-central1`) | + +## Optional Inputs + +| Parameter | Type | Description | +|-----------|------|-------------| +| `storage_class` | string | Performance and durability tier for bucket objects (default: `STANDARD`) | + +## Storage Classes + +| Class | Use Case | Access Frequency | Min Storage Duration | +|-------|----------|------------------|---------------------| +| `STANDARD` | Frequently accessed data | Daily/weekly | None | +| `NEARLINE` | Infrequently accessed data | Monthly | 30 days | +| `COLDLINE` | Rarely accessed data | Quarterly | 90 days | +| `ARCHIVE` | Long-term archival | Yearly | 365 days | + +## Prerequisites + +- Enabled Cloud Storage API +- Sufficient IAM permissions to create buckets + +## Usage Examples + +**Note:** This example shows platform file syntax. You can configure this plugin directly in the Suga Platform Builder UI without writing YAML. + +### Standard Storage for Frequent Access +```yaml +buckets: + uploads: + plugin: "gcp-storage-bucket" + properties: + project_id: "my-project-123" + region: "us-central1" + storage_class: "STANDARD" +``` + +### Archive Storage for Long-term Backup +```yaml +buckets: + backups: + plugin: "gcp-storage-bucket" + properties: + project_id: "my-project-123" + region: "us-central1" + storage_class: "ARCHIVE" +``` + +### Nearline Storage for Monthly Reports +```yaml +buckets: + reports: + plugin: "gcp-storage-bucket" + properties: + project_id: "my-project-123" + region: "us-central1" + storage_class: "NEARLINE" +``` + +## Features + +- **Cost Optimization**: Choose the right storage class for your access patterns +- **Global Accessibility**: Access data from anywhere with internet connectivity +- **Automatic Upload**: Seamlessly upload files during deployment +- **Security**: Integration with GCP IAM for access control +- **Scalability**: Virtually unlimited storage capacity +- **Durability**: 99.999999999% (11 9's) annual durability + +## Storage Class Selection Guide + +- **STANDARD**: Web content, streaming videos, mobile apps +- **NEARLINE**: Monthly backups, data analytics workloads +- **COLDLINE**: Quarterly backups, disaster recovery +- **ARCHIVE**: Regulatory compliance, long-term preservation + +## References + +- [Cloud Storage Documentation](https://cloud.google.com/storage/docs) +- [Storage Classes and Pricing](https://cloud.google.com/storage/docs/storage-classes) +- [Bucket Locations](https://cloud.google.com/storage/docs/locations) +- [Cloud Storage Pricing](https://cloud.google.com/storage/pricing) \ No newline at end of file From bfe50cfbb89b23684cbd91e07cc3bd5e2f23406d Mon Sep 17 00:00:00 2001 From: Ryan Cartwright Date: Thu, 25 Sep 2025 10:37:18 +1000 Subject: [PATCH 07/10] docs: add default to readme --- cdn/README.md | 18 +++++++++--------- cloudrun/README.md | 35 ++++++++++++++++++----------------- serviceaccount/README.md | 34 +++++++++++++++++++--------------- storage/README.md | 32 ++++++++++++++++++-------------- 4 files changed, 64 insertions(+), 55 deletions(-) diff --git a/cdn/README.md b/cdn/README.md index 6fd4fbc..cd9fe6b 100644 --- a/cdn/README.md +++ b/cdn/README.md @@ -14,18 +14,18 @@ This plugin provisions a complete CDN solution using Google Cloud CDN with: ## Required Inputs -| Parameter | Type | Description | -| --------------- | ------ | --------------------------------------------------- | -| `project_id` | string | Google Cloud Project ID (e.g. `my-project-123`) | -| `region` | string | Google Cloud region (e.g. `us-central1`) | -| `domain_name` | string | Domain name for the CDN (A records will be created) | -| `dns_zone_name` | string | Name of the existing Cloud DNS zone | +| Parameter | Type | Description | +| --------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| `project_id` | string | Google Cloud Project ID (e.g. `my-project-123`) | +| `region` | string | Google Cloud region (e.g. `us-central1`) | +| `domain_name` | string | Domain name for the CDN (A records will be created) | +| `dns_zone_name` | string | Name of the existing [Cloud DNS](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/dns_managed_zone#dns_name-2) | ## Optional Inputs -| Parameter | Type | Description | -| ------------ | ------ | ----------------------------------------------- | -| `domain_ttl` | number | TTL for DNS A records in seconds (default: 300) | +| Parameter | Type | Description | Default | +| ------------ | ------ | -------------------------------- | ------- | +| `domain_ttl` | number | TTL for DNS A records in seconds | `300` | ## Prerequisites diff --git a/cloudrun/README.md b/cloudrun/README.md index 953eb92..c3a5425 100644 --- a/cloudrun/README.md +++ b/cloudrun/README.md @@ -5,6 +5,7 @@ Deploys containerized applications to Google Cloud Run with automatic scaling, t ## Overview This plugin provisions Cloud Run services with: + - Automatic scaling based on traffic - Configurable CPU, memory, and concurrency limits - Environment variable management @@ -14,25 +15,25 @@ This plugin provisions Cloud Run services with: ## Required Inputs -| Parameter | Type | Description | -|-----------|------|-------------| -| `project_id` | string | Google Cloud Project ID (e.g. `my-project-123`) | -| `region` | string | Google Cloud region for service deployment (e.g. `us-central1`) | +| Parameter | Type | Description | +| ------------ | ------ | --------------------------------------------------------------- | +| `project_id` | string | Google Cloud Project ID (e.g. `my-project-123`) | +| `region` | string | Google Cloud region for service deployment (e.g. `us-central1`) | ## Optional Inputs -| Parameter | Type | Description | -|-----------|------|-------------| -| `environment` | map(string) | Environment variables (e.g. `{"NODE_ENV": "production", "API_KEY": "secret"}`) | -| `memory_mb` | number | Memory allocation in MB (e.g. `512`) | -| `cpus` | number | CPU allocation (e.g. `1`) | -| `gpus` | number | GPU allocation (e.g. `0`) | -| `min_instances` | number | Minimum instances to keep running (e.g. `0`) | -| `max_instances` | number | Maximum instances that can be created (e.g. `10`) | -| `container_concurrency` | number | Maximum concurrent requests per instance (e.g. `80`) | -| `timeout_seconds` | number | Maximum request timeout in seconds (e.g. `10`) | -| `container_port` | number | Container port number (e.g. `9001`) | -| `ingress` | string | Traffic ingress setting (`INGRESS_TRAFFIC_ALL`, `INGRESS_TRAFFIC_INTERNAL_ONLY`, `INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER`) | +| Parameter | Type | Description | Default | +| ----------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | +| `environment` | map(string) | Environment variables (e.g. `{"NODE_ENV": "production", "API_KEY": "secret"}`) | `{}` | +| `memory_mb` | number | Memory allocation in MB | `512` | +| `cpus` | number | CPU allocation | `1` | +| `gpus` | number | GPU allocation | `0` | +| `min_instances` | number | Minimum instances to keep running | `0` | +| `max_instances` | number | Maximum instances that can be created | `10` | +| `container_concurrency` | number | Maximum concurrent requests per instance | `80` | +| `timeout_seconds` | number | Maximum request timeout in seconds | `10` | +| `container_port` | number | Container port number | `9001` | +| `ingress` | string | Traffic ingress setting. Must be one of `INGRESS_TRAFFIC_ALL`, `INGRESS_TRAFFIC_INTERNAL_ONLY`, or `INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER` | `INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER` | ## Prerequisites @@ -78,4 +79,4 @@ services: - [Cloud Run Documentation](https://cloud.google.com/run/docs) - [Cloud Run Pricing](https://cloud.google.com/run/pricing) - [Cloud Run Quotas and Limits](https://cloud.google.com/run/quotas) -- [Container Runtime Contract](https://cloud.google.com/run/docs/container-contract) \ No newline at end of file +- [Container Runtime Contract](https://cloud.google.com/run/docs/container-contract) diff --git a/serviceaccount/README.md b/serviceaccount/README.md index 5b2c083..3cb4056 100644 --- a/serviceaccount/README.md +++ b/serviceaccount/README.md @@ -5,6 +5,7 @@ Creates and manages Google Cloud IAM service accounts with customizable permissi ## Overview This plugin provisions IAM service accounts with: + - Fine-grained permission management via trusted actions - Automatic key generation and rotation support - Integration with other GCP services @@ -13,15 +14,15 @@ This plugin provisions IAM service accounts with: ## Required Inputs -| Parameter | Type | Description | -|-----------|------|-------------| -| `project_id` | string | Google Cloud Project ID (e.g. `my-project-123`) | +| Parameter | Type | Description | +| ------------ | ------ | ----------------------- | +| `project_id` | string | Google Cloud Project ID | ## Optional Inputs -| Parameter | Type | Description | -|-----------|------|-------------| -| `trusted_actions` | list(string) | IAM actions/roles this service account can perform (e.g. `["storage.objects.get", "storage.objects.create", "cloudsql.instances.connect"]`) | +| Parameter | Type | Description | Default | +| ----------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | +| `trusted_actions` | list(string) | IAM actions/roles this service account can perform (e.g. `["storage.objects.get", "storage.objects.create", "cloudsql.instances.connect"]`) | `["monitoring.timeSeries.create", "resourcemanager.projects.get"]` | ## Prerequisites @@ -33,6 +34,7 @@ This plugin provisions IAM service accounts with: **Note:** This example shows platform file syntax. You can configure this plugin directly in the Suga Platform Builder UI without writing YAML. ### Basic Storage Access + ```yaml services: storage-worker: @@ -48,6 +50,7 @@ services: ``` ### Full Storage and Cloud SQL Access + ```yaml services: backend: @@ -63,6 +66,7 @@ services: ``` ### Read-Only Monitoring Access + ```yaml services: monitoring: @@ -78,14 +82,14 @@ services: ## Common IAM Actions -| Action/Role | Description | -|-------------|-------------| -| `storage.objects.get` | Read objects from Cloud Storage | -| `storage.objects.create` | Create objects in Cloud Storage | -| `roles/storage.admin` | Full Storage access | -| `roles/cloudsql.client` | Connect to Cloud SQL instances | -| `secretmanager.versions.access` | Access Secret Manager secrets | -| `roles/monitoring.viewer` | View monitoring data | +| Action/Role | Description | +| ------------------------------- | ------------------------------- | +| `storage.objects.get` | Read objects from Cloud Storage | +| `storage.objects.create` | Create objects in Cloud Storage | +| `roles/storage.admin` | Full Storage access | +| `roles/cloudsql.client` | Connect to Cloud SQL instances | +| `secretmanager.versions.access` | Access Secret Manager secrets | +| `roles/monitoring.viewer` | View monitoring data | ## Security Best Practices @@ -99,4 +103,4 @@ services: - [IAM Service Accounts Documentation](https://cloud.google.com/iam/docs/service-accounts) - [Understanding Service Accounts](https://cloud.google.com/iam/docs/understanding-service-accounts) - [IAM Permissions Reference](https://cloud.google.com/iam/docs/permissions-reference) -- [Service Account Best Practices](https://cloud.google.com/iam/docs/best-practices-service-accounts) \ No newline at end of file +- [Service Account Best Practices](https://cloud.google.com/iam/docs/best-practices-service-accounts) diff --git a/storage/README.md b/storage/README.md index 4faaedd..a908247 100644 --- a/storage/README.md +++ b/storage/README.md @@ -5,6 +5,7 @@ Provisions Google Cloud Storage buckets with configurable storage classes, acces ## Overview This plugin creates Cloud Storage buckets with: + - Multiple storage class options for cost optimization - Configurable access controls and permissions - Automatic file upload from local directories @@ -14,25 +15,25 @@ This plugin creates Cloud Storage buckets with: ## Required Inputs -| Parameter | Type | Description | -|-----------|------|-------------| -| `project_id` | string | Google Cloud Project ID (e.g. `my-project-123`) | -| `region` | string | Google Cloud region for storage bucket (e.g. `us-central1`) | +| Parameter | Type | Description | +| ------------ | ------ | ----------------------------------------------------------- | +| `project_id` | string | Google Cloud Project ID (e.g. `my-project-123`) | +| `region` | string | Google Cloud region for storage bucket (e.g. `us-central1`) | ## Optional Inputs -| Parameter | Type | Description | -|-----------|------|-------------| -| `storage_class` | string | Performance and durability tier for bucket objects (default: `STANDARD`) | +| Parameter | Type | Description | Default | +| --------------- | ------ | -------------------------------------------------- | ---------- | +| `storage_class` | string | Performance and durability tier for bucket objects | `STANDARD` | ## Storage Classes -| Class | Use Case | Access Frequency | Min Storage Duration | -|-------|----------|------------------|---------------------| -| `STANDARD` | Frequently accessed data | Daily/weekly | None | -| `NEARLINE` | Infrequently accessed data | Monthly | 30 days | -| `COLDLINE` | Rarely accessed data | Quarterly | 90 days | -| `ARCHIVE` | Long-term archival | Yearly | 365 days | +| Class | Use Case | Access Frequency | Min Storage Duration | +| ---------- | -------------------------- | ---------------- | -------------------- | +| `STANDARD` | Frequently accessed data | Daily/weekly | None | +| `NEARLINE` | Infrequently accessed data | Monthly | 30 days | +| `COLDLINE` | Rarely accessed data | Quarterly | 90 days | +| `ARCHIVE` | Long-term archival | Yearly | 365 days | ## Prerequisites @@ -44,6 +45,7 @@ This plugin creates Cloud Storage buckets with: **Note:** This example shows platform file syntax. You can configure this plugin directly in the Suga Platform Builder UI without writing YAML. ### Standard Storage for Frequent Access + ```yaml buckets: uploads: @@ -55,6 +57,7 @@ buckets: ``` ### Archive Storage for Long-term Backup + ```yaml buckets: backups: @@ -66,6 +69,7 @@ buckets: ``` ### Nearline Storage for Monthly Reports + ```yaml buckets: reports: @@ -97,4 +101,4 @@ buckets: - [Cloud Storage Documentation](https://cloud.google.com/storage/docs) - [Storage Classes and Pricing](https://cloud.google.com/storage/docs/storage-classes) - [Bucket Locations](https://cloud.google.com/storage/docs/locations) -- [Cloud Storage Pricing](https://cloud.google.com/storage/pricing) \ No newline at end of file +- [Cloud Storage Pricing](https://cloud.google.com/storage/pricing) From 250f6eaeaf4a29f36e03f07d0ec4324143eb9dc2 Mon Sep 17 00:00:00 2001 From: Ryan Cartwright Date: Thu, 25 Sep 2025 10:37:38 +1000 Subject: [PATCH 08/10] fix(cloudrun): change naming of environment to environment variables for clarity --- cloudrun/manifest.yaml | 2 +- cloudrun/module/main.tf | 2 +- cloudrun/module/variables.tf | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cloudrun/manifest.yaml b/cloudrun/manifest.yaml index f97c2ec..2e8a58e 100644 --- a/cloudrun/manifest.yaml +++ b/cloudrun/manifest.yaml @@ -12,7 +12,7 @@ runtime: go_module: github.com/nitrictech/plugins/gcp/cloudrun inputs: - environment: + environment_variables: type: map(string) description: 'Cloud Run service environment variables (e.g. `{"NODE_ENV": "production", "API_KEY": "secret"}`)' memory_mb: diff --git a/cloudrun/module/main.tf b/cloudrun/module/main.tf index 7f8c755..2eb9c41 100644 --- a/cloudrun/module/main.tf +++ b/cloudrun/module/main.tf @@ -130,7 +130,7 @@ resource "google_cloud_run_v2_service" "service" { } dynamic "env" { - for_each = merge(var.environment, var.suga.env) + for_each = merge(var.environment_variables, var.suga.env) content { name = env.key value = env.value diff --git a/cloudrun/module/variables.tf b/cloudrun/module/variables.tf index 2c7ee84..588cfeb 100644 --- a/cloudrun/module/variables.tf +++ b/cloudrun/module/variables.tf @@ -10,10 +10,10 @@ variable "suga" { }) } -variable "environment" { - type = map(string) - description = "Environment variables to set on the lambda function" - default = {} +variable "environment_variables" { + type = map(string) + description = "Environment variables to set on the lambda function" + default = {} } # TODO: review defaults variable "memory_mb" { From d30b2339bc02dde36123871c5c89c3b6f6d6c789 Mon Sep 17 00:00:00 2001 From: Ryan Cartwright Date: Thu, 25 Sep 2025 10:57:10 +1000 Subject: [PATCH 09/10] lock provider versions --- cdn/module/main.tf | 11 +++++------ cdn/module/providers.tf | 16 ++++++++++++++-- cdn/module/variables.tf | 8 ++++---- cloudrun/module/main.tf | 8 ++++---- cloudrun/module/providers.tf | 14 +++++++++++++- cloudrun/module/variables.tf | 12 ++++++------ serviceaccount/module/providers.tf | 9 ++++++++- storage/module/providers.tf | 9 ++++++++- 8 files changed, 62 insertions(+), 25 deletions(-) diff --git a/cdn/module/main.tf b/cdn/module/main.tf index f28a122..38f3281 100644 --- a/cdn/module/main.tf +++ b/cdn/module/main.tf @@ -63,7 +63,6 @@ locals { } } - # Enable the required services resource "google_project_service" "required_services" { for_each = toset(local.required_services) @@ -265,7 +264,7 @@ resource "google_compute_url_map" "https_url_map" { # Lookup the Managed Zone for the CDN Domain data "google_dns_managed_zone" "cdn_zone" { - name = var.dns_zone_name + name = var.dns_zone_name project = var.project_id depends_on = [google_project_service.required_services] @@ -277,8 +276,8 @@ resource "google_dns_record_set" "cdn_dns_record" { managed_zone = data.google_dns_managed_zone.cdn_zone.name type = "A" rrdatas = [google_compute_global_address.cdn_ip.address] - ttl = var.domain_ttl - project = var.project_id + ttl = var.domain_ttl + project = var.project_id } resource "google_dns_record_set" "www_cdn_dns_record" { @@ -286,8 +285,8 @@ resource "google_dns_record_set" "www_cdn_dns_record" { managed_zone = data.google_dns_managed_zone.cdn_zone.name type = "A" rrdatas = [google_compute_global_address.cdn_ip.address] - ttl = var.domain_ttl - project = var.project_id + ttl = var.domain_ttl + project = var.project_id } resource "google_certificate_manager_certificate" "cdn_cert" { diff --git a/cdn/module/providers.tf b/cdn/module/providers.tf index 64a66dd..9182d61 100644 --- a/cdn/module/providers.tf +++ b/cdn/module/providers.tf @@ -1,13 +1,25 @@ terraform { + required_version = ">= 1.0" + required_providers { + google = { + source = "hashicorp/google" + version = "7.4.0" + } + google-beta = { source = "hashicorp/google-beta" - version = "~> 6.17.0" + version = "7.4.0" + } + + random = { + source = "hashicorp/random" + version = "~> 3.1" } corefunc = { source = "northwood-labs/corefunc" - version = "~> 1.4" + version = "2.1.0" } } } diff --git a/cdn/module/variables.tf b/cdn/module/variables.tf index 1f7cd0f..dcde4ce 100644 --- a/cdn/module/variables.tf +++ b/cdn/module/variables.tf @@ -26,17 +26,17 @@ variable "region" { } variable "domain_name" { - type = string + type = string description = "New A records will be created in the hosted zone to establish this domain name for the CDN" } variable "dns_zone_name" { - type = string + type = string description = "The name of the existing Cloud DNS zone that you would like your domain to be configured in" } variable "domain_ttl" { - type = string + type = string description = "The time to live (TTL) for the A record created (in seconds). Defaults to 300 seconds" - default = 300 + default = 300 } \ No newline at end of file diff --git a/cloudrun/module/main.tf b/cloudrun/module/main.tf index 2eb9c41..ab64462 100644 --- a/cloudrun/module/main.tf +++ b/cloudrun/module/main.tf @@ -81,10 +81,10 @@ resource "random_password" "event_token" { resource "google_cloud_run_v2_service" "service" { name = replace(var.suga.name, "_", "-") - location = var.region - project = var.project_id - launch_stage = "GA" - ingress = var.ingress + location = var.region + project = var.project_id + launch_stage = "GA" + ingress = var.ingress deletion_protection = false diff --git a/cloudrun/module/providers.tf b/cloudrun/module/providers.tf index f67116c..4be8f28 100644 --- a/cloudrun/module/providers.tf +++ b/cloudrun/module/providers.tf @@ -1,8 +1,20 @@ terraform { + required_version = ">= 1.0" + required_providers { + google = { + source = "hashicorp/google" + version = "7.4.0" + } + + random = { + source = "hashicorp/random" + version = "~> 3.1" + } + docker = { source = "kreuzwerker/docker" - version = "3.6.0" + version = "3.6.2" } } } \ No newline at end of file diff --git a/cloudrun/module/variables.tf b/cloudrun/module/variables.tf index 588cfeb..a411796 100644 --- a/cloudrun/module/variables.tf +++ b/cloudrun/module/variables.tf @@ -11,9 +11,9 @@ variable "suga" { } variable "environment_variables" { - type = map(string) - description = "Environment variables to set on the lambda function" - default = {} + type = map(string) + description = "Environment variables to set on the lambda function" + default = {} } # TODO: review defaults variable "memory_mb" { @@ -75,7 +75,7 @@ variable "container_port" { } variable "ingress" { - description = "The ingress for this Service. Possible values are INGRESS_TRAFFIC_ALL, INGRESS_TRAFFIC_INTERNAL_ONLY, or INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" - type = string - default = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" + description = "The ingress for this Service. Possible values are INGRESS_TRAFFIC_ALL, INGRESS_TRAFFIC_INTERNAL_ONLY, or INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" + type = string + default = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" } \ No newline at end of file diff --git a/serviceaccount/module/providers.tf b/serviceaccount/module/providers.tf index 1a895e0..31f09dd 100644 --- a/serviceaccount/module/providers.tf +++ b/serviceaccount/module/providers.tf @@ -1,8 +1,15 @@ terraform { + required_version = ">= 1.0" + required_providers { + google = { + source = "hashicorp/google" + version = "7.4.0" + } + corefunc = { source = "northwood-labs/corefunc" - version = "~> 1.4" + version = "2.1.0" } } } diff --git a/storage/module/providers.tf b/storage/module/providers.tf index 1a895e0..31f09dd 100644 --- a/storage/module/providers.tf +++ b/storage/module/providers.tf @@ -1,8 +1,15 @@ terraform { + required_version = ">= 1.0" + required_providers { + google = { + source = "hashicorp/google" + version = "7.4.0" + } + corefunc = { source = "northwood-labs/corefunc" - version = "~> 1.4" + version = "2.1.0" } } } From 6b4013fc2f52c05519b4da71f316d175abb0978a Mon Sep 17 00:00:00 2001 From: Ryan Cartwright Date: Thu, 25 Sep 2025 11:17:02 +1000 Subject: [PATCH 10/10] implement unused variables --- cloudrun/module/main.tf | 13 +++++++++---- serviceaccount/module/main.tf | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/cloudrun/module/main.tf b/cloudrun/module/main.tf index ab64462..ac55d5e 100644 --- a/cloudrun/module/main.tf +++ b/cloudrun/module/main.tf @@ -89,6 +89,8 @@ resource "google_cloud_run_v2_service" "service" { deletion_protection = false template { + max_instance_request_concurrency = var.container_concurrency + scaling { min_instance_count = var.min_instances max_instance_count = var.max_instances @@ -97,10 +99,13 @@ resource "google_cloud_run_v2_service" "service" { containers { image = "${local.service_image_url}@${docker_registry_image.push.sha256_digest}" resources { - limits = { - cpu = var.cpus - memory = "${var.memory_mb}Mi" - } + limits = merge( + { + cpu = var.cpus + memory = "${var.memory_mb}Mi" + }, + var.gpus > 0 ? { "nvidia.com/gpu" = var.gpus } : {} + ) } ports { diff --git a/serviceaccount/module/main.tf b/serviceaccount/module/main.tf index 18f72fa..b369623 100644 --- a/serviceaccount/module/main.tf +++ b/serviceaccount/module/main.tf @@ -28,4 +28,24 @@ resource "google_service_account" "service_account" { description = "Service account which runs the ${var.suga.name}" depends_on = [google_project_service.required_services] +} + +# Create custom role for trusted actions +resource "google_project_iam_custom_role" "trusted_actions_role" { + count = length(var.trusted_actions) > 0 ? 1 : 0 + + role_id = "${replace(local.service_account_name, "-", "_")}_trusted_actions" + title = "${var.suga.name} Trusted Actions Role" + description = "Custom role for trusted actions for ${var.suga.name}" + permissions = var.trusted_actions + project = var.project_id +} + +# Bind the custom role to the service account +resource "google_project_iam_member" "trusted_actions_binding" { + count = length(var.trusted_actions) > 0 ? 1 : 0 + + project = var.project_id + role = google_project_iam_custom_role.trusted_actions_role[0].name + member = "serviceAccount:${google_service_account.service_account.email}" } \ No newline at end of file