forge

Deploy a New Tenant

This checklist tells you exactly what to update to onboard a new Forge tenant.


1. Create Tenant Config Files

Copy these templates and place them at the correct paths.

Templates to Copy

Destination Paths

examples/deployments/forge-tenant/terragrunt/_global_settings/tenants/<tenant_name>.hcl

examples/deployments/forge-tenant/terragrunt/environments/<aws_account>/regions/<aws_region>/vpcs/<vpc_alias>/tenants/<tenant_name>/terragrunt.hcl

examples/deployments/forge-tenant/terragrunt/environments/<aws_account>/regions/<aws_region>/vpcs/<vpc_alias>/tenants/<tenant_name>/runner_settings.hcl

examples/deployments/forge-tenant/terragrunt/environments/<aws_account>/regions/<aws_region>/vpcs/<vpc_alias>/tenants/<tenant_name>/config.yml

Example for tenant=sbg, account=sec-plat, region=eu-west-1, vpc_alias=shared

cp examples/templates/tenant/_global_settings/tenant.hcl \
   examples/deployments/forge-tenant/terragrunt/_global_settings/tenants/sbg.hcl

mkdir -p examples/deployments/forge-tenant/terragrunt/environments/sec-plat/regions/eu-west-1/vpcs/shared/tenants/sbg

cp examples/templates/tenant/tenant/terragrunt.hcl \
   examples/deployments/forge-tenant/terragrunt/environments/sec-plat/regions/eu-west-1/vpcs/shared/tenants/sbg/terragrunt.hcl

cp examples/templates/tenant/tenant/runner_settings.hcl \
   examples/deployments/forge-tenant/terragrunt/environments/sec-plat/regions/eu-west-1/vpcs/shared/tenants/sbg/runner_settings.hcl

cp examples/templates/tenant/tenant/config.yml \
   examples/deployments/forge-tenant/terragrunt/environments/sec-plat/regions/eu-west-1/vpcs/shared/tenants/sbg/config.yml

2. Edit config.yml — Tenant Configuration Fields

Controls GitHub integration, IAM roles, EC2-wide runner settings, and runner specs (EC2 & ARC).


Top-level Structure & Key Fields

gh_config:
  ghes_url: <GITHUB_URL>              # Empty string for github.com, full GHES URL otherwise
  ghes_org: <GITHUB_ORG>              # Exact GitHub organization name
  repository_selection: <repository_selection> # Type of repository selection (all or selected)
  github_webhook_relay:               # (Optional) Forward incoming GitHub webhook events cross-account via EventBridge
    enabled: false                    # Set true to forward events to another account/region
    destination_account_id: ""       # Target AWS account ID owning the destination EventBridge bus
    destination_event_bus_name: ""    # Destination EventBridge bus name (blank => default bus when supported)
    destination_region: ""            # Destination AWS region for the forwarding rule
    destination_reader_role_arn: ""   # IAM role in destination allowed to read forwarded events (leave blank if not needed)
    # NOTE: Leave all destination_* fields blank when enabled=false; they are ignored.
  github_app:
    id: <GITHUB_APP_ID>                          # Numeric GitHub App ID from GitHub settings
    client_id: <GITHUB_APP_CLIENT_ID>            # OAuth client ID shown on the GitHub App page
    installation_id: <GITHUB_APP_INSTALLATION_ID> # Installation ID after installing the app in your org/account
    name: <GITHUB_APP_NAME>                      # Exact GitHub App name (must match the created app)

tenant:
  iam_roles_to_assume:                # List of full AWS IAM role ARNs runners may assume for workloads
    - arn:aws:iam::<ACCOUNT_ID>:role/<ROLE_NAME>
  ecr_registries:                     # Allowed ECR repo URLs (full), e.g. 123456789012.dkr.ecr.us-east-1.amazonaws.com
    - <ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com
  github_logs_reader_role_arns:       # (Optional) IAM role ARNs granted read (+ KMS decrypt) access to archived GitHub job/workflow logs
    - arn:aws:iam::<ACCOUNT_ID>:role/<ROLE_NAME>

ec2_config:
  enable_dynamic_labels: <true|false>  # Enable dynamic ghr-* labels for EC2 runners

