--- title: "DD-0014: GitLab CI Runner EC2 Instance — Manual Operator Steps" --- # 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) **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: - ``` 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@ ``` 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) - [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)