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
- Install docker with OrbStack or Docker Desktop on local machine
- 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
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
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" |
0 1 2 3 4 5 6 |
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 |
0 1 2 3 4 5 |
aws cloudformation describe-stacks \ --stack-name vpc-nat-instance-connect \ --region us-east-1 \ --query "Stacks[0].StackStatus" |
0 1 2 3 4 5 |
aws cloudformation describe-stacks \ --stack-name vpc-nat-instance-connect \ --region us-east-1 \ --query "Stacks[0].Outputs" |
0 1 2 3 4 |
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
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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
0 1 2 3 4 5 |
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
- Create a new Repo in GitLab
- Follow the AWS guide to step 6 to create and clone the repo
- Go to GitLab create a Person Access Token
- Login to GitLab with token on your own CLI
0 1 2 3 |
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.
0 1 2 3 4 5 6 7 8 |
$ aws configure list-profiles default $ aws configure list Name Value Type Location ---- ----- ---- -------- profile <not set> None None |
0 1 2 |
./deploy-runner.sh properties.conf us-east-1 default runner-stack |

13. Runner is Online

Testing
- Turn off Instance Runners

2. Confirm AWS Runner

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

