terraform-aws-github-backup¶
A Terraform module that backs up all repositories in a GitHub organization to S3 using an ECS Fargate scheduled task.
Overview¶
This module deploys a scheduled ECS Fargate task that:
- Authenticates to GitHub using a customer-owned GitHub App
- Lists all repositories accessible to the App installation
- Creates a
git bundle(full mirror) of each repository - Uploads the bundles to a versioned S3 bucket
- Writes a manifest and publishes CloudWatch metrics
Architecture¶
EventBridge Schedule (e.g. daily 2am)
|
v
ECS Fargate Task
1. Read GitHub App private key from Secrets Manager
2. Generate JWT, exchange for installation token
3. List all repos via GitHub API
4. git clone --mirror each repo
5. git bundle create
6. Upload bundles to S3
7. Report success/failure metrics to CloudWatch
S3 Bucket (versioned, lifecycle policies)
github-backup/
2026-02-10/
manifest.json
org-name/
repo-a.bundle
repo-b.bundle
S3 Replica Bucket (cross-region)
Replicates all objects for disaster recovery
Getting Started¶
Prerequisites¶
- Terraform >= 1.5
- AWS provider >= 6.0
- A GitHub App installed on your organization with read-only repository access
1. Create a GitHub App¶
- Go to your GitHub organization Settings > Developer settings > GitHub Apps > New GitHub App
- Set the following permissions:
- Repository permissions > Contents: Read-only
- Repository permissions > Metadata: Read-only
- Install the App on your organization
- Note the App ID (from App settings) and Installation ID (from the installation URL)
- Generate a private key (PEM file) and save it securely
2. Deploy the Module¶
module "github_backup" {
source = "registry.infrahouse.com/infrahouse/github-backup/aws"
version = "~> 1.0"
github_app_id = "123456"
github_app_installation_id = "78901234"
alarm_emails = ["devops@example.com"]
github_app_key_secret_writers = [aws_iam_role.deployer.arn]
replica_region = "us-east-1"
subnets = ["subnet-abc123", "subnet-def456"]
}
3. Store the App Private Key¶
The module creates a Secrets Manager secret for the GitHub App private key. After deployment, write the PEM key into the secret:
aws secretsmanager put-secret-value \
--secret-id "$(terraform output -raw github_app_key_secret_arn)" \
--secret-string file://github-app.pem
The role(s) specified in github_app_key_secret_writers must be used for this operation. The Terraform caller role automatically gets admin access to the secret.
Configuration¶
Required Variables¶
| Variable | Type | Description |
|---|---|---|
github_app_id | string | The GitHub App ID. Found in the App's settings page. |
github_app_installation_id | string | The installation ID of the GitHub App on the target organization. |
alarm_emails | list(string) | Email addresses to receive CloudWatch alarm notifications. At least one required. |
replica_region | string | AWS region for cross-region backup replication. |
github_app_key_secret_writers | list(string) | IAM role ARNs allowed to write the GitHub App private key into the secret. At least one required. |
subnets | list(string) | Subnet IDs for the Fargate task. Must have internet access. |
Optional Variables¶
| Variable | Type | Default | Description |
|---|---|---|---|
environment | string | "development" | Name of environment. Lowercase, numbers, and underscores only. |
service_name | string | "github-backup" | Descriptive name for the service. Used for naming resources. |
schedule_expression | string | "rate(1 day)" | EventBridge schedule expression for backup frequency. |
backup_retention_days | number | 365 | Days to retain backups in S3. Set to 0 to disable expiration. |
image_uri | string | "public.ecr.aws/infrahouse/github-backup:latest" | Docker image URI for the backup runner. |
s3_bucket_name | string | null | Custom name for the S3 backup bucket. Auto-generated if null. |
force_destroy | bool | false | Allow destroying S3 buckets with objects. For testing only. |
tags | map(string) | {} | Tags to apply to all resources. |
Outputs¶
| Output | Description |
|---|---|
s3_bucket_name | Name of the S3 bucket where backups are stored. |
s3_bucket_arn | ARN of the S3 bucket where backups are stored. |
github_app_key_secret_arn | ARN of the Secrets Manager secret for the GitHub App private key. |
ecs_cluster_arn | ARN of the ECS cluster. |
ecs_cluster_name | Name of the ECS cluster. |
task_definition_arn | ARN of the ECS task definition. |
task_role_arn | ARN of the IAM role used by the backup task. |
log_group_name | Name of the CloudWatch log group. |
schedule_rule_arn | ARN of the EventBridge schedule rule. |
security_group_id | ID of the security group for the Fargate task. |
Key Design Decisions¶
- Fargate -- no always-on compute, no Lambda timeout limits
- Customer-owned GitHub App -- no shared credentials, short-lived tokens
- InfraHouse-published container image on public ECR
- S3 with versioning and lifecycle policies for retention
- Cross-region replication for disaster recovery
- AWS provider v6 -- per-resource
regionattribute, no aliased providers - Module-managed secret -- the module creates and manages the Secrets Manager secret with a fine-grained resource policy (admin/writers/readers separation)
Disaster Recovery¶
RPO and RTO¶
| Metric | Value | Notes |
|---|---|---|
| RPO (Recovery Point Objective) | Up to the schedule interval (default: 24h) | Determined by schedule_expression. Worst case is one full interval of data loss. |
| RTO (Recovery Time Objective) | Minutes per repository | Restoring a single repo from a bundle takes seconds. Full org restore depends on the number and size of repositories. |
Backup Storage¶
Backups are stored in two locations:
- Primary bucket -- in the region where the module is deployed
- Replica bucket -- in
replica_region, automatically synchronized via S3 cross-region replication
Both buckets are versioned, so even if a backup is overwritten or deleted, previous versions are retained according to the lifecycle policy.
Restore Procedures¶
Git bundles are portable and self-contained. Each bundle is a full mirror of the repository at the time of backup, including all branches, tags, and history.
Restore a single repository¶
# Download the bundle from S3
aws s3 cp s3://BUCKET/github-backup/2026-02-10/org-name/repo.bundle repo.bundle
# Verify the bundle is valid
git bundle verify repo.bundle
# Clone from the bundle
git clone repo.bundle repo-restored
# Point the restored repo back to GitHub
cd repo-restored
git remote set-url origin git@github.com:org-name/repo.git
git push --mirror origin
Restore from the replica region¶
If the primary region is unavailable:
# Download from the replica bucket
aws s3 cp s3://BUCKET-replica/github-backup/2026-02-10/org-name/repo.bundle repo.bundle \
--region REPLICA_REGION
# Then follow the same restore steps above
Restore all repositories from a specific date¶
# List available backup dates
aws s3 ls s3://BUCKET/github-backup/
# Download all bundles for a specific date
aws s3 cp s3://BUCKET/github-backup/2026-02-10/ ./restore/ --recursive
# Check the manifest for details
cat restore/manifest.json
# Restore each bundle
for bundle in restore/org-name/*.bundle; do
repo_name=$(basename "$bundle" .bundle)
git clone "$bundle" "restored/$repo_name"
done
Add a bundle as a remote to an existing repo¶
Failure Scenarios¶
| Scenario | Detection | Recovery |
|---|---|---|
| Single repo fails to back up | backup-failure CloudWatch alarm | Check logs, re-run task manually |
| Task does not run | task-not-running CloudWatch alarm | Check EventBridge rule and ECS cluster |
| Primary bucket unavailable | AWS region outage | Restore from replica bucket |
| GitHub App key compromised | Revoke in GitHub App settings | Generate new key, update the Secrets Manager secret |
| Backup corruption | Future: verification task (#21) | Restore from a previous day's backup (S3 versioning) |
Requirements¶
| Name | Version |
|---|---|
| terraform | ~> 1.5 |
| aws | ~> 6.0 |