diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1eb65d7..442d8a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,3 +75,25 @@ jobs: exit 1 ;; esac + + terraform: + name: Terraform / validate + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.8.5 + + - name: Terraform format check + run: terraform -chdir=infra/terraform fmt -check -recursive + + - name: Terraform validate + run: | + terraform -chdir=infra/terraform init -backend=false + terraform -chdir=infra/terraform validate diff --git a/AGENTS.md b/AGENTS.md index 6563e9a..35c648b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,9 +1,11 @@ # AGENTS.md ## Project + Household Telegram bot + mini app monorepo for shared rent/utilities/purchase accounting. ## Core stack + - Runtime/tooling: Bun - Language: TypeScript (strict) - Typecheck: tsgo (`@typescript/native-preview`) @@ -15,24 +17,29 @@ Household Telegram bot + mini app monorepo for shared rent/utilities/purchase ac - Deploy: Cloud Run + Cloud Scheduler (planned) ## Architecture + Hexagonal architecture: + - `packages/domain`: pure business rules/value objects only - `packages/application`: use-cases only - `packages/ports`: interfaces for external boundaries - `apps/*`: composition/wiring and delivery endpoints Boundary rules: + - No framework/DB/HTTP imports in `packages/domain` - `packages/application` must not import adapter implementations - External SDK usage belongs outside domain/application core ## Money and accounting rules + - Never use floating-point money math - Store amounts in minor units - Deterministic split behavior only - Persist raw parsed purchase text + confidence + parser mode ## Workflow + - Work from Linear tickets and linked specs in `docs/specs/` - One ticket at a time, small commits - Before implementation: re-check ticket/spec and assumptions @@ -40,7 +47,9 @@ Boundary rules: - Run CodeRabbit review before merge (`bun run review:coderabbit`) ## Quality gates + Required before PR/merge: + - `bun run format:check` - `bun run lint` - `bun run typecheck` @@ -48,6 +57,7 @@ Required before PR/merge: - `bun run build` ## CI/CD + - CI workflow runs parallel quality jobs on push/PR to `main` - CD workflow deploys on successful `main` CI or manual trigger - Required CD secrets: @@ -56,6 +66,7 @@ Required before PR/merge: - `GCP_SERVICE_ACCOUNT` ## Docs as source of truth + - Roadmap: `docs/roadmap.md` - Specs template: `docs/specs/README.md` - ADRs: `docs/decisions/*` diff --git a/docs/runbooks/dev-setup.md b/docs/runbooks/dev-setup.md index 38dde37..da21b08 100644 --- a/docs/runbooks/dev-setup.md +++ b/docs/runbooks/dev-setup.md @@ -4,6 +4,7 @@ - Bun 1.3+ - Node.js 22+ +- Terraform 1.8+ (for IaC checks/plans) ## First-time setup @@ -21,6 +22,8 @@ bun run format:check bun run typecheck bun run test bun run build +bun run infra:fmt:check +bun run infra:validate ``` ## App commands @@ -47,8 +50,13 @@ bun run review:coderabbit - CI runs in parallel matrix jobs on push/PR to `main`: - `format:check`, `lint`, `typecheck`, `test`, `build` + - `terraform fmt -check`, `terraform validate` - CD deploys on successful `main` CI completion (or manual dispatch). - CD is enabled when GitHub secrets are configured: - `GCP_PROJECT_ID` - `GCP_WORKLOAD_IDENTITY_PROVIDER` - `GCP_SERVICE_ACCOUNT` + +## IaC Runbook + +- See `docs/runbooks/iac-terraform.md` for provisioning flow. diff --git a/docs/runbooks/iac-terraform.md b/docs/runbooks/iac-terraform.md new file mode 100644 index 0000000..65343f9 --- /dev/null +++ b/docs/runbooks/iac-terraform.md @@ -0,0 +1,55 @@ +# Terraform IaC Runbook + +## Purpose + +Provision and maintain GCP infrastructure for bot API, mini app, scheduler, and runtime secrets. + +## Prerequisites + +- Terraform `>= 1.8` +- GCP project with billing enabled +- Local auth: + +```bash +gcloud auth application-default login +``` + +## Bootstrap + +```bash +cp infra/terraform/terraform.tfvars.example infra/terraform/terraform.tfvars +terraform -chdir=infra/terraform init +terraform -chdir=infra/terraform plan +terraform -chdir=infra/terraform apply +``` + +## Quality checks + +```bash +bun run infra:fmt:check +bun run infra:validate +``` + +## Add secret values + +After first apply, add secret versions: + +```bash +echo -n "" | gcloud secrets versions add telegram-webhook-secret --data-file=- --project +echo -n "" | gcloud secrets versions add scheduler-shared-secret --data-file=- --project +``` + +## Environment strategy + +- Keep separate states for `dev` and `prod`. +- Prefer separate GCP projects for stronger isolation. +- Keep environment-specific variables in dedicated `*.tfvars` files. + +## Destructive operations + +Review plan output before apply/destroy: + +```bash +terraform -chdir=infra/terraform plan -destroy +terraform -chdir=infra/terraform destroy +``` diff --git a/docs/specs/HOUSEBOT-007-terraform-iac-baseline.md b/docs/specs/HOUSEBOT-007-terraform-iac-baseline.md new file mode 100644 index 0000000..42f3fad --- /dev/null +++ b/docs/specs/HOUSEBOT-007-terraform-iac-baseline.md @@ -0,0 +1,77 @@ +# HOUSEBOT-007 Terraform IaC Baseline + +## Summary + +Define a reproducible GCP infrastructure baseline for deployment of the bot API and mini app, including scheduling and secrets. + +## Goals + +- Provision Cloud Run services for bot API and mini app. +- Provision Cloud Scheduler reminder trigger. +- Provision Secret Manager placeholders and runtime access bindings. +- Provision Artifact Registry repository for container images. +- Provide optional GitHub OIDC Workload Identity resources. + +## Non-goals + +- Business feature implementation. +- Full observability stack (Grafana/Prometheus) in this ticket. +- Multi-region failover. + +## Scope + +- In: Terraform scaffold, docs, CI validation. +- Out: runtime deploy script rewrites, production dashboard configuration. + +## Interfaces and Contracts + +- Scheduler sends HTTP request to `POST /internal/scheduler/reminders`. +- Bot runtime reads secret-backed env vars: + - `TELEGRAM_WEBHOOK_SECRET` + - `SCHEDULER_SHARED_SECRET` + - `SUPABASE_URL` (optional) + - `SUPABASE_PUBLISHABLE_KEY` (optional) + +## Domain Rules + +- N/A (infrastructure-only change). + +## Data Model Changes + +- None. + +## Security and Privacy + +- Runtime access to secrets is explicit via `roles/secretmanager.secretAccessor`. +- Scheduler uses OIDC token with dedicated service account. +- GitHub OIDC setup is optional and repository-scoped. + +## Observability + +- Out of scope for this ticket. + +## Edge Cases and Failure Modes + +- Missing secret versions causes runtime startup/read failures. +- Scheduler remains paused by default to avoid accidental reminders. +- Incorrect `bot_api_image` or `mini_app_image` tags causes deployment failures. + +## Test Plan + +- Unit: N/A +- Integration: `terraform validate` +- E2E: Apply in dev project and verify service URLs + scheduler job presence. + +## Acceptance Criteria + +- [ ] `terraform plan` succeeds with provided vars. +- [ ] Two Cloud Run services and one Scheduler job are provisioned. +- [ ] Runtime secret access is bound explicitly. +- [ ] CI validates Terraform formatting and configuration. +- [ ] Runbook documents local and CI workflow. + +## Rollout Plan + +- Apply to dev first with scheduler paused. +- Add secret versions. +- Unpause scheduler after reminder endpoint is implemented and verified. diff --git a/infra/terraform/.gitignore b/infra/terraform/.gitignore new file mode 100644 index 0000000..3d2bd4f --- /dev/null +++ b/infra/terraform/.gitignore @@ -0,0 +1,4 @@ +.terraform +*.tfstate +*.tfstate.* +terraform.tfvars diff --git a/infra/terraform/.terraform.lock.hcl b/infra/terraform/.terraform.lock.hcl new file mode 100644 index 0000000..78b8795 --- /dev/null +++ b/infra/terraform/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/google" { + version = "6.50.0" + constraints = "~> 6.0" + hashes = [ + "h1:79CwMTsp3Ud1nOl5hFS5mxQHyT0fGVye7pqpU0PPlHI=", + "zh:1f3513fcfcbf7ca53d667a168c5067a4dd91a4d4cccd19743e248ff31065503c", + "zh:3da7db8fc2c51a77dd958ea8baaa05c29cd7f829bd8941c26e2ea9cb3aadc1e5", + "zh:3e09ac3f6ca8111cbb659d38c251771829f4347ab159a12db195e211c76068bb", + "zh:7bb9e41c568df15ccf1a8946037355eefb4dfb4e35e3b190808bb7c4abae547d", + "zh:81e5d78bdec7778e6d67b5c3544777505db40a826b6eb5abe9b86d4ba396866b", + "zh:8d309d020fb321525883f5c4ea864df3d5942b6087f6656d6d8b3a1377f340fc", + "zh:93e112559655ab95a523193158f4a4ac0f2bfed7eeaa712010b85ebb551d5071", + "zh:d3efe589ffd625b300cef5917c4629513f77e3a7b111c9df65075f76a46a63c7", + "zh:d4a4d672bbef756a870d8f32b35925f8ce2ef4f6bbd5b71a3cb764f1b6c85421", + "zh:e13a86bca299ba8a118e80d5f84fbdd708fe600ecdceea1a13d4919c068379fe", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:fec30c095647b583a246c39d557704947195a1b7d41f81e369ba377d997faef6", + ] +} diff --git a/infra/terraform/README.md b/infra/terraform/README.md new file mode 100644 index 0000000..a1858ff --- /dev/null +++ b/infra/terraform/README.md @@ -0,0 +1,79 @@ +# Terraform Infrastructure (WHE-28) + +This directory contains baseline IaC for deploying the household bot platform on GCP. + +## Provisioned resources + +- Artifact Registry Docker repository +- Cloud Run service: bot API (public webhook endpoint) +- Cloud Run service: mini app (public web UI) +- Cloud Scheduler job for reminder triggers +- Runtime and scheduler service accounts with least-privilege bindings +- Secret Manager secrets (IDs only, secret values are added separately) +- Optional GitHub OIDC Workload Identity setup for deploy automation + +## Architecture (v1) + +- `bot-api`: Telegram webhook + app API endpoints +- `mini-app`: front-end delivery +- `scheduler`: triggers `bot-api` internal reminder endpoint using OIDC token + +## Prerequisites + +- Terraform `>= 1.8` +- Authenticated GCP CLI context (`gcloud auth application-default login` for local) +- Enabled billing on the target GCP project + +## Usage + +1. Initialize: + +```bash +terraform -chdir=infra/terraform init +``` + +2. Prepare variables: + +```bash +cp infra/terraform/terraform.tfvars.example infra/terraform/terraform.tfvars +``` + +3. Plan: + +```bash +terraform -chdir=infra/terraform plan +``` + +4. Apply: + +```bash +terraform -chdir=infra/terraform apply +``` + +5. Add secret values (after apply): + +```bash +echo -n "" | gcloud secrets versions add telegram-webhook-secret --data-file=- --project +echo -n "" | gcloud secrets versions add scheduler-shared-secret --data-file=- --project +``` + +## Environments + +Recommended approach: + +- Keep one state per environment (dev/prod) using separate backend configs or workspaces +- Use `terraform.tfvars` per environment (`dev.tfvars`, `prod.tfvars`) +- Keep `project_id` separate for dev/prod when possible + +## CI validation + +CI runs: + +- `terraform -chdir=infra/terraform fmt -check -recursive` +- `terraform -chdir=infra/terraform init -backend=false` +- `terraform -chdir=infra/terraform validate` + +## Notes + +- Scheduler job defaults to `paused = true` to prevent accidental sends before app logic is ready. +- Bot API is public to accept Telegram webhooks; scheduler endpoint should still verify app-level auth. diff --git a/infra/terraform/locals.tf b/infra/terraform/locals.tf new file mode 100644 index 0000000..7c2bbcd --- /dev/null +++ b/infra/terraform/locals.tf @@ -0,0 +1,37 @@ +locals { + name_prefix = "${var.service_prefix}-${var.environment}" + + common_labels = merge( + { + environment = var.environment + managed_by = "terraform" + project = "household-bot" + }, + var.labels + ) + + artifact_location = coalesce(var.artifact_repository_location, var.region) + + runtime_secret_ids = toset(compact([ + var.telegram_webhook_secret_id, + var.scheduler_shared_secret_id, + var.supabase_url_secret_id, + var.supabase_publishable_key_secret_id + ])) + + api_services = toset([ + "artifactregistry.googleapis.com", + "cloudscheduler.googleapis.com", + "iam.googleapis.com", + "iamcredentials.googleapis.com", + "run.googleapis.com", + "secretmanager.googleapis.com", + "sts.googleapis.com" + ]) + + github_deploy_roles = toset([ + "roles/artifactregistry.writer", + "roles/iam.serviceAccountUser", + "roles/run.admin" + ]) +} diff --git a/infra/terraform/main.tf b/infra/terraform/main.tf new file mode 100644 index 0000000..fe105f2 --- /dev/null +++ b/infra/terraform/main.tf @@ -0,0 +1,218 @@ +data "google_project" "current" { + project_id = var.project_id +} + +resource "google_project_service" "enabled" { + for_each = local.api_services + project = var.project_id + service = each.value + disable_on_destroy = false + disable_dependent_services = false +} + +resource "google_artifact_registry_repository" "containers" { + location = local.artifact_location + project = var.project_id + repository_id = var.artifact_repository_id + description = "Container images for household bot" + format = "DOCKER" + + labels = local.common_labels + + depends_on = [google_project_service.enabled] +} + +resource "google_service_account" "bot_runtime" { + project = var.project_id + account_id = "${var.environment}-bot-runtime" + display_name = "${local.name_prefix} bot runtime" +} + +resource "google_service_account" "mini_runtime" { + project = var.project_id + account_id = "${var.environment}-mini-runtime" + display_name = "${local.name_prefix} mini runtime" +} + +resource "google_service_account" "scheduler_invoker" { + project = var.project_id + account_id = "${var.environment}-scheduler" + display_name = "${local.name_prefix} scheduler invoker" +} + +resource "google_secret_manager_secret" "runtime" { + for_each = local.runtime_secret_ids + + project = var.project_id + secret_id = each.value + + replication { + auto {} + } + + labels = local.common_labels + + depends_on = [google_project_service.enabled] +} + +resource "google_secret_manager_secret_iam_member" "bot_runtime_access" { + for_each = google_secret_manager_secret.runtime + + project = var.project_id + secret_id = each.value.secret_id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${google_service_account.bot_runtime.email}" +} + +module "bot_api_service" { + source = "./modules/cloud_run_service" + + project_id = var.project_id + region = var.region + name = "${local.name_prefix}-bot-api" + service_account_email = google_service_account.bot_runtime.email + image = var.bot_api_image + allow_unauthenticated = true + min_instance_count = var.bot_min_instances + max_instance_count = var.bot_max_instances + labels = local.common_labels + + env = { + NODE_ENV = var.environment + } + + secret_env = merge( + { + TELEGRAM_WEBHOOK_SECRET = var.telegram_webhook_secret_id + SCHEDULER_SHARED_SECRET = var.scheduler_shared_secret_id + }, + var.supabase_url_secret_id == null ? {} : { + SUPABASE_URL = var.supabase_url_secret_id + }, + var.supabase_publishable_key_secret_id == null ? {} : { + SUPABASE_PUBLISHABLE_KEY = var.supabase_publishable_key_secret_id + } + ) + + depends_on = [ + google_project_service.enabled, + google_secret_manager_secret.runtime + ] +} + +module "mini_app_service" { + source = "./modules/cloud_run_service" + + project_id = var.project_id + region = var.region + name = "${local.name_prefix}-mini-app" + service_account_email = google_service_account.mini_runtime.email + image = var.mini_app_image + allow_unauthenticated = true + min_instance_count = var.mini_min_instances + max_instance_count = var.mini_max_instances + labels = local.common_labels + + env = { + NODE_ENV = var.environment + } + + depends_on = [google_project_service.enabled] +} + +resource "google_cloud_run_v2_service_iam_member" "scheduler_invoker" { + project = var.project_id + location = var.region + name = module.bot_api_service.name + role = "roles/run.invoker" + member = "serviceAccount:${google_service_account.scheduler_invoker.email}" +} + +resource "google_service_account_iam_member" "scheduler_token_creator" { + service_account_id = google_service_account.scheduler_invoker.name + role = "roles/iam.serviceAccountTokenCreator" + member = "serviceAccount:service-${data.google_project.current.number}@gcp-sa-cloudscheduler.iam.gserviceaccount.com" +} + +resource "google_cloud_scheduler_job" "reminders" { + project = var.project_id + region = var.region + name = "${local.name_prefix}-reminders" + schedule = var.scheduler_cron + time_zone = var.scheduler_timezone + paused = var.scheduler_paused + + http_target { + uri = "${module.bot_api_service.uri}${var.scheduler_path}" + http_method = var.scheduler_http_method + + headers = { + "Content-Type" = "application/json" + } + + body = base64encode(var.scheduler_body_json) + + oidc_token { + service_account_email = google_service_account.scheduler_invoker.email + audience = module.bot_api_service.uri + } + } + + depends_on = [ + module.bot_api_service, + google_service_account_iam_member.scheduler_token_creator + ] +} + +resource "google_service_account" "github_deployer" { + count = var.create_workload_identity ? 1 : 0 + + project = var.project_id + account_id = var.github_deploy_service_account_id + display_name = "${local.name_prefix} GitHub deployer" +} + +resource "google_iam_workload_identity_pool" "github" { + count = var.create_workload_identity ? 1 : 0 + + project = var.project_id + workload_identity_pool_id = var.workload_identity_pool_id + display_name = "GitHub Actions Pool" +} + +resource "google_iam_workload_identity_pool_provider" "github" { + count = var.create_workload_identity ? 1 : 0 + + project = var.project_id + workload_identity_pool_id = google_iam_workload_identity_pool.github[0].workload_identity_pool_id + workload_identity_pool_provider_id = var.workload_identity_provider_id + display_name = "GitHub Actions Provider" + + attribute_mapping = { + "google.subject" = "assertion.sub" + "attribute.actor" = "assertion.actor" + "attribute.repository" = "assertion.repository" + } + + attribute_condition = "assertion.repository == \"${var.github_repository}\"" + + oidc { + issuer_uri = "https://token.actions.githubusercontent.com" + } +} + +resource "google_service_account_iam_member" "github_oidc" { + count = var.create_workload_identity ? 1 : 0 + + service_account_id = google_service_account.github_deployer[0].name + role = "roles/iam.workloadIdentityUser" + member = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github[0].name}/attribute.repository/${var.github_repository}" +} + +resource "google_project_iam_member" "github_deployer_roles" { + for_each = var.create_workload_identity ? local.github_deploy_roles : toset([]) + + project = var.project_id + role = each.value + member = "serviceAccount:${google_service_account.github_deployer[0].email}" +} diff --git a/infra/terraform/modules/cloud_run_service/main.tf b/infra/terraform/modules/cloud_run_service/main.tf new file mode 100644 index 0000000..f604c61 --- /dev/null +++ b/infra/terraform/modules/cloud_run_service/main.tf @@ -0,0 +1,67 @@ +resource "google_cloud_run_v2_service" "this" { + project = var.project_id + location = var.region + name = var.name + + ingress = "INGRESS_TRAFFIC_ALL" + deletion_protection = false + + labels = var.labels + + template { + service_account = var.service_account_email + + scaling { + min_instance_count = var.min_instance_count + max_instance_count = var.max_instance_count + } + + containers { + image = var.image + + ports { + container_port = var.container_port + } + + resources { + limits = var.limits + } + + dynamic "env" { + for_each = var.env + content { + name = env.key + value = env.value + } + } + + dynamic "env" { + for_each = var.secret_env + content { + name = env.key + value_source { + secret_key_ref { + secret = env.value + version = "latest" + } + } + } + } + } + } + + traffic { + percent = 100 + type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST" + } +} + +resource "google_cloud_run_v2_service_iam_member" "public_invoker" { + count = var.allow_unauthenticated ? 1 : 0 + + project = var.project_id + location = var.region + name = google_cloud_run_v2_service.this.name + role = "roles/run.invoker" + member = "allUsers" +} diff --git a/infra/terraform/modules/cloud_run_service/outputs.tf b/infra/terraform/modules/cloud_run_service/outputs.tf new file mode 100644 index 0000000..a9bcbd8 --- /dev/null +++ b/infra/terraform/modules/cloud_run_service/outputs.tf @@ -0,0 +1,7 @@ +output "name" { + value = google_cloud_run_v2_service.this.name +} + +output "uri" { + value = google_cloud_run_v2_service.this.uri +} diff --git a/infra/terraform/modules/cloud_run_service/variables.tf b/infra/terraform/modules/cloud_run_service/variables.tf new file mode 100644 index 0000000..0706e31 --- /dev/null +++ b/infra/terraform/modules/cloud_run_service/variables.tf @@ -0,0 +1,63 @@ +variable "project_id" { + type = string +} + +variable "region" { + type = string +} + +variable "name" { + type = string +} + +variable "image" { + type = string +} + +variable "service_account_email" { + type = string + default = null +} + +variable "allow_unauthenticated" { + type = bool + default = false +} + +variable "env" { + type = map(string) + default = {} +} + +variable "secret_env" { + type = map(string) + default = {} +} + +variable "labels" { + type = map(string) + default = {} +} + +variable "container_port" { + type = number + default = 8080 +} + +variable "min_instance_count" { + type = number + default = 0 +} + +variable "max_instance_count" { + type = number + default = 3 +} + +variable "limits" { + type = map(string) + default = { + cpu = "1" + memory = "512Mi" + } +} diff --git a/infra/terraform/outputs.tf b/infra/terraform/outputs.tf new file mode 100644 index 0000000..cb6aa63 --- /dev/null +++ b/infra/terraform/outputs.tf @@ -0,0 +1,44 @@ +output "artifact_repository" { + description = "Artifact Registry repository" + value = google_artifact_registry_repository.containers.id +} + +output "bot_api_service_name" { + description = "Cloud Run bot API service name" + value = module.bot_api_service.name +} + +output "bot_api_service_url" { + description = "Cloud Run bot API URL" + value = module.bot_api_service.uri +} + +output "mini_app_service_name" { + description = "Cloud Run mini app service name" + value = module.mini_app_service.name +} + +output "mini_app_service_url" { + description = "Cloud Run mini app URL" + value = module.mini_app_service.uri +} + +output "scheduler_job_name" { + description = "Cloud Scheduler job for reminders" + value = google_cloud_scheduler_job.reminders.name +} + +output "runtime_secret_ids" { + description = "Secret Manager IDs expected by runtime" + value = sort([for secret in google_secret_manager_secret.runtime : secret.secret_id]) +} + +output "github_deployer_service_account" { + description = "GitHub OIDC deployer service account email" + value = var.create_workload_identity ? google_service_account.github_deployer[0].email : null +} + +output "github_workload_identity_provider" { + description = "Full Workload Identity Provider resource name" + value = var.create_workload_identity ? google_iam_workload_identity_pool_provider.github[0].name : null +} diff --git a/infra/terraform/providers.tf b/infra/terraform/providers.tf new file mode 100644 index 0000000..fc74f25 --- /dev/null +++ b/infra/terraform/providers.tf @@ -0,0 +1,4 @@ +provider "google" { + project = var.project_id + region = var.region +} diff --git a/infra/terraform/terraform.tfvars.example b/infra/terraform/terraform.tfvars.example new file mode 100644 index 0000000..5a03af3 --- /dev/null +++ b/infra/terraform/terraform.tfvars.example @@ -0,0 +1,16 @@ +project_id = "my-gcp-project" +region = "europe-west1" +environment = "dev" +service_prefix = "household" + +artifact_repository_id = "household-bot" + +bot_api_image = "europe-west1-docker.pkg.dev/my-gcp-project/household-bot/bot-api:latest" +mini_app_image = "europe-west1-docker.pkg.dev/my-gcp-project/household-bot/mini-app:latest" + +scheduler_cron = "0 9 * * *" +scheduler_timezone = "Asia/Tbilisi" +scheduler_paused = true + +create_workload_identity = true +github_repository = "whekin/household-bot" diff --git a/infra/terraform/variables.tf b/infra/terraform/variables.tf new file mode 100644 index 0000000..dc817e1 --- /dev/null +++ b/infra/terraform/variables.tf @@ -0,0 +1,167 @@ +variable "project_id" { + description = "GCP project ID" + type = string +} + +variable "region" { + description = "Primary GCP region for Cloud Run services" + type = string + default = "europe-west1" +} + +variable "environment" { + description = "Environment name (e.g. dev, prod)" + type = string + default = "dev" +} + +variable "service_prefix" { + description = "Prefix for service names" + type = string + default = "household" +} + +variable "artifact_repository_id" { + description = "Artifact Registry repository ID" + type = string + default = "household-bot" +} + +variable "artifact_repository_location" { + description = "Artifact Registry location (defaults to region)" + type = string + default = null + nullable = true +} + +variable "bot_api_image" { + description = "Container image for bot API service" + type = string +} + +variable "mini_app_image" { + description = "Container image for mini app service" + type = string +} + +variable "telegram_webhook_secret_id" { + description = "Secret Manager ID for Telegram webhook secret token" + type = string + default = "telegram-webhook-secret" +} + +variable "scheduler_shared_secret_id" { + description = "Secret Manager ID for app-level scheduler secret" + type = string + default = "scheduler-shared-secret" +} + +variable "supabase_url_secret_id" { + description = "Optional Secret Manager ID for SUPABASE_URL" + type = string + default = null + nullable = true +} + +variable "supabase_publishable_key_secret_id" { + description = "Optional Secret Manager ID for SUPABASE_PUBLISHABLE_KEY" + type = string + default = null + nullable = true +} + +variable "scheduler_path" { + description = "Reminder endpoint path on bot API" + type = string + default = "/internal/scheduler/reminders" +} + +variable "scheduler_http_method" { + description = "Scheduler HTTP method" + type = string + default = "POST" +} + +variable "scheduler_cron" { + description = "Cron expression for reminder scheduler" + type = string + default = "0 9 * * *" +} + +variable "scheduler_timezone" { + description = "Scheduler timezone" + type = string + default = "Asia/Tbilisi" +} + +variable "scheduler_body_json" { + description = "JSON payload for scheduler requests" + type = string + default = "{\"kind\":\"monthly-reminder\"}" +} + +variable "scheduler_paused" { + description = "Whether scheduler should be paused initially" + type = bool + default = true +} + +variable "bot_min_instances" { + description = "Minimum bot API instances" + type = number + default = 0 +} + +variable "bot_max_instances" { + description = "Maximum bot API instances" + type = number + default = 3 +} + +variable "mini_min_instances" { + description = "Minimum mini app instances" + type = number + default = 0 +} + +variable "mini_max_instances" { + description = "Maximum mini app instances" + type = number + default = 2 +} + +variable "labels" { + description = "Additional labels" + type = map(string) + default = {} +} + +variable "create_workload_identity" { + description = "Create GitHub OIDC Workload Identity resources" + type = bool + default = false +} + +variable "github_repository" { + description = "GitHub repository in owner/repo format" + type = string + default = "whekin/household-bot" +} + +variable "workload_identity_pool_id" { + description = "Workload Identity Pool ID" + type = string + default = "github-pool" +} + +variable "workload_identity_provider_id" { + description = "Workload Identity Provider ID" + type = string + default = "github-provider" +} + +variable "github_deploy_service_account_id" { + description = "Service account ID used by GitHub Actions via OIDC" + type = string + default = "github-deployer" +} diff --git a/infra/terraform/versions.tf b/infra/terraform/versions.tf new file mode 100644 index 0000000..f5a1a2b --- /dev/null +++ b/infra/terraform/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.8.0" + + required_providers { + google = { + source = "hashicorp/google" + version = "~> 6.0" + } + } +} diff --git a/package.json b/package.json index 22bc1b6..4489871 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,9 @@ "format": "bunx oxfmt .", "format:check": "bunx oxfmt --check .", "review:coderabbit": "coderabbit --prompt-only --base main || ~/.local/bin/coderabbit --prompt-only --base main", + "infra:fmt": "terraform -chdir=infra/terraform fmt -recursive", + "infra:fmt:check": "terraform -chdir=infra/terraform fmt -check -recursive", + "infra:validate": "terraform -chdir=infra/terraform init -backend=false && terraform -chdir=infra/terraform validate", "dev:bot": "bun run --filter @household/bot dev", "dev:miniapp": "bun run --filter @household/miniapp dev" },