Skip to content

Commit bdd5c39

Browse files
author
Fernando Korndorfer
committed
feat: Enhance runner management with cloud provider support and monitoring scripts
1 parent 7c459f3 commit bdd5c39

6 files changed

Lines changed: 272 additions & 72 deletions

File tree

README.md

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -86,40 +86,60 @@ sudo apt-get install -y docker-ce docker-ce-cli containerd.io
8686
sudo systemctl start docker
8787
sudo systemctl enable docker
8888

89-
# Get the runner startup and stop scripts and make them executable:
89+
# Get the runner management scripts and make them executable:
9090
sudo curl -sO https://raw.githubusercontent.com/fok666/gitlab-selfhosted-runner/main/run.sh
9191
sudo curl -sO https://raw.githubusercontent.com/fok666/gitlab-selfhosted-runner/main/stop.sh
92+
sudo curl -sO https://raw.githubusercontent.com/fok666/gitlab-selfhosted-runner/main/vmss_monitor.sh
93+
sudo curl -sO https://raw.githubusercontent.com/fok666/gitlab-selfhosted-runner/main/ec2_monitor.sh
9294
sudo chmod +x *.sh
9395

9496
# Set the parameters from GitLab:
9597
export GITLAB_URL="https://gitlab.com"
9698
export GITLAB_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxx"
97-
export RUNNER_EXECUTOR="docker"
9899
export RUNNER_TAGS="docker,linux"
99-
export RUNNER_DOCKER_IMAGE="ubuntu:24.04"
100-
101-
# Start the runner in privileged mode using the parameters above:
102-
sudo docker run -d --privileged \
103-
-e GITLAB_URL="${GITLAB_URL}" \
104-
-e GITLAB_TOKEN="${GITLAB_TOKEN}" \
105-
-e RUNNER_EXECUTOR="${RUNNER_EXECUTOR}" \
106-
-e RUNNER_TAGS="${RUNNER_TAGS}" \
107-
-e RUNNER_DOCKER_IMAGE="${RUNNER_DOCKER_IMAGE}" \
108-
-v /var/run/docker.sock:/var/run/docker.sock \
109-
fok666/gitlab-runner:latest
100+
101+
# Start the runners in privileged mode, one runner for each vCPU (default), using the parameters above:
102+
sudo ./run.sh fok666/gitlab-runner:latest $GITLAB_URL $GITLAB_TOKEN $RUNNER_TAGS
103+
104+
# Or specify a custom number of runners (e.g., 4 runners):
105+
sudo ./run.sh fok666/gitlab-runner:latest $GITLAB_URL $GITLAB_TOKEN $RUNNER_TAGS 4
110106
```
111107

112108

113-
## Azure VMSS support
109+
## Cloud Provider Support
110+
111+
This project supports graceful shutdown for multiple cloud providers:
112+
113+
### Azure VMSS (Virtual Machine Scale Sets)
114+
- `vmss_monitor.sh`: Monitor Azure VMSS scheduled events for termination notices
115+
- Reference: https://learn.microsoft.com/en-us/azure/virtual-machines/linux/scheduled-events
116+
117+
### AWS EC2 Spot Instances
118+
- `ec2_monitor.sh`: Monitor EC2 spot instance termination notices
119+
- Reference: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-instance-termination-notices.html
114120

115-
This project is designed to use Azure Virtual Machine Scale Sets, but can be used with different settings.
121+
### Setup
122+
123+
```bash
124+
# Copy stop script to /opt for monitor scripts to use:
125+
sudo cp stop.sh /opt/stop.sh
126+
sudo chmod +x /opt/stop.sh
127+
128+
# For Azure VMSS - check every minute:
129+
(crontab -l 2>/dev/null; echo "* * * * * /opt/vmss_monitor.sh >> /var/log/vmss_monitor.log 2>&1") | crontab -
130+
131+
# For AWS EC2 Spot - check every 5 seconds:
132+
(crontab -l 2>/dev/null; echo "* * * * * /opt/ec2_monitor.sh >> /var/log/ec2_monitor.log 2>&1") | crontab -
133+
(crontab -l 2>/dev/null; echo "* * * * * sleep 5; /opt/ec2_monitor.sh >> /var/log/ec2_monitor.log 2>&1") | crontab -
134+
(crontab -l 2>/dev/null; echo "* * * * * sleep 10; /opt/ec2_monitor.sh >> /var/log/ec2_monitor.log 2>&1") | crontab -
135+
(crontab -l 2>/dev/null; echo "* * * * * sleep 15; /opt/ec2_monitor.sh >> /var/log/ec2_monitor.log 2>&1") | crontab -
136+
```
116137

117-
- `stop.sh`: Add this script to `/opt/stop.sh` to enable graceful Runner shutdown. Requires SUDO.
138+
Both monitor scripts require `curl` and `jq` to be installed.
118139

119140

120141
# TO DO
121142

122143
- Add Google Compute Cloud (GCP) CLI bundles
123144
- Add GKE auth support
124-
- Improve support for Spot/Preemptive VM instances
125-
- Improve support for other Cloud providers (AWS, GCP...)
145+
- Add GCP Preemptible VM support

ec2_monitor.sh

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# AWS EC2 Spot Instance Termination Monitor
5+
# Monitors EC2 spot instance termination notices
6+
# Reference: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-instance-termination-notices.html
7+
8+
METADATA_ENDPOINT='http://169.254.169.254/latest/meta-data/spot/instance-action'
9+
STOP_SCRIPT='/opt/stop.sh'
10+
11+
echo "Checking for EC2 spot instance termination notice..."
12+
13+
# Query EC2 Instance Metadata Service for spot termination notice
14+
# Returns HTTP 404 if no termination notice is present
15+
RESPONSE=$(curl -s -f "$METADATA_ENDPOINT" 2>/dev/null || echo "")
16+
17+
# Check if termination notice exists
18+
if [ -n "$RESPONSE" ]; then
19+
echo "Spot instance termination notice detected!"
20+
echo "Termination details: $RESPONSE"
21+
22+
# Parse termination time
23+
TERMINATION_TIME=$(echo "$RESPONSE" | jq -r '.time' 2>/dev/null || echo "Unknown")
24+
ACTION=$(echo "$RESPONSE" | jq -r '.action' 2>/dev/null || echo "terminate")
25+
26+
echo "Action: $ACTION"
27+
echo "Termination time: $TERMINATION_TIME"
28+
echo "Initiating graceful shutdown..."
29+
30+
# Start agent shutdown, if script exists and is executable
31+
if [ -x "$STOP_SCRIPT" ]; then
32+
echo "Executing stop script: $STOP_SCRIPT"
33+
"$STOP_SCRIPT"
34+
else
35+
echo "Warning: Stop script not found or not executable: $STOP_SCRIPT"
36+
fi
37+
38+
echo "Agents shutdown complete. Instance will terminate at: $TERMINATION_TIME"
39+
else
40+
echo "No spot instance termination notice"
41+
fi
42+
43+
exit 0

run.sh

Lines changed: 119 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,138 @@
11
#!/bin/bash
2+
set -e
23

3-
AZP_IMAGE="$1"
4-
AZP_URL="$2"
5-
AZP_TOKEN="$3"
6-
AZP_POOL="$4"
4+
# GitLab Self-Hosted Runner Script
5+
# Reference: https://docs.gitlab.com/runner/
76

8-
USAGE_HELP="Usage: $0 <AZP_IMAGE> <AZP_URL> <AZP_TOKEN> <AZP_POOL>"
7+
RUNNER_IMAGE="$1"
8+
GITLAB_URL="$2"
9+
GITLAB_TOKEN="$3"
10+
RUNNER_TAGS="${4:-docker,linux}"
11+
RUNNER_COUNT="${5}"
912

10-
# Test if AZP_IMAGE is empty, exit with error if true
11-
if [ -z "$AZP_IMAGE" ]; then
12-
echo $USAGE_HELP
13+
USAGE_HELP="Usage: $0 <RUNNER_IMAGE> <GITLAB_URL> <GITLAB_TOKEN> [RUNNER_TAGS] [RUNNER_COUNT]
14+
15+
Parameters:
16+
RUNNER_IMAGE - Docker image name for GitLab runner
17+
GITLAB_URL - GitLab instance URL
18+
Examples:
19+
- GitLab.com: https://gitlab.com
20+
- Self-hosted: https://gitlab.example.com
21+
GITLAB_TOKEN - GitLab runner registration token
22+
Can be found in:
23+
- Project: Settings > CI/CD > Runners
24+
- Group: Settings > CI/CD > Runners
25+
- Instance: Admin Area > CI/CD > Runners
26+
RUNNER_TAGS - Comma-separated tags for runner (default: 'docker,linux')
27+
RUNNER_COUNT - Number of runner instances (default: auto-detect from CPU count)
28+
29+
Example:
30+
$0 gitlab-runner:18.7.1 https://gitlab.com glrt-xxxxxxxxxxxx \"docker,linux,production\" 4
31+
"
32+
33+
# Validate required parameters
34+
if [ -z "$RUNNER_IMAGE" ]; then
35+
echo "Error: RUNNER_IMAGE is required"
36+
echo "$USAGE_HELP"
1337
exit 1
1438
fi
1539

16-
# Test if AZP_URL is empty, exit with error if true
17-
if [ -z "$AZP_URL" ]; then
18-
echo $USAGE_HELP
40+
if [ -z "$GITLAB_URL" ]; then
41+
echo "Error: GITLAB_URL is required"
42+
echo "$USAGE_HELP"
1943
exit 1
2044
fi
2145

22-
# Test if AZP_TOKEN is empty, exit with error if true
23-
if [ -z "$AZP_TOKEN" ]; then
24-
echo $USAGE_HELP
46+
if [ -z "$GITLAB_TOKEN" ]; then
47+
echo "Error: GITLAB_TOKEN is required"
48+
echo "$USAGE_HELP"
2549
exit 1
2650
fi
2751

28-
# Test if AZP_POOL is empty, exit with error if true
29-
if [ -z "$AZP_POOL" ]; then
30-
echo $USAGE_HELP
52+
# Validate GitLab URL format
53+
if [[ ! "$GITLAB_URL" =~ ^https?://[^/]+/?$ ]]; then
54+
echo "Error: Invalid GITLAB_URL format. Must be https://gitlab.com or https://gitlab.example.com"
3155
exit 1
3256
fi
3357

3458
# Get total CPU count from the system
35-
CPU_COUNT=$(lscpu -p=CPU | grep -v "^#" | wc -l)
36-
# Limit the number of vCPU count per agent to 2 when there are more than 1 vCPU is available, cap it to 1 vCPU otherwise
37-
MAX_CPU=$(($CPU_COUNT>1 ? 2 : 1))
59+
CPU_COUNT=$(lscpu -p=CPU | grep -v "^#" | wc -l 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo "2")
60+
61+
# Set runner count (use provided value or default to CPU count)
62+
RUNNER_COUNT=${RUNNER_COUNT:-$CPU_COUNT}
63+
64+
# Limit the number of vCPU count per runner to 2 when there are more than 1 vCPU available, cap it to 1 vCPU otherwise
65+
MAX_CPU=$((CPU_COUNT > 1 ? 2 : 1))
66+
3867
# Get the Docker socket endpoint from current context
39-
DOCKER_SOCK_ENDPOINT=$(docker context inspect | jq -r '.[]|.Endpoints.docker.Host')
40-
41-
for R in `seq 1 $CPU_COUNT`; do
42-
sudo mkdir -p /mnt/agent${R}/w
43-
sudo docker run \
44-
--privileged \
45-
--tty \
46-
--detach \
47-
--cpus="${MAX_CPU}" \
48-
-e AZP_URL="$AZP_URL" \
49-
-e AZP_TOKEN="$AZP_TOKEN" \
50-
-e AZP_POOL="$AZP_POOL" \
51-
-e AZP_AGENT_NAME=sha-`hostname`-$R \
52-
-v ${DOCKER_SOCK_ENDPOINT#unix://*}:/var/run/docker.sock \
53-
-v /mnt/agent${R}/w:/_work \
54-
--restart always \
55-
--name agent$R \
56-
$AZP_IMAGE
68+
DOCKER_SOCK_ENDPOINT=$(docker context inspect 2>/dev/null | jq -r '.[]|.Endpoints.docker.Host' || echo "unix:///var/run/docker.sock")
69+
70+
# Extract socket path
71+
DOCKER_SOCK_PATH=${DOCKER_SOCK_ENDPOINT#unix://}
72+
DOCKER_SOCK_PATH=${DOCKER_SOCK_PATH:-/var/run/docker.sock}
73+
74+
echo "Starting $RUNNER_COUNT GitLab runner(s)..."
75+
echo "Image: $RUNNER_IMAGE"
76+
echo "GitLab URL: $GITLAB_URL"
77+
echo "Tags: $RUNNER_TAGS"
78+
echo "CPUs per runner: $MAX_CPU"
79+
echo ""
80+
81+
# Launch runners
82+
for R in $(seq 1 $RUNNER_COUNT); do
83+
RUNNER_NAME="gitlab-runner-$(hostname)-$R"
84+
CONFIG_DIR="/mnt/gitlab-runner${R}/config"
85+
DATA_DIR="/mnt/gitlab-runner${R}/data"
86+
CONTAINER_NAME="gitlab-runner-$R"
87+
88+
# Create config and data directories
89+
sudo mkdir -p "$CONFIG_DIR"
90+
sudo mkdir -p "$DATA_DIR"
91+
92+
echo "Starting runner $R/$RUNNER_COUNT: $RUNNER_NAME"
93+
94+
# Check if container already exists
95+
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
96+
echo " Removing existing container: $CONTAINER_NAME"
97+
docker rm -f "$CONTAINER_NAME" > /dev/null 2>&1 || true
98+
fi
99+
100+
# Run GitLab runner container
101+
docker run \
102+
--privileged \
103+
--tty \
104+
--detach \
105+
--cpus="${MAX_CPU}" \
106+
-e GITLAB_URL="$GITLAB_URL" \
107+
-e GITLAB_TOKEN="$GITLAB_TOKEN" \
108+
-e RUNNER_NAME="$RUNNER_NAME" \
109+
-e RUNNER_TAGS="$RUNNER_TAGS" \
110+
-e RUNNER_EXECUTOR="docker" \
111+
-e RUNNER_DOCKER_IMAGE="alpine:latest" \
112+
-e RUNNER_RUN_UNTAGGED="false" \
113+
-e RUNNER_LOCKED="false" \
114+
-e RUNNER_ACCESS_LEVEL="not_protected" \
115+
-v "$CONFIG_DIR":/etc/gitlab-runner \
116+
-v "$DATA_DIR":/runner \
117+
-v "$DOCKER_SOCK_PATH":/var/run/docker.sock \
118+
--restart unless-stopped \
119+
--name "$CONTAINER_NAME" \
120+
"$RUNNER_IMAGE"
121+
122+
echo " Container $CONTAINER_NAME started successfully"
57123
done
124+
125+
echo ""
126+
echo "All runners started successfully!"
127+
echo ""
128+
echo "To check runner status:"
129+
echo " docker ps --filter name=gitlab-runner"
130+
echo ""
131+
echo "To view runner logs:"
132+
echo " docker logs -f gitlab-runner-1"
133+
echo ""
134+
echo "To verify runner registration in GitLab:"
135+
echo " Go to your project/group/instance Settings > CI/CD > Runners"
136+
echo ""
137+
echo "To stop all runners:"
138+
echo " ./stop.sh"

stop.sh

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,37 @@
11
#!/bin/bash
2+
set -e
23

3-
CPU_COUNT=$(lscpu -p=CPU | grep -v "^#" | wc -l)
4-
MAX_CPU=$(($CPU_COUNT>1 ? 2 : 1))
4+
# GitLab Runner Stop Script
5+
# Gracefully stops and removes all GitLab runner containers
56

6-
# Get URL and PAT from first running instance:
7-
eval $(sudo docker inspect agent1 | jq -r '. [] | . | .Config.Env[]' | grep "AZP_TOKEN\|AZP_URL")
7+
echo "Stopping GitLab runners..."
8+
echo ""
89

9-
# Graceful agent shutdown
10-
for R in `seq 1 $MAX_CPU`; do
11-
sudo docker exec -ti \
12-
-e VSTS_AGENT_INPUT_AUTH="pat" \
13-
-e VSTS_AGENT_INPUT_URL="$AZP_URL" \
14-
-e VSTS_AGENT_INPUT_TOKEN="$AZP_TOKEN" \
15-
agent$R \
16-
./config.sh remove --unattended \
17-
&& sudo docker stop agent$R \
18-
&& sudo docker rm agent$R &
19-
done
10+
# Get list of running runner containers
11+
RUNNER_CONTAINERS=$(docker ps --filter "name=gitlab-runner-" --format "{{.Names}}" | sort)
12+
13+
if [ -z "$RUNNER_CONTAINERS" ]; then
14+
echo "No running GitLab runner containers found."
15+
exit 0
16+
fi
2017

21-
wait
18+
CONTAINER_COUNT=$(echo "$RUNNER_CONTAINERS" | wc -l | tr -d ' ')
19+
echo "Found $CONTAINER_COUNT runner container(s)"
20+
echo ""
21+
22+
# Stop each runner gracefully
23+
for CONTAINER_NAME in $RUNNER_CONTAINERS; do
24+
echo "Stopping $CONTAINER_NAME..."
25+
26+
# Try graceful shutdown first (runner will unregister itself via start.sh cleanup)
27+
docker stop -t 30 "$CONTAINER_NAME" > /dev/null 2>&1 || true
28+
29+
# Remove the container
30+
docker rm "$CONTAINER_NAME" > /dev/null 2>&1 || true
31+
32+
echo " $CONTAINER_NAME stopped and removed"
33+
done
2234

35+
echo ""
36+
echo "All GitLab runners stopped successfully!"
2337
exit 0

test-tools.sh

100644100755
File mode changed.

vmss_monitor.sh

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# Azure VMSS Scheduled Events Monitor
5+
# Monitors Azure VM Scale Set scheduled events for termination signals
6+
# Reference: https://learn.microsoft.com/en-us/azure/virtual-machines/linux/scheduled-events
7+
8+
METADATA_ENDPOINT='http://169.254.169.254/metadata/scheduledevents?api-version=2020-07-01'
9+
EVENTS_FILE='/tmp/scheduledevents.json'
10+
STOP_SCRIPT='/opt/stop.sh'
11+
12+
echo "Checking for VMSS scheduled events..."
13+
14+
# Query Azure Instance Metadata Service for scheduled events
15+
curl -s "$METADATA_ENDPOINT" -H 'Metadata: true' > "$EVENTS_FILE"
16+
17+
# Check if termination event is scheduled
18+
if grep -q "Terminate" "$EVENTS_FILE"; then
19+
echo "Terminate event detected! Initiating graceful shutdown..."
20+
21+
# Start agent shutdown, if script exists and is executable
22+
if [ -x "$STOP_SCRIPT" ]; then
23+
echo "Executing stop script: $STOP_SCRIPT"
24+
"$STOP_SCRIPT"
25+
else
26+
echo "Warning: Stop script not found or not executable: $STOP_SCRIPT"
27+
fi
28+
29+
# Acknowledge the event to Azure
30+
EventId=$(jq -r '.Events[] | select(.EventType == "Terminate") | .EventId' "$EVENTS_FILE")
31+
if [ -n "$EventId" ]; then
32+
echo "Acknowledging event: $EventId"
33+
curl -s -X POST "$METADATA_ENDPOINT" \
34+
-H 'Metadata: true' \
35+
-d "{\"StartRequests\": [{\"EventId\": \"${EventId}\"}]}"
36+
echo "Event acknowledged successfully"
37+
fi
38+
else
39+
echo "No termination events scheduled"
40+
fi
41+
42+
exit 0

0 commit comments

Comments
 (0)