Skip to content

Instantly share code, notes, and snippets.

@j3parker
Created September 30, 2021 18:25
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save j3parker/9e3a3b1b6a143fce00fe15b7d6109eaa to your computer and use it in GitHub Desktop.
Save j3parker/9e3a3b1b6a143fce00fe15b7d6109eaa to your computer and use it in GitHub Desktop.
GitHub runner boot sketch
#!/bin/bash
echo "Hello!"
cd /home/runner/dist
INSTANCE_ID=$(ec2metadata --instance-id)
PARAMETER="/GitHubRunnerRegistrationTokens/${INSTANCE_ID}"
get_registration_data() {
REGISTRATION_DATA=$(aws ssm get-parameter --with-decryption --name "${PARAMETER}")
}
echo "Getting registration token from SSM parameter: ${PARAMETER}..."
# Poll parameter store for this token. It may be there immediately or it may
# take awhile (if we are warm standby capacity).
while ! get_registration_data ; do
echo "Waiting for ${PARAMETER}"
sleep 5
done
# Enable crashing on error
set -euo pipefail
REGISTRATION_TOKEN=$(echo "${REGISTRATION_DATA}" | jq -r .Parameter.Value)
echo "Got registration token!"
echo "Figuring out more about this instance..."
REGION=$(ec2metadata --availability-zone | sed 's:.$::')
TAGS=$(aws ec2 describe-tags --filter "Name=resource-id,Values=$INSTANCE_ID" | jq .Tags[])
ORG=$(echo "${TAGS}" | jq -r '. | select(.Key == "Org").Value')
REPO=$(echo "${TAGS}" | jq -r '. | select(.Key == "Repo").Value')
SCOPE="PerOrg"
SLUG="${ORG}"
if [[ ! -z "${REPO}" ]]; then
SCOPE="PerRepo"
SLUG="${ORG}/${REPO}"
fi
if [[ -z "${SLUG}" ]]; then
ENTERPRISE=$(echo "${TAGS}" | jq -r '. | select(.Key == "Enterprise").Value')
SCOPE="PerEnterprise"
SLUG="enterprises/${ENTERPRISE}"
fi
EPHEMERAL=$(echo "${TAGS}" | jq -r '. | select(.Key == "Ephemeral")')
echo "Configuring runner..."
if [[ -n "$EPHEMERAL" ]]; then
./home/runner/detach-on-receiving-job.sh &
./config.sh \
--url "https://github.com/${SLUG}" \
--token "${REGISTRATION_TOKEN}" \
--name "${INSTANCE_ID}" \
--labels "AWS-ephemeral,${REGION},${SCOPE}" \
--work ../work \
--ephemeral
echo "Launching runner..."
./bin/runsvc.sh
echo "Runner exited. Shutting down..."
sudo shutdown -h now
else
sudo /home/runner/persistent-runner-hacks.sh &
./config.sh \
--url "https://github.com/${SLUG}" \
--token "${REGISTRATION_TOKEN}" \
--name "${INSTANCE_ID}" \
--labels "AWS,${REGION},${SCOPE}" \
--work ../work
echo "Launching runner..."
exec bin/runsvc.sh
fi
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Amazon;
using Amazon.EC2;
using Amazon.EC2.Model;
using Amazon.Lambda.Core;
using Amazon.Lambda.SQSEvents;
using Amazon.SimpleSystemsManagement;
using Amazon.SimpleSystemsManagement.Model;
using GitHubUtil;
// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer( typeof( Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer ) )]
namespace RegisterRunnerLambda {
public static class Function {
public static async Task HandleAsync( SQSEvent ev ) {
var tasks = ev.Records.Select( HandleMessageAsync );
await Task.WhenAll( tasks );
}
public static async Task HandleMessageAsync( SQSEvent.SQSMessage msg ) {
var req = JsonSerializer.Deserialize<CloudWatchEvent>( msg.Body );
var instanceId = req.Detail.InstanceId;
var ec2 = new AmazonEC2Client( RegionEndpoint.USEast1 );
var res = await ec2.DescribeInstancesAsync(
new DescribeInstancesRequest {
Filters = new List<Filter> {
new Filter( "instance-id", new List<string>{ instanceId } )
}
}
);
var instance = res.Reservations.SelectMany( r => r.Instances ).Single();
if( !TryGetRegistrationDetails( instance, out string tokenName, out string registrationUrl ) ) {
Console.WriteLine( $"Instance {instance.InstanceId} not tagged like a runner. Skipping." );
return;
}
var ssm = new AmazonSimpleSystemsManagementClient( RegionEndpoint.USEast1 );
var github = await GitHubClientFactory.CreateAsync( ssm, tokenName );
var res2 = await github.Connection.Post<TokenResponse>(
new Uri( registrationUrl, UriKind.Relative )
);
if ( (int)res2.HttpResponse.StatusCode >= 300 ) {
throw new Exception( "Unexpected status code: " + res2.HttpResponse.StatusCode );
}
var ssmRes = await ssm.PutParameterAsync(
new PutParameterRequest {
Name = "/GitHubRunnerRegistrationTokens/" + instanceId,
Type = ParameterType.SecureString,
Value = res2.Body.Token,
Overwrite = true
}
);
}
private static bool TryGetRegistrationDetails(
Instance instance,
out string tokenName,
out string registrationUrl
) {
var org = instance.Tags.FirstOrDefault( t => t.Key == "Org" )?.Value;
if( org != null ) {
var repo = instance.Tags.FirstOrDefault( t => t.Key == "Repo" )?.Value;
if( repo == null ) {
Console.WriteLine( $"Registering {instance.InstanceId} as a runner for org: {org}" );
tokenName = org;
registrationUrl = $"/orgs/{org}/actions/runners/registration-token";
return true;
}
// Per-repo runner
Console.WriteLine( $"Registering {instance.InstanceId} as a runner for repository: {org}/{repo}" );
tokenName = org;
registrationUrl = $"/repos/{org}/{repo}/actions/runners/registration-token";
return true;
}
var enterprise = instance.Tags.FirstOrDefault( t => t.Key == "Enterprise" )?.Value;
if( enterprise != null ) {
Console.WriteLine( $"Registering {instance.InstanceId} as a runner for enterprise: {enterprise}" );
tokenName = enterprise;
registrationUrl = $"/enterprises/{enterprise}/actions/runners/registration-token";
return true;
}
tokenName = default;
registrationUrl = default;
return false;
}
}
}
# Create an SQS queue that recieves notifications that a new instance is launched
# A Lambda function is at the other end of the queue. It creates a registration token
# and stores it in the EC2 Metadata Service in response.
resource "aws_sqs_queue" "register_runner_queue" {
name = "register-runner""
visibility_timeout_seconds = 30
message_retention_seconds = 300
redrive_policy = jsonencode({
deadLetterTargetArn = ...
maxReceiveCount = 10
})
fifo_queue = true
content_based_deduplication = true
tags = var.tags
}
# Filters CloudWatch Events for "new instance" notifications
# (instances start in the "pending" state.)
resource "aws_cloudwatch_event_rule" "instance_launch" {
name = "instance-launch"
event_pattern = jsonencode({
"source": ["aws.ec2"],
"detail-type": ["EC2 Instance State-change Notification"],
"detail": {
"state": ["pending"]
}
})
}
resource "aws_cloudwatch_event_target" "register" {
target_id = "register-runner"
rule = aws_cloudwatch_event_rule.instance_launch.name
arn = aws_sqs_queue.register_runner_queue.arn
sqs_target {
message_group_id = "cloudwatch"
}
}
resource "aws_sqs_queue_policy" "register" {
queue_url = aws_sqs_queue.register_runner_queue.id
policy = data.aws_iam_policy_document.register_sqs_policy.json
}
data "aws_iam_policy_document" "register_sqs_policy" {
statement {
principals {
type = "Service"
identifiers = ["events.amazonaws.com"]
}
actions = ["sqs:SendMessage"]
resources = [aws_sqs_queue.register_runner_queue.arn]
condition {
test = "ArnEquals"
variable = "aws:SourceArn"
values = [aws_cloudwatch_event_rule.instance_launch.arn]
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment