infrastructure/
├── comet.yaml # Comet configuration
├── secrets.yaml # Unencrypted secrets template
├── secrets.enc.yaml # SOPS-encrypted secrets
├── .gitignore # Ignore generated files
├── stacks/ # Stack definitions
│ ├── shared.js # Shared settings
│ ├── dev.js
│ ├── staging.js
│ └── production.js
└── modules/ # Terraform modules
├── vpc/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── kubernetes/
└── database/
DO: Use a shared configuration file
// stacks/shared.js
const settings = {
project_name: 'myapp',
domain: 'example.com',
regions: {
primary: 'us-central1',
secondary: 'us-east1'
}
}
module.exports = { settings }DO: Import shared settings in each stack
// stacks/production.js
const { settings } = require('./shared.js')
stack('production', { settings })DON'T: Duplicate configuration across stacks
// ❌ Bad: Duplicated in each stack
stack('production', {
project_name: 'myapp', // Repeated
domain: 'example.com' // Repeated
})DO: Small, single-purpose components
// Good: Each component has one responsibility
const vpc = component('vpc', 'modules/vpc', {
cidr_block: '10.0.0.0/16'
})
const gke = component('gke', 'modules/gke', {
network: vpc.id,
cluster_name: 'my-cluster'
})DON'T: Mega-components that do everything
// ❌ Bad: One component doing too much
const infrastructure = component('all', 'modules/everything', {
// Too many responsibilities
})DO: Clear, descriptive component names
component('vpc-prod', 'modules/vpc', ...)
component('gke-main-cluster', 'modules/gke', ...)
component('cloudsql-primary', 'modules/database', ...)DON'T: Cryptic abbreviations
// ❌ Bad: Unclear names
component('v1', 'modules/vpc', ...)
component('k', 'modules/gke', ...)DO: Only reference what you need
// Good: Specific references
const app = component('app', 'modules/app', {
vpc_id: '{{ (state "infra" "vpc").id }}'
})DON'T: Create unnecessary coupling
// ❌ Bad: Circular or complex dependencies
// stack1 → stack2 → stack3 → stack1 (circular!)DO: Comment cross-stack references
// Requires: 'infra' stack must be applied first
const app = component('app', 'modules/app', {
// Reference to infrastructure stack VPC
vpc_id: '{{ (state "infra" "vpc").id }}'
})DO: Encrypt all secrets
# Create encrypted secrets file
sops secrets.enc.yaml// Reference encrypted secrets
const db = component('database', 'modules/db', {
password: secrets('sops://secrets.enc.yaml#/database/password')
})DON'T: Hardcode sensitive values
// ❌ Bad: Plaintext secrets
const db = component('database', 'modules/db', {
password: 'super-secret-password' // Never do this!
})SOPS requires the SOPS_AGE_KEY to be set before stack parsing begins. Use comet.yaml to pre-load it:
DO: Configure in comet.yaml (recommended)
# comet.yaml
env:
# Load SOPS AGE key from 1Password before stack parsing
SOPS_AGE_KEY: op://ci-cd/sops-age-key/privateAlternative: Export in shell
# In your shell profile or CI/CD
export SOPS_AGE_KEY="AGE-SECRET-KEY-1..."Why use comet.yaml?
- ✅ Automatic - no manual shell setup needed
- ✅ Team-consistent - everyone uses the same config
- ✅ CI/CD friendly - works seamlessly in pipelines
- ✅ Secure - secrets fetched from 1Password on-demand
# secrets.enc.yaml
database:
dev:
password: "dev-password"
prod:
password: "prod-password"
github:
token: "ghp_..."
cloudflare:
api_token: "..."// Reference with clear paths
password: secrets('sops://secrets.enc.yaml#/database/{{ .stack }}/password')DO: Include stack and component in state path
backend('gcs', {
bucket: 'my-terraform-state',
prefix: '${project}/{{ .stack }}/{{ .component }}/terraform.tfstate'
})DON'T: Use flat state structure
// ❌ Bad: No organization
backend('gcs', {
bucket: 'my-terraform-state',
prefix: 'state' // All state in one place
})Always ignore generated files:
# Terraform
.terraform/
*.tfstate
*.tfstate.*
*.tfvars
.terraform.lock.hcl
# Comet generated
**/backend.tf.json
**/providers_gen.tf
**/*-*.tfvars.json
**/*.planfile
# Secrets (keep only encrypted)
secrets.yaml
!secrets.enc.yamlDO commit:
- ✅ Stack definitions (
.jsfiles) - ✅ Module source code
- ✅ Encrypted secrets (
.enc.yaml) - ✅
comet.yamlconfiguration - ✅ Documentation
DON'T commit:
- ❌ Generated Terraform files
- ❌
.terraform/directories - ❌ State files
- ❌ Unencrypted secrets
- ❌ Plan files
# Check if stacks parse correctly
comet list
comet list dev# Always plan first
comet plan production vpc
# Review the plan carefully
comet apply production vpc# Development → Staging → Production
comet apply dev
comet apply staging
comet apply productionInclude in PRs:
- Stack definition changes
- Module changes
- Documentation updates
- Test results from
comet plan
# Example GitHub Actions workflow
name: Terraform Plan
on:
pull_request:
paths:
- 'stacks/**'
- 'modules/**'
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Comet
run: |
# Install comet
- name: Plan Dev
run: comet plan dev
- name: Plan Staging
run: comet plan staging# 1. Test in dev
comet apply dev
# 2. Verify in staging
comet apply staging
# 3. Plan production
comet plan production
# 4. Apply production (after approval)
comet apply productionWhen components are independent:
# Apply multiple components in parallel
comet apply dev vpc &
comet apply dev database &
wait- Keep components small and focused
- Avoid massive components with hundreds of resources
- Split large infrastructures across multiple components
- Use remote backends (GCS, S3) for team collaboration
- Enable state locking to prevent conflicts
- Use separate state files per component (Comet does this automatically)
Issue: "Backend configuration has changed"
# Solution: Re-initialize Terraform
rm -rf .terraform
comet init <stack> <component>Issue: "Required plugins are not installed"
# Solution: Initialize providers and backends
comet init <stack> <component>Issue: Cross-stack reference returns null
# Solution: Ensure referenced stack is applied first
comet apply infra vpc
comet init app webapp # Initialize to query outputs
comet apply app webapp # Now vpc outputs are availableIssue: SOPS decryption fails
# Solution 1: Set SOPS_AGE_KEY in comet.yaml (recommended)
# comet.yaml:
# env:
# SOPS_AGE_KEY: op://ci-cd/sops-age-key/private
# Solution 2: Export in shell
export SOPS_AGE_KEY="your-age-key"
comet apply devWhen upgrading Comet:
- Test in dev environment first
- Review changelog for breaking changes
- Update one environment at a time
- Keep Comet version consistent across team
# Check version
comet version
# Upgrade gradually
# dev → staging → productionAdd comments to stack files:
/**
* Production Environment Stack
*
* This stack provisions the production infrastructure including:
* - VPC with public and private subnets
* - GKE cluster with autoscaling
* - Cloud SQL database with failover
*
* Prerequisites:
* - GCP project must exist
* - SOPS_AGE_KEY must be set
*
* Usage:
* comet plan production
* comet apply production
*/
const { settings } = require('./shared.js')
stack('production', { settings })
// ... component definitionsKeep a project-level README with:
- Quick start guide
- Environment descriptions
- Deployment procedures
- Troubleshooting tips