For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt, and this page is available as Markdown at /dd/0014-gitlab-ci-runner-ec2-instance.md.

Themelio compatibility reference. This page describes the retained Themelio implementation or its historical design. Check the parallel-operation guide before applying it to the current platform.

DD-0014: GitLab CI Runner EC2 Instance — Manual Operator Steps

Related ADR: ADR-0014: GitLab CI Runner on Dedicated AWS EC2 Instance

Status: Draft

Date: 2026-06-03

Authors: Thanasis Katsadas

Overview

This document is the operator runbook for provisioning and securing the EC2 instance decided in ADR-0014. It covers all manual steps performed in the AWS console: creating a dedicated VPC, importing the SSH key pair, allocating an Elastic IP, creating a security group, launching the instance with the correct configuration, and verifying SSH connectivity. Once these steps are complete, the instance is ready for Ansible onboarding as described in DD-0015.

Goals and Non-Goals

Goals

  • Allocate and configure all required AWS resources (key pair, Elastic IP, security group, EC2 instance) manually via the AWS console in the runner account.
  • Leave the instance reachable via SSH from GRNET/NTUA VPN-connected workstations and ready for Ansible.

Non-Goals

  • Ansible provisioning, Docker engine setup, NVMe mount configuration — covered in DD-0015.
  • GitLab runner installation and registration — covered in DD-0015.
  • Terraform automation of any of these steps.

Design

Prerequisites

Before starting, the operator must have:

  1. AWS console access to the runner AWS account (separate from the main Themelio account).
  2. VPN active — GRNET or NTUA VPN must be connected on the operator workstation.
  3. Fleet SSH public key available locally at ~/.ssh/pharos-admin-key.pub. If not present, run make fetch-keys from devops/ansible/ to download it from AWS Secrets Manager (pharos-cloud/admin-ssh-key-public).

Step 1: Create the dedicated VPC

  1. VPC → Create VPC → select "VPC and more" (creates the subnet, internet gateway, and route table automatically).
  2. Name tag: pharos-gitlab-ci-runner-vpc.
  3. IPv4 CIDR: 10.0.0.0/24.
  4. Tenancy: Default.
  5. Number of AZs: 1. Public subnets: 1. Private subnets: 0.
  6. NAT gateways: None.
  7. Tags: Project = themelio.
  8. Create VPC.
  9. Verify the internet gateway: VPC → Internet Gateways — confirm one is attached to pharos-gitlab-ci-runner-vpc.
  10. Verify the route table: VPC → Route Tables → select the route table associated with the public subnet → confirm a route 0.0.0.0/0 → igw-xxxxxxxx exists.

Step 2: Import the fleet SSH key pair

All fleet nodes use the pharos-admin-key key pair. Because the runner lives in a separate AWS account, the public key must be imported there before the instance can be launched.

  1. In the runner AWS account: EC2 → Key Pairs → Import key pair.
  2. Name: pharos-admin-key.
  3. Paste the contents of ~/.ssh/pharos-admin-key.pub.
  4. Import.

Step 3: Allocate the Elastic IP

  1. EC2 → Elastic IPs → Allocate Elastic IP address.
  2. Network border group: eu-central-1.
  3. Tags: Name = pharos-gitlab-ci-runner-eip, Project = themelio.
  4. Record the allocated IP address — this value is entered into inventory/runner/hosts.yml during Ansible onboarding (DD-0015).

Step 4: Create the Security Group

  1. EC2 → Security Groups → Create security group.

  2. Name: pharos-gitlab-ci-runner-sg.

  3. Description: GitLab CI runner — SSH from VPN only.

  4. VPC: select the VPC in the runner account where the instance will be launched.

  5. Inbound rules:

    TypePortSourceNotes
    SSH22195.251.29.0/25GRNET VPN
    SSH22147.102.4.167/32NTUA
    SSH22147.102.3.0/24NTUA
    SSH22147.102.4.0/24NTUA
  6. Outbound rules: keep the default (allow all — the runner needs outbound HTTPS to gitlab.grnet.gr, Docker registries, and OS package mirrors).

  7. Tags: Name = pharos-gitlab-ci-runner-sg, Project = themelio.


