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 Build
on:
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: boolean
default: true
environment:
description: "Target environment"
type: choice
options: [development, staging, production]
default: development
terraform_action:
description: "Terraform action"
type: choice
options: [apply, destroy]
default: apply
permissions:
contents: read
pull-requests: write
id-token: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: udx/github-rabbit-action@v1
with:
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-route53
id: my-domain
domain: example.com
records:
- type: A
name: ""
alias:
name: d1234.cloudfront.net
zone_id: Z2FDTNDATAQYW2services:
- module: aws-cloudfront-distribution
id: my-cdn-#{Environment}
domain: example.com
origins:
- domain_name: my-app.example.com
origin_id: app-originservices:
- module: k8s-namespace
id: my-app
- module: k8s-deployment
id: my-app
image: my-org/my-app:latest
replicas: 2
ports:
- containerPort: 8080
- module: k8s-http-gateway-route
id: my-app
hostname: my-app.example.com
service_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 use
id: <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-deployment
id: my-app-#{Environment}
namespace: #{Namespace}
image: gcr.io/#{GcpProject}/my-app:latestReference secrets directly in your YAML:
services:
- module: k8s-secret
id: app-secrets
data:
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: read
pull-requests: write # For PR comments with plan summary
id-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@v1
with:
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@v1
with:
r2a_version: "4.8.0"Add delete to your workflow triggers and matching feature branches:
on:
delete: # Triggers destroy when branch is deleted
push:
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@v1
with:
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