1
0
Fork 0
mirror of https://github.com/imjasonh/terraform-playground synced 2026-07-18 15:08:24 +00:00
terraform-playground/driftlessaf/summary.md
Jason Hall a0b5d1f369 vars
Signed-off-by: Jason Hall <jason@chainguard.dev>
2026-02-03 13:43:18 -05:00

344 lines
15 KiB
Markdown

# Driftlessaf Infrastructure Deployment Summary
This document summarizes the deployment of a GitHub PR reconciler using Chainguard's terraform-infra-common modules.
## Architecture
The system receives GitHub webhook events, routes them through a CloudEvents broker, and processes PR events in a reconciler service that logs PR details.
```
GitHub Webhooks → github-events → cloudevent-broker → cloudevents-workqueue → pr-reconciler
```
## Components Deployed
- **Networking**: VPC with regional subnets in us-central1 and us-east4
- **CloudEvent Broker**: Regional Pub/Sub topics for event routing
- **GitHub Events**: Webhook receiver that converts GitHub webhooks to CloudEvents with extensions like `pullrequesturl`
- **CloudEvents Workqueue**: Subscribes to all events with `pullrequesturl` extension (pull_request, issue_comment on PRs, PR reviews) and enqueues them for processing
- **PR Reconciler**: Regional Go gRPC service that processes PR events via workqueue, logging PR details including title, state, labels, reviews, status checks, changed files, and latest comments
- **CloudEvent Recorder**: Records all GitHub events to GCS (via Pub/Sub cloud_storage_config) and imports to BigQuery via Data Transfer Service every 15 minutes. Supports 10 event types: check_run, check_suite, issue_comment, issues, projects_v2_item, pull_request, pull_request_review, pull_request_review_comment, push, workflow_run
## Issues Encountered and Resolutions
### 1. Module Source Confusion
**Problem**: The original plan referenced `driftlessaf/reconcilers/infra//modules/regional-go-reconciler` which doesn't exist.
**Resolution**: Used the correct source `chainguard-dev/common/infra//modules/regional-go-reconciler`.
### 2. Missing Required Variables
**Problem**: Several modules require a `team` variable that wasn't documented in the plan.
**Resolution**: Added `team` variable to all modules (cloudevent-broker, github-events, regional-go-reconciler, cloudevent-trigger).
### 3. Trigger Output Mismatch
**Problem**: Tried to use `module.pr-reconciler.names[each.key]` but the regional-go-reconciler module doesn't expose a `names` output.
**Resolution**: Used `module.pr-reconciler.receiver.name` which is the workqueue receiver that triggers should connect to.
### 4. Secret Volume Mount Not Supported
**Problem**: The `regional-go-reconciler` module's `volumes` variable only supports `empty_dir` and `csi` types, not `secret` volumes. The secret mount at `/secrets/github/private-key.pem` was empty.
**Resolution**: Changed from volume mount to environment variable with `secret_key_ref`:
```hcl
env = [
{
name = "GITHUB_PRIVATE_KEY"
value_source = {
secret_key_ref = {
secret = google_secret_manager_secret.github_app_key.secret_id
version = "latest"
}
}
},
]
```
Updated Go code to read `GITHUB_PRIVATE_KEY` from environment instead of file.
### 5. Private DNS Zone Logging
**Problem**:
```
Error: Invalid value for 'Logging may only be enabled for PUBLIC ManageZones.
```
The networking module tried to enable logging on private DNS zones, which GCP doesn't support.
**Resolution**: Added `hosted_zone_logging_enabled = false` to the networking module.
### 6. Pre-existing GCP Resources
**Problem**: Multiple resources already existed from previous deployments:
- Cloud Run services named `driftlessaf`
- DNS policy `dns-logging-policy`
- DNS managed zones
- Serverless VPC addresses
**Resolution**:
- Deleted existing Cloud Run services: `gcloud run services delete driftlessaf --region=<region>`
- Imported existing DNS resources: `terraform import 'module.networking.google_dns_policy.dns_logging_policy' jason-chainguard/dns-logging-policy`
- Removed stuck subnets from state: `terraform state rm 'module.networking.google_compute_subnetwork.regional["us-central1"]'`
### 7. Orphaned Serverless Addresses
**Problem**: Subnets couldn't be modified because they were held by serverless IP addresses from deleted Cloud Run services.
**Resolution**: Changed the networking module name from `driftlessaf` to `driftlessaf-networking` to create new subnets with different names, avoiding the locked subnets.
### 8. Deletion Protection
**Problem**: Couldn't destroy/recreate resources due to deletion protection.
**Resolution**: Added `deletion_protection = var.deletion_protection` to all modules:
- `broker.tf`
- `github-events.tf`
- `reconciler.tf`
### 9. Multi-Installation Support
**Problem**: Original design hardcoded a single `github_installation_id`, limiting the app to one org.
**Resolution**: Refactored Go code to:
1. Authenticate as the GitHub App using `ghinstallation.NewAppsTransport`
2. Dynamically look up installations per repo using `Apps.FindRepositoryInstallation(owner, repo)`
3. Cache GitHub clients per installation ID
This allows the same GitHub App to be installed on multiple organizations.
### 10. github_organizations Filter Doesn't Work for User Repos
**Problem**: The `github_organizations` filter in the github-events module only works for organization-owned repositories. For user-owned repos (like `imjasonh/terraform-playground`), the Organization field in GitHub events is empty, so the filter rejects them with "ignoring event from repository '' due to non-matching org".
**Resolution**: Commented out the `github_organizations` filter to allow events from all repositories where the GitHub App is installed.
### 11. CloudEvent Trigger vs CloudEvents Workqueue
**Problem**: Initially used the `cloudevent-trigger` module to route events to the workqueue receiver. This module pushes HTTP POST messages from Pub/Sub, but the workqueue receiver expects gRPC calls, resulting in 404 errors.
**Resolution**: Replaced `cloudevent-trigger` with `cloudevents-workqueue` module, which is specifically designed to:
1. Subscribe to CloudEvents from the broker
2. Extract a key from a CloudEvent extension (e.g., `pullrequesturl`)
3. Enqueue work items to the workqueue via gRPC
```hcl
module "pr-workqueue" {
source = "chainguard-dev/common/infra//modules/cloudevents-workqueue"
# Empty filter matches all events that have pullrequesturl extension
# This includes pull_request, issue_comment on PRs, pull_request_review, etc.
filters = [{}]
extension_key = "pullrequesturl"
workqueue = {
name = module.pr-reconciler.receiver.name
}
}
```
### 12. httpmetrics.ServeMetrics() Blocks
**Problem**: The gRPC server never started. Requests would arrive at Cloud Run but the Process handler was never called. After extensive debugging, discovered that `httpmetrics.ServeMetrics()` was being called directly instead of in a goroutine, causing it to block forever.
**Symptoms**:
- Startup logs appeared but "Starting gRPC server" never logged
- HTTP requests completed with 200 status after exactly 300 seconds (Cloud Run timeout)
- No logs from the Process handler
**Resolution**: Run ServeMetrics in a goroutine:
```go
// WRONG - blocks forever
httpmetrics.ServeMetrics()
// CORRECT - runs in background
go httpmetrics.ServeMetrics()
```
All example code in terraform-infra-common uses `go httpmetrics.ServeMetrics()`.
### 13. VPC Egress Blocks External API Access
**Problem**: After fixing the gRPC server startup, the reconciler could receive requests but failed to reach the GitHub API with timeout errors:
```
dial tcp 140.82.112.5:443: i/o timeout
```
**Root Cause**: The reconciler was configured with `vpc-access-egress: all-traffic`, which routes ALL outbound traffic through the VPC. The VPC's private DNS zones redirect `*.run.app` to `private.googleapis.com` for internal Cloud Run access, but there's no NAT gateway for external internet access.
**Resolution**: Set the reconciler's egress to `PRIVATE_RANGES_ONLY`:
```hcl
module "pr-reconciler" {
source = "chainguard-dev/common/infra//modules/regional-go-reconciler"
# Allow direct internet access for GitHub API calls
egress = "PRIVATE_RANGES_ONLY"
# ...
}
```
This routes only private IP traffic (10.x.x.x, 172.16.x.x, 192.168.x.x) through the VPC, while allowing direct internet access for public APIs like GitHub.
## Secrets to Populate
After `terraform apply`, populate these secrets:
```bash
# GitHub App private key
gcloud secrets versions add driftlessaf-github-app-private-key \
--project=jason-chainguard \
--data-file=path/to/private-key.pem
# Webhook secret (generate random value)
openssl rand -hex 32 | gcloud secrets versions add driftlessaf-webhook-secret \
--project=jason-chainguard \
--data-file=-
```
Configure the GitHub App's webhook settings with:
- **Webhook URL**: From `terraform output github_webhook_urls`
- **Webhook Secret**: The value generated above
- **Events**: Enable "Pull requests" (and optionally "Issues", "Issue comments")
### 14. Empty Filters Create Zero Triggers
**Problem**: When trying to allow all PR-related events (not just `pull_request`), commented out the `filters` array. But this resulted in no events being processed at all.
**Root Cause**: The `cloudevents-workqueue` module creates triggers using:
```hcl
for_each = {
for pair in setproduct(keys(var.regions), range(length(var.filters))) :
...
}
```
When `filters = []` (empty array), `range(length(var.filters)) = range(0) = []`, and `setproduct(regions, []) = []`. So **zero triggers are created**, meaning no Pub/Sub subscriptions exist to deliver events.
**Resolution**: Use an empty filter `[{}]` to match all events:
```hcl
# WRONG - creates zero triggers
filters = []
# ALSO WRONG - commented out means filters defaults to [], same problem
# filters = [{ "type" = "..." }]
# CORRECT - creates triggers that match ALL events
filters = [{}]
```
The `extension_key = "pullrequesturl"` still applies via `filter_has_attributes`, so only events with that extension are delivered.
### 15. Issue Comments on PRs Set pullrequesturl Extension
**Problem**: Initially thought PR comments (`issue_comment` events) weren't being processed because they lacked the `pullrequesturl` extension.
**Investigation**: Created a temporary Pub/Sub subscription without filters to inspect actual messages. Found that the trampoline DOES set `ce-pullrequesturl` for issue_comment events on PRs:
```json
{
"ce-type": "dev.chainguard.github.issue_comment",
"ce-pullrequesturl": "https://github.com/imjasonh/terraform-playground/pull/1"
}
```
The trampoline checks if `issue.pull_request` is non-null in the webhook payload, and if so, extracts the PR URL.
**Resolution**: Use `filters = [{}]` to accept all events with `pullrequesturl`. This includes:
- `pull_request` events
- `issue_comment` events on PRs
- `pull_request_review` events
- `pull_request_review_comment` events
### 16. CloudEvent Recorder Setup
**Goal**: Record all GitHub events to BigQuery for analytics and auditing.
**Solution**: Use the `cloudevent-recorder` module with `method = "gcs"`:
```hcl
module "github-events-recorder" {
source = "chainguard-dev/common/infra//modules/cloudevent-recorder"
project_id = var.project_id
name = "github-events-recorder"
regions = module.networking.regional-networks
broker = module.cloudevent-broker.broker
# Use GCS method - Pub/Sub writes directly to GCS buckets
method = "gcs"
# Identity applying terraform (needed for BQ DTS permissions)
provisioner = "user:your-email@example.com"
# BigQuery location and retention
location = "US"
retention-period = 30 # days
# Record all GitHub event types using schemas from github-events module
types = module.github-events.recorder-schemas
team = var.team
notification_channels = []
deletion_protection = var.deletion_protection
}
```
**Data Flow**:
1. GitHub webhook → github-events trampoline → CloudEvent broker (Pub/Sub)
2. Pub/Sub subscription with `cloud_storage_config` writes JSON to GCS (batched every 5 minutes)
3. BigQuery Data Transfer Service imports from GCS every 15 minutes
4. Data available in BigQuery dataset `cloudevents_github_events_recorder`
**BigQuery Tables Created**:
- `dev_chainguard_github_check_run`
- `dev_chainguard_github_check_suite`
- `dev_chainguard_github_issue_comment`
- `dev_chainguard_github_issues`
- `dev_chainguard_github_projects_v2_item`
- `dev_chainguard_github_pull_request`
- `dev_chainguard_github_pull_request_review`
- `dev_chainguard_github_pull_request_review_comment`
- `dev_chainguard_github_push`
- `dev_chainguard_github_workflow_run`
## Lessons Learned
1. **Read module variable definitions carefully** - especially for complex nested types like `volumes` and `containers`.
2. **Check module outputs** - don't assume output names; verify with the actual module source.
3. **Secrets as env vars vs volume mounts** - not all modules support secret volumes; env vars with `secret_key_ref` are more universally supported.
4. **GCP resource cleanup can be tricky** - serverless addresses can remain locked even after deleting the services that created them.
5. **Import existing resources** - when resources already exist, `terraform import` is often cleaner than trying to delete and recreate.
6. **Private DNS zones don't support logging** - use DNS Server Policies for logging instead.
7. **Plan for multi-tenancy early** - designing for multiple GitHub App installations from the start avoids refactoring later.
8. **CloudEvent routing is not trivial** - the `cloudevent-trigger` module pushes HTTP, while `cloudevents-workqueue` properly bridges to gRPC workqueues. Choose the right module for your use case.
9. **Always run httpmetrics.ServeMetrics() in a goroutine** - it blocks! Check example code in terraform-infra-common for the correct pattern.
10. **VPC egress settings matter** - `ALL_TRAFFIC` routes everything through the VPC, breaking external API access unless you have a NAT gateway. Use `PRIVATE_RANGES_ONLY` when your service needs to call external APIs.
11. **Add debug logging early** - when things don't work, having logs at key points (gRPC handler entry, before/after network calls) saves hours of debugging.
12. **Cloud Run request logs show latency** - if requests consistently hit the timeout (300s), something is blocking. Check if the handler is even being called.
13. **Empty filter arrays create zero resources** - in Terraform `for_each` with `setproduct(list, range(length([])))`, an empty list results in zero iterations. Use `[{}]` instead of `[]` for "match all" filters.
14. **Debug Pub/Sub by creating unfiltered subscriptions** - when events seem to disappear, create a temporary subscription without filters to inspect what's actually being published. Remember to delete it after debugging.
15. **CloudEvent extensions are set correctly for PR-related events** - the github-events trampoline sets `pullrequesturl` for `issue_comment`, `pull_request_review`, and `pull_request_review_comment` events when they're on PRs, not just for `pull_request` events.
16. **cloudevent-recorder GCS method needs IAM propagation** - when using `method = "gcs"`, the module creates IAM bindings for the Pub/Sub service account to write to GCS buckets. These may need a second `terraform apply` to propagate before subscriptions can be created.
17. **GCS Pub/Sub subscriptions batch writes** - the `cloud_storage_config` has a `max_duration` (default 5 minutes) that batches messages before writing to GCS. Don't expect immediate writes - data appears after the batch window.
18. **Set ignore_unknown_values for GitHub event recording** - the `github-events` module's schemas don't include all fields from GitHub's API. BQ DTS imports will fail with errors like "No such field: body.issue.user.id". Set `ignore_unknown_values = true` in the `cloudevent-recorder` module as documented in the github-events README.