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@v5
- uses: google-github-actions/auth@v3
with:
workload_identity_provider: ${{ vars.GCP_AUTH_PROVIDER }}
service_account: ${{ vars.GCP_SERVICE_ACCOUNT }}
- uses: aws-actions/configure-aws-credentials@v6
if: vars.AWS_REGION != ''
with:
role-to-assume: ${{ secrets.AWS_GITHUB_ACTIONS_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
- uses: udx/github-rabbit-action@v1
with:
project_id: ${{ vars.GCP_PROJECT_ID }}
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. Resolve Lifecycle │
│ In-repo branch/env │
│ policy and protection │
└─────────────┬──────────────┘
│
┌─────────────▼──────────────┐
│ 2. Merge Configs │
│ Discover .rabbit/ YAML │
│ Deep merge by module::id │
└─────────────┬──────────────┘
│
┌─────────────▼──────────────┐
│ 3. Safety Checks │
│ Block production manual │
│ Block production destroy │
│ Auto plan-only for PRs │
└─────────────┬──────────────┘
│
┌─────────────▼──────────────┐
│ 4. Terraform Engine │
│ Caller credentials │
│ passed into R2A │
└─────────────┬──────────────┘
│
┌─────────────▼──────────────┐
│ 5. Reporting │
│ Plan summary table │
│ PR comment │
│ GitHub step summary │
│ Slack notification │
└────────────────────────────┘
The environment is automatically resolved from the workflow event, then resolved against this action's lifecycle policy:
| 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 the configured
source_dirare ignored (must be in a lifecycle directory) - Only direct lifecycle roots under
source_dirare eligible; usesource_dir: .rabbit/infra_configsfor nested config roots
See docs/configuration.md for the repo-owned Rabbit config layout and merge contract.
| 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
The module library is the current configuration reference for every R2A module, including its schema, prerequisites, examples, and outputs.
Services deploy in ascending module order; destroy operations use the reverse order.
Use the maintained @v1 major tag in caller workflows. Patch releases are
published as immutable v1.x.y GitHub releases, then v1 moves to the tested
compatible release. See the release guide and
changelog.
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
The caller workflow owns cloud authentication. Authenticate with Google Cloud before invoking the action; the action mounts the resulting GOOGLE_APPLICATION_CREDENTIALS file read-only into the R2A container and never copies it into the workspace. Configure AWS credentials in the caller when the configuration uses AWS; the action forwards the resulting AWS session variables to R2A.
The optional state-backend inputs are passed through to the IaC engine. Omit them to retain its existing GCS default; provide the backend type, configuration, and state-path key only when the selected backend requires an override.
| Input | Required | Default | Description |
|---|---|---|---|
project_id |
✅ | — | Project identifier for state and resource operations |
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 |
state_backend |
— | container default (GCS) | Optional backend override, such as s3, azurerm, http, or consul |
state_backend_config |
— | — | Backend configuration as key=value lines |
state_prefix_key |
— | — | Backend configuration key for the state path, such as prefix or key |
source_dir |
— | .rabbit |
Config source directory |
lifecycle_policy_path |
— | bundled policy | Optional caller-repository lifecycle policy shared by resolution and config merging |
github_token |
— | github.token |
GitHub token passed to lifecycle resolution and used for PR comments |
| Output | Description |
|---|---|
environment |
Resolved environment name |
lifecycle |
Resolved lifecycle (production/staging/development) |
is_protected |
Whether GitHub reported the environment branch as protected |
resolution_reason |
Lifecycle rule that selected the lifecycle |
lifecycle_policy_path |
Lifecycle policy used for resolution and config merging |
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 - Set
source_direxplicitly when configs live below.rabbit/infra_configsor another nested root - Schedule nightly runs (
cron: "0 2 * * *") to detect infrastructure drift - Keep
.rabbit/configs small and focused — one concern per file
The local validation contract is documented in docs/validation.md. Run make test and rabbit.ci before updating a PR.
GPL-2.0