Skip to content
Manual revision 01 · GitHub.com · Linux x64 and ARM64 · AWS

Getting started

Install Gondola on AWS.

The Terraform module installs the controller and runner infrastructure. It uses your existing network and does not create a VPC or NAT gateway.

Deployment boundaryCurrent support
AWSOne account and one region per controller deployment
GitHubGitHub.com organization or user account; private GitHub App
RunnersLinux x64 and ARM64; one ephemeral EC2 instance per job
NetworkExisting VPC and outbound HTTPS; no inbound endpoint

Prerequisites

  • Terraform 1.5 or newer and AWS credentials authorized for the module resources.
  • An existing VPC with controller and runner subnets that can reach GitHub, AWS APIs, and the configured image registries over HTTPS.
  • Subnets in at least two Availability Zones for the default two-controller topology.
  • A digest-pinned Gondola controller image and a compatible digest-pinned runner image.
  • Authority to create and install a private GitHub App for the repositories Gondola will serve.
  • A protected remote Terraform backend, versioning, and a recovery procedure appropriate for your environment.

Start in a non-production AWS account and GitHub repository. Review the Terraform plan, IAM policies, egress path, and runner image before granting access to sensitive workloads.

Plan the inputs before applying

Input groupDecide and review
IdentityGitHub owner, App client ID, installation ID, private-key secret ARN
ControllerName, region, image digest, VPC, two subnets, task count, log retention
Each fleetLabel, architecture, capacity mode, instance type, AMI, runner image digest
TrustSubnets, security groups, runner role, allowed egress, repository scope
GuardrailsMinimum, maximum, lifetime, root-volume size, metrics, alarms, cost tags

1. Create the GitHub App

Run the local setup flow from a Gondola binary. It opens GitHub’s App manifest flow, verifies the installation, and writes the private key plus installation identifiers under .gondola/github-app.

gondola github-app setup \
  --owner example \
  --account-type organization \
  --name "Example Gondola Runners"

The organization App requests repository Administration read/write, organization self-hosted runners read/write, and Metadata read access. A user-account App requests repository Administration read/write and Metadata read access. Webhooks and OAuth user authorization are not enabled. Install it only for the repositories Gondola should serve.

  • private-key.pem is secret and is written with mode 0600.
  • installation.json contains the non-secret client and installation identifiers.
  • The App client ID is the value beginning with Iv1, not the numeric App ID.

2. Store the private key

Put the PEM directly in AWS Secrets Manager. Pass only its ARN to Terraform so the key value does not enter Terraform state.

aws secretsmanager create-secret \
  --name gondola/github-app-private-key \
  --secret-string file://.gondola/github-app/private-key.pem

After verifying the secret, securely remove the local PEM according to your organization’s credential-handling procedure.

Prefer a version-qualified secret reference when your rotation process supports one. If the secret value changes at the same ARN, change deployment_generation_nonce so ECS creates a new controller generation that reads the new version.

3. Configure Terraform

Use an immutable release reference and OCI digest supplied with your Gondola release. The placeholders below must be replaced before applying.

module "gondola" {
  source = "github.com/gregtuc/gondola//deploy/terraform?ref=<release>"

  name            = "gondola-production"
  vpc_id          = var.vpc_id
  subnet_ids      = var.controller_subnet_ids
  container_image = "<registry>/gondola@sha256:<digest>"

  github_config_url                 = "https://github.com/example"
  github_app_client_id              = var.github_app_client_id
  github_app_installation_id        = var.github_app_installation_id
  github_app_private_key_secret_arn = aws_secretsmanager_secret.github_app_private_key.arn

  desired_count                = 2
  require_image_digest         = true
  require_runner_image_digest  = true

  fleets = {
    linux_x64 = {
      scale_set_name = "example-linux-x64"
      architecture   = "x64"
      capacity_mode  = "spot-with-on-demand-fallback"
      instance_type  = "m7i.large"
      subnet_ids     = var.runner_subnet_ids
      min_runners    = 0
      max_runners    = 10
    }
  }
}

If you bring an existing runner instance profile, set both its profile ARN and role ARN. Gondola receives iam:PassRole only for configured runner roles. Prefer GitHub OIDC for job-specific AWS access instead of adding standing permissions to every runner.

4. Review the network path

Controller subnets need DNS plus outbound HTTPS to GitHub, AWS APIs, and the controller registry. Runner subnets additionally need the endpoints used by workflow dependencies, source downloads, caches, and artifacts. No inbound rule is required by Gondola.

  • The two default controller subnets are in different Availability Zones.
  • Security-group egress and network ACLs permit the reviewed destinations.
  • Private subnets have a measured egress path; Gondola does not create a NAT gateway.
  • Runner subnets have enough free addresses for the configured maximum concurrency.
  • The selected AMI and runner container image both support the fleet architecture.

For Spot fleets, create the account’s EC2 Spot service-linked role once if it does not already exist:

aws iam create-service-linked-role \
  --aws-service-name spot.amazonaws.com

5. Plan and apply

terraform init
terraform validate
terraform plan -out=gondola.tfplan
terraform apply gondola.tfplan

Review the exact ECS task definition, IAM permissions, launch templates, security groups, log group, secret references, and coordination table. Do not apply a plan that introduces an unexpected mutable image tag, broad pass-role permission, public ingress, or new network path.

Resources and useful outputs

OutputUse
cluster_arn / service_nameInspect controller tasks, deployments, and circuit-breaker events
log_group_nameLocate structured controller logs
coordination_table_nameRead leader and generation state during incident triage
deployment_generationCorrelate tasks, leases, runners, and upgrades
fleet_scale_set_namesCopy the stable labels into GitHub workflows
runner_launch_template_idsVerify the instance policy and exact template versions
alarm_arnsConnect optional fleet alarms to the operator’s response path

The controller reports ready only after every fleet has initialized its GitHub scale set, validated its session, completed a queue poll, and reconciled AWS capacity. A running ECS task is not by itself proof of a usable control plane.

6. Verify a disposable job

jobs:
  acceptance:
    runs-on: example-linux-x64
    steps:
      - uses: actions/checkout@<reviewed-sha>
      - run: make test
  • The ECS service reaches its desired count and the active controller reports ready.
  • The workflow is assigned to the intended fleet and EC2 launch-template version.
  • The runner receives only the fleet IAM role and network policy you approved.
  • The EC2 instance terminates after the job and no offline runner remains registered.
  • CloudWatch logs contain no credentials, JIT configuration, or workflow payloads.

7. Record the installation

Keep the Terraform state location, release and image digests, GitHub App owner and installation ID, secret ARN, deployment generation, scale-set names, fleet owners, network decision, rollback digest, and support route in your operating record. Do not copy the private key or JIT configuration into that record.

Before admitting sensitive workloads, test controller loss, a normal upgrade, rollback, one job on every fleet, and runner termination. Then set budgets, log retention, and any optional alarms deliberately.

Next

Design the fleet catalog

Separate trust levels, architectures, networks, and capacity choices.