Created
September 30, 2021 18:25
-
-
Save j3parker/9e3a3b1b6a143fce00fe15b7d6109eaa to your computer and use it in GitHub Desktop.
GitHub runner boot sketch
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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