Declare cloud infrastructure in YAML. Deploy with git push.
A GitHub Marketplace composite action that discovers YAML configuration from your .rabbit/ directory and deploys cloud infrastructure across AWS, GCP, and Kubernetes using Terraform — all from a single uses: step.
Create .github/workflows/infra-build.yaml in your repository:
name: Infrastructure Buildon:
pull_request:
branches: ["production", "staging", "develop-*"]push:
branches: ["production", "staging", "develop-*"]paths: [".rabbit/**"]delete:
schedule:
- cron: "0 2 * * *"workflow_dispatch:
inputs:
plan_only:
description: "Plan only (no apply)"type: booleandefault: trueenvironment:
description: "Target environment"type: choiceoptions: [development, staging, production]default: developmentterraform_action:
description: "Terraform action"type: choiceoptions: [apply, destroy]default: applypermissions:
contents: readpull-requests: writeid-token: writejobs:
deploy:
runs-on: ubuntu-lateststeps:
- uses: actions/checkout@v4
- uses: udx/github-rabbit-action@v1with:
project_id: ${{ vars.GCP_PROJECT_ID }}gcp_auth_provider: ${{ vars.GCP_AUTH_PROVIDER }}gcp_service_account: ${{ vars.GCP_SERVICE_ACCOUNT }}aws_region: ${{ vars.AWS_REGION }}aws_role_arn: ${{ secrets.AWS_GITHUB_ACTIONS_ROLE_ARN }}slack_webhook: ${{ secrets.SLACK_WEBHOOK_ROUTINE }}dockerhub_username: ${{ vars.DOCKERHUB_USER_LOGIN }}dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN_PULL_R2A }}r2a_version: "4.8.0"terraform_action: ${{ inputs.terraform_action || 'apply' }}Create YAML files inside .rabbit/<environment>/:
services:
- module: aws-route53id: my-domaindomain: example.comrecords:
- type: Aname: ""alias:
name: d1234.cloudfront.netzone_id: Z2FDTNDATAQYW2services:
- module: aws-cloudfront-distributionid: my-cdn-#{Environment}domain: example.comorigins:
- domain_name: my-app.example.comorigin_id: app-originservices:
- module: k8s-namespaceid: my-app
- module: k8s-deploymentid: my-appimage: my-org/my-app:latestreplicas: 2ports:
- containerPort: 8080
- module: k8s-http-gateway-routeid: my-apphostname: my-app.example.comservice_port: 8080- Open a PR → automatic plan preview posted as PR comment
- Merge to production → infrastructure applied automatically
- Delete a branch → ephemeral environment destroyed
┌─────────────────────────────────────────────────────┐
│ GitHub Action Trigger (push / PR / delete / manual) │
└──────────────────────┬──────────────────────────────┘
│
┌─────────────▼──────────────┐
│ 1. Merge Configs │
│ Discover .rabbit/ YAML │
│ Resolve lifecycle │
│ Deep merge by module::id │
└─────────────┬──────────────┘
│
┌─────────────▼──────────────┐
│ 2. Safety Checks │
│ Block production manual │
│ Block production destroy │
│ Auto plan-only for PRs │
└─────────────┬──────────────┘
│
┌─────────────▼──────────────┐
│ 3. Cloud Auth │
│ GCP Workload Identity │
│ AWS OIDC (optional) │
└─────────────┬──────────────┘
│
┌─────────────▼──────────────┐
│ 4. Terraform Engine │
│ Docker: r2a container │
│ Per-service init/plan/ │
│ apply in deploy order │
└─────────────┬──────────────┘
│
┌─────────────▼──────────────┐
│ 5. Reporting │
│ Plan summary table │
│ PR comment │
│ GitHub step summary │
│ Slack notification │
└────────────────────────────┘
The environment is automatically resolved from:
| Trigger | Environment Source |
|---|---|
push | Branch name (production, staging, develop-foo) |
pull_request | Base branch (github.base_ref) |
delete | Deleted branch ref |
workflow_dispatch | User-selected input |
schedule | Branch name (default branch) |
Infrastructure configs live in .rabbit/ directories organized by lifecycle:
.rabbit/
├── production/ # Protected branches → production lifecycle
│ ├── 10-infra.yaml
│ ├── 20-app.yaml
│ └── us-east-1/ # Environment-specific overrides (smart merged)
│ └── 10-infra.yaml
├── staging/ # Root-only (no subdirectories)
│ └── infra.yaml
└── development/ # Fallback lifecycle
├── infra.yaml
└── dev-andy/ # Per-developer environments
└── infra.yaml
- Files are sorted by name (
10-infra.yamlbefore20-monitoring.yaml) - Services with the same
module::idare deep-merged across files - Root-level files in
.rabbit/are ignored (must be in a lifecycle directory)
| Trigger | Mode |
|---|---|
pull_request | Plan only (preview changes) |
schedule | Plan only (drift detection) |
push | Apply (deploy changes) |
workflow_dispatch | User choice |
delete | Destroy (remove environment) |
- Manual production apply blocked — production changes must go through the merge pipeline
- Production destroy blocked — production infrastructure cannot be destroyed
- Delete environment mismatch — aborts if branch name doesn't match resolved environment
- PR always plans — pull requests never apply changes
| Module | Description | Order |
|---|---|---|
aws-route53 | DNS zones and records | 5 |
aws-acm | SSL/TLS certificates | 8 |
aws-waf | Web Application Firewall rules | 125 |
aws-cloudfront-distribution | CDN distribution with origins, behaviors, cache | 130 |
| Module | Description | Order |
|---|---|---|
gcp-networking | VPC networks and firewall rules | 10 |
gcp-static-ip | Regional/global static IP addresses | 15 |
gcp-postgresql-instance | Cloud SQL PostgreSQL instances | 20 |
gcp-sql-instance | Cloud SQL MySQL instances | 20 |
gcp-gke-cluster | GKE cluster provisioning | 30 |
gcp-gke-nodepool | GKE node pool configuration | 40 |
gcp-iam | IAM roles and service accounts | — |
gcp-secret-manager | Secret Manager entries | — |
gcp-storage | Cloud Storage buckets | — |
gcp-monitoring | Monitoring alert policies | 140 |
| Module | Description | Order |
|---|---|---|
k8s-shared-http-gateway | Shared HTTP gateway for routing | 55 |
k8s-namespace | Namespace with labels and annotations | 60 |
k8s-secret | Kubernetes secrets from config or GCP Secret Manager | 70 |
k8s-access | RBAC roles and bindings | 80 |
k8s-service | ClusterIP/LoadBalancer/NodePort services | 90 |
k8s-http-health-check-policy | Health check policies for gateway routes | 92 |
k8s-http-gateway-route | HTTP routing rules for gateway | 93 |
k8s-configmap | ConfigMaps from inline data or files | 95 |
k8s-deployment | Deployments with rolling updates | 100 |
k8s-memcached | Memcached StatefulSet | 102 |
k8s-hpa | Horizontal Pod Autoscaler | 110 |
k8s-pdb | Pod Disruption Budget | 120 |
| Module | Description | Order |
|---|---|---|
newrelic-synthetic-monitors | New Relic synthetic monitoring | 150 |
Deployment Order — services are deployed in ascending order by their module's deployment order. Destroy operations reverse the order.
Each service in your YAML config follows this structure:
services:
- module: <module-name> # Required: Terraform module to useid: <unique-id> # Required: Unique identifier for this service# ... module-specific fieldsUse #{Variable} syntax in YAML values — they're replaced at runtime:
| Placeholder | Value |
|---|---|
#{Environment} | Resolved environment name |
#{Lifecycle} | Resolved lifecycle (production/staging/development) |
#{GcpProject} | GCP project ID |
#{GitOwner} | GitHub repository owner |
#{GitRepository} | GitHub repository name |
#{Namespace} | Kubernetes namespace (derived from repo name) |
#{SharedProject} | Shared GCP project ID |
Example:
services:
- module: k8s-deploymentid: my-app-#{Environment}namespace: #{Namespace}image: gcr.io/#{GcpProject}/my-app:latestReference secrets directly in your YAML:
services:
- module: k8s-secretid: app-secretsdata:
DATABASE_URL: gcp://projects/my-project/secrets/db-url/versions/latestThe action automatically resolves gcp:// prefixed values to actual secret values at deploy time.
Set these in your repository or organization settings → Variables:
| Variable | Description |
|---|---|
GCP_PROJECT_ID | Google Cloud project ID |
GCP_AUTH_PROVIDER | GCP Workload Identity Provider resource name |
GCP_SERVICE_ACCOUNT | GCP Service Account email |
| Variable | Description |
|---|---|
AWS_REGION | AWS region (e.g., us-east-1) |
DOCKERHUB_USER_LOGIN | Docker Hub username for image pulls |
K8S_CLUSTER_NAME | GKE cluster name |
NEWRELIC_ACCOUNT_ID | New Relic account ID |
SHARED_PROJECT | Shared GCP project for cross-project access |
| Secret | Description |
|---|---|
SLACK_WEBHOOK_ROUTINE | Slack incoming webhook for notifications |
| Secret | Description |
|---|---|
AWS_GITHUB_ACTIONS_ROLE_ARN | AWS IAM OIDC role for Route53/CloudFront/WAF/ACM |
DOCKERHUB_TOKEN_PULL_R2A | Docker Hub token (paired with DOCKERHUB_USER_LOGIN) |
DOCKERHUB_HELM_TOKEN | Docker Hub token for Helm OCI charts |
NEWRELIC_API_KEY | New Relic API key |
permissions:
contents: readpull-requests: write # For PR comments with plan summaryid-token: write # For GCP Workload Identity & AWS OIDCOverride specific values per environment using subdirectories:
.rabbit/
└── production/
├── 10-infra.yaml # Base production config
└── us-east-1/
└── 10-infra.yaml # Overrides for us-east-1 environment
Files in subdirectories are deep-merged on top of the parent directory files. Services with matching module::id pairs are merged, not duplicated.
For monorepo or multi-repo setups where multiple repositories share infrastructure:
- uses: udx/github-rabbit-action@v1with:
multi_repo: "true"shared_project: "shared-infra-project"# ... other inputsThis isolates Terraform state per repository while allowing shared GCP project access.
Always pin to a specific version for reproducible builds:
- uses: udx/github-rabbit-action@v1with:
r2a_version: "4.8.0"Add delete to your workflow triggers and matching feature branches:
on:
delete: # Triggers destroy when branch is deletedpush:
branches: ["production", "staging", "develop-*"]When a develop-* branch is deleted, the action automatically runs terraform destroy for that environment.
Enable detailed config output in logs:
- uses: udx/github-rabbit-action@v1with:
print_config: "true"The workflow dispatch inputs provide safe manual control:
- Plan only = true → preview changes without applying
- Environment = production + Plan only = false → blocked (safety guardrail)
- Terraform action = destroy + Environment = production → blocked
| Input | Required | Default | Description |
|---|---|---|---|
project_id | ✅ | — | GCP project ID |
gcp_auth_provider | ✅ | — | GCP Workload Identity Provider |
gcp_service_account | ✅ | — | GCP Service Account email |
aws_role_arn | — | — | AWS IAM OIDC role ARN |
aws_region | — | — | AWS region |
dockerhub_username | — | — | Docker Hub username |
dockerhub_token | — | — | Docker Hub pull token |
dockerhub_helm_token | — | — | Docker Hub Helm OCI token |
r2a_version | — | latest | R2A Docker image tag |
terraform_action | — | apply | apply or destroy |
plan_only | — | auto | Override plan mode |
environment | — | auto | Override environment |
print_config | — | true | Debug config output |
multi_repo | — | false | Per-repo state isolation |
shared_project | — | — | Shared GCP project |
k8s_cluster_name | — | — | GKE cluster name |
newrelic_account_id | — | — | New Relic account ID |
newrelic_api_key | — | — | New Relic API key |
slack_webhook | — | — | Slack webhook URL |
source_dir | — | .rabbit | Config source directory |
github_token | — | github.token | GitHub token for PR comments |
| Output | Description |
|---|---|
environment | Resolved environment name |
lifecycle | Resolved lifecycle (production/staging/development) |
plan_only | Whether run was plan-only |
terraform_action | Action executed (apply/destroy/skip) |
has_changes | Whether Terraform detected changes |
changes_msg | Human-readable change summary |
cloudfront_distribution_id | CloudFront distribution ID (if applicable) |
k8s_namespace | Kubernetes namespace |
config_path | Path to merged config file |
Every run writes a configuration summary and deployment results to the GitHub Actions step summary — visible directly on the Actions run page.
Pull requests get an automatically updated comment with a detailed Terraform plan breakdown:
## Terraform Plan Summary
| Module / Service | Add | Change | Destroy |
| --- | --- | --- | --- |
| **Total** | 5 | 2 | 0 |
| `aws-cloudfront-distribution/my-cdn` | 0 | 1 | 0 |
| `k8s-deployment/my-app` | 3 | 1 | 0 |
| `k8s-http-gateway-route/my-app` | 2 | 0 | 0 |
Notifications are sent when:
- Infrastructure changes are detected or applied
- Any step fails
Notifications include environment, change counts, failure stage, and a link to the action run.
- Use a Docker Hub token (
dockerhub_username+dockerhub_token) to prevent rate limits when pulling the R2A image - Pin
r2a_versionto a specific tag for reproducible deploys (e.g.,4.8.0instead oflatest) - Name files with numeric prefixes (
10-dns.yaml,20-cdn.yaml,30-app.yaml) for deterministic ordering - Use
#{Environment}placeholders in service IDs to keep configs environment-aware - Schedule nightly runs (
cron: "0 2 * * *") to detect infrastructure drift - Keep
.rabbit/configs small and focused — one concern per file
GPL-2.0