# terraform-aws-gitlab-runner: autoscaling GitLab runners on EC2 spot instances

> The cattle-ops Terraform module builds GitLab CI runners on AWS EC2 spot instances with a shared S3 cache, CloudWatch log streaming and automatic runner agent registration. It offers three deployment shapes, and the docker-machine one is where spot interruptions actually bite.

**cattle-ops/terraform-aws-gitlab-runner** — Terraform module for AWS GitLab runners on ec2 (spot) instances

- Repository: https://github.com/cattle-ops/terraform-aws-gitlab-runner
- Website: https://registry.terraform.io/modules/cattle-ops/gitlab-runner/aws
- Stars: 629 · Forks: 356
- Language: HCL
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/cattle-ops-terraform-aws-gitlab-runner

## The module creates runners, not a GitLab installation

The scope is worth being precise about, because the name invites confusion. This module creates a GitLab Runner, which is the agent that picks up CI jobs from a GitLab instance. It does not create the GitLab server, the project, the runner registration token, or the CI configuration. The runner agents are registered automatically, according to the README, but registration against your own GitLab remains the step that ties the module to your instance.

The design descends from two published pieces of work. A blog post describes the original version of the runner, and the module's setup is based on an article about autoscaling GitLab CI runners on EC2. The upstream idea is simple: keep one agent instance as the autoscaler, and let it create short-lived worker instances on demand.

That is where the economics come from. The runner agents use spot instances by default for running builds with the docker+machine executor, so worker capacity is priced like interruptible capacity. The trade is that a spot instance can be reclaimed mid-job, and every design decision in this module follows from that fact.

## Three runner shapes, and only one of them scales

The README documents three main scenarios, and the differences between them decide whether the module fits your pipeline.

The first is a GitLab CI docker-machine runner with one runner agent. The agent runs on a single EC2 node, and the actual runners are created by docker machine using spot instances. Runners scale automatically based on the configuration, and the module creates an S3 cache by default which is shared across those spot instances.

The second is the same shape with multiple runner agents, where you instantiate the module several times with different configuration. Scaling still works, but the S3 cache has to be managed outside the module if you want it shared between instances, because each instantiation creates its own.

The third is a plain GitLab CI docker runner. Here docker machine is not used, docker schedules the builds, and the builds run on the same EC2 instance as the agent. The README says plainly that no auto-scaling is supported. So the module name applies to two of the three scenarios, and the third exists mainly for setups that want the IAM and networking plumbing without a fleet.

## First use: the registry module and the examples directory

There is no install command to copy here, because a Terraform module is not installed. The module is published to the Terraform Registry under the cattle-ops organisation in the aws namespace, and the registry page is listed as the project homepage. Consumption happens by referencing that module source from a root module you write yourself, and the module's own requirements are what your root module has to satisfy: terraform 1.3 or newer, the aws provider at 6.0.0 or newer, local at 2.4.0 or newer and tls at 3 or newer.

The fastest way to reach a working setup is to start from the examples rather than from a blank root module. The examples/ directory contains runner-default, runner-docker, runner-public, runner-windows, runner-certificates and runner-fleeting-plugin, each isolating one decision so you can see which knob produces which behaviour. runner-default is the docker-machine scenario, runner-docker is the non-scaling one, and runner-fleeting-plugin shows the alternative autoscaler.

Two things have to come from you regardless of example. The GitLab side needs a runner registration token and the GitLab instance address, since the module creates the runner but does not create GitLab. The AWS side needs a VPC and subnets to place the agent and the workers, and the IAM roles are created for you with the policies the module needs. For detailed concepts and usage the README points at docs/usage.md, which is the document to read before you write your root module.

## Spot capacity is the mechanism, so interruptions are the design assumption

