Deploying a GitLab Runner on AWS

Problem

I recently encountered an informational message on GitLab. I had used almost all the 400 minutes that the free tier provides for GitLab runners. I have previously run GitLab runners locally using Docker, however I wanted something to run in the cloud.

Setup

  1. Install docker with OrbStack or Docker Desktop on local machine
  2. Create a new VPC with 2 private subnets and that is connected to the internet via NAT gateway, allowing outbound traffic.
    • vpc-with-nat-and-instance-connect.yaml 
    • Run that cf template 

vpc-with-nat-and-instance-connect.yaml

AWSTemplateFormatVersion: '2010-09-09'
Description: 'VPC with 2 private subnets, NAT gateway, and Instance Connect endpoint'

Parameters:
  S3BucketName:
    Type: String
    Default: "ntwklab-aws-gitlab-runner"
    Description: "Name of the S3 bucket for Lambda functions"

Resources:
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref S3BucketName
      VersioningConfiguration:
        Status: Enabled
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsHostnames: true
      EnableDnsSupport: true

  InternetGateway:
    Type: AWS::EC2::InternetGateway

  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: !Select [0, !GetAZs '']
      MapPublicIpOnLaunch: true

  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.2.0/24
      AvailabilityZone: !Select [0, !GetAZs '']

  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.3.0/24
      AvailabilityZone: !Select [1, !GetAZs '']

  NATGatewayEIP:
    Type: AWS::EC2::EIP
    DependsOn: AttachGateway
    Properties:
      Domain: vpc

  NATGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NATGatewayEIP.AllocationId
      SubnetId: !Ref PublicSubnet

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC

  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: AttachGateway
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref PublicRouteTable

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC

  PrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NATGateway

  PrivateSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateRouteTable

  PrivateSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet2
      RouteTableId: !Ref PrivateRouteTable

  InstanceConnectEndpoint:
    Type: AWS::EC2::InstanceConnectEndpoint
    Properties:
      SubnetId: !Ref PrivateSubnet1
      SecurityGroupIds:
        - !Ref InstanceConnectEndpointSecurityGroup

  InstanceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for EC2 instances with Instance Connect
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          SourceSecurityGroupId: !Ref InstanceConnectEndpointSecurityGroup
      Tags:
        - Key: Name
          Value: InstanceConnectSG

  InstanceConnectEndpointSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for Instance Connect Endpoint
      VpcId: !Ref VPC
      SecurityGroupEgress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 10.0.0.0/16
      Tags:
        - Key: Name
          Value: InstanceConnectEndpointSG

Outputs:
  VPCId:
    Description: VPC ID
    Value: !Ref VPC
    Export:
      Name: !Sub "${AWS::StackName}-VPC-ID"

  PrivateSubnet1Id:
    Description: Private Subnet 1 ID
    Value: !Ref PrivateSubnet1
    Export:
      Name: !Sub "${AWS::StackName}-PrivateSubnet1-ID"

  PrivateSubnet2Id:
    Description: Private Subnet 2 ID
    Value: !Ref PrivateSubnet2
    Export:
      Name: !Sub "${AWS::StackName}-PrivateSubnet2-ID"

  InstanceConnectEndpointId:
    Description: Instance Connect Endpoint ID
    Value: !Ref InstanceConnectEndpoint
    Export:
      Name: !Sub "${AWS::StackName}-InstanceConnectEndpoint-ID"

  InstanceSecurityGroupId:
    Description: Security Group ID for instances
    Value: !Ref InstanceSecurityGroup
    Export:
      Name: !Sub "${AWS::StackName}-InstanceSG-ID"

  S3BucketName:
    Description: S3 Bucket Name
    Value: !Ref S3Bucket
    Export:
      Name: !Sub "${AWS::StackName}-S3Bucket-Name"
aws cloudformation create-stack \
  --stack-name vpc-nat-instance-connect \
  --template-body file://vpc-with-nat-and-instance-connect.yaml \
  --parameters ParameterKey=S3BucketName,ParameterValue=ntwklab-aws-gitlab-runner \
  --region us-east-1