Step 5: Launch the EC2 instance

  1. EC2 → Launch Instances.

  2. Name: pharos-gitlab-ci-runner-1.

  3. AMI: Search the AWS Marketplace for the official Debian 12 (Bookworm) AMI in eu-central-1. Verify the publisher is "Debian". Note the AMI ID used.

  4. Instance type: c6id.4xlarge.

  5. Key pair: pharos-admin-key (imported in Step 2).

  6. Network settings:

    • VPC: same VPC as the security group above.
    • Subnet: a public subnet with an internet gateway route. The instance needs outbound internet access to reach gitlab.grnet.gr and Docker registries.
    • Auto-assign public IP: Disable — the Elastic IP from Step 3 replaces this.
    • Security group: pharos-gitlab-ci-runner-sg (Step 4).
  7. Storage: root volume only. The default gp3 volume (8–20 GB) is sufficient. The local NVMe (~950 GB) is provided automatically by the c6id.4xlarge instance type — no additional EBS volume is needed.

  8. Advanced details → User data: paste the following cloud-init configuration. This creates the admin user with passwordless sudo and the fleet SSH key on first boot, consistent with all other fleet nodes.

    #cloud-config
    users:
      - name: admin
        groups: sudo
        shell: /bin/bash
        sudo: ALL=(ALL) NOPASSWD:ALL
        ssh_authorized_keys:
          - <paste the contents of ~/.ssh/pharos-admin-key.pub here>
  9. Tags: Name = pharos-gitlab-ci-runner-1, Project = themelio, Environment = production.

  10. Launch instance.


Step 6: Associate the Elastic IP

  1. EC2 → Elastic IPs → select the EIP allocated in Step 3.
  2. Actions → Associate Elastic IP address.
  3. Resource type: Instance. Select pharos-gitlab-ci-runner-1.
  4. Confirm. The EIP is now permanently bound to the instance and persists across stop/start/reboot cycles.

Step 7: Purchase the Reserved Instance

The instance launched in Step 5 starts as on-demand. The 1-year all-upfront reservation is a separate billing action:

  1. EC2 → Reserved Instances → Purchase Reserved Instances.
  2. Platform: Linux/UNIX. Instance type: c6id.4xlarge. Region: EU (Frankfurt).
  3. Term: 1 year. Offering class: Standard. Payment option: All Upfront.
  4. Confirm purchase (~$4,803). AWS automatically applies the reservation to any matching running c6id.4xlarge instance in the account.

Step 8: Verify SSH access

From a workstation with GRNET or NTUA VPN active:

ssh -i ~/.ssh/pharos-admin-key admin@<EIP>

Once logged in, confirm:

# Passwordless sudo works
sudo whoami
# Expected: root

# Local NVMe is visible (~950 GB)
lsblk
# Expected: an nvme device (e.g. nvme1n1) of ~950 GB

If SSH fails, check:

  • The operator workstation is on GRNET or NTUA VPN.
  • The security group inbound rules match exactly the CIDRs in Step 4.
  • The EIP is associated with the instance (Step 6).
  • The instance has finished booting (allow ~60 seconds after launch).

Open Questions

Q1: GRNET and NTUA VPN CIDR ranges

Resolved: 195.251.29.0/25 (GRNET), 147.102.4.167/32, 147.102.3.0/24, 147.102.4.0/24 (NTUA) — sourced from inventory/{env}/group_vars/ssl-services.yml.

Q2: Runner AWS account ownership and IAM access

Who owns the runner AWS account and which operators have console access to perform these steps?

Q3: SSH key strategy

Resolved: Reuse pharos-admin-key. Public key at ~/.ssh/pharos-admin-key.pub, downloaded via make fetch-keys from AWS Secrets Manager (pharos-cloud/admin-ssh-key-public). Must be imported into the runner account (Step 2) before instance launch.

References

Page source

Source snapshot · Revision f928834c8.