mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 12:04:02 +00:00
Merge pull request #4 from whekin/codex/whe-28-terraform
feat(WHE-28): Terraform baseline for Cloud Run, Scheduler, and Secrets
This commit is contained in:
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
@@ -79,3 +79,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
|
||||
|
||||
11
AGENTS.md
11
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/*`
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
- Bun 1.3+
|
||||
- Node.js 22+
|
||||
- Terraform 1.8+ (for IaC checks/plans)
|
||||
|
||||
## First-time setup
|
||||
|
||||
@@ -24,6 +25,8 @@ bun run build
|
||||
bun run db:generate
|
||||
bun run db:check
|
||||
bun run db:migrate
|
||||
bun run infra:fmt:check
|
||||
bun run infra:validate
|
||||
```
|
||||
|
||||
## App commands
|
||||
@@ -54,9 +57,14 @@ 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`
|
||||
- optional for automated migrations: `DATABASE_URL`
|
||||
|
||||
## IaC Runbook
|
||||
|
||||
- See `docs/runbooks/iac-terraform.md` for provisioning flow.
|
||||
|
||||
55
docs/runbooks/iac-terraform.md
Normal file
55
docs/runbooks/iac-terraform.md
Normal file
@@ -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 "<telegram-webhook-secret>" | gcloud secrets versions add telegram-webhook-secret --data-file=- --project <project_id>
|
||||
echo -n "<scheduler-shared-secret>" | gcloud secrets versions add scheduler-shared-secret --data-file=- --project <project_id>
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
77
docs/specs/HOUSEBOT-007-terraform-iac-baseline.md
Normal file
77
docs/specs/HOUSEBOT-007-terraform-iac-baseline.md
Normal file
@@ -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.
|
||||
4
infra/terraform/.gitignore
vendored
Normal file
4
infra/terraform/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.terraform
|
||||
*.tfstate
|
||||
*.tfstate.*
|
||||
terraform.tfvars
|
||||
22
infra/terraform/.terraform.lock.hcl
generated
Normal file
22
infra/terraform/.terraform.lock.hcl
generated
Normal file
@@ -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",
|
||||
]
|
||||
}
|
||||
79
infra/terraform/README.md
Normal file
79
infra/terraform/README.md
Normal file
@@ -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 "<value>" | gcloud secrets versions add telegram-webhook-secret --data-file=- --project <project_id>
|
||||
echo -n "<value>" | gcloud secrets versions add scheduler-shared-secret --data-file=- --project <project_id>
|
||||
```
|
||||
|
||||
## 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.
|
||||
37
infra/terraform/locals.tf
Normal file
37
infra/terraform/locals.tf
Normal file
@@ -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"
|
||||
])
|
||||
}
|
||||
218
infra/terraform/main.tf
Normal file
218
infra/terraform/main.tf
Normal file
@@ -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}"
|
||||
}
|
||||
67
infra/terraform/modules/cloud_run_service/main.tf
Normal file
67
infra/terraform/modules/cloud_run_service/main.tf
Normal file
@@ -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"
|
||||
}
|
||||
7
infra/terraform/modules/cloud_run_service/outputs.tf
Normal file
7
infra/terraform/modules/cloud_run_service/outputs.tf
Normal file
@@ -0,0 +1,7 @@
|
||||
output "name" {
|
||||
value = google_cloud_run_v2_service.this.name
|
||||
}
|
||||
|
||||
output "uri" {
|
||||
value = google_cloud_run_v2_service.this.uri
|
||||
}
|
||||
63
infra/terraform/modules/cloud_run_service/variables.tf
Normal file
63
infra/terraform/modules/cloud_run_service/variables.tf
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
44
infra/terraform/outputs.tf
Normal file
44
infra/terraform/outputs.tf
Normal file
@@ -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
|
||||
}
|
||||
4
infra/terraform/providers.tf
Normal file
4
infra/terraform/providers.tf
Normal file
@@ -0,0 +1,4 @@
|
||||
provider "google" {
|
||||
project = var.project_id
|
||||
region = var.region
|
||||
}
|
||||
16
infra/terraform/terraform.tfvars.example
Normal file
16
infra/terraform/terraform.tfvars.example
Normal file
@@ -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"
|
||||
167
infra/terraform/variables.tf
Normal file
167
infra/terraform/variables.tf
Normal file
@@ -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"
|
||||
}
|
||||
10
infra/terraform/versions.tf
Normal file
10
infra/terraform/versions.tf
Normal file
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.8.0"
|
||||
|
||||
required_providers {
|
||||
google = {
|
||||
source = "hashicorp/google"
|
||||
version = "~> 6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,9 @@
|
||||
"db:push": "bunx drizzle-kit push --config packages/db/drizzle.config.ts",
|
||||
"db:studio": "bunx drizzle-kit studio --config packages/db/drizzle.config.ts",
|
||||
"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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user