> For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt.

> **Themelio compatibility reference.** This page describes the retained Themelio implementation or its historical design. Check the [parallel-operation guide](/compatibility/index.md) 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](/adr/0014-gitlab-ci-runner-ec2-instance.md)

**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**:

   | Type | Port | Source             | Notes     |
   | ---- | ---- | ------------------ | --------- |
   | SSH  | 22   | `195.251.29.0/25`  | GRNET VPN |
   | SSH  | 22   | `147.102.4.167/32` | NTUA      |
   | SSH  | 22   | `147.102.3.0/24`   | NTUA      |
   | SSH  | 22   | `147.102.4.0/24`   | NTUA      |

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.

   ```yaml
   #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:

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

Once logged in, confirm:

```bash
# 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

- [ADR-0014: GitLab CI Runner on Dedicated AWS EC2 Instance](/adr/0014-gitlab-ci-runner-ec2-instance.md)
- [AWS EC2 c6id instance family](https://aws.amazon.com/ec2/instance-types/c6i/)
- [AWS Elastic IP Addresses](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/elastic-ip-addresses-eip.html)
- [Debian 12 on AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-l5gv52ndg5q26)
- [cloud-init user configuration](https://cloudinit.readthedocs.io/en/latest/reference/modules.html#users-and-groups)

<details class="page-source"><summary>Page source</summary>

<a href="/source/docs/content/dd/0014-gitlab-ci-runner-ec2-instance.md.txt">Source snapshot</a> · Revision `f928834c8`.

</details>
