feat(infra): implement multi-environment deployment strategy

- Update CD workflow for branch-based environments (main -> Prod, dev -> Dev)
- Support Terraform workspaces for environment isolation
- Add manage_runtime_secrets flag to prevent accidental secret destruction
- Add infra management and secret setup utility scripts
- Prefix GitHub deployer identity with environment name
- Synchronize bot environment variables with latest runtime config
This commit is contained in:
2026-03-15 19:11:18 +04:00
parent 594c370677
commit f4fe4470f7
7 changed files with 211 additions and 44 deletions

View File

@@ -0,0 +1,44 @@
#!/bin/bash
# Configuration
PROJECT_ID="gen-lang-client-0200379851"
REGION="europe-west1"
WORKSPACE="${1:-prod}" # Takes first argument, defaults to 'prod'
# Change directory to terraform folder
cd infra/terraform || exit 1
echo "--- Shared Resource Import Utility ---"
echo "Target Project: $PROJECT_ID"
echo "Target Workspace: $WORKSPACE"
# 1. Ensure the workspace exists and is selected
terraform workspace select "$WORKSPACE" || terraform workspace new "$WORKSPACE"
# 2. Construct Resource IDs
echo -e "\nConstructing Resource IDs..."
REPO_ID="projects/$PROJECT_ID/locations/$REGION/repositories/household-bot"
POOL_ID="projects/$PROJECT_ID/locations/global/workloadIdentityPools/github-pool"
PROV_ID="projects/$PROJECT_ID/locations/global/workloadIdentityPools/github-pool/providers/github-provider"
echo "1. Repository Resource ID: $REPO_ID"
echo "2. Identity Pool Resource ID: $POOL_ID"
echo "3. Provider Resource ID: $PROV_ID"
# 3. Perform the Imports
echo -e "\nStarting Terraform Imports..."
# Import Repository
echo -e "\n--- Importing Artifact Registry ---"
terraform import -input=false -var-file="$WORKSPACE.tfvars" google_artifact_registry_repository.containers "$REPO_ID"
# Import Workload Identity Pool
echo -e "\n--- Importing Workload Identity Pool ---"
terraform import -input=false -var-file="$WORKSPACE.tfvars" 'google_iam_workload_identity_pool.github[0]' "$POOL_ID"
# Import Workload Identity Provider
echo -e "\n--- Importing Workload Identity Provider ---"
terraform import -input=false -var-file="$WORKSPACE.tfvars" 'google_iam_workload_identity_pool_provider.github[0]' "$PROV_ID"
echo -e "\n--- Import Complete for $WORKSPACE! ---"
echo "You can now run: bun run infra:apply:$WORKSPACE"

View File

@@ -0,0 +1,103 @@
import { $ } from 'bun'
const PROJECT_ID = 'gen-lang-client-0200379851'
async function secretExists(name: string): Promise<boolean> {
const result =
(await $`gcloud secrets describe ${name} --project=${PROJECT_ID}`.quiet().exitCode) === 0
return result
}
async function createSecret(name: string, value: string) {
console.log(`\n[Checking] ${name}...`)
if (await secretExists(name)) {
console.log(`[Skipping] ${name} already exists. If you want to change it, use the GCP console.`)
return
}
try {
console.log(`[Creating] ${name} for the first time...`)
await $`echo -n ${value} | gcloud secrets create ${name} --data-file=- --replication-policy="automatic" --project=${PROJECT_ID}`.quiet()
console.log(`[Success] ${name} is ready.`)
} catch (err) {
console.error(`[Error] Failed to setup ${name}:`, err)
}
}
console.log('--- Production & Test Environment Secret Setup ---')
console.log(`Target Project: ${PROJECT_ID}`)
// 1. PRODUCTION Bot Token
let prodBotToken = ''
if (!(await secretExists('telegram-bot-token'))) {
prodBotToken = prompt('1. Enter your PRODUCTION Telegram Bot Token (the original one):') || ''
}
// 2. PRODUCTION Database URL
let prodDbUrl = ''
if (!(await secretExists('database-url'))) {
prodDbUrl = prompt('2. Enter your PRODUCTION Supabase DATABASE_URL (for public schema):') || ''
}
// 3. TEST Bot Token
let testBotToken = ''
if (!(await secretExists('telegram-bot-token-test'))) {
testBotToken = prompt('3. Enter your TEST Telegram Bot Token (from @BotFather):') || ''
}
// 4. TEST Database URL (Derived from prod if not exists)
let testDbUrlPrompt = ''
if (!(await secretExists('database-url-test'))) {
testDbUrlPrompt =
prompt(
'4. Enter your TEST Supabase DATABASE_URL (or leave empty to reuse prod with ?options=-csearch_path=test):'
) || ''
}
// 5. OpenAI API Key (Shared)
let openaiKey = ''
if (!(await secretExists('openai-api-key'))) {
openaiKey = prompt('5. Enter your OpenAI API Key:') || ''
}
// Logic for test DB URL
const testDbUrl =
testDbUrlPrompt ||
(prodDbUrl &&
(prodDbUrl.includes('?')
? `${prodDbUrl}&options=-csearch_path%3Dtest`
: `${prodDbUrl}?options=-csearch_path%3Dtest`))
// Logic for prod DB URL
const finalProdDbUrl =
prodDbUrl &&
(prodDbUrl.includes('?')
? `${prodDbUrl}&options=-csearch_path%3Dpublic`
: `${prodDbUrl}?options=-csearch_path%3Dpublic`)
// Generate random secrets (Always safe to recreate if missing)
const webhookSecret = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64')
const schedulerSecret = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64')
console.log('\nStarting GCP operations...')
if (prodBotToken) await createSecret('telegram-bot-token', prodBotToken.trim())
if (finalProdDbUrl) await createSecret('database-url', finalProdDbUrl.trim())
if (testBotToken) await createSecret('telegram-bot-token-test', testBotToken.trim())
if (testDbUrl) await createSecret('database-url-test', testDbUrl.trim())
if (openaiKey) await createSecret('openai-api-key', openaiKey.trim())
// Create unique secrets per environment if missing
await createSecret('telegram-webhook-secret-test', webhookSecret)
await createSecret('scheduler-shared-secret-test', schedulerSecret)
await createSecret(
'telegram-webhook-secret',
Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64')
)
await createSecret(
'scheduler-shared-secret',
Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64')
)
console.log('\n--- Setup Complete! ---')
console.log('You can now run the import commands and then infra:apply:prod')