|
| 1 | +# GitHub Terraform CI Bootstrap Module |
| 2 | + |
| 3 | +This module creates the necessary infrastructure for **GitHub Terraform CI** - managing Terraform infrastructure through GitHub Actions CI/CD pipelines. It provisions service accounts with appropriate GCP permissions and uses Workload Identity Federation for keyless authentication to run `terraform plan` and `terraform apply` operations. |
| 4 | + |
| 5 | +## Purpose |
| 6 | + |
| 7 | +Each module invocation creates a dedicated service account for a complete Terraform configuration managed in CI. This enables: |
| 8 | + |
| 9 | +- **Isolated Terraform CI**: Each Terraform setup gets its own service account and state bucket for CI operations |
| 10 | +- **Secure GitHub Actions**: Run `terraform plan` and `terraform apply` in GitHub Actions without storing keys |
| 11 | +- **Cross-Project Deployments**: Single service account can manage Terraform resources across multiple GCP projects |
| 12 | +- **Environment Separation**: Separate CI service accounts for prod, staging, dev, etc. |
| 13 | + |
| 14 | +## Features |
| 15 | + |
| 16 | +- **Shared Infrastructure**: Uses a single Workload Identity Pool in khan-internal-services for all GitHub Terraform CI |
| 17 | +- **Dedicated Service Accounts**: Creates unique service accounts for each Terraform configuration managed in CI |
| 18 | +- **Workload Identity Federation**: Uses modern, keyless authentication for GitHub Actions |
| 19 | +- **Cross-Project Support**: Service accounts can deploy Terraform resources across multiple GCP projects |
| 20 | +- **Least Privilege**: Only grants permissions for specified GCP services in target projects |
| 21 | +- **Terraform State Management**: Automatic permissions for GCS-based Terraform state buckets |
| 22 | +- **Secret Management**: Optional access to Google Secret Manager secrets needed by Terraform |
| 23 | +- **Configurable Services**: Enable only the GCP services your Terraform configuration manages |
| 24 | +- **Repository Scoped**: Restricts access to a specific GitHub repository containing Terraform code |
| 25 | + |
| 26 | +## Architecture |
| 27 | + |
| 28 | +All GitHub Terraform CI infrastructure is centralized in the `khan-internal-services` project: |
| 29 | +- **Single Pool**: `khan-internal-services-github-ci` pool shared by all Terraform configurations managed in CI |
| 30 | +- **Unique Providers**: Each Terraform configuration gets its own provider within the shared pool |
| 31 | +- **Cross-Project Permissions**: Service accounts get permissions in target projects for Terraform resource management |
| 32 | +- **State Bucket Access**: Service accounts get appropriate permissions for Terraform state storage in CI |
| 33 | + |
| 34 | +## Usage |
| 35 | + |
| 36 | +```hcl |
| 37 | +# Bootstrap GitHub Terraform CI for the culture-cron production configuration |
| 38 | +module "culture_cron_terraform_ci" { |
| 39 | + source = "git::https://github.com/Khan/terraform-modules.git//terraform/modules/github-ci-bootstrap?ref=v1.0.0" |
| 40 | +
|
| 41 | + # Terraform configuration managed in CI |
| 42 | + service_name = "culture-cron-prod" # YOU choose this name: project + environment |
| 43 | + github_repository = "Khan/culture-cron" # GitHub repo containing the Terraform code |
| 44 | + |
| 45 | + # Target projects where this Terraform configuration deploys resources via CI |
| 46 | + target_projects = { |
| 47 | + "khan-academy" = { |
| 48 | + required_services = ["cloudfunctions", "storage", "pubsub", "scheduler"] |
| 49 | + } |
| 50 | + } |
| 51 | + |
| 52 | + # Terraform state bucket (optional - defaults to terraform-khan-<github_repository>-<service_name>) |
| 53 | + # terraform_state_bucket = "custom-bucket-name" |
| 54 | + |
| 55 | + # Secrets that the Terraform configuration needs access to (optional) |
| 56 | + secret_ids = [ |
| 57 | + "projects/khan-academy/secrets/slack-token" |
| 58 | + ] |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +## Inputs |
| 63 | + |
| 64 | +| Name | Description | Type | Default | Required | |
| 65 | +|------|-------------|------|---------|:--------:| |
| 66 | +| `service_name` | User-defined unique identifier for this Terraform configuration and environment (e.g., 'culture-cron-prod', 'webapp-staging') | `string` | n/a | yes | |
| 67 | +| `github_repository` | GitHub repository containing the Terraform configuration in format 'org/repo' | `string` | n/a | yes | |
| 68 | +| `target_projects` | Map of GCP projects where this Terraform configuration will deploy resources. Keys are project IDs. | `map(object)` | `{}` | no | |
| 69 | +| `terraform_state_bucket` | GCS bucket name for storing Terraform state for this configuration | `string` | `terraform-{org}-{repo}-{service}` | no | |
| 70 | +| `secrets_project_id` | Project ID where secrets needed by the Terraform configuration are stored | `string` | `"khan-academy"` | no | |
| 71 | +| `secret_ids` | List of secret IDs that the Terraform configuration needs access to | `list(string)` | `[]` | no | |
| 72 | + |
| 73 | +### Target Projects Structure |
| 74 | + |
| 75 | +The `target_projects` variable accepts a map where each key is a GCP project ID: |
| 76 | + |
| 77 | +```hcl |
| 78 | +target_projects = { |
| 79 | + "khan-academy" = { |
| 80 | + required_services = ["storage", "pubsub"] # Services needed in this project |
| 81 | + } |
| 82 | + "khan-academy-staging" = { |
| 83 | + required_services = ["cloudfunctions"] |
| 84 | + } |
| 85 | +} |
| 86 | +``` |
| 87 | + |
| 88 | +### Available Services |
| 89 | + |
| 90 | +These services correspond to GCP resources that your Terraform configuration can deploy and manage: |
| 91 | + |
| 92 | +- `cloudfunctions` - Enables deploying and managing Cloud Functions via Terraform |
| 93 | +- `storage` - Enables creating and managing Cloud Storage buckets via Terraform |
| 94 | +- `pubsub` - Enables creating and managing Pub/Sub topics and subscriptions via Terraform |
| 95 | +- `scheduler` - Enables creating and managing Cloud Scheduler jobs via Terraform |
| 96 | + |
| 97 | +### Terraform State Bucket Default |
| 98 | + |
| 99 | +If `terraform_state_bucket` is not specified, the module automatically generates a bucket name based on your GitHub repository and service name: |
| 100 | + |
| 101 | +- **Pattern**: `terraform-{org}-{repo}-{service}` (normalized for GCS bucket naming rules) |
| 102 | +- **Normalization**: Converted to lowercase, underscores replaced with hyphens |
| 103 | +- **Example**: `Khan/culture-cron` + `culture-cron-prod` → `terraform-khan-culture-cron-culture-cron-prod` |
| 104 | +- **Example**: `Khan/webapp` + `webapp-staging` → `terraform-khan-webapp-webapp-staging` |
| 105 | +- **Example**: `Khan/Mobile_App` + `mobile_app_prod` → `terraform-khan-mobile-app-mobile-app-prod` |
| 106 | + |
| 107 | +This ensures each Terraform setup gets its own isolated state bucket while maintaining consistent, predictable naming that complies with GCS bucket naming requirements. |
| 108 | + |
| 109 | +### Service Name Guidelines |
| 110 | + |
| 111 | +The `service_name` is a **user-defined identifier** that you choose yourself to distinguish different Terraform configurations managed in CI. This is not something you need to look up - you get to assign it based on your own naming conventions. |
| 112 | + |
| 113 | +#### How to Choose a Service Name |
| 114 | + |
| 115 | +**You should choose a name that clearly identifies:** |
| 116 | +1. **What service/application** this Terraform configuration manages |
| 117 | +2. **Which environment** (prod, staging, dev, etc.) |
| 118 | +3. **What scope** (if you have multiple Terraform configurations per service) |
| 119 | + |
| 120 | +#### Recommended Patterns |
| 121 | + |
| 122 | +- **Basic**: `{service}-{environment}` (e.g., `culture-cron-prod`, `webapp-staging`) |
| 123 | +- **With scope**: `{service}-{scope}-{environment}` (e.g., `webapp-frontend-prod`, `webapp-backend-staging`) |
| 124 | +- **Shared resources**: `{purpose}-{environment}` (e.g., `shared-infra-prod`, `monitoring-dev`) |
| 125 | + |
| 126 | +#### Examples by Use Case |
| 127 | + |
| 128 | +| Scenario | Service Name | What It Represents | |
| 129 | +|----------|--------------|-------------------| |
| 130 | +| Culture Cron production | `culture-cron-prod` | Production deployment of Culture Cron service | |
| 131 | +| Webapp staging environment | `webapp-staging` | Staging environment for the main webapp | |
| 132 | +| API development environment | `api-dev` | Development environment for API service | |
| 133 | +| Shared infrastructure | `shared-infra-prod` | Production shared infrastructure (networking, etc.) | |
| 134 | +| Multiple configs per service | `webapp-frontend-prod`<br/>`webapp-backend-prod` | Separate Terraform configs for frontend and backend | |
| 135 | + |
| 136 | +#### Technical Requirements |
| 137 | + |
| 138 | +- **Characters**: Lowercase letters, numbers, and hyphens only (no underscores) |
| 139 | +- **Uniqueness**: Must be unique across all your Terraform CI configurations |
| 140 | +- **Purpose**: Creates isolated CI infrastructure for each configuration |
| 141 | +- **Usage**: Used to generate service account names, state bucket names, and provider IDs |
| 142 | + |
| 143 | +#### Multi-Configuration Repositories |
| 144 | + |
| 145 | +A single GitHub repository can have multiple `service_name` values for different purposes: |
| 146 | +- Different environments (`myapp-prod`, `myapp-staging`, `myapp-dev`) |
| 147 | +- Different components (`myapp-frontend-prod`, `myapp-backend-prod`) |
| 148 | +- Different deployment scopes (`myapp-us-prod`, `myapp-eu-prod`) |
| 149 | + |
| 150 | +Each `service_name` gets its own isolated: |
| 151 | +- Service account (`{service_name}-ci`) |
| 152 | +- Terraform state bucket (`terraform-{org}-{repo}-{service_name}`) |
| 153 | +- Workload Identity provider (`{service_name}-provider`) |
| 154 | + |
| 155 | +**Note**: GitHub repository names may contain underscores, which will be automatically converted to hyphens in generated bucket names to comply with GCS naming requirements. |
| 156 | + |
| 157 | +## Outputs |
| 158 | + |
| 159 | +| Name | Description | |
| 160 | +|------|-------------| |
| 161 | +| `service_account_email` | Email of the created service account | |
| 162 | +| `workload_identity_provider` | Full resource name of the Workload Identity provider | |
| 163 | +| `terraform_state_bucket` | The GCS bucket name used for Terraform state (computed or provided) | |
| 164 | +| `service_name` | The unique identifier for this Terraform configuration and environment | |
| 165 | +| `target_projects` | Map of target projects configured | |
| 166 | + |
| 167 | +## GitHub Actions Configuration |
| 168 | + |
| 169 | +After applying this module, configure your GitHub Actions workflow to manage Terraform in CI: |
| 170 | + |
| 171 | +```yaml |
| 172 | +permissions: |
| 173 | + contents: read |
| 174 | + id-token: write |
| 175 | + |
| 176 | +jobs: |
| 177 | + deploy: |
| 178 | + runs-on: ubuntu-latest |
| 179 | + steps: |
| 180 | + - uses: actions/checkout@v4 |
| 181 | + |
| 182 | + - name: Authenticate to Google Cloud |
| 183 | + uses: google-github-actions/auth@v2 |
| 184 | + with: |
| 185 | + workload_identity_provider: ${{ outputs.workload_identity_provider }} |
| 186 | + service_account: ${{ outputs.service_account_email }} |
| 187 | + |
| 188 | + - name: Set up Cloud SDK |
| 189 | + uses: google-github-actions/setup-gcloud@v2 |
| 190 | +``` |
| 191 | +
|
| 192 | +## Security Features |
| 193 | +
|
| 194 | +- **No Service Account Keys**: Uses Workload Identity Federation for keyless auth |
| 195 | +- **Repository Scoped**: Access restricted to specified GitHub repository |
| 196 | +- **Least Privilege**: Only grants permissions for enabled services in target projects |
| 197 | +- **Secret Scoping**: Fine-grained access to specific secrets only |
| 198 | +- **Centralized Management**: All CI infrastructure managed in khan-internal-services project |
| 199 | +
|
| 200 | +## Examples |
| 201 | +
|
| 202 | +### Single Project Terraform Configuration (Using Default State Bucket) |
| 203 | +```hcl |
| 204 | +# CI for culture-cron production Terraform configuration |
| 205 | +module "culture_cron_prod_ci" { |
| 206 | + source = "git::https://github.com/Khan/terraform-modules.git//terraform/modules/github-ci-bootstrap?ref=v1.0.0" |
| 207 | + |
| 208 | + service_name = "culture-cron-prod" |
| 209 | + github_repository = "Khan/culture-cron" |
| 210 | + |
| 211 | + # This Terraform config deploys resources to khan-academy project |
| 212 | + target_projects = { |
| 213 | + "khan-academy" = { |
| 214 | + required_services = ["cloudfunctions", "storage", "pubsub", "scheduler"] |
| 215 | + } |
| 216 | + } |
| 217 | + |
| 218 | + # Terraform state bucket defaults to: terraform-khan-culture-cron-culture-cron-prod |
| 219 | +} |
| 220 | +``` |
| 221 | + |
| 222 | +### Multi-Project Terraform Configuration |
| 223 | +```hcl |
| 224 | +# CI for webapp staging Terraform configuration that deploys across multiple projects |
| 225 | +module "webapp_staging_ci" { |
| 226 | + source = "git::https://github.com/Khan/terraform-modules.git//terraform/modules/github-ci-bootstrap?ref=v1.0.0" |
| 227 | +
|
| 228 | + service_name = "webapp-staging" |
| 229 | + github_repository = "Khan/webapp" |
| 230 | + |
| 231 | + # This Terraform config deploys resources to multiple projects |
| 232 | + target_projects = { |
| 233 | + "khan-academy-staging" = { |
| 234 | + required_services = ["storage", "pubsub"] |
| 235 | + } |
| 236 | + "khan-shared-services" = { |
| 237 | + required_services = ["storage"] |
| 238 | + } |
| 239 | + } |
| 240 | + |
| 241 | + # Terraform state bucket defaults to: terraform-khan-webapp-webapp-staging |
| 242 | +} |
| 243 | +``` |
| 244 | + |
| 245 | +### Terraform Configuration with Secrets Access (Custom State Bucket) |
| 246 | +```hcl |
| 247 | +# CI for API production Terraform configuration that needs access to secrets |
| 248 | +module "api_prod_ci" { |
| 249 | + source = "git::https://github.com/Khan/terraform-modules.git//terraform/modules/github-ci-bootstrap?ref=v1.0.0" |
| 250 | +
|
| 251 | + service_name = "api-prod" |
| 252 | + github_repository = "Khan/api" |
| 253 | + |
| 254 | + target_projects = { |
| 255 | + "khan-academy" = { |
| 256 | + required_services = ["cloudfunctions", "storage"] |
| 257 | + } |
| 258 | + } |
| 259 | + |
| 260 | + # Use custom state bucket instead of default (terraform-khan-api-api-prod) |
| 261 | + terraform_state_bucket = "shared-terraform-state" |
| 262 | + |
| 263 | + # Secrets that the Terraform configuration needs access to |
| 264 | + secret_ids = [ |
| 265 | + "projects/khan-academy/secrets/api-key", |
| 266 | + "projects/khan-academy/secrets/database-url" |
| 267 | + ] |
| 268 | +} |
| 269 | +``` |
| 270 | + |
| 271 | +### Terraform Configuration with Storage-Only Access |
| 272 | +```hcl |
| 273 | +# CI for static site Terraform configuration that only manages storage buckets |
| 274 | +module "static_site_prod_ci" { |
| 275 | + source = "git::https://github.com/Khan/terraform-modules.git//terraform/modules/github-ci-bootstrap?ref=v1.0.0" |
| 276 | +
|
| 277 | + service_name = "static-site-prod" |
| 278 | + github_repository = "Khan/static-site" |
| 279 | + |
| 280 | + # This Terraform config only creates storage buckets |
| 281 | + target_projects = { |
| 282 | + "khan-academy" = { |
| 283 | + required_services = ["storage"] |
| 284 | + } |
| 285 | + } |
| 286 | + |
| 287 | + # Terraform state bucket defaults to: terraform-khan-static-site-static-site-prod |
| 288 | +} |
| 289 | +``` |
0 commit comments