Skip to content

Guides / Private networking

Connect GitHub Actions to a private RDS database

Run an integration test against a private staging database from an EC2 runner in your VPC. Gondola launches the runner in the subnets and security groups you choose; your workflow selects it by label.

Start by connecting to an existing RDS PostgreSQL database and running SELECT 1. Once that works, replace the check with your application's integration tests.

Inside your AWS VPC

Gondola controllers
launch
EC2 runner
connects to
Private RDS database
The runner connects to PostgreSQL over TLS on port 5432. No public database endpoint is needed.

Controllers and runners still need outbound access to GitHub, AWS APIs and the registries your jobs use. Gondola uses your existing network; it does not create a NAT gateway or egress proxy. Check GitHub's communication requirements when configuring that access.

Before you start

  • A Gondola installation, a private repository and an existing RDS PostgreSQL staging database with public access disabled. Use a dedicated database login with only the privileges needed for this check.
  • Private runner subnets in the database's VPC and region, with working DNS, available addresses and outbound connectivity. This example assumes normal VPC routing without a network appliance between the runner and database.
  • A GitHub runner group named gondola-staging, restricted to the intended private repository. Configure a staging environment that permits your reviewed branch. Private-repository environments require an eligible GitHub plan.
  • An x64 runner image containing psql and the current RDS CA bundle at /opt/gondola/rds-global-bundle.pem. Gondola's standard images do not include psql; follow the runner image guide to prepare your own and pin its digest. The EC2 runner must be able to pull it; private registries need runner-side authentication and do not inherit the controller's registry credentials.

Anyone who can run code on this fleet can use its network access. Keep it separate from public or untrusted pull-request jobs. Terminating the instance after a job does not limit what that job can do while it runs.

1. Allow the database connection

Create a dedicated runner security group. Allow outbound PostgreSQL to the database group, and allow the database to receive PostgreSQL from the runner group. These references work as runner instances come and go; you do not need a list of their IP addresses. See AWS security-group references.

Add this to your Terraform or OpenTofu configuration alongside the Gondola module. Set var.vpc_id to the staging VPC and var.staging_database_security_group_id to a group already attached to the database.

resource "aws_security_group" "staging_runner" {
  name_prefix = "gondola-staging-runner-"
  description = "Runner access for staging database checks"
  vpc_id      = var.vpc_id
}

resource "aws_vpc_security_group_egress_rule" "runner_postgres" {
  security_group_id            = aws_security_group.staging_runner.id
  referenced_security_group_id = var.staging_database_security_group_id
  ip_protocol                 = "tcp"
  from_port                   = 5432
  to_port                     = 5432
}

resource "aws_vpc_security_group_ingress_rule" "database_from_runner" {
  security_group_id            = var.staging_database_security_group_id
  referenced_security_group_id = aws_security_group.staging_runner.id
  ip_protocol                 = "tcp"
  from_port                   = 5432
  to_port                     = 5432
}

resource "aws_vpc_security_group_egress_rule" "runner_https" {
  security_group_id = aws_security_group.staging_runner.id
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "tcp"
  from_port         = 443
  to_port           = 443
}

The last rule allows HTTPS to any IPv4 address. Use your existing firewall policy if you need to restrict destinations further. Other workflow dependencies may need additional access.

This connection needs no inbound runner rule. Check every security group attached to both resources: their permissions combine. If another stack or inline Terraform rules already manage the database's rules, make the equivalent change there instead of managing a rule twice.

2. Assign the network to a runner fleet

Add this entry to the fleets map in your Gondola module block, keeping existing entries. Set var.staging_runner_subnet_ids to the private runner subnets and var.approved_staging_runner_image to the full image reference, pinned by digest, prepared above.

fleets = {
  staging = {
    scale_set_name         = "gondola-staging"
    runner_group           = "gondola-staging"
    architecture           = "x64"
    capacity_mode          = "on-demand"
    vpc_id                 = var.vpc_id
    subnet_ids             = var.staging_runner_subnet_ids
    security_group_ids     = [aws_security_group.staging_runner.id]
    runner_container_image = var.approved_staging_runner_image
    instance_type          = "m7i.large"
    policy_arns            = []
    min_runners            = 0
    max_runners            = 2
    max_runner_lifetime    = "30m"
  }
}

This example adds no AWS policies to the generated runner role. A password-authenticated PostgreSQL connection does not need AWS API permissions. If your job also calls AWS services, use a job-specific role through GitHub OIDC.

Keep the default two controllers across two Availability Zones; their subnets can differ from the runner subnets. When you supply runner security groups, you manage their rules. Gondola does not add its default egress rules to them.

Review the plan for resource replacements and runner-label changes before applying, especially when moving from older single-fleet settings to a fleet map. The fleet configuration guide describes the available fields.

3. Check the connection from GitHub Actions

Add these values to the repository's staging environment:

NameTypeValue
STAGING_DB_HOSTVariableThe RDS endpoint hostname
STAGING_DB_NAMEVariableThe staging database name
STAGING_DB_USERVariableThe dedicated database login
STAGING_DB_PASSWORDSecretThat login's password

Save this workflow on your reviewed main branch. If you use another branch name, update the branch check and environment policy together.

name: Check staging database

on:
  workflow_dispatch:

permissions: {}

jobs:
  database-check:
    if: github.ref == 'refs/heads/main'
    runs-on: gondola-staging
    environment: staging
    timeout-minutes: 10
    steps:
      - name: Check the private database connection
        env:
          PGHOST: ${{ vars.STAGING_DB_HOST }}
          PGPORT: "5432"
          PGDATABASE: ${{ vars.STAGING_DB_NAME }}
          PGUSER: ${{ vars.STAGING_DB_USER }}
          PGPASSWORD: ${{ secrets.STAGING_DB_PASSWORD }}
          PGSSLMODE: verify-full
          PGSSLROOTCERT: /opt/gondola/rds-global-bundle.pem
          PGCONNECT_TIMEOUT: "10"
          PGOPTIONS: "-c default_transaction_read_only=on"
        run: |
          psql --no-psqlrc --no-password --set=ON_ERROR_STOP=1 \
            --command='SELECT 1;'

verify-full checks the database certificate and hostname against the RDS CA bundle. Use the actual RDS endpoint and keep the bundle in your image current. See PostgreSQL certificate verification on RDS.

Run the workflow manually. A successful query returns 1. Confirm the job's EC2 instance terminates afterward. When adding integration tests, grant only the database privileges they need and use staging data. Tests that need writes must also remove the read-only PGOPTIONS setting above. Keep credentials and sensitive query results out of logs.

If the check fails

SymptomCheck
Job stays queuedThe fleet label, runner-group repository access and fleet readiness.
Runner fails before the job startsOutbound connectivity, image-pull access and the image digest.
Hostname does not resolveThe database endpoint and VPC DNS configuration.
Connection times outRunner subnets, routes, network ACLs and security-group rules.
Certificate verification failsThe RDS endpoint hostname and current CA bundle.
Authentication failsThe environment secret, database name and login privileges.

Include networking in your costs

min_runners = 0 avoids keeping idle runners. Controllers and network services can still incur charges between jobs. Account for EC2, EBS, controllers, database usage, logs and networking. NAT gateways have hourly and data-processing charges; cross-AZ traffic and interface endpoints can add costs too.

Use the cost calculator with your own workload and network estimate. If you haven't installed Gondola yet, start with one workflow. You can email us if you get stuck.