The resource list makes the architecture concrete. There are two autoscaling groups, aws_autoscaling_group.autoscaler and aws_autoscaling_group.gitlab_runner_instance, and with a lifecycle hook and scale-out and scale-in schedules alongside, so the module drives capacity both by scheduled action and by the autoscaling group reacting to queue depth. Two IAM instance profiles exist for the docker machine and docker autoscaler paths, plus a runner profile for the agent itself, and the policies are split per role rather than shared, which matters because the worker role is the one that can create spot instances.

Two supporting pieces sit underneath. The cache submodule is a local module at./modules/cache, and the terminate agent hook submodule at./modules/terminate-agent-hook handles shutting down an agent cleanly, which is the piece that keeps a spot reclaim from looking like a mysterious failure.

The limitation to plan around is not a bug but the nature of the capacity. A spot instance can be interrupted while a build is running, and the module's design assumes that builds are short enough or retryable enough to survive it. Long-running jobs, database migrations inside the pipeline, and steps that assume uninterrupted network access are the cases where this module is the wrong shape of tool.

## S3 cache and CloudWatch logs are the two shared services

Two shared services carry most of the operational value. The first is the S3 cache with life cycle management to clear objects after x days, which is the reason the module creates a cache by default rather than leaving caching to each worker. On spot capacity, where every worker is disposable, a local cache is worthless: the next build lands on a different instance. A shared bucket is what makes the cache mean anything, and the lifecycle rule is what stops that bucket growing without bound.

The second is log streaming to CloudWatch, wired through the aws_cloudwatch_log_group.environment resource. When workers are ephemeral and agents come and go, logs are the only durable record of what a build did. The instance logs are also what makes an interrupted spot job diagnosable after the instance is gone.

One constraint follows from the S3 cache: if you use the multiple-agent scenario, you have to manage that cache outside the module, because each module instantiation brings its own. That is documented, but it means the boundary between module-managed and externally-managed resources sits in a place you have to remember.

## Major version migrations are the upgrade cost

The README calls out two migration guides by name: issue 819 for migrating to v7 smoothly, and pull request 1204 for migrating to v8 smoothly. That is an unusually direct signal about upgrade cost in a module, and it points at the real problem. Runner modules accumulate configuration surface, and every major bump moves input variables, changes the runner registration flow, or alters the IAM and networking resources in ways that make an in-place update fail rather than simply apply.

The version numbers in the repository are moving at a reasonable pace. Recent releases include v9.6.0 on 2026-10-09, v9.5.2 on 2026-06-11 and v9.5.1 on 2026-05-17, and the last push was on 2026-10-22, so the repository is not archived and work continues on the branch between tags.

The provider constraints are what will bite first. The requirements table lists terraform at 1.3 or newer, aws at 6.0.0 or newer, local at 2.4.0 or newer and tls at 3 or newer, and the provider versions pinned in the documentation are aws 6.68.0, local 2.9.1 and tls 4.4.1. The aws provider floor of 6.0.0 is a large jump for many organizations, and the local and tls providers are used here for certificate and file handling during registration, which means you are pulling in providers you may not otherwise want. There is a.terraform-version file and a.terraform.lock.hcl in the repository, so the maintainers pin their own toolchain and expect you to pin yours.

## Packer images, examples and the rest of the tree

The repository is more than the Terraform root. A packer_images/ directory and a template/ directory sit alongside the .tf files, which is how the module produces the machine images the spot workers boot from, and a migrations/ directory is where the version upgrade paths live. The examples/ directory holds several ready shapes: runner-default, runner-docker, runner-public, runner-windows, runner-certificates and runner-fleeting-plugin, each with its own configuration.

That example set is the fastest way to understand the module, since each one isolates a choice: docker versus docker-machine, a public subnet versus a private one, Windows workers, certificates for the runner registration, and the fleeting plugin as an alternative autoscaler. The docs directory holds a usage document that the README points to for detailed concepts and usage, so the README is an overview and the usage file is the reference.