aws cloudformation describe-stacks \
  --stack-name vpc-nat-instance-connect \
  --region us-east-1 \
  --query "Stacks[0].StackStatus"
aws cloudformation describe-stacks \
  --stack-name vpc-nat-instance-connect \
  --region us-east-1 \
  --query "Stacks[0].Outputs"
aws cloudformation delete-stack \
  --stack-name vpc-nat-instance-connect \
  --region us-east-1

3. Create a test instance in the new VPC and test connection 

SG_ID=$(aws cloudformation describe-stacks \
  --stack-name vpc-nat-instance-connect \
  --region us-east-1 \
  --query "Stacks[0].Outputs[?OutputKey=='InstanceSecurityGroupId'].OutputValue" \
  --output text)

SUBNET_ID=$(aws cloudformation describe-stacks \
  --stack-name vpc-nat-instance-connect \
  --region us-east-1 \
  --query "Stacks[0].Outputs[?OutputKey=='PrivateSubnet1Id'].OutputValue" \ 
  --output text)

aws ec2 run-instances \
  --image-id ami-0c02fb55956c7d316 \
  --instance-type t2.micro \
  --subnet-id $SUBNET_ID \
  --security-group-ids $SG_ID \
  --region us-east-1 \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=TestInstance}]'

Test connection to the new instance

aws ec2-instance-connect ssh \
  --instance-id i-1234567890abcdef0 \
  --os-user ec2-user \
  --region us-east-1

5. Install Node: https://nodejs.org/en/download

Creation

  1. Create a new Repo in GitLab 
  2. Follow the AWS guide to step 6 to create and clone the repo
  3. Go to GitLab create a Person Access Token
    • + 
User Settings / Access tokens 
Search settings 
Q Search or go to ... 
Personal access tokens 
Add new token 
User settings 
You can generate a personal access token for each application you use that needs access to the GitLab API. You can also use personal access 
8 Profile 
tokens to authenticate against Git over HTTP. They are the only accepted password when you have Two-Factor Authentication (2FA) enabled. 
8¢ 
Account 
Your token 
Copy token 
X 
Billing 
Applications 
Make sure you save it - you won't be able to access it again. 
Integration accounts 
Access tokens 
Active tokens 
Tokens expiring in 2 weeks 
Revoked tokens 
Expired tokens 
Emails 
1 
0 
3 
3 
Password 
Filter list 
Filter list 
Filter list 
Filter list 
Notifications 
State 
= 
Active X 
X 
Q 
Expiration date v 
1= 
SSH Keys 
GPG keys 
Name 
Status 
Scopes 
Usage ? 
Lifetime 
º Preferences 
GitLabRunner 
api, ai_features, create_runner, 
Last used: 
Comment templates 
Active 
x in 4 weeks 
: 
GitLab Runner on 
manage_runner 
Never 
Aug 04, 2025 
Active sessions 
AWS
  4. Login to GitLab with token on your own CLI
TOKEN=<token> 
echo "$TOKEN" | docker login registry.example.com -u <username> --password-stdin

5. Go To GitLab new repo: Deploy >>> Registry 

6. Copy the build command and run in the new repo

7. Push to the GitLab container registry 

8. Refresh GitLab container registry

9. Create a Runner and save the token output

10. Edit the runner to ensure it can run on multiple projects 

11. Create a properties file. Get the following: 

  • VPCID 
  • SubnetIds (2 private subnets) 
  • ImageID (An update AMI, use latest) 
  • Docker image path 
  • RunnersToken 
  • S3 bucket name (From the original CF template to set up the VPC) 

12. Run the deploy runner script. In my example, default is the name of the AWS profile I am using. You can find out the name of your profile using the commands below. If it is blank then it is default.

$ aws configure list-profiles
default

$ aws configure list
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile                <not set>             None    None
./deploy-runner.sh properties.conf us-east-1 default runner-stack 

13. Runner is Online 

Testing

  1. Turn off Instance Runners 

2. Confirm AWS Runner 

3. Run a job to test and check in the AWS Lambda application logs

Leave a Comment

Your email address will not be published. Required fields are marked *