ec2_runner_specs:
  <runner_type-alias>:               # e.g. small, medium, gpu, mac
    type: <runner_type>              # Runner type label advertised to GitHub
    ami_name: <AMI_NAME_PATTERN>     # AMI name pattern, supports wildcard *, e.g. forge-gh-runner-v*
    ami_owner: <ACCOUNT_ID>          # AWS account ID owning AMI
    ami_kms_key_arn: <KMS_ARN>       # Set to '' if AMI is unencrypted, else KMS ARN string
    runner_os: <OS>                  # linux, osx, or windows
    runner_architecture: <ARCH>      # x64 or arm64
    runner_user: <RUNNER_USER>       # OS user that runs the GitHub runner process
    placement:                       # Required for macOS dedicated-host runners; omit otherwise
      host_resource_group_arn: <HOST_RESOURCE_GROUP_ARN>
      tenancy: host
      availability_zone: <AVAILABILITY_ZONE>
    max_instances: <MAX_PARALLEL>    # Max EC2 runners allowed in parallel
    license_specifications: <LICENSE_SPECIFICATIONS> # Optional License Manager config for dedicated hosts
    use_dedicated_host: <true|false> # Set true for macOS EC2 runners
    vpc_id: <VPC_ID>                 # Optional override; defaults to tenant VPC when omitted
    subnet_ids:                      # Optional override; defaults to tenant subnets when omitted
      - <SUBNET_ID>
    instance_types:                  # List of allowed instance types
      - <AWS_INSTANCE_TYPE>          # e.g. t3.large, m5.large, mac2.metal
    pool_config:                     # Warm pool config for pre-warming runners; empty list [] disables
      - size: <POOL_SIZE>            # Number of instances to keep warm
        schedule_expression: <AWS_CRON_EXPR>  # AWS cron expression (6 fields, use AWS docs)
        schedule_expression_timezone: <TIMEZONE>  # Optional timezone, e.g. UTC, America/New_York
    volume:
      size: <VOLUME_SIZE>
      device_name: <VOLUME_DEVICE_NAME>
      iops: <VOLUME_IOPS>
      throughput: <VOLUME_THROUGHPUT>
      type: <VOLUME_TYPE>

arc_runner_specs:
  <runner_type>:                    # e.g. dependabot, k8s
    runner_size:
      max_runners: <MAX>            # Max pods/runners (Max Pod allowed in parallel)
      min_runners: <MIN>            # Min pods/runners (warm pool)
    scale_set_name: <NAME>          # Used for ARC annotations and scale set identification
    scale_set_type: <dind|k8s>      # Must be exactly 'dind' or 'k8s', no other values allowed
    scale_set_labels:               # GitHub runner labels advertised by this ARC scale set
      - <LABEL>                     # e.g. dependabot, k8s, dind
    container_actions_runner: <ECR_IMAGE_URL>   # Full ECR container image URL for the runner container
    container_requests_cpu: <CPU>   # Kubernetes CPU requests, e.g. 500m (mandatory unit)
    container_requests_memory: <MEM> # Kubernetes memory requests, e.g. 1Gi (mandatory unit)
    container_limits_cpu: <CPU>     # Kubernetes CPU limits
    container_limits_memory: <MEM>  # Kubernetes memory limits
    volume_requests_storage_type: <STORAGE_TYPE> # Storage class/type for runner workspace volume
    volume_requests_storage_size: <STORAGE_SIZE> # Size for runner workspace volume

arc_cluster_name: <CLUSTER_NAME>
migrate_arc_cluster: <true|false>


Field Guidance & Gotchas

github_webhook_relay Guidance

macOS EC2 Runner Guidance

macOS runners must run on EC2 Dedicated Hosts. Configure the runner spec with use_dedicated_host: true, host placement, and Mac instance types:

ec2_runner_specs:
  mac:
    type: mac
    ami_name: forge-gh-runner-macarm-v*
    ami_owner: '123456789012'
    ami_kms_key_arn: ''
    runner_os: osx
    runner_architecture: arm64
    runner_user: ec2-user
    placement:
      host_resource_group_arn: arn:aws:resource-groups:<REGION>:<ACCOUNT_ID>:group/<HOST_RESOURCE_GROUP>
      tenancy: host
      availability_zone: <AVAILABILITY_ZONE>
    license_specifications:
      - license_configuration_arn: arn:aws:license-manager:<REGION>:<ACCOUNT_ID>:license-configuration:<LICENSE_CONFIGURATION_ID>
    use_dedicated_host: true
    vpc_id: <VPC_ID>
    subnet_ids:
      - <SUBNET_ID>
    max_instances: <MAX_PARALLEL>
    instance_types:
      - mac2.metal
    pool_config: []
    volume:
      size: <VOLUME_SIZE>
      device_name: <VOLUME_DEVICE_NAME>
      iops: <VOLUME_IOPS>
      throughput: <VOLUME_THROUGHPUT>
      type: gp3