The linting setup is unusually thorough for a Terraform module, and it is a signal about how the maintainers work. The tree includes .checkov.yml for policy scanning of infrastructure code, .kics.yml and terrascan-config.toml for other scanners, .tflint.hcl for Terraform linting, .gitleaks.toml for secret detection, .pre-commit-config.yaml, .ruff.toml, .flake8 and .pylintrc for the Python tooling, .mega-linter.yml, .markdownlint.json, .cspell.json and renovate.json for dependency updates. The licence is MIT.

## What this module is not, and when to use something else

The realistic alternative is the managed runner that GitLab itself provides, where someone else operates the runner infrastructure and you only configure your jobs. The difference in approach is exactly where the cost sits. The managed path moves the fleet, the patching, the scaling and the interruption handling to GitLab and costs money per minute of compute. This module keeps that in your AWS account, uses spot pricing, and hands you the responsibility: capacity planning, image patching, interruption tolerance, and the IAM surface that comes with instance roles that can create further instances.

Another alternative is running the same autoscaling pattern yourself on the GitLab docker-machine executor without a module, which is what many teams did before this module existed. The module's value over that is the packaging: the IAM policies, the S3 cache with lifecycle, the CloudWatch log group, the terminate hook and the spot fleet configuration arrive together and are versioned.

Pick this module when the pipeline is bursty, the jobs are short, and AWS is already where your workloads live. Pick the managed runner when build reliability matters more than unit cost, when jobs run long enough that interruption dominates, or when your organization has no appetite for the IAM and spot exposure that instance-profile-based autoscaling brings.

## Conclusion

Adopt terraform-aws-gitlab-runner when your GitLab CI pipeline has bursty parallel jobs and you already run on AWS, since the spot fleet and shared S3 cache are where the module earns its keep. Do not reach for it when jobs are long-running or stateful, because spot interruption is the design assumption rather than an edge case. Before applying anything, read the migration notes for your target major version, the v7 and v8 upgrade paths referenced in the README, and check that your Terraform version is at least 1.3 with the aws provider at 6.0.0 or newer.

## FAQ

### What does terraform-aws-gitlab-runner create?

It creates a GitLab Runner that picks up CI jobs, not the GitLab instance itself. The runner agents are registered automatically, but you supply the registration details for your own GitLab.

### Does terraform-aws-gitlab-runner support autoscaling?

Yes, in the docker-machine scenarios. The runner agent runs on a single EC2 node and creates workers on spot instances through docker machine, scaling automatically based on the configuration. The plain docker runner scenario runs builds on the agent instance and supports no auto-scaling.

### How does the module cache build artifacts between spot instances?

It creates an S3 cache shared across runners with life cycle management that clears objects after a set number of days. In the multiple-agent scenario the cache has to be managed outside the module so it can be shared between instances.

### What are the provider version requirements for terraform-aws-gitlab-runner?

The requirements table lists terraform at 1.3 or newer, aws at 6.0.0 or newer, local at 2.4.0 or newer and tls at 3 or newer. The documented provider versions are aws 6.68.0, local 2.9.1 and tls 4.4.1.

### How do I upgrade terraform-aws-gitlab-runner across major versions?

The README links migration guidance for the v7 and v8 transitions, in issue 819 and pull request 1204. Major bumps move module inputs and runner registration behaviour, so the upgrade path has to be followed rather than applied in place.

## Sources

- [cattle-ops/terraform-aws-gitlab-runner on GitHub](https://github.com/cattle-ops/terraform-aws-gitlab-runner)
- [License: MIT](https://github.com/cattle-ops/terraform-aws-gitlab-runner/blob/main/LICENSE)
- [Project website](https://registry.terraform.io/modules/cattle-ops/gitlab-runner/aws)
- [README](https://github.com/cattle-ops/terraform-aws-gitlab-runner/blob/main/README.md)
- [Releases](https://github.com/cattle-ops/terraform-aws-gitlab-runner/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/cattle-ops-terraform-aws-gitlab-runner
