-
Notifications
You must be signed in to change notification settings - Fork 1
Deploy to GCP using Terraform #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| # Git | ||
| .git | ||
| .gitignore | ||
|
|
||
| # CI/CD and infrastructure | ||
| terraform/ | ||
| .github/ | ||
|
|
||
| # Development and test files | ||
| node_modules/ | ||
| e2e/ | ||
| test/ | ||
| playwright-report/ | ||
| playwright/ | ||
| test-results/ | ||
|
|
||
| # IDE and editor files | ||
| .idea/ | ||
| .vscode/ | ||
| *.swp | ||
| *.swo | ||
| *~ | ||
|
|
||
| # Logs and temp | ||
| log/* | ||
| tmp/* | ||
|
|
||
| # OS files | ||
| .DS_Store | ||
|
|
||
| # Documentation | ||
| docs/ | ||
| *.md | ||
|
|
||
| # Environment files | ||
| .env | ||
| .env.* | ||
|
|
||
| # Storage (databases and uploads are created at runtime) | ||
| storage/* | ||
|
|
||
| # Playwright MCP | ||
| .playwright-mcp/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| # Deploy Dill to GCP Cloud Run | ||
| # | ||
| # Prerequisites: | ||
| # - gcloud CLI installed and authenticated (gcloud auth login) | ||
| # - Docker installed | ||
| # - Terraform installed | ||
| # - A GCP project with billing enabled (free tier is sufficient) | ||
| # | ||
| # Usage: | ||
| # cd terraform | ||
| # ./deploy.sh | ||
|
|
||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| APP_DIR="$(dirname "$SCRIPT_DIR")" | ||
|
|
||
| # Load terraform variables | ||
| if [ ! -f "$SCRIPT_DIR/terraform.tfvars" ]; then | ||
| echo "Error: terraform/terraform.tfvars not found." | ||
| echo "Copy terraform.tfvars.example to terraform.tfvars and fill in your values." | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Initialize Terraform and read variables using terraform console to avoid brittle parsing | ||
| ( | ||
| cd "$SCRIPT_DIR" | ||
| terraform init -input=false >/dev/null | ||
| ) | ||
|
|
||
| PROJECT_ID=$(cd "$SCRIPT_DIR" && terraform console -input=false -var-file="terraform.tfvars" -execute='var.project_id' | tr -d '\r') | ||
| REGION=$(cd "$SCRIPT_DIR" && terraform console -input=false -var-file="terraform.tfvars" -execute='var.region' | tr -d '\r') | ||
| APP_NAME=$(cd "$SCRIPT_DIR" && terraform console -input=false -var-file="terraform.tfvars" -execute='try(var.app_name, "dill")' | tr -d '\r' || echo "dill") | ||
| if [ -z "$APP_NAME" ] || [ "$APP_NAME" = "dill" ]; then | ||
| APP_NAME="dill" | ||
| fi | ||
|
|
||
| DOCKER_TAG="${REGION}-docker.pkg.dev/${PROJECT_ID}/${APP_NAME}/${APP_NAME}:latest" | ||
|
|
||
| echo "==> Deploying ${APP_NAME} to GCP Cloud Run" | ||
| echo " Project: ${PROJECT_ID}" | ||
| echo " Region: ${REGION}" | ||
| echo " Image: ${DOCKER_TAG}" | ||
| echo "" | ||
|
|
||
| # Step 1: Provision infrastructure with Terraform | ||
| echo "==> Step 1: Running Terraform to provision infrastructure..." | ||
| cd "$SCRIPT_DIR" | ||
| terraform init | ||
| terraform apply -auto-approve | ||
| cd "$APP_DIR" | ||
|
|
||
| # Step 2: Configure Docker to push to Artifact Registry | ||
| echo "" | ||
| echo "==> Step 2: Configuring Docker authentication for Artifact Registry..." | ||
| gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet | ||
|
|
||
| # Step 3: Build the Docker image | ||
| echo "" | ||
| echo "==> Step 3: Building Docker image..." | ||
| docker build -t "$DOCKER_TAG" "$APP_DIR" | ||
|
|
||
| # Step 4: Push the image to Artifact Registry | ||
| echo "" | ||
| echo "==> Step 4: Pushing image to Artifact Registry..." | ||
| docker push "$DOCKER_TAG" | ||
|
|
||
| # Step 5: Deploy to Cloud Run (update the service to use the new image) | ||
| echo "" | ||
| echo "==> Step 5: Deploying new image to Cloud Run..." | ||
| gcloud run services update "$APP_NAME" \ | ||
| --region "$REGION" \ | ||
| --image "$DOCKER_TAG" \ | ||
| --project "$PROJECT_ID" \ | ||
| --quiet | ||
|
|
||
| # Print the service URL | ||
| echo "" | ||
| echo "==> Deployment complete!" | ||
| SERVICE_URL=$(gcloud run services describe "$APP_NAME" --region "$REGION" --project "$PROJECT_ID" --format='value(status.url)') | ||
| echo " Application URL: ${SERVICE_URL}" | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,176 @@ | ||||||||||||||||
| terraform { | ||||||||||||||||
| required_version = ">= 1.5" | ||||||||||||||||
|
|
||||||||||||||||
| required_providers { | ||||||||||||||||
| google = { | ||||||||||||||||
| source = "hashicorp/google" | ||||||||||||||||
| version = "~> 5.0" | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| provider "google" { | ||||||||||||||||
| project = var.project_id | ||||||||||||||||
| region = var.region | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| # Enable required GCP APIs | ||||||||||||||||
| resource "google_project_service" "apis" { | ||||||||||||||||
| for_each = toset([ | ||||||||||||||||
| "run.googleapis.com", | ||||||||||||||||
| "artifactregistry.googleapis.com", | ||||||||||||||||
| "cloudbuild.googleapis.com", | ||||||||||||||||
| "secretmanager.googleapis.com", | ||||||||||||||||
| ]) | ||||||||||||||||
|
|
||||||||||||||||
| project = var.project_id | ||||||||||||||||
| service = each.value | ||||||||||||||||
| disable_on_destroy = false | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| # Artifact Registry repository for Docker images | ||||||||||||||||
| resource "google_artifact_registry_repository" "app" { | ||||||||||||||||
| location = var.region | ||||||||||||||||
| repository_id = var.app_name | ||||||||||||||||
| format = "DOCKER" | ||||||||||||||||
| description = "Docker repository for ${var.app_name}" | ||||||||||||||||
|
|
||||||||||||||||
| depends_on = [google_project_service.apis] | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| # Store the Rails master key in Secret Manager | ||||||||||||||||
| resource "google_secret_manager_secret" "rails_master_key" { | ||||||||||||||||
| secret_id = "${var.app_name}-rails-master-key" | ||||||||||||||||
|
|
||||||||||||||||
| replication { | ||||||||||||||||
| auto {} | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| depends_on = [google_project_service.apis] | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| resource "google_secret_manager_secret_version" "rails_master_key" { | ||||||||||||||||
| secret = google_secret_manager_secret.rails_master_key.id | ||||||||||||||||
| secret_data = var.rails_master_key | ||||||||||||||||
| } | ||||||||||||||||
|
Comment on lines
+52
to
+55
|
||||||||||||||||
| resource "google_secret_manager_secret_version" "rails_master_key" { | |
| secret = google_secret_manager_secret.rails_master_key.id | |
| secret_data = var.rails_master_key | |
| } | |
| # NOTE: Secret versions (actual Rails master key values) should be | |
| # created outside Terraform (e.g., via `gcloud secrets versions add`) | |
| # to avoid storing secret data in Terraform state. |
Copilot
AI
Feb 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cloud Run container is configured to use port 80, but the Docker image runs as a non-root user (UID 1000). Binding to privileged ports (<1024) will fail at runtime, so the service likely won’t start. Use an unprivileged port (typically 8080) and let the app read PORT (already in config/puma.rb).
| container_port = 80 | |
| container_port = 8080 |
Copilot
AI
Feb 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This unconditionally makes the Cloud Run service publicly invokable by allUsers. If this app is not intended to be fully public, this should be gated behind a variable (e.g., public_access = true/false) or use a more restrictive principal, otherwise it’s easy to accidentally expose internal environments.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| output "service_url" { | ||
| description = "The URL of the deployed Cloud Run service" | ||
| value = google_cloud_run_v2_service.app.uri | ||
| } | ||
|
|
||
| output "artifact_registry_repository" { | ||
| description = "The Artifact Registry repository URL for pushing Docker images" | ||
| value = "${var.region}-docker.pkg.dev/${var.project_id}/${var.app_name}" | ||
| } | ||
|
|
||
| output "docker_image_tag" { | ||
| description = "The full Docker image tag to use when pushing" | ||
| value = "${var.region}-docker.pkg.dev/${var.project_id}/${var.app_name}/${var.app_name}:latest" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # Copy this file to terraform.tfvars and fill in your values | ||
| project_id = "your-gcp-project-id" | ||
| region = "us-central1" | ||
| rails_master_key = "your-rails-master-key-from-config/master.key" | ||
|
|
||
| # Optional: set a custom domain | ||
| # domain = "dill.example.com" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| variable "project_id" { | ||
| description = "The GCP project ID" | ||
| type = string | ||
| } | ||
|
|
||
| variable "region" { | ||
| description = "The GCP region for deployment" | ||
| type = string | ||
| default = "us-central1" | ||
| } | ||
|
|
||
| variable "app_name" { | ||
| description = "Application name used for resource naming" | ||
| type = string | ||
| default = "dill" | ||
| } | ||
|
|
||
| variable "rails_master_key" { | ||
| description = "Rails master key for decrypting credentials (from config/master.key)" | ||
| type = string | ||
| sensitive = true | ||
| } | ||
|
|
||
| variable "domain" { | ||
| description = "Custom domain for the application (optional, leave empty to use Cloud Run default URL)" | ||
| type = string | ||
| default = "" | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The deploy script updates the Cloud Run service with
gcloud run services updateafter Terraform creates it. This will cause configuration drift from Terraform state and makes futureterraform applyruns potentially revert or conflict with the deployed image. Consider making the image tag a Terraform variable and updating it via Terraform, or add a Terraformlifecycle { ignore_changes = [template[0].containers[0].image] }ifgcloudis intended to own image updates.