├── runner-monitor-lambda ├── .gitignore ├── package.json ├── tsconfig.json └── src │ ├── lib │ ├── utils.ts │ ├── publishMetrics.ts │ ├── adjustScaling.ts │ ├── config.ts │ ├── asgInfo.ts │ └── gatherMetrics.ts │ └── index.ts ├── Images ├── example-metrics.png ├── available-runners-1.png ├── available-runners-2.png ├── gitlab-runner-demo-asg.png ├── gitlab-runner-asg-update.png ├── gitlab-runner-architecture.png ├── gitlab-runner-autoscaling.png ├── gitlab-runner-lifecycle-hook.png └── gitlab-runner-architecture.drawio ├── CODE_OF_CONDUCT.md ├── sample-runner.properties ├── Dockerfile ├── cycle-runner.sh ├── LICENSE ├── CONTRIBUTING.md ├── gitlab-runner-lifecycle-hook.py ├── deploy-runner.sh ├── README.md └── gitlab-runner.yaml /runner-monitor-lambda/.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | node_modules/ 3 | 4 | *.js 5 | *.js.map 6 | 7 | package-lock.json -------------------------------------------------------------------------------- /Images/example-metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ec2-gitlab-runner/HEAD/Images/example-metrics.png -------------------------------------------------------------------------------- /Images/available-runners-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ec2-gitlab-runner/HEAD/Images/available-runners-1.png -------------------------------------------------------------------------------- /Images/available-runners-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ec2-gitlab-runner/HEAD/Images/available-runners-2.png -------------------------------------------------------------------------------- /Images/gitlab-runner-demo-asg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ec2-gitlab-runner/HEAD/Images/gitlab-runner-demo-asg.png -------------------------------------------------------------------------------- /Images/gitlab-runner-asg-update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ec2-gitlab-runner/HEAD/Images/gitlab-runner-asg-update.png -------------------------------------------------------------------------------- /Images/gitlab-runner-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ec2-gitlab-runner/HEAD/Images/gitlab-runner-architecture.png -------------------------------------------------------------------------------- /Images/gitlab-runner-autoscaling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ec2-gitlab-runner/HEAD/Images/gitlab-runner-autoscaling.png -------------------------------------------------------------------------------- /Images/gitlab-runner-lifecycle-hook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-ec2-gitlab-runner/HEAD/Images/gitlab-runner-lifecycle-hook.png -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /runner-monitor-lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "runner-monitor-lambda", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tsc && copyfiles --up 1 \"src/**/*.ts\" \"bin\" && copyfiles \"node_modules/**\" \"bin\"" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@aws-sdk/client-auto-scaling": "^3.19.0", 13 | "@aws-sdk/client-cloudwatch": "^3.19.0", 14 | "@aws-sdk/client-ec2": "^3.19.0", 15 | "@aws-sdk/types": "^3.18.0", 16 | "@types/aws-lambda": "^8.10.77", 17 | "@types/node": "^15.12.4", 18 | "copyfiles": "^2.4.1", 19 | "typescript": "^4.3.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sample-runner.properties: -------------------------------------------------------------------------------- 1 | VpcID=vpc-0eb9e5f6ee7548e00 2 | SubnetIds=subnet-0efb3c776ffa5cf6a,subnet-00d2a4aaa4301785d 3 | ImageId=ami-0d5eff06f840b45e9 4 | InstanceType=t2.micro 5 | InstanceName=amazon-ec2-gitlab-runner 6 | VolumeSize=200 7 | VolumeType=gp2 8 | MaxSize=4 9 | MinSize=1 10 | DesiredCapacity=1 11 | MaxBatchSize=1 12 | MinInstancesInService=1 13 | MaxInstanceLifetime=604800 14 | GitlabServerURL="https://gitlab.com" 15 | DockerImagePath="registry.gitlab.com/exampleuser/gitlab-runner:latest" 16 | RunnersToken=as8RjBSruy1sdRdP2nZA,LT0-dt4oXgqu2eymabHb 17 | RunnerVersion=v1 18 | RunnerEnvironment=dev 19 | LambdaS3Bucket=amazon-ec2-gitlab-runner-demo 20 | Concurrent=2 21 | CheckInterval=3 22 | CostCenter=10000 23 | AppId=1111 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker:19.03.1 2 | 3 | LABEL runner-executor=docker 4 | LABEL description="A simple docker image for gitlab runner use" 5 | 6 | RUN set -x \ 7 | && echo -e "\e[93m==> Adding runtime dependencies...\e[39m" \ 8 | && apk add --no-cache --virtual .run-deps \ 9 | alpine-sdk \ 10 | make \ 11 | zip \ 12 | bash \ 13 | jq \ 14 | py3-pip \ 15 | python3-dev \ 16 | groff \ 17 | \ 18 | && echo -e "\e[93m==> Udpating pip...\e[39m" \ 19 | && pip3 install --upgrade --no-cache-dir pip \ 20 | \ 21 | && echo -e "\e[93m==> Installing awscli...\e[39m" \ 22 | && pip3 install --no-cache-dir awscli \ 23 | \ 24 | && echo -e "\e[93m==> Installing boto3...\e[39m" \ 25 | && pip3 install --no-cache-dir --user boto3 -------------------------------------------------------------------------------- /cycle-runner.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # SPDX-License-Identifier: MIT-0 5 | 6 | set -e 7 | 8 | if [[ "$#" -lt 2 || "$#" -gt 3 ]] 9 | then 10 | echo "Incorrect number of arguments" 11 | echo "Usage: $0 " 12 | exit 1 13 | fi 14 | 15 | AutoScalingGroupName=$1 16 | echo "Cycle instances in autoscaling group: "$AutoScalingGroupName 17 | 18 | region=$2 19 | 20 | if [[ -n "$3" ]]; then 21 | profile=$3 22 | IGNORE=$(aws sts get-caller-identity --profile "${profile}") 23 | else 24 | profile="default" 25 | echo "No AWS profile was provided, hence using the default profile." 26 | fi 27 | 28 | aws autoscaling start-instance-refresh \ 29 | --auto-scaling-group-name="${AutoScalingGroupName}" \ 30 | --region="${region}" \ 31 | --profile="${profile}" 32 | 33 | echo "Refresh initiated" 34 | 35 | exit 0 36 | 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /runner-monitor-lambda/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ 4 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 5 | "strict": true, /* Enable all strict type-checking options. */ 6 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 7 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 8 | "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ 9 | "sourceMap": true, 10 | "outDir": "bin", 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /runner-monitor-lambda/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | 3 | export const sumReducer = (previous: number, current : number) : number => { 4 | if (current) { 5 | return previous + current 6 | } 7 | return previous 8 | } 9 | 10 | export const fetchUrl = (url: string): Promise => { 11 | return new Promise( 12 | (onSuccess, onError) => { 13 | const options: http.RequestOptions = { 14 | timeout: 15 * 1000, 15 | } 16 | 17 | let data = "" 18 | const request = http.request(url, options, (response) => { 19 | response.setEncoding('utf8'); 20 | response.on('data', (dataChunk) => { 21 | data += dataChunk 22 | }) 23 | response.on('end', () => { 24 | onSuccess(data) 25 | }) 26 | }) 27 | 28 | request.on('error', (error) => { 29 | onError(error) 30 | }) 31 | 32 | request.end() 33 | } 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /runner-monitor-lambda/src/lib/publishMetrics.ts: -------------------------------------------------------------------------------- 1 | import * as config from './config' 2 | import {ClusterMetrics} from "./gatherMetrics"; 3 | import * as cw from "@aws-sdk/client-cloudwatch"; 4 | 5 | export const publishMetrics = async (metrics : ClusterMetrics, desiredCapacity : number) => { 6 | const now = new Date() 7 | const standardUnitCount = "Count" 8 | 9 | const jobCountMetric : cw.MetricDatum = { 10 | Value: metrics.currentJobCount, 11 | MetricName: config.runnerJobCountMetricName(), 12 | Timestamp: now, 13 | Unit: standardUnitCount, 14 | } 15 | 16 | const targetCapacityMetric : cw.MetricDatum = { 17 | Value: desiredCapacity, 18 | MetricName: config.runnerTargetCapacityMetricName(), 19 | Timestamp: now, 20 | Unit: standardUnitCount, 21 | } 22 | 23 | const actualCapacityMetric : cw.MetricDatum = { 24 | Value: metrics.currentInstances, 25 | MetricName: config.runnerActualCapacityMetricName(), 26 | Timestamp: now, 27 | Unit: standardUnitCount, 28 | } 29 | 30 | const client = new cw.CloudWatchClient({ 31 | tls: true, 32 | }) 33 | 34 | const command = new cw.PutMetricDataCommand({ 35 | MetricData: [ 36 | jobCountMetric, 37 | targetCapacityMetric, 38 | actualCapacityMetric, 39 | ], 40 | Namespace: config.runnerMetricNamespace(), 41 | }) 42 | 43 | await client.send(command) 44 | } -------------------------------------------------------------------------------- /runner-monitor-lambda/src/lib/adjustScaling.ts: -------------------------------------------------------------------------------- 1 | import * as asg from "@aws-sdk/client-auto-scaling"; 2 | import {ClusterMetrics} from "./gatherMetrics"; 3 | import {SdkError} from "@aws-sdk/types"; 4 | 5 | export const adjustScaling = async (autoScalingGroupName : string, metrics : ClusterMetrics, desiredCount : number) => { 6 | if (metrics.currentInstances == desiredCount) { 7 | // nothing to do 8 | console.log(`Autoscaling group capacity does not require updates`) 9 | return 10 | } 11 | 12 | const client = new asg.AutoScalingClient({ 13 | tls: true, 14 | }) 15 | 16 | let honorCooldown: boolean 17 | if (metrics.currentInstances < desiredCount) { 18 | // we need to scale up, so do it immediately 19 | honorCooldown = false 20 | } else { 21 | // we want to scale down, but might have more requests coming in, 22 | // use the ASG delay 23 | honorCooldown = true 24 | } 25 | 26 | try { 27 | const command = new asg.SetDesiredCapacityCommand({ 28 | AutoScalingGroupName: autoScalingGroupName, 29 | DesiredCapacity: desiredCount, 30 | HonorCooldown: honorCooldown, 31 | }) 32 | console.log(`Setting desired capacity to ${desiredCount}`) 33 | await client.send(command) 34 | } catch (ex) { 35 | const checkError = ex as SdkError 36 | if (checkError?.name == "ScalingActivityInProgress") { 37 | console.log(`Cannot modify autoscaling, since scaling is still in-progress, or in cool-down.`) 38 | } else { 39 | throw ex 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /runner-monitor-lambda/src/lib/config.ts: -------------------------------------------------------------------------------- 1 | 2 | export const autoScalingGroupName = () : string => { 3 | if (process.env.AUTOSCALING_GROUP_NAME) { 4 | return process.env.AUTOSCALING_GROUP_NAME 5 | } 6 | throw Error("Environment Variable AUTOSCALING_GROUP_NAME not found") 7 | } 8 | 9 | // the number of jobs that can run concurrently per runner 10 | export const maximumConcurrentJobsPerRunner = () : number => { 11 | if (process.env.MAXIMUM_CONCURRENT_JOBS_PER_RUNNER) { 12 | const count = parseInt(process.env.MAXIMUM_CONCURRENT_JOBS_PER_RUNNER) 13 | if (count) { 14 | return count 15 | } 16 | } 17 | throw Error("Environment Variable MAXIMUM_CONCURRENT_JOBS_PER_RUNNER not found") 18 | } 19 | 20 | // the number of new jobs we want to be able to handle within triggering another scaling event 21 | export const countOfNewJobsBeforeScaling = () : number => { 22 | if (process.env.COUNT_OF_NEW_JOBS_BEFORE_SCALING) { 23 | const count = parseInt(process.env.COUNT_OF_NEW_JOBS_BEFORE_SCALING) 24 | if (count) { 25 | return count 26 | } 27 | } 28 | throw Error("Environment Variable COUNT_OF_NEW_JOBS_BEFORE_SCALING not found") 29 | } 30 | 31 | export const runnerMetricNamespace = () : string => { 32 | if (process.env.RUNNER_METRIC_NAMESPACE) { 33 | return process.env.RUNNER_METRIC_NAMESPACE 34 | } 35 | throw Error("Environment Variable RUNNER_METRIC_NAMESPACE not found") 36 | } 37 | 38 | export const runnerJobCountMetricName = () : string => { 39 | if (process.env.RUNNER_JOB_COUNT_METRIC_NAME) { 40 | return process.env.RUNNER_JOB_COUNT_METRIC_NAME 41 | } 42 | throw Error("Environment Variable RUNNER_JOB_COUNT_METRIC_NAME not found") 43 | } 44 | 45 | export const runnerTargetCapacityMetricName = () : string => { 46 | if (process.env.RUNNER_TARGET_CAPACITY_METRIC_NAME) { 47 | return process.env.RUNNER_TARGET_CAPACITY_METRIC_NAME 48 | } 49 | throw Error("Environment Variable RUNNER_TARGET_CAPACITY_METRIC_NAME not found") 50 | } 51 | 52 | export const runnerActualCapacityMetricName = () : string => { 53 | if (process.env.RUNNER_ACTUAL_CAPACITY_METRIC_NAME) { 54 | return process.env.RUNNER_ACTUAL_CAPACITY_METRIC_NAME 55 | } 56 | throw Error("Environment Variable RUNNER_ACTUAL_CAPACITY_METRIC_NAME not found") 57 | } -------------------------------------------------------------------------------- /runner-monitor-lambda/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import * as config from './lib/config' 20 | import * as lambda from "aws-lambda"; 21 | import {ClusterMetrics, gatherClusterMetrics} from "./lib/gatherMetrics"; 22 | import {publishMetrics} from "./lib/publishMetrics"; 23 | import {adjustScaling} from "./lib/adjustScaling"; 24 | 25 | export const handler = async (event : lambda.ScheduledEvent, context : lambda.Context | null) : Promise => { 26 | const autoScalingGroupName = config.autoScalingGroupName() 27 | 28 | console.log(`Retrieving cluster metrics`) 29 | const metrics = await gatherClusterMetrics(autoScalingGroupName) 30 | 31 | console.log(`Determining desired capacity`) 32 | const desiredCapacity = determineDesiredCapacity(metrics) 33 | console.log(`Desired Capacity: ${desiredCapacity}`) 34 | 35 | console.log(`Publishing metrics`) 36 | await publishMetrics(metrics, desiredCapacity) 37 | 38 | console.log(`Adjusting autoscaling`) 39 | await adjustScaling(autoScalingGroupName, metrics, desiredCapacity) 40 | 41 | console.log(`Done`) 42 | } 43 | 44 | const determineDesiredCapacity = (metrics : ClusterMetrics) : number => { 45 | // the number of jobs that can run concurrently per runner 46 | const concurrentJobsPerRunner = config.maximumConcurrentJobsPerRunner() 47 | // the number of new jobs we want to be able to handle within triggering another scaling event 48 | const newJobCountBeforeScaling = config.countOfNewJobsBeforeScaling(); 49 | 50 | // pretend like we're going to add more jobs, and figure out the number of instances we need to handle that 51 | let desiredCapacity = (metrics.currentJobCount + newJobCountBeforeScaling) / concurrentJobsPerRunner 52 | desiredCapacity = Math.ceil(desiredCapacity) 53 | // Make sure we dont go below our min 54 | desiredCapacity = Math.max(desiredCapacity, metrics.minInstances) 55 | // Make sure we dont go above our max 56 | desiredCapacity = Math.min(desiredCapacity, metrics.maxInstances) 57 | return desiredCapacity 58 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /runner-monitor-lambda/src/lib/asgInfo.ts: -------------------------------------------------------------------------------- 1 | import * as asg from "@aws-sdk/client-auto-scaling"; 2 | import * as ec2 from "@aws-sdk/client-ec2"; 3 | 4 | export interface AutoScalingGroupInfo { 5 | autoScalingGroupName: string 6 | maxSize: number 7 | minSize: number 8 | currentCapacity: number 9 | instanceIds: string[] 10 | } 11 | 12 | export interface Ec2InstanceInfo { 13 | instanceId: string 14 | privateIpAddress: string 15 | launchTime: Date | undefined 16 | } 17 | 18 | export const getAutoscalingGroupInfo = async (autoScalingGroupName : string): Promise => { 19 | const client = new asg.AutoScalingClient({ 20 | tls: true, 21 | }) 22 | 23 | const command = new asg.DescribeAutoScalingGroupsCommand({ 24 | AutoScalingGroupNames: [ 25 | autoScalingGroupName, 26 | ] 27 | }) 28 | 29 | const response = await client.send(command) 30 | if (response.AutoScalingGroups == null || response.AutoScalingGroups.length == 0) { 31 | throw new Error("autoScalingGroup not found") 32 | } 33 | const group = response.AutoScalingGroups[0] 34 | if (group.Instances == null) { 35 | throw new Error("autoScalingGroup.Instances not found") 36 | } 37 | if (group.MinSize == null) { 38 | throw new Error("autoScalingGroup.MinSize not found") 39 | } 40 | if (group.MaxSize == null) { 41 | throw new Error("autoScalingGroup.MaxSize not found") 42 | } 43 | if (group.DesiredCapacity == null) { 44 | throw new Error("autoScalingGroup.DesiredCapacity not found") 45 | } 46 | 47 | const activeInstances = group.Instances.filter(i => { 48 | switch (i.LifecycleState) { 49 | case asg.LifecycleState.IN_SERVICE: 50 | case asg.LifecycleState.PENDING: 51 | return true 52 | default: 53 | return false 54 | } 55 | }) 56 | 57 | const activeInstanceIds = activeInstances.map((i) => i.InstanceId || "").filter(i => i != "") 58 | 59 | return { 60 | autoScalingGroupName: autoScalingGroupName, 61 | instanceIds: activeInstanceIds, 62 | maxSize: group.MaxSize, 63 | minSize: group.MinSize, 64 | currentCapacity: group.DesiredCapacity, 65 | } 66 | } 67 | 68 | export const getEc2InstanceInfo = async (instanceIds: string[]): Promise => { 69 | const client = new ec2.EC2Client({ 70 | tls: true, 71 | }) 72 | 73 | const command = new ec2.DescribeInstancesCommand({ 74 | InstanceIds: instanceIds, 75 | }) 76 | 77 | const result = await client.send(command) 78 | if (result.Reservations == null) { 79 | throw new Error("instance reservations not found") 80 | } 81 | 82 | const runnerInstances: ec2.Instance[] = [] 83 | 84 | for (let x = 0; x < result.Reservations.length; x++) { 85 | const r = result.Reservations[x] 86 | if (r.Instances == null) { 87 | continue 88 | } 89 | 90 | const validInstances = r.Instances.filter(instance => { 91 | if (instance == null || instance.PrivateIpAddress == null) { 92 | return false 93 | } 94 | 95 | if (instance.State?.Name != "running") { 96 | return false 97 | } 98 | 99 | return true 100 | }) 101 | 102 | runnerInstances.push(...validInstances) 103 | } 104 | 105 | const instanceData = runnerInstances.map((instance): Ec2InstanceInfo => { 106 | return { 107 | instanceId: instance.InstanceId as string, 108 | privateIpAddress: instance.PrivateIpAddress as string, 109 | launchTime: instance.LaunchTime, 110 | } 111 | }) 112 | 113 | return instanceData 114 | } 115 | -------------------------------------------------------------------------------- /runner-monitor-lambda/src/lib/gatherMetrics.ts: -------------------------------------------------------------------------------- 1 | // assumptions: < 100 ec2 instances to support the gitlab runners 2 | 3 | import {fetchUrl, sumReducer} from "./utils"; 4 | import {Ec2InstanceInfo, getAutoscalingGroupInfo, getEc2InstanceInfo} from "./asgInfo"; 5 | 6 | interface InstanceMetrics { 7 | instanceId: string 8 | jobCount: number 9 | error : string | null 10 | } 11 | 12 | export interface ClusterMetrics { 13 | minInstances: number 14 | maxInstances: number 15 | currentInstances: number 16 | activeInstances: number 17 | currentJobCount: number 18 | instances : InstanceMetrics[], 19 | } 20 | 21 | export const gatherClusterMetrics = async (autoScalingGroupName : string) : Promise => { 22 | const asgInfo = await getAutoscalingGroupInfo(autoScalingGroupName) 23 | if (asgInfo.instanceIds.length == 0) { 24 | console.log("no instances available in the autoScalingGroup") 25 | return { 26 | currentJobCount: 0, 27 | currentInstances: asgInfo.currentCapacity, 28 | activeInstances: 0, 29 | minInstances: asgInfo.minSize, 30 | maxInstances: asgInfo.maxSize, 31 | instances: [], 32 | } 33 | } 34 | 35 | console.log(`Instances in autoscaling group: ${asgInfo.instanceIds.length}`) 36 | const runnerInstanceInfo = await getEc2InstanceInfo(asgInfo.instanceIds) 37 | if (runnerInstanceInfo.length == 0) { 38 | console.log("no runner instances are available in the autoScalingGroup") 39 | return { 40 | currentJobCount: 0, 41 | currentInstances: asgInfo.currentCapacity, 42 | activeInstances: 0, 43 | minInstances: asgInfo.minSize, 44 | maxInstances: asgInfo.maxSize, 45 | instances: [], 46 | } 47 | } 48 | 49 | const asyncMetrics = runnerInstanceInfo.map((instance) => { 50 | return getMetricsForInstance(instance) 51 | }) 52 | 53 | const metrics = await Promise.all(asyncMetrics) 54 | 55 | console.log(`All metrics retrieved`) 56 | 57 | const totalBuildCount = metrics 58 | .map((metric) => metric.jobCount) 59 | .reduce(sumReducer, 0) 60 | 61 | console.log(`Total build count: ${totalBuildCount}`) 62 | 63 | const activeInstanceCount = metrics 64 | .filter(metric => instanceHasErrors(metric) == false) 65 | .length 66 | 67 | console.log(`Active Instance Count: ${activeInstanceCount}`) 68 | 69 | return { 70 | currentJobCount: totalBuildCount, 71 | currentInstances: asgInfo.currentCapacity, 72 | activeInstances: activeInstanceCount, 73 | minInstances: asgInfo.minSize, 74 | maxInstances: asgInfo.maxSize, 75 | instances: metrics, 76 | } 77 | } 78 | 79 | const getMetricsForInstance = async (instance: Ec2InstanceInfo): Promise => { 80 | const prometheusUrl = `http://${instance.privateIpAddress}:9252/metrics` 81 | try { 82 | console.log(`Fetching metrics from url: ${prometheusUrl}`) 83 | const prometheusContent = await fetchUrl(prometheusUrl) 84 | 85 | const buildCount = countCurrentBuilds(prometheusContent) 86 | console.log(`For url: ${prometheusUrl} Build Count is ${buildCount}`) 87 | return { 88 | instanceId : instance.instanceId, 89 | jobCount : buildCount, 90 | error: null, 91 | } 92 | } catch (ex) { 93 | if (ex instanceof Error) { 94 | console.log(`Error getting metrics from url ${prometheusUrl}, error: ${ex}`) 95 | return { 96 | instanceId: instance.instanceId, 97 | jobCount: 0, 98 | error: ex.toString() 99 | } 100 | } else { 101 | throw ex 102 | } 103 | } 104 | } 105 | 106 | const countCurrentBuilds = (prometheusContent: string): number => { 107 | const contentLines = prometheusContent.split('\n') 108 | const buildCount = contentLines 109 | .filter((line) => { 110 | // Find the lines that correspond to the job counts 111 | return line.startsWith("gitlab_runner_jobs{") 112 | }) 113 | .map((line) => { 114 | // The last field in this line is the # of builds 115 | const lineParts = line.split(' ') 116 | const counterText = lineParts[lineParts.length - 1] 117 | return parseInt(counterText) 118 | }) 119 | .reduce(sumReducer, 0) 120 | return buildCount 121 | } 122 | 123 | export const instanceHasErrors = (instance : InstanceMetrics) : boolean => { 124 | return (instance.error) ? true : false 125 | } -------------------------------------------------------------------------------- /gitlab-runner-lifecycle-hook.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | import boto3 5 | import json 6 | import logging 7 | import time 8 | import os 9 | 10 | logger = logging.getLogger() 11 | logger.setLevel(logging.DEBUG) 12 | ssm_client = boto3.client("ssm") 13 | 14 | LIFECYCLE_KEY = "LifecycleHookName" 15 | ASG_KEY = "AutoScalingGroupName" 16 | EC2_KEY = "EC2InstanceId" 17 | DOCUMENT_NAME = os.environ['DOCUMENT_NAME'] 18 | DOCUMENT_LOG_GROUP_NAME = os.environ['DOCUMENT_LOG_GROUP_NAME'] 19 | RETRY_IF_INSTANCE_DOES_NOT_EXIST = os.getenv('RETRY_IF_INSTANCE_DOES_NOT_EXIST', '') 20 | RESPONSE_DOCUMENT_KEY = "DocumentIdentifiers" 21 | 22 | def check_response(response_json): 23 | try: 24 | if response_json['ResponseMetadata']['HTTPStatusCode'] == 200: 25 | return True 26 | else: 27 | return False 28 | except KeyError: 29 | return False 30 | 31 | def list_document(): 32 | document_filter_parameters = {'key': 'Name', 'value': DOCUMENT_NAME} 33 | response = ssm_client.list_documents( 34 | DocumentFilterList=[ document_filter_parameters ] 35 | ) 36 | return response 37 | 38 | def check_document(): 39 | # If the document already exists, it will not create it. 40 | try: 41 | response = list_document() 42 | if check_response(response): 43 | logger.info("Documents list: %s", response) 44 | if response[RESPONSE_DOCUMENT_KEY]: 45 | logger.info("Documents exists: %s", response) 46 | return True 47 | else: 48 | return False 49 | else: 50 | logger.error("Documents' list error: %s", response) 51 | return False 52 | except Exception as e: 53 | logger.error("Document error: %s", str(e)) 54 | return None 55 | 56 | def send_command(instance_id): 57 | # Until the document is not ready, waits in accordance to a backoff mechanism. 58 | while True: 59 | timewait = 1 60 | response = list_document() 61 | if any(response[RESPONSE_DOCUMENT_KEY]): 62 | break 63 | time.sleep(timewait) 64 | timewait += timewait 65 | 66 | while True: 67 | logger.info("Sending SSM Run Command") 68 | try: 69 | response = ssm_client.send_command( 70 | InstanceIds = [ instance_id ], 71 | DocumentName = DOCUMENT_NAME, 72 | TimeoutSeconds = 120, 73 | CloudWatchOutputConfig={ 74 | 'CloudWatchLogGroupName': DOCUMENT_LOG_GROUP_NAME, 75 | 'CloudWatchOutputEnabled': True 76 | } 77 | ) 78 | if check_response(response): 79 | logger.info("Command sent: %s", response) 80 | return response['Command']['CommandId'] 81 | else: 82 | logger.error("Command could not be sent: %s", response) 83 | return None 84 | except Exception as e: 85 | logger.error("Command could not be sent: %s", str(e)) 86 | if RETRY_IF_INSTANCE_DOES_NOT_EXIST == "1": 87 | error_text = str(e) 88 | if error_text.find("InvalidInstanceId") != -1: 89 | logger.info("InvalidInstanceId, retrying after 40 seconds") 90 | time.sleep(40) 91 | pass 92 | continue 93 | else: 94 | return None 95 | else: 96 | return None 97 | 98 | def check_command(command_id, instance_id): 99 | timewait = 1 100 | while True: 101 | response_iterator = ssm_client.list_command_invocations( 102 | CommandId = command_id, 103 | InstanceId = instance_id, 104 | Details=False 105 | ) 106 | if check_response(response_iterator): 107 | if response_iterator['CommandInvocations']: 108 | response_iterator_status = response_iterator['CommandInvocations'][0]['Status'] 109 | if response_iterator_status != 'Pending': 110 | if response_iterator_status == 'InProgress' or response_iterator_status == 'Success': 111 | logging.info( "Status: %s", response_iterator_status) 112 | return True 113 | else: 114 | logging.error("ERROR: status: %s", response_iterator) 115 | return False 116 | time.sleep(timewait) 117 | timewait += timewait 118 | 119 | def abandon_lifecycle(life_cycle_hook, auto_scaling_group, instance_id): 120 | asg_client = boto3.client('autoscaling') 121 | try: 122 | response = asg_client.complete_lifecycle_action( 123 | LifecycleHookName=life_cycle_hook, 124 | AutoScalingGroupName=auto_scaling_group, 125 | LifecycleActionResult='ABANDON', 126 | InstanceId=instance_id 127 | ) 128 | if check_response(response): 129 | logger.info("Lifecycle hook abandoned correctly: %s", response) 130 | else: 131 | logger.error("Lifecycle hook could not be abandoned: %s", response) 132 | except Exception as e: 133 | logger.error("Lifecycle hook abandon could not be executed: %s", str(e)) 134 | return None 135 | 136 | def lambda_handler(event, context): 137 | try: 138 | logger.info(json.dumps(event)) 139 | message = event['detail'] 140 | if LIFECYCLE_KEY in message and ASG_KEY in message: 141 | life_cycle_hook = message[LIFECYCLE_KEY] 142 | auto_scaling_group = message[ASG_KEY] 143 | instance_id = message[EC2_KEY] 144 | if check_document(): 145 | command_id = send_command(instance_id) 146 | if command_id != None: 147 | if check_command(command_id, instance_id): 148 | logging.info("Lambda executed correctly") 149 | else: 150 | abandon_lifecycle(life_cycle_hook, auto_scaling_group, instance_id) 151 | else: 152 | abandon_lifecycle(life_cycle_hook, auto_scaling_group, instance_id) 153 | else: 154 | abandon_lifecycle(life_cycle_hook, auto_scaling_group, instance_id) 155 | else: 156 | logging.error("No valid JSON message: %s", message) 157 | except Exception as e: 158 | logging.error("Error: %s", str(e)) 159 | -------------------------------------------------------------------------------- /deploy-runner.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # SPDX-License-Identifier: MIT-0 5 | 6 | if [[ "$#" -ne 4 ]] 7 | then 8 | echo "Incorrect number of arguments" 9 | echo "Usage: $0 " 10 | exit 1 11 | fi 12 | 13 | errorExit() 14 | { 15 | echo $1 16 | exit 1 17 | } 18 | 19 | callCloudFormation() 20 | { 21 | aws cloudformation $1 \ 22 | --stack-name ${stack_name} \ 23 | --template-body file://gitlab-runner.yaml \ 24 | --capabilities CAPABILITY_NAMED_IAM \ 25 | --profile ${profile} \ 26 | --region ${region} \ 27 | --parameters \ 28 | ParameterKey=VpcID,ParameterValue=${VpcID} \ 29 | ParameterKey=SubnetIds,ParameterValue=\"${SubnetIds}\" \ 30 | ParameterKey=ImageId,ParameterValue=\"${ImageId}\" \ 31 | ParameterKey=InstanceType,ParameterValue=${InstanceType} \ 32 | ParameterKey=InstanceName,ParameterValue=${InstanceName} \ 33 | ParameterKey=VolumeSize,ParameterValue=${VolumeSize} \ 34 | ParameterKey=VolumeType,ParameterValue=${VolumeType} \ 35 | ParameterKey=MaxSize,ParameterValue=${MaxSize} \ 36 | ParameterKey=MinSize,ParameterValue=${MinSize} \ 37 | ParameterKey=DesiredCapacity,ParameterValue=${DesiredCapacity} \ 38 | ParameterKey=MaxBatchSize,ParameterValue=${MaxBatchSize} \ 39 | ParameterKey=MinInstancesInService,ParameterValue=${MinInstancesInService} \ 40 | ParameterKey=MaxInstanceLifetime,ParameterValue=${MaxInstanceLifetime} \ 41 | ParameterKey=GitlabServerURL,ParameterValue=${GitlabServerURL} \ 42 | ParameterKey=DockerImagePath,ParameterValue=${DockerImagePath} \ 43 | ParameterKey=RunnersToken,ParameterValue=\"${RunnersToken}\" \ 44 | ParameterKey=RunnerVersion,ParameterValue=${RunnerVersion} \ 45 | ParameterKey=RunnerEnvironment,ParameterValue=${RunnerEnvironment} \ 46 | ParameterKey=LambdaS3Bucket,ParameterValue=${LambdaS3Bucket} \ 47 | ParameterKey=TimeStamp,ParameterValue=$2 \ 48 | ParameterKey=Concurrent,ParameterValue=${Concurrent} \ 49 | ParameterKey=CheckInterval,ParameterValue=${CheckInterval} \ 50 | ParameterKey=CostCenter,ParameterValue=${CostCenter} \ 51 | ParameterKey=AppId,ParameterValue=${AppId} 52 | } 53 | 54 | verifyStackSuccess() 55 | { 56 | status=$(aws cloudformation describe-stacks \ 57 | --stack-name ${stack_name} \ 58 | --query "Stacks[0].StackStatus" \ 59 | --profile ${profile} \ 60 | --region ${region} \ 61 | --output text) \ 62 | || errorExit "Error getting stack status" 2> /dev/null 63 | 64 | if [ "${status}" == "$1" ]; then 65 | echo "Stack $2 was successful" 66 | else 67 | errorExit "Stack $2 failed (status ${status}). See CloudFormation console for error details" 68 | fi 69 | } 70 | 71 | uploadLifecycleHookToS3() 72 | { 73 | file_name="${stack_name}-lifecycle-hook-${time_stamp}.zip" 74 | case "$OSTYPE" in 75 | solaris*) echo "SOLARIS" ;; 76 | darwin*) echo "OSX" & zip ${file_name} gitlab-runner-lifecycle-hook.py;; 77 | linux*) echo "LINUX" & zip ${file_name} gitlab-runner-lifecycle-hook.py;; 78 | bsd*) echo "BSD" ;; 79 | msys|cygwin*) echo "WINDOWS" & set MSYS_NO_PATHCONV=1 & jar -cvf ${file_name} gitlab-runner-lifecycle-hook.py;; 80 | *) echo "unknown: $OSTYPE" ;; 81 | esac 82 | 83 | aws s3 cp --profile ${profile} ${file_name} s3://${LambdaS3Bucket}/${stack_name}/${file_name} 84 | if [ $? -ne 0 ]; then 85 | errorExit "Uploading lifecycle-hook lambda function code to S3 did not complete successfully." 86 | fi 87 | rm -f ${file_name} 88 | } 89 | 90 | uploadRunnerMonitorToS3() { 91 | # Note: Current lambda runtime for nodejs14 only supports aws-sdk 2.888.0, 92 | # this project uses 3.x, so we have to include the aws-sdk in our function 93 | 94 | file_name="${stack_name}-runner-monitor-${time_stamp}.zip" 95 | 96 | cd runner-monitor-lambda 97 | npm install 98 | npm run build 99 | cd bin 100 | 101 | case "$OSTYPE" in 102 | solaris*) echo "SOLARIS" ;; 103 | darwin*) echo "OSX" & zip -r ../../${file_name} *;; 104 | linux*) echo "LINUX" & zip -r ../../${file_name} *;; 105 | bsd*) echo "BSD" ;; 106 | msys|cygwin*) echo "WINDOWS" & set MSYS_NO_PATHCONV=1 & jar -cvf ../../${file_name} *;; 107 | *) echo "unknown: $OSTYPE" ;; 108 | esac 109 | 110 | cd ../.. 111 | 112 | aws s3 cp --profile ${profile} ${file_name} s3://${LambdaS3Bucket}/${stack_name}/${file_name} 113 | if [ $? -ne 0 ]; then 114 | errorExit "Uploading lifecycle-hook lambda function code to S3 did not complete successfully." 115 | fi 116 | rm -f ${file_name} 117 | } 118 | 119 | deployStack() 120 | { 121 | echo -n "Checking if stack already exists..." 122 | aws cloudformation describe-stacks --stack-name ${stack_name} --profile ${profile} --region ${region} --no-paginate --query="Stacks[].{Name: StackName}" 123 | 124 | if [ $? -ne 0 ]; then 125 | echo "Stack does not exist, creating it..." 126 | callCloudFormation "create-stack" ${time_stamp} 127 | 128 | if [ $? -ne 0 ]; then 129 | errorExit "Stack creation failed" 130 | fi 131 | 132 | echo -n "Waiting for stack creation to complete..." 133 | aws cloudformation wait stack-create-complete --stack-name ${stack_name} --profile ${profile} --region ${region} 2> /dev/null 134 | if [ $? -ne 0 ]; then 135 | echo 136 | echo -n "Stack creation completed with failure..." 137 | fi 138 | 139 | verifyStackSuccess "CREATE_COMPLETE" "create" 140 | else 141 | echo "Stack already exists. Attempting to update it..." 142 | callCloudFormation "update-stack" ${time_stamp} 2> temp.txt 143 | 144 | status=$? 145 | Temp=`cat temp.txt` 146 | rm -f temp.txt 147 | 148 | if [ ${status} -ne 0 ]; then 149 | if echo ${Temp} | grep "No updates are to be performed" > /dev/null ; then 150 | echo "No updates are needed" 151 | exit 0 152 | else 153 | errorExit "Stack update failed: ${Temp}" 154 | fi 155 | fi 156 | 157 | echo "Waiting for stack update to complete..." 158 | aws cloudformation wait stack-update-complete --stack-name ${stack_name} --profile ${profile} --region ${region} 2> /dev/null 159 | if [ $? -ne 0 ]; then 160 | echo "Stack update completed with failure..." 161 | fi 162 | 163 | verifyStackSuccess "UPDATE_COMPLETE" "update" 164 | fi 165 | } 166 | 167 | # Read properties for the stack 168 | source $1 || errorExit "Unable to load properties" 169 | 170 | region=$2 171 | profile=$3 172 | stack_name=$4 173 | 174 | echo "Deploying Gitlab runner in region: "$region", using AWS profile: "$profile 175 | echo "CloudFormation stack name: "$stack_name 176 | 177 | #Suffix for the lambda zip 178 | time_stamp=`date "+%Y%m%d%H%M%S"` 179 | 180 | uploadLifecycleHookToS3 181 | uploadRunnerMonitorToS3 182 | deployStack -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deploy and Manage Gitlab Runners on Amazon EC2 2 | 3 | This solution automates Gitlab Runner deployment and administrative tasks on Amazon EC2 through Infrastructure-as-Code (IaC). 4 | 5 | The solution is published in this [blog post](https://aws.amazon.com/blogs/devops/deploy-and-manage-gitlab-runners-on-amazon-ec2/). 6 | 7 | > NOTE: The solution presented in this post illustrates a possible way of implementing Gitlab runner autoscaling. Support for this solution is limited. A vendor supported solution is available here: [GitLab HA Scaling Runner Vending Machine for AWS EC2 ASG](https://gitlab.com/guided-explorations/aws/gitlab-runner-autoscaling-aws-asg). 8 | As a result of this [breaking change](https://gitlab.com/gitlab-org/gitlab/-/issues/380872), the solution's token registration process has been be updated on 04/22/2024. The previsou code has been tagged as v1.0. 9 | 10 | ## Overview of the solution 11 | The following diagram displays the solution architecture. It also shows the workflow of deploying the Gitlab Runner and registering it to Gitlab projects. 12 | 13 | 14 | 1. We use AWS CloudFormation to describe the infrastructure that is hosting the Gitlab Runner. The user runs a deploy script in order to deploy the CloudFormation template. The template is parameterized, and the parameters are defined in a properties file. The properties file specifies the infrastructure configuration, as well as the environment in which to deploy the template. 15 | 16 | 2. The deploy script calls CloudFormation CreateStack API to create a Gitlab Runner stack in the specified environment. 17 | 18 | 3. During stack creation, an EC2 autoscaling group is created with the desired number of EC2 instances. Each instance is launched via a launch template, which is created with values from the properties file. An IAM role is created and attached to the EC2 instance. The role contains permissions required for the Gitlab Runner to execute pipeline jobs. A lifecycle hook is attached to the autoscaling group on instance termination events. This ensures graceful instance termination. 19 | 20 | 4. During instance launch, CloudFormation uses a cfn-init helper script to install and configure the Gitlab Runner: 21 | 22 | - (4a) cfn-init installs the Gitlab Runner software on the EC2 instance. 23 | - (4b) cfn-init configures the Gitlab Runner as a docker executor using a pre-defined docker image in the Gitlab Container Registry. The docker executor implementation lets the Gitlab Runner run each build in a separate and isolated container. The docker image contains the software required to run the pipeline workloads, thereby eliminating the need to install these packages during each build. 24 | - (4c) cfn-init registers the Gitlab Runner to Gitlab projects specified in the properties file, so that these projects can utilize the Gitlab Runner to run pipelines. 25 | 26 | 5. The user may repeat the same steps to deploy Gitlab Runner into another environment. 27 | 28 | ## Prerequisites 29 | For this walkthrough, you need the following: 30 | - A Gitlab account (all tiers including Gitlab Free self-managed, Gitlab Free SaaS, and higher tiers). This demo uses gitlab.com free tier. 31 | - A Gitlab Container Registry. 32 | - A [Git client](https://git-scm.com/downloads) to clone the source code provided. 33 | - An AWS account with local credentials properly configured (typically under ~/.aws/credentials) with these permissions: 34 | - AmazonEC2FullAccess 35 | - AutoScalingFullAccess 36 | - AmazonS3FullAccess 37 | - AmazonSSMFullAccess 38 | - AmazonEventBridgeFullAccess 39 | - AWSCloudFormationFullAccess 40 | - AWSLambda_FullAccess 41 | - IAMFullAccess 42 | - AmazonECS_FullAccess 43 | - AmazonEC2ContainerRegistryPowerUser 44 | - The latest version of the AWS CLI. For more information, see [Installing, updating, and uninstalling the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html). 45 | - Docker is installed and running on the localhost/laptop. 46 | - Nodejs and npm installed on the localhost/laptop. 47 | - A VPC with 2 private subnets and that is connected to the internet via NAT gateway allowing outbound traffic. 48 | - The following IAM service-linked role created in the AWS account: AWSServiceRoleForAutoScaling 49 | - An S3 bucket for storing Lambda deployment packages. 50 | - Familiarity with Git, Gitlab CI/CD, Docker, EC2, and CloudFormation. 51 | 52 | ## Build a docker executor image for the Gitlab Runner 53 | The Gitlab Runner in this solution is implemented as docker executor. The Docker executor connects to Docker Engine and runs each build in a separate and isolated container via a predefined docker image. The first step in deploying the Gitlab Runner is building a docker executor image. We provided a simple Dockerfile in order to build this image. You may customize the Dockerfile to install your own requirements. 54 | 55 | ### To build a docker image using the sample Dockerfile: 56 | 1. Create a directory where we will store our demo code. From your terminal run: 57 | ``` 58 | mkdir demo-repos && cd demo-repos 59 | ``` 60 | 2. Clone the source code repository found in the following location: 61 | ``` 62 | git clone 63 | ``` 64 | 3. Create a new project on your Gitlab server. Name the project any name you like. 65 | 4. Clone your newly created repo to your laptop. Ignore the warning about cloning an empty repository. 66 | ``` 67 | git clone 68 | ``` 69 | 5. Copy the demo repo files into your newly created repo on your laptop, and push it to your Gitlab repository. You may customize the Dockerfile before pushing it to Gitlab. 70 | ``` 71 | cp -r amazon-ec2-gitlab-runner/* 72 | cd 73 | git add . 74 | git commit -m “Initial commit” 75 | git push 76 | ``` 77 | 6. On the Gitlab console, go to your repository’s Package & Registries -> Container Registry. Follow the instructions provided on the Container Registry page in order to build and push a docker image to your repository’s container registry. 78 | 79 | ## Deploy the Gitlab Runner stack 80 | Once the docker executor image has been pushed to the Gitlab Container Registry, we can deploy the Gitlab Runner. The Gitlab Runner infrastructure is described in the Cloudformation template gitlab-runner.yaml. Its configuration is stored in a properties file called sample-runner.properties. A launch template is created with the values in the properties file. Then it is used to launch instances. This architecture lets you deploy Gitlab Runner to as many environments as you like by utilizing the configurations provided in the appropriate properties files. 81 | 82 | ### To deploy the Gitlab Runner stack: 83 | 1. Obtain the runner registration tokens of the Gitlab projects that you want registered to the Gitlab Runner. Obtain the token by selecting the project’s Settings > CI/CD and expand the Runners section. 84 | 2. Update the sample-runner.properties file parameters according to your own environment. Refer to the gitlab-runner.yaml file for a description of these parameters. Rename the file if you like. You may also create an additional properties file for deploying into other environments. 85 | 3. Run the deploy script to deploy the runner: 86 | ``` 87 | cd 88 | ./deploy-runner.sh 89 | 90 | is the name of the properties file. 91 | is the region where you want to deploy the stack. 92 | is the name of the CLI profile you set up in the prerequisites section. 93 | is the name you chose for the CloudFormation stack. 94 | ``` 95 | For example: 96 | ``` 97 | ./deploy-runner.sh sample-runner.properties us-east-1 dev gitlab-runner-demo 98 | ``` 99 | 100 | After the stack is deployed successfully, you will see the Gitlab Runner autoscaling group created in the EC2 console. 101 | 102 | 103 | 104 | Now go to your Gitlab project Settings->CICD->Runners->Available specific runners, you will see the fully configured Gitlab Runner. The green circle indicates that the Gitlab Runner is ready for use. 105 | 106 | 107 | 108 | ## Updating the Gitlab Runner 109 | There are times when you would want to update the Gitlab Runner. For example, updating the instance VolumeSize in order to resolve a disk space issue, or updating the AMI ID when a new AMI becomes available. 110 | Utilizing the properties file and launch template makes it easy to update the Gitlab Runner. Simply update the Gitlab Runner configuration parameters in the properties file. Then, run the deploy script to udpate the Gitlab Runner stack. 111 | 112 | Below is an example of updating the Gitlab Runner instance type. 113 | 114 | ### To update the instance type of the runner instance: 115 | 1. Update the “InstanceType” parameter in the properties file. 116 | ``` 117 | InstanceType=t2.medium 118 | ``` 119 | 2. Run the deploy-runner.sh script to update the CloudFormation stack: 120 | ``` 121 | cd 122 | ./deploy-runner.sh 123 | ``` 124 | 125 | ## Terminate the Gitlab Runner 126 | There are times when an autoscaling group instance must be terminated. For example, during an autoscaling scale-in event, or when the instance is being replaced by a new instance during a stack update, as seen previously. When terminating an instance, you must ensure that the Gitlab Runner finishes executing any running jobs before the instance is terminated, otherwise your environment could be left in an inconsistent state. Also, we want to ensure that the terminated Gitlab Runner is removed from the Gitlab project. We utilize an autoscaling lifecycle hook to achieve these goals. 127 | 128 | 129 | 130 | The lifecycle hook works like this: A CloudWatch event rule actively listens for the EC2 Instance-terminate events. When one is detected, the event rule triggers a Lambda function. The Lambda function calls SSM Run Command to run a series of commands on the EC2 instances, via a SSM Document. The commands include stopping the Gitlab Runner gracefully when all running jobs are finished, de-registering the runner from Gitlab projects, and signaling the autoscaling group to terminate the instance. 131 | 132 | There are also times when you want to terminate an instance manually. For example, when an instance is suspected to not be functioning properly. To terminate an instance from the Gitlab Runner autoscaling group, use the following command: 133 | 134 | ``` 135 | aws autoscaling terminate-instance-in-auto-scaling-group \ 136 | --instance-id="${InstanceId}" \ 137 | --no-should-decrement-desired-capacity \ 138 | --region="${region}" \ 139 | --profile="${profile}" 140 | ``` 141 | The above command terminates the instance. The lifecycle hook ensures that the cleanup steps are conducted properly, and the autoscaling group launches another new instance to replace the old one. 142 | 143 | Note that if you terminate the instance by using the "ec2 terminate-instance" command, then the autoscaling lifecycle hook actions will not be triggered. 144 | 145 | ## Add/Remove Gitlab projects from the Gitlab Runner 146 | As new projects are added to your enterprise, you may want to register them to the Gitlab Runner, so that those projects can utilize the Gitlab Runner to run pipelines. On the other hand, you would want to remove the Gitlab Runner from a project if it no longer wants to utilize the Gitlab Runner, or if it qualifies to utilize the Gitlab Runner. For example, if a project is no longer allowed to deploy to an environment configured by the Gitlab Runner. Our architecture offers a simple way to add and remove projects from the Gitlab Runner. To add new projects to the Gitlab Runner, update the RunnerRegistrationTokens parameter in the properties file, and then rerun the deploy script to update the Gitlab Runner stack. 147 | 148 | ### To add new projects to the Gitlab Runner: 149 | 1. Update the RunnerRegistrationTokens parameter in the properties file. For example: 150 | ``` 151 | RunnerRegistrationTokens=ps8RjBSruy1sdRdP2nZX,XbtZNv4yxysbYhqvjEkC 152 | ``` 153 | 2. Update the Gitlab Runner stack. This updates the SSM parameter which stores the tokens. 154 | ``` 155 | cd 156 | ./deploy-runner.sh 157 | ``` 158 | 3. Relaunch the instances in the Gitlab Runner autoscaling group. The new instances will use the new RunnerRegistrationTokens value. Run the following command to relaunch the instances: 159 | ``` 160 | ./cycle-runner.sh 161 | ``` 162 | To remove projects from the Gitlab Runner, follow the steps described above, with just one difference. Instead of adding new tokens to the RunnerRegistrationTokens parameter, remove the token(s) of the project that you want to dissociate from the runner. 163 | 164 | ## Autoscale the runner based on custom performance metrics 165 | Each Gitlab Runner can be configured to handle a fixed number of [concurrent jobs](https://docs.gitlab.com/runner/configuration/advanced-configuration.html). Once this capacity is reached for every runner, any new jobs will be in a Queued/Waiting status until the current jobs complete, which would be a poor experience for our team. Setting the number of concurrent jobs too high on our runners would also result in a poor experience, because all jobs leverage the same CPU, memory, and storage in order to conduct the builds. 166 | 167 | In this solution, we utilize a scheduled Lambda function that runs every minute in order to inspect the number of jobs running on every runner, leveraging the [Prometheus Metrics](https://docs.gitlab.com/ee/administration/monitoring/prometheus/gitlab_metrics.html#gitlab-prometheus-metrics) endpoint that the runners expose. If we approach the concurrent build limit of the group, then we increase the Autoscaling Group size so that it can take on more work. As the number of concurrent jobs decreases, then the scheduled Lambda function will scale the Autoscaling Group back in an effort to minimize cost. The Scaling-Up operation will ignore the Autoscaling Group’s cooldown period, which will help ensure that our team is not waiting on a new instance, whereas the Scale-Down operation will obey the group’s cooldown period. 168 | 169 | Here is the logical sequence diagram for the work: 170 | 171 | 172 | 173 | For operational monitoring, the Lambda function also publishes custom CloudWatch Metrics for the count of active jobs, along with the target and actual capacities of the Autoscaling group. We can utilize this information to validate that the system is working properly and determine if we need to modify any of our autoscaling parameters. 174 | 175 | 176 | 177 | ## Troubleshooting 178 | Problem: I deployed the CloudFormation template, but no runner is listed in my repository. 179 | 180 | Possible Cause: Errors have been encountered during cfn-init, causing runner registration to fail. Connect to your runner EC2 instance, and check /var/log/cfn-*.log files. 181 | 182 | # Security 183 | 184 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 185 | 186 | # License 187 | 188 | This library is licensed under the MIT-0 License. See the LICENSE file. 189 | 190 | -------------------------------------------------------------------------------- /gitlab-runner.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | AWSTemplateFormatVersion: "2010-09-09" 5 | Description: "Setting up Gitlab CI Runner AutoScalingGroup" 6 | Parameters: 7 | VpcID: 8 | Type: "AWS::EC2::VPC::Id" 9 | Description: "VPC ID on which EC2 runner instance will reside" 10 | SubnetIds: 11 | Type: "List" 12 | Description: "Use Private App Subnets" 13 | ImageId: 14 | Type: 'AWS::EC2::Image::Id' 15 | Description: The image ID to use to create the EC2 runner instance. 16 | InstanceType: 17 | Type: "String" 18 | Default: "t3.medium" 19 | Description: "Instance type for EC2 runner instance" 20 | InstanceName: 21 | Type: "String" 22 | Default: "gitlab-runner" 23 | Description: "Name of the runner instance being created" 24 | VolumeSize: 25 | Type: "Number" 26 | Default: 200 27 | Description: "Volume size" 28 | VolumeType: 29 | Type: "String" 30 | Default: "gp2" 31 | Description: "Volume Type Attached to instance" 32 | MaxSize: 33 | Type: "Number" 34 | Default: 6 35 | Description: "The maximum number of instances in the EC2 autoscaling group." 36 | MinSize: 37 | Type: "Number" 38 | Default: 1 39 | Description: "The minimum number of instances in the EC2 autoscaling group." 40 | DesiredCapacity: 41 | Type: "Number" 42 | Default: 1 43 | Description: "The size of the Auto Scaling group." 44 | MaxBatchSize: 45 | Type: "Number" 46 | Default: 1 47 | Description: "The maximum number of instances that AWS CloudFormation updates at once." 48 | MinInstancesInService: 49 | Type: "Number" 50 | Default: 1 51 | Description: "The minimum number of instances that must be in service within the Auto Scaling group while CloudFormation updates old instances." 52 | MaxInstanceLifetime: 53 | Type: Number 54 | Description: The maximum amount of time, in seconds, that an instance can be in service. If specified, the value must be either 0 or a number equal to or greater than 86,400 seconds (1 day). 55 | Default: 604800 56 | GitlabServerURL: 57 | Type: String 58 | Description: URL of the Gitlab server 59 | DockerImagePath: 60 | Type: String 61 | Description: Docker path of the GitLab Runner Docker image 62 | RunnersToken: 63 | Type: "String" 64 | Description: "Comma delimited tokens for the Gitlab projects you want to register to the runner. See runner registration process: https://docs.gitlab.com/runner/register/. " 65 | RunnerVersion: 66 | Type: "String" 67 | Default: "v1" 68 | Description: "Version of the Runner" 69 | RunnerEnvironment: 70 | Type: "String" 71 | AllowedValues: 72 | - "dev" 73 | - "qa" 74 | - "prod" 75 | Description: "The environment that the runner is in" 76 | LambdaS3Bucket: 77 | Type: String 78 | Default: "" 79 | Description: "The s3 bucket to store the lambda functions in" 80 | TimeStamp: 81 | Type: "String" 82 | Description: "A timestamp for this deployment. This is used to identify the correct lambda code on S3 for deployment." 83 | Concurrent: 84 | Type: "Number" 85 | Description: "Number of concurrent jobs allowed on a Gitlab Runner" 86 | CheckInterval: 87 | Type: "Number" 88 | Description: "The interval length, in seconds, between new jobs check. If set to 0 or lower, the default value will be used." 89 | CostCenter: 90 | Type: String 91 | Description: Cost center code for the application. 92 | AppId: 93 | Type: String 94 | Description: ID of the application. 95 | ScaleInCoolDownInSeconds: 96 | Type: Number 97 | Default: 600 98 | Description: The delay before we begin scaling down an autoscaling group. Scale Up does not obey this value. 99 | CountOfNewJobsBeforeScaling: 100 | Type: Number 101 | Default: 2 102 | Description: The number of jobs that we should be able to take before needing to wait on a scaling event. 103 | 104 | Mappings: 105 | GitlabRunnerRegisterOptionsMap: 106 | dev: 107 | isLOCKED: "false" 108 | ACCESS: "not_protected" 109 | qa: 110 | isLOCKED: "true" 111 | ACCESS: "ref_protected" 112 | prod: 113 | isLOCKED: "true" 114 | ACCESS: "ref_protected" 115 | 116 | Resources: 117 | GitlabRunnerRole: 118 | Type: 'AWS::IAM::Role' 119 | Properties: 120 | AssumeRolePolicyDocument: 121 | Version: 2012-10-17 122 | Statement: 123 | - Effect: Allow 124 | Principal: 125 | Service: 126 | - ec2.amazonaws.com 127 | - lambda.amazonaws.com 128 | Action: 'sts:AssumeRole' 129 | ManagedPolicyArns: 130 | - 'arn:aws:iam::aws:policy/CloudWatchAgentAdminPolicy' 131 | - 'arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore' 132 | RoleName: GitlabRunnerRole 133 | InstanceProfile: 134 | Type: 'AWS::IAM::InstanceProfile' 135 | Properties: 136 | InstanceProfileName: GitlabRunnerRole 137 | Path: / 138 | Roles: 139 | - !Ref GitlabRunnerRole 140 | GitlabRunnerPolicy: 141 | Type: 'AWS::IAM::ManagedPolicy' 142 | Properties: 143 | PolicyDocument: 144 | Version: 2012-10-17 145 | Statement: 146 | - Sid: AllowInvokeLambda 147 | Action: 'lambda:InvokeFunction' 148 | Resource: !GetAtt LifeCycleHookFunction.Arn 149 | Effect: Allow 150 | - Sid: AllowSSMParameterStoreRead 151 | Action: 'ssm:GetParameter*' 152 | Resource: !Sub 'arn:aws:ssm:*:*:parameter/${AWS::StackName}/ci-tokens' 153 | Effect: Allow 154 | - Sid: AllowSSMDocumentRead 155 | Action: 156 | - 'ssm:ListDocuments' 157 | - 'ssm:ListCommandInvocations' 158 | Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:*' 159 | Effect: Allow 160 | - Sid: AllowSSMDocumentSend 161 | Action: 162 | - 'ssm:SendCommand' 163 | Resource: 164 | - !Sub 'arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/*' 165 | - !Sub 'arn:aws:ssm:*:${AWS::AccountId}:document/${Document}' 166 | Effect: Allow 167 | - Sid: AllowCompleteLifycycle 168 | Action: 'autoscaling:CompleteLifecycleAction' 169 | Resource: !Sub 'arn:aws:autoscaling:${AWS::Region}:${AWS::AccountId}:autoScalingGroup:*:autoScalingGroupName/${AWS::StackName}-asg' 170 | Effect: Allow 171 | ManagedPolicyName: !Sub ${AWS::StackName}-policy 172 | Roles: 173 | - !Ref GitlabRunnerRole 174 | 175 | RunnerTokenParameter: 176 | Type: "AWS::SSM::Parameter" 177 | Properties: 178 | Name: !Sub "/${AWS::StackName}/ci-tokens" 179 | Type: "String" 180 | Value: !Ref RunnersToken 181 | 182 | RunnerSecurityGroup: 183 | Type: "AWS::EC2::SecurityGroup" 184 | Properties: 185 | GroupDescription: !Join [ "", [ "Security group for ", !Ref "InstanceName" ] ] 186 | VpcId: !Ref "VpcID" 187 | 188 | RunnerMonitorSecurityGroup: 189 | Type: "AWS::EC2::SecurityGroup" 190 | Properties: 191 | GroupDescription: !Join [ "", [ "Security group for Gitlab Runner Monitor for ", !Ref "InstanceName" ] ] 192 | VpcId: !Ref "VpcID" 193 | 194 | AllowRunnerMonitorToRunner: 195 | Type: "AWS::EC2::SecurityGroupIngress" 196 | Properties: 197 | Description: "Allow Runner Monitor to access the metric port on the runner" 198 | GroupId: !Ref RunnerSecurityGroup 199 | FromPort: 9252 200 | ToPort: 9252 201 | IpProtocol: "tcp" 202 | SourceSecurityGroupId: !Ref RunnerMonitorSecurityGroup 203 | 204 | AllowRunnerMonitorToInternet: 205 | Type: "AWS::EC2::SecurityGroupEgress" 206 | Properties: 207 | Description: "Allow Runner Monitor to access the internet for gitlab/metrics" 208 | GroupId: !Ref RunnerMonitorSecurityGroup 209 | CidrIp: "0.0.0.0/0" 210 | IpProtocol: "-1" 211 | 212 | RunnerLaunchTemplate: 213 | Type: 'AWS::EC2::LaunchTemplate' 214 | DependsOn: GitlabRunnerRole 215 | Properties: 216 | LaunchTemplateName: !Sub ${AWS::StackName}-launch-template 217 | LaunchTemplateData: 218 | BlockDeviceMappings: 219 | - DeviceName: /dev/xvda 220 | Ebs: 221 | VolumeSize: !Ref VolumeSize 222 | VolumeType: !Ref VolumeType 223 | Encrypted: true 224 | IamInstanceProfile: 225 | Arn: !GetAtt InstanceProfile.Arn 226 | ImageId: !Ref ImageId 227 | InstanceType: !Ref InstanceType 228 | SecurityGroupIds: 229 | - !GetAtt RunnerSecurityGroup.GroupId 230 | UserData: 231 | Fn::Base64: !Sub | 232 | #!/bin/bash -xe 233 | yum update -y aws-cfn-bootstrap 234 | /opt/aws/bin/cfn-init -v --stack ${AWS::StackId} --resource RunnerLaunchTemplate --region ${AWS::Region} 235 | /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackId} --resource RunnerAutoScalingGroup --region ${AWS::Region} 236 | Metadata: 237 | 'AWS::CloudFormation::Init': 238 | config: 239 | files: 240 | /etc/cfn/cfn-hup.conf: 241 | owner: 'root' 242 | group: 'root' 243 | mode: '000400' 244 | content: !Sub | 245 | [main] 246 | stack=${AWS::StackId} 247 | region=${AWS::Region} 248 | /etc/cfn/hooks.d/cfn-auto-reloader.conf: 249 | content: !Sub | 250 | [cfn-auto-reloader-hook] 251 | triggers=post.update 252 | path=Resources.RunnerLaunchTemplate.Metadata.AWS::CloudFormation::Init 253 | action=/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --region ${AWS::Region} --resource RunnerLaunchTemplate 254 | runas=root 255 | /etc/gitlab-runner/config.toml: 256 | content: !Sub | 257 | concurrent = ${Concurrent} 258 | check_interval = ${CheckInterval} 259 | listen_address = ":9252" 260 | log_format = "json" 261 | # https://docs.gitlab.com/runner/best_practice/#graceful-shutdown 262 | /etc/systemd/system/gitlab-runner.service.d/kill.conf: 263 | content: | 264 | [Service] 265 | KillSignal=SIGQUIT 266 | TimeoutStopSec=3600 267 | commands: 268 | 00SetNewMachineID: 269 | command: | 270 | sudo rm -f /etc/machine-id ; 271 | sudo systemd-firstboot --root=/ --setup-machine-id 272 | 01InstallDocker: 273 | command: sudo yum -y install docker 274 | 02StartDocker: 275 | command: sudo service docker start 276 | 03DownloadGitlabRunner: 277 | command: sudo wget -O /usr/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64 278 | 04ChmodGitlabRunner: 279 | command: sudo chmod a+x /usr/bin/gitlab-runner 280 | 05AddUser: 281 | command: sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash 282 | 06InstallGitlabRunner: 283 | command: sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner 284 | 07StartGitlabRunner: 285 | command: sudo gitlab-runner start 286 | 08SetRegion: 287 | command: !Sub 'aws configure set default.region ${AWS::Region}' 288 | 09ConfigureDockerExecutor: 289 | command: !Sub 290 | - | 291 | for GitlabGroupToken in `aws ssm get-parameters --names /${AWS::StackName}/ci-tokens --query 'Parameters[0].Value' | sed -e "s/\"//g" | sed "s/,/ /g"`;do 292 | sudo gitlab-runner register \ 293 | --non-interactive \ 294 | --url "${GitlabServerURL}" \ 295 | --token $GitlabGroupToken \ 296 | --executor "docker" \ 297 | --docker-image "${DockerImagePath}" \ 298 | --description "Gitlab Runner with Docker Executor" \ 299 | --docker-volumes "/var/run/docker.sock:/var/run/docker.sock" 300 | done 301 | - isLOCKED: !FindInMap [GitlabRunnerRegisterOptionsMap, !Ref RunnerEnvironment, isLOCKED] 302 | ACCESS: !FindInMap [GitlabRunnerRegisterOptionsMap, !Ref RunnerEnvironment, ACCESS] 303 | 10StartCfnHup: 304 | command: systemctl start cfn-hup && systemctl enable cfn-hup 305 | RunnerAutoScalingGroup: 306 | Type: AWS::AutoScaling::AutoScalingGroup 307 | CreationPolicy: 308 | ResourceSignal: 309 | Count: !Ref DesiredCapacity 310 | Timeout: PT5M 311 | UpdatePolicy: 312 | AutoScalingRollingUpdate: 313 | MinInstancesInService: !Ref MinInstancesInService 314 | MaxBatchSize: !Ref MaxBatchSize 315 | PauseTime: "PT5M" 316 | WaitOnResourceSignals: true 317 | SuspendProcesses: 318 | - HealthCheck 319 | - ReplaceUnhealthy 320 | - AZRebalance 321 | - AlarmNotification 322 | - ScheduledActions 323 | Properties: 324 | AutoScalingGroupName: !Sub ${AWS::StackName}-asg 325 | MaxInstanceLifetime: !Ref MaxInstanceLifetime 326 | LaunchTemplate: 327 | LaunchTemplateId: !Ref RunnerLaunchTemplate 328 | Version: !GetAtt 329 | - RunnerLaunchTemplate 330 | - LatestVersionNumber 331 | DesiredCapacity: !Ref DesiredCapacity 332 | MinSize: !Ref MinSize 333 | MaxSize: !Ref MaxSize 334 | Cooldown: !Ref ScaleInCoolDownInSeconds 335 | MetricsCollection: 336 | - Granularity: "1Minute" 337 | Metrics: 338 | - "GroupMinSize" 339 | - "GroupMaxSize" 340 | Tags: 341 | - Key: "APP_ID" 342 | Value: !Ref AppId 343 | PropagateAtLaunch : true 344 | - Key: "Name" 345 | Value: !Ref "InstanceName" 346 | PropagateAtLaunch : true 347 | - Key: "COST_CENTER" 348 | Value: !Ref CostCenter 349 | PropagateAtLaunch : true 350 | TerminationPolicies: 351 | # If the launch configuration is out of date, then replace it. 352 | # Otherwise we kill the oldest instance and let the runnerManager bring up new instances with latest config ie tokens 353 | - OldestLaunchConfiguration 354 | - OldestInstance 355 | VPCZoneIdentifier: !Ref "SubnetIds" 356 | ServiceLinkedRoleARN: !Sub "arn:aws:iam::${AWS::AccountId}:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling" 357 | 358 | RunnerAutoScalingGroupParameter: 359 | Type: "AWS::SSM::Parameter" 360 | Properties: 361 | Name: !Sub "/${AWS::StackName}/runner-autoscaling-group" 362 | Type: "String" 363 | Value: !Ref RunnerAutoScalingGroup 364 | 365 | LifecycleHook: 366 | Type: AWS::AutoScaling::LifecycleHook 367 | Properties: 368 | AutoScalingGroupName: !Ref RunnerAutoScalingGroup 369 | LifecycleTransition: autoscaling:EC2_INSTANCE_TERMINATING 370 | 371 | DocumentLogGroup: 372 | Type: AWS::Logs::LogGroup 373 | Properties: 374 | RetentionInDays: 14 375 | 376 | Document: 377 | Type: AWS::SSM::Document 378 | Properties: 379 | DocumentType: Command 380 | Content: !Sub | 381 | { 382 | "schemaVersion": "2.2", 383 | "description": "Gitlab runner graceful shutdown", 384 | "parameters": {}, 385 | "mainSteps": [ 386 | { 387 | "action": "aws:runShellScript", 388 | "name": "TerminateGitlabRunner", 389 | "inputs": 390 | { 391 | "runCommand": [ 392 | "ASGNAME='${RunnerAutoScalingGroup}'", 393 | "echo $ASGNAME", 394 | "LIFECYCLEHOOKNAME='${LifecycleHook}'", 395 | "echo $LIFECYCLEHOOKNAME", 396 | "INSTANCEID=$(curl http://169.254.169.254/latest/meta-data/instance-id)", 397 | "echo $INSTANCEID", 398 | "REGION=$(curl http://169.254.169.254/latest/meta-data/placement/availability-zone)", 399 | "REGION=${!REGION::-1}", 400 | "echo $REGION", 401 | "HOOKRESULT='CONTINUE'", 402 | "MESSAGE='Gitlab runner process is running'", 403 | "echo 'unregistering this runner'", 404 | "sudo gitlab-runner unregister --all-runners", 405 | "echo 'Waiting for gitlab-runner to stop gracefully'", 406 | "sudo gitlab-runner stop", 407 | "echo 'completing lifecycle action'", 408 | "aws autoscaling complete-lifecycle-action --lifecycle-hook-name ${!LIFECYCLEHOOKNAME} --auto-scaling-group-name ${!ASGNAME} --lifecycle-action-result ${!HOOKRESULT} --instance-id ${!INSTANCEID} --region ${!REGION}", 409 | "echo 'done'" 410 | ] 411 | } 412 | } 413 | ] 414 | } 415 | Tags: 416 | - Key: "APP_ID" 417 | Value: !Ref AppId 418 | - Key: "COST_CENTER" 419 | Value: !Ref CostCenter 420 | 421 | LifeCycleHookFunction: 422 | Type: AWS::Lambda::Function 423 | Properties: 424 | FunctionName: !Sub ${AWS::StackName}-asg-lifecycle-hook 425 | Description: "Gitlab runner autoscaling group lifecycle hook on instance termination." 426 | Code: 427 | S3Bucket: !Ref LambdaS3Bucket 428 | S3Key: !Sub "${AWS::StackName}/${AWS::StackName}-lifecycle-hook-${TimeStamp}.zip" 429 | Handler: gitlab-runner-lifecycle-hook.lambda_handler 430 | # TODO: This role is likely too permissive 431 | Role: !GetAtt GitlabRunnerRole.Arn 432 | Runtime: python3.9 433 | Environment: 434 | Variables: 435 | DOCUMENT_NAME: !Ref Document 436 | DOCUMENT_LOG_GROUP_NAME: !Ref DocumentLogGroup 437 | Tags: 438 | - Key: "APP_ID" 439 | Value: !Ref AppId 440 | - Key: "COST_CENTER" 441 | Value: !Ref CostCenter 442 | 443 | Permission: 444 | Type: AWS::Lambda::Permission 445 | Properties: 446 | Action: "lambda:InvokeFunction" 447 | FunctionName: !GetAtt LifeCycleHookFunction.Arn 448 | Principal: events.amazonaws.com 449 | 450 | Rule: 451 | Type: AWS::Events::Rule 452 | Properties: 453 | EventPattern: !Sub 454 | - | 455 | { 456 | "source": ["aws.autoscaling"], 457 | "detail-type": ["EC2 Instance-terminate Lifecycle Action"], 458 | "detail": { 459 | "AutoScalingGroupName": ["${ASG}"] 460 | } 461 | } 462 | - { ASG: !Ref RunnerAutoScalingGroup } 463 | Targets: 464 | - Arn: !GetAtt LifeCycleHookFunction.Arn 465 | Id: target 466 | 467 | RunnerMonitorRole: 468 | Type: AWS::IAM::Role 469 | Properties: 470 | AssumeRolePolicyDocument: 471 | Version: 2012-10-17 472 | Statement: 473 | - Effect: Allow 474 | Principal: 475 | Service: 476 | - lambda.amazonaws.com 477 | Action: 'sts:AssumeRole' 478 | Policies: 479 | - PolicyName: RunnerMonitorPermissions 480 | PolicyDocument: 481 | Version: 2012-10-17 482 | Statement: 483 | - Effect: Allow 484 | Action: 485 | - "autoscaling:DescribeAutoScalingGroups" 486 | - "autoscaling:SetDesiredCapacity" 487 | - "cloudwatch:PutMetricData" 488 | - "ec2:DescribeInstances" 489 | Resource: 490 | - "*" 491 | ManagedPolicyArns: 492 | - 'arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole' 493 | 494 | RunnerMonitorLambda: 495 | Type: AWS::Lambda::Function 496 | Properties: 497 | FunctionName: !Sub ${AWS::StackName}-runner-monitor 498 | Description: "Monitors the state of the gitlab runners to trigger autoscaling events" 499 | Code: 500 | S3Bucket: !Ref LambdaS3Bucket 501 | S3Key: !Sub "${AWS::StackName}/${AWS::StackName}-runner-monitor-${TimeStamp}.zip" 502 | Handler: index.handler 503 | Role: !GetAtt RunnerMonitorRole.Arn 504 | Runtime: nodejs20.x 505 | Timeout: 900 506 | MemorySize: 256 507 | ReservedConcurrentExecutions: 1 508 | VpcConfig: 509 | SecurityGroupIds: 510 | - !GetAtt RunnerMonitorSecurityGroup.GroupId 511 | SubnetIds: !Ref SubnetIds 512 | Environment: 513 | Variables: 514 | AUTOSCALING_GROUP_NAME: !Ref RunnerAutoScalingGroup 515 | MAXIMUM_CONCURRENT_JOBS_PER_RUNNER: !Ref Concurrent 516 | COUNT_OF_NEW_JOBS_BEFORE_SCALING: !Ref CountOfNewJobsBeforeScaling 517 | RUNNER_METRIC_NAMESPACE: !Sub "${AWS::StackName}-runner-metrics" 518 | RUNNER_JOB_COUNT_METRIC_NAME: !Sub "${AWS::StackName}-job-count" 519 | RUNNER_TARGET_CAPACITY_METRIC_NAME: !Sub "${AWS::StackName}-target-capacity" 520 | RUNNER_ACTUAL_CAPACITY_METRIC_NAME: !Sub "${AWS::StackName}-actual-capacity" 521 | Tags: 522 | - Key: "APP_ID" 523 | Value: !Ref AppId 524 | - Key: "COST_CENTER" 525 | Value: !Ref CostCenter 526 | 527 | RunnerMonitorLambdaPermission: 528 | Type: AWS::Lambda::Permission 529 | Properties: 530 | Action: "lambda:InvokeFunction" 531 | FunctionName: !GetAtt RunnerMonitorLambda.Arn 532 | Principal: events.amazonaws.com 533 | SourceArn: !GetAtt ExecuteRunnerMonitor.Arn 534 | 535 | ExecuteRunnerMonitor: 536 | Type: AWS::Events::Rule 537 | Properties: 538 | Description: Monitor Gitlab-Runners every minute 539 | ScheduleExpression: rate(1 minute) 540 | Targets: 541 | - Arn: !GetAtt RunnerMonitorLambda.Arn 542 | Id: TargetFunction1 543 | 544 | Outputs: 545 | RunnerInstanceID: 546 | Description: "AutoScalingGroup Name of the GITLAB CI Runner is: " 547 | Value: !Ref "RunnerAutoScalingGroup" -------------------------------------------------------------------------------- /Images/gitlab-runner-architecture.drawio: -------------------------------------------------------------------------------- 1 | 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 --------------------------------------------------------------------------------