Use a subnet in the same availability zone as the dedicated host placement. If your host resource group does not require License Manager, omit license_specifications.


Common Pitfalls — Avoid These


3. Create GitHub App

  1. Pull the registration UI container (amd64):
docker pull ghcr.io/cisco-open/forge-forge-github-app-register:main
  1. Run it locally, exposing port 5000:
docker run --rm -p 5000:5000 ghcr.io/cisco-open/forge-forge-github-app-register:main
  1. Open the UI:

Go to http://localhost:5000/ in your browser.

  1. In the UI:
${local.tenant_name}-${local.region_alias}-${local.vpc_alias}-${include.env.locals.runner_group_name_suffix}

Example:

sec-plat-euw1-shared-sbg-cicd-forge
  1. After creation:

Tips:


4. Minimal Working config.yml Example

gh_config:
  ghes_url: ''
  ghes_org: cisco-sbg
  repository_selection: selected
  github_webhook_relay:  
    enabled: false
    destination_account_id: ""
    destination_event_bus_name: ""
    destination_region: ""
    destination_reader_role_arn: ""
  github_app:
    id: 1234567890
    client_id: abcdefghijklmnopqrstuvwx
    installation_id: 9876543210
    name: forge-github-app

tenant:
  iam_roles_to_assume:
    - arn:aws:iam::123456789012:role/role_for_forge_runners
  ecr_registries:
    - 123456789012.dkr.ecr.us-east-1.amazonaws.com
  github_logs_reader_role_arns:
    - arn:aws:iam::123456789012:role/github_logs_reader

ec2_config:
  enable_dynamic_labels: false

ec2_runner_specs:
  small:
    type: small
    ami_name: forge-gh-runner-v*
    ami_owner: '123456789012'
    ami_kms_key_arn: ''
    runner_os: linux
    runner_architecture: x64
    runner_user: ubuntu
    max_instances: 10
    vpc_id: vpc-0abc1234def567890
    subnet_ids:
      - subnet-0abc1234def567890
    instance_types:
      - t3.small
      - t3.medium
    pool_config:
      - size: 2
        schedule_expression: "cron(*/10 8 * * ? *)"
        schedule_expression_timezone: "America/Los_Angeles"
    volume:
      size: 200
      device_name: /dev/sda1
      iops: 3000
      throughput: 125
      type: gp3

arc_runner_specs:
  dependabot:
    runner_size:
      max_runners: 100
      min_runners: 1
    scale_set_name: dependabot
    scale_set_type: dind
    scale_set_labels:
      - dependabot
      - dind
    container_actions_runner: 123456789012.dkr.ecr.us-east-1.amazonaws.com/actions-runner:latest
    container_requests_cpu: 500m
    container_requests_memory: 1Gi
    container_limits_cpu: '1'
    container_limits_memory: 2Gi
    volume_requests_storage_type: gp2
    volume_requests_storage_size: 10Gi

arc_cluster_name: forge-arc-cluster
migrate_arc_cluster: false

5. Deploy

  1. Navigate to your tenant directory:
cd examples/deployments/forge-tenant/terragrunt/environments/<aws_account_alias>/regions/<aws_region>/vpcs/<vpc_alias>/tenants/<tenant_name>
  1. Deploy everything in one go:
terragrunt apply
  1. Verify success:

6. Set GitHub App Secrets

Run the update-github-app-secrets.sh script to inject critical GitHub App values into your secrets:

./scripts/update-github-app-secrets.sh /full/path/to/tenant_dir /path/to/private-key.pem

Notes:


7. Redeploy with secrets updated

  1. Navigate to your tenant directory:
cd examples/deployments/forge-tenant/terragrunt/environments/<aws_account_alias>/regions/<aws_region>/vpcs/<vpc_alias>/tenants/<tenant_name>
  1. Deploy everything in one go:
terragrunt apply
  1. Verify success:

For more advanced scenarios or troubleshooting, see the full documentation.