Skip to content
This repository has been archived by the owner on Oct 23, 2023. It is now read-only.
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: superblk/ec2-actions-runner
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.3.0
Choose a base ref
...
head repository: superblk/ec2-actions-runner
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref

Commits on Aug 27, 2021

  1. Use EC2 launch template to pass instance config (#4)

    * Use EC2 launch template to pass instance config
    
    * Refactor
    jpalomaki authored Aug 27, 2021
    Copy the full SHA
    b05226c View commit details

Commits on Aug 28, 2021

  1. Update action.yml

    jpalomaki authored Aug 28, 2021
    Copy the full SHA
    f44e8d4 View commit details
  2. Update README.md

    jpalomaki authored Aug 28, 2021
    Copy the full SHA
    27fd63e View commit details
  3. Update README.md

    jpalomaki authored Aug 28, 2021
    Copy the full SHA
    fa3ee70 View commit details

Commits on Aug 29, 2021

  1. Update README.md

    jpalomaki authored Aug 29, 2021
    Copy the full SHA
    458ad9d View commit details
  2. Update README.md

    jpalomaki authored Aug 29, 2021
    Copy the full SHA
    c7bdf19 View commit details

Commits on Aug 30, 2021

  1. Copy the full SHA
    108d9c6 View commit details

Commits on Sep 3, 2021

  1. Update README.md

    jpalomaki authored Sep 3, 2021
    Copy the full SHA
    b43beb4 View commit details

Commits on Sep 5, 2021

  1. Make stop action safer (#6)

    This might help in situations where the start job is cancelled, making the stop job more likely to successfully terminate the instance
    jpalomaki authored Sep 5, 2021
    Copy the full SHA
    1deeec2 View commit details
  2. Update README.md

    jpalomaki authored Sep 5, 2021
    Copy the full SHA
    6350bdb View commit details

Commits on Sep 13, 2021

  1. Add auto-shutdown (#7)

    jpalomaki authored Sep 13, 2021
    Copy the full SHA
    12da049 View commit details
  2. Update README.md

    jpalomaki authored Sep 13, 2021
    Copy the full SHA
    97c383a View commit details
  3. Update action.yml

    jpalomaki authored Sep 13, 2021
    Copy the full SHA
    53880d5 View commit details
  4. Update action.yml

    jpalomaki authored Sep 13, 2021
    Copy the full SHA
    7eee636 View commit details
  5. Update action.yml

    jpalomaki authored Sep 13, 2021
    Copy the full SHA
    fd03d5c View commit details
  6. Update README.md

    jpalomaki authored Sep 13, 2021
    Copy the full SHA
    c21dad8 View commit details

Commits on Sep 26, 2021

  1. Copy the full SHA
    8bb2976 View commit details
  2. Update README.md

    jpalomaki authored Sep 26, 2021
    Copy the full SHA
    b00e70f View commit details

Commits on Oct 22, 2021

  1. Copy the full SHA
    d0e9389 View commit details

Commits on Dec 2, 2021

  1. Refactor action code (#12)

    * Refactor action code
    
    * Update action.yml
    jpalomaki authored Dec 2, 2021
    Copy the full SHA
    10fd826 View commit details
  2. Make AWS access keys optional and document OIDC (#11)

    * Make AWS access keys optional
    
    For OIDC (AssumeRoleWithWebIdentity)
    
    * Update action.yml
    
    * Update README.md
    
    * Update README.md
    
    * Update README.md
    
    * Update action.yml
    
    * Update action.yml
    
    * Update README.md
    
    * Update README.md
    
    * Update action.yml
    
    * Update action.yml
    
    * Update README.md
    
    * Update README.md
    
    * Update README.md
    
    * Update README.md
    jpalomaki authored Dec 2, 2021
    Copy the full SHA
    b26f0df View commit details

Commits on Dec 7, 2021

  1. Update documentation to cover instance profiles (#13)

    * Update documentation to cover instance profiles
    
    * Update action.yml
    
    * Update action.yml
    
    * Update README.md
    jpalomaki authored Dec 7, 2021
    Copy the full SHA
    7fb1b9e View commit details

Commits on Oct 23, 2023

  1. Update README.md

    jpalomaki authored Oct 23, 2023
    Copy the full SHA
    eaff1cf View commit details
Showing with 270 additions and 97 deletions.
  1. +192 −35 README.md
  2. +48 −39 start/action.yml
  3. +30 −23 stop/action.yml
227 changes: 192 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,73 +1,230 @@
# ec2-actions-runner

⚠️ This is a new project and is very experimental
Composite actions for managing an on-demand, self-hosted GitHub actions _repository_ runner (Linux on EC2)

Composite actions for managing an on-demand, self-hosted GitHub actions _repository_ runner (Linux on EC2).
⚠️ This project is deprecated and is no longer maintained. Have a look at e.g. [terraform-aws-github-runner](https://github.com/philips-labs/terraform-aws-github-runner) instead.

Inspired by <https://github.com/machulav/ec2-github-runner>
⚠️ Self-hosted runners should **not** be used with _public_ repositories (see GitHub [documentation](https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners#self-hosted-runner-security))

## Requirements
Inspired by <https://github.com/machulav/ec2-github-runner> ❤️

## Pre-requisites

- AWS account
- Default VPC + subnet
- AWS credentials with EC2 permissions
- Linux runner AMI (amd64 or arm64), with
- [Runner](https://github.com/actions/runner) software and [dependencies](https://github.com/actions/runner/blob/main/docs/start/envlinux.md)
- Non-root user to run actions-runner with
- See e.g. <https://github.com/superblk/ec2-actions-runner-ami-linux-arm64>
- GitHub personal access token (PAT) with `repo` scope
- Permissions to provision IAM, EC2 and VPC resources (to set up the runner AWS scaffolding)
- VPC network
- Subnet with Internet access (required because self-hosted runners communicate with `github.com`)

## Limitations

- GitHub Enterprise Server (GHES) is not currently supported

## Setup

1. AWS: Configure GitHub OIDC identity provider (GitHub [documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services))
- Use of OIDC is recommended (safer), because static AWS access keys need not be stored in GitHub secrets
- NOTE: if you cannot configure OIDC-assumable roles, it is possible to use an IAM user with static access keys
2. AWS: Configure the IAM role that is assumed by the workflow, used only _for starting and stopping the runner EC2 instances_
- Example OIDC assume role (trust) policy, that defines which GitHub repos can assume the role (see example [CloudFormation template](https://github.com/aws-actions/configure-aws-credentials#sample-iam-role-cloudformation-template))

```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::<account>:oidc-provider/token.actions.githubusercontent.com"
},
"Action": [
"sts:AssumeRoleWithWebIdentity",
"sts:TagSession"
],
"Condition": {
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:<owner>/<repository>:*"
}
}
}
]
}
```
- Example role policy (inline or customer-managed), that defines the _minimum_ permissions needed for starting/stopping runner EC2 instances

```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:RunInstances",
"ec2:TerminateInstances"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ec2:CreateTags"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"ec2:CreateAction": "RunInstances"
}
}
}
]
}
```
- If you need to assign an IAM instance profile (role) to the EC2 instances, you need to use a policy that includes the `iam:PassRole` permission

```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:RunInstances",
"ec2:TerminateInstances"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ec2:CreateTags"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"ec2:CreateAction": "RunInstances"
}
}
},
{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::<account>:role/<role for EC2>"
}
]
}
```
4. AWS: Linux runner AMI (amd64 or arm64), with the following things pre-configured:
- Non-root user to run the actions-runner service as
- [Actions-runner](https://github.com/actions/runner) v2.283.1+ and required [dependencies](https://github.com/actions/runner/blob/main/docs/start/envlinux.md)
- `git`, `docker`, `curl` and optionally `at` (if using the `auto-shutdown-at` feature)
- See e.g. <https://github.com/superblk/ec2-actions-runner-ami-ubuntu-18.04-arm64> for an example
5. AWS: EC2 runner launch template (defines AMI, instance type, VPC subnet, security groups, instance profile, spot options etc)
- See example [Cloudformation template](https://gist.github.com/jpalomaki/003c4d173a856cf64c6d35f8869a2de8) that sets up a launch template
6. GitHub: personal access token (PAT) with `repo` scope, required for registering self-hosted repository runners

## Example workflows

See [start/action.yml](start/action.yml) and [stop/action.yml](stop/action.yml) for all available input parameters

💡 EC2 instance ID is automatically assigned as a unique, self-hosted runner label

⚠️ Do not simply copy these examples verbatim, but adjust action version, AWS region, launch template name etc to match your configuration

## Example workflow
### Simple

:warning: do not copy this example verbatim, but adjust AWS region, AMI id etc to match your config
Leverages ephemeral runners that are automatically deregistered from GitHub after the `main` job has run to completion

```yaml
jobs:
start-runner:
runs-on: ubuntu-18.04
permissions:
id-token: write
runs-on: ubuntu-20.04
steps:
- id: runner
name: Start runner
uses: superblk/ec2-actions-runner/start@v0.3.0
uses: superblk/ec2-actions-runner/start@<release>
with:
aws-region: eu-north-1
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-role-to-assume: arn:aws:iam::<account>:role/<role>
aws-launch-template: LaunchTemplateName=my-special-runner
github-token: ${{ secrets.GH_PAT }}
github-repo: ${{ github.repository }}
ami-id: ami-12345678901234567
instance-type: t4g.small
runner-labels: ubuntu-18.04-arm64
outputs:
runner-id: ${{ steps.runner.outputs.runner-id }}
instance-id: ${{ steps.runner.outputs.instance-id }}

complex-build:
main:
needs: start-runner
runs-on: ${{ needs.start-runner.outputs.instance-id }}
steps:
- run: uname -a

stop-runner:
if: always()
permissions:
id-token: write
needs: [start-runner, main]
runs-on: ubuntu-20.04
steps:
- name: Stop runner
uses: superblk/ec2-actions-runner/stop@<release>
with:
aws-region: eu-north-1
aws-role-to-assume: arn:aws:iam::<account>:role/<role>
instance-id: ${{ needs.start-runner.outputs.instance-id }}
```

### Advanced

A more fail-safe alternative. Deregisters GitHub runner explicitly (not relying on ephemeral runner auto-deregistration behavior alone). Also leverages EC2 [instance-initiated shutdown](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/terminating-instances.html#Using_ChangingInstanceInitiatedShutdownBehavior) terminate behavior for ensuring the EC2 instance is terminated, even if the `stop-runner` job fails to run

💡 This example also illustrates the use of extra runner labels and a matrix `main` job, that uses both GitHub-hosted and self-hosted runners

⚠️ For the automatic dead-man's switch termination to work, the AMI must include the `at` tool, and the EC2 launch template must specify instance-initiated shutdown behavior as _terminate_

```yaml
jobs:
start-runner:
permissions:
id-token: write
runs-on: ubuntu-20.04
steps:
- id: runner
name: Start runner
uses: superblk/ec2-actions-runner/start@<release>
with:
aws-region: eu-north-1
aws-role-to-assume: arn:aws:iam::<account>:role/<role>
aws-launch-template: LaunchTemplateName=my-special-runner
runner-labels: ubuntu-18.04-arm64-${{ github.run_id }}
github-token: ${{ secrets.GH_PAT }}
auto-shutdown-at: 'now + 3 hours'
outputs:
instance-id: ${{ steps.runner.outputs.instance-id }}
runner-id: ${{ steps.runner.outputs.runner-id }}

main:
needs: start-runner
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include:
- runner: ubuntu-18.04
message: GitHub-hosted runner
- runner: ubuntu-18.04-arm64
message: Self-hosted EC2 runner
- runner: ubuntu-18.04-arm64-${{ github.run_id }}
steps:
- run: echo '${{ matrix.message }}'
- run: uname -a

stop-runner:
if: always()
needs: [start-runner, complex-build]
runs-on: ubuntu-18.04
permissions:
id-token: write
needs: [start-runner, main]
runs-on: ubuntu-20.04
steps:
- name: Stop runner
uses: superblk/ec2-actions-runner/stop@v0.3.0
uses: superblk/ec2-actions-runner/stop@<release>
with:
aws-region: eu-north-1
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
github-token: ${{ secrets.GH_PAT }}
github-repo: ${{ github.repository }}
runner-id: ${{ needs.start-runner.outputs.runner-id }}
aws-role-to-assume: arn:aws:iam::<account>:role/<role>
instance-id: ${{ needs.start-runner.outputs.instance-id }}
runner-id: ${{ needs.start-runner.outputs.runner-id }}
github-token: ${{ secrets.GH_PAT }}
```
87 changes: 48 additions & 39 deletions start/action.yml
Original file line number Diff line number Diff line change
@@ -4,47 +4,52 @@ description: Starts and registers a self-hosted GitHub actions repository runner

inputs:
aws-region:
description: AWS region
description: AWS region, e.g. eu-west-1
required: true
aws-access-key-id:
description: AWS access key ID
required: true
description: AWS access key ID (pass via GitHub secret). Required only if **not** using OIDC
required: false
aws-secret-access-key:
description: AWS secret access key
required: true
description: AWS secret access key (pass via GitHub secret). Required only if **not** using OIDC
required: false
aws-role-to-assume:
description: AWS IAM role to assume
description: AWS IAM role (ARN) to assume, for launching the instance. Required if using OIDC (AssumeRoleWithWebIdentity)
required: false
aws-launch-template:
description: AWS EC2 launch template (AWS CLI format, e.g. LaunchTemplateId=lt-0abcd290751193123)
required: true
github-token:
description: GitHub token (PAT with repo scope)
description: GitHub token (PAT with repo scope, pass via GitHub secret)
required: true
github-repo:
description: Github repository, e.g. ghost/example
required: true
ami-id:
description: AMI id, e.g. ami-12345678901234567
required: true
instance-type:
description: EC2 instance type, e.g. t4g.small
required: true
description: Github repository, e.g. ghost/example. Defaults to current repository
default: ${{ github.repository }}
required: false
runner-labels:
description: Extra runner labels (comma-separated)
description: Extra runner labels (comma-separated). Can be referenced in job 'runs-on'
required: false
runner-home:
description: Runner OS home directory
description: Directory that contains actions-runner software and scripts
required: false
default: /home/ubuntu/actions-runner
runner-user:
description: Runner OS user
description: User to run the actions-runner service as
required: false
default: ubuntu
auto-shutdown-at:
description: Automatically shutdown instance at the specified time?. E.g. 'now + 72 hours'. Requires https://linux.die.net/man/1/at
required: false
ephemeral:
description: Flag the runner as ephemeral? An ephemeral runner is automatically de-registered after running _one_ workflow job
required: false
default: true

outputs:
runner-id:
description: GitHub repository runner id
value: ${{ steps.main.outputs.runner-id }}
instance-id:
description: EC2 instance id
description: AWS EC2 instance id
value: ${{ steps.main.outputs.instance-id }}

runs:
@@ -64,37 +69,42 @@ runs:
- id: main
shell: bash
run: |
runner_token="$(gh api -X POST -H 'Accept: application/vnd.github.v3+json' "repos/$GITHUB_REPO/actions/runners/registration-token" | jq -r .token)"
runner_token="$(gh api -X POST "repos/$GH_REPO/actions/runners/registration-token" | jq -r .token)"
if [ "$EPHEMERAL" = "true" ]; then
extra_flags="--ephemeral"
fi
if [ -n "$RUNNER_LABELS" ]; then
extra_labels=",$RUNNER_LABELS"
fi
user_data=$(cat <<HERE
#!/bin/bash
cd "$RUNNER_HOME"
instance_id="\$(curl -s http://169.254.169.254/latest/meta-data/instance-id)"
sudo -u "$RUNNER_USER" ./config.sh --unattended --name "\$instance_id" --url "https://github.com/$GITHUB_REPO" --token "$runner_token" --labels "$RUNNER_LABELS"
instance_id="\$(cat /var/lib/cloud/data/instance-id)"
sudo -u "$RUNNER_USER" ./config.sh --unattended $extra_flags --name "\$instance_id" --url "https://github.com/$GH_REPO" --token "$runner_token" --labels "\$instance_id$extra_labels"
./svc.sh install "$RUNNER_USER"
./svc.sh start
test -z "$SHUTDOWN_AT" || echo 'shutdown -P now' | at -M "$SHUTDOWN_AT"
HERE
)
echo "Starting EC2 instance ..."
instance_id=$(aws ec2 run-instances \
--image-id "$AMI_ID" \
--instance-type "$INSTANCE_TYPE" \
--user-data "$user_data" \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=GitHub self-hosted runner}]' \
| jq -r .Instances[0].InstanceId)
instance_id="$(aws ec2 run-instances --launch-template "$LAUNCH_TEMPLATE" --user-data "$user_data" | jq -r .Instances[0].InstanceId)"
echo "::set-output name=instance-id::$instance_id"
echo "Started EC2 instance: $instance_id"
echo "Waiting for repository runner to be registered ..."
for i in {1..12}; do
sleep 10
runner_id=$(gh api -X GET -H 'Accept: application/vnd.github.v3+json' \
-f per_page=100 "repos/$GITHUB_REPO/actions/runners" | jq -r --arg INSTANCE_ID "$instance_id" \
'.runners[] | select(.name == $INSTANCE_ID and .status == "online") | .id')
if [ ! -z "$runner_id" ]; then
runner_id=$(gh api -X GET "repos/$GH_REPO/actions/runners" -f per_page=100 \
| jq -r --arg INSTANCE_ID "$instance_id" '.runners[] | select(.name == $INSTANCE_ID and .status == "online") | .id')
if [ -n "$runner_id" ]; then
break
fi
done
@@ -104,17 +114,16 @@ runs:
aws ec2 terminate-instances --instance-ids "$instance_id"
exit 1
else
echo "Repository runner registered with id: $runner_id"
echo "::set-output name=runner-id::$runner_id"
echo "Repository runner started (ID: $runner_id)"
fi
echo "::set-output name=runner-id::$runner_id"
echo "::set-output name=instance-id::$instance_id"
env:
GH_TOKEN: ${{ inputs.github-token }}
AWS_DEFAULT_REGION: ${{ inputs.aws-region }}
GITHUB_REPO: ${{ inputs.github-repo }}
LAUNCH_TEMPLATE: ${{ inputs.aws-launch-template }}
GH_REPO: ${{ inputs.github-repo }}
RUNNER_HOME: ${{ inputs.runner-home }}
RUNNER_USER: ${{ inputs.runner-user }}
RUNNER_LABELS: ${{ inputs.runner-labels }}
AMI_ID: ${{ inputs.ami-id }}
INSTANCE_TYPE: ${{ inputs.instance-type }}
SHUTDOWN_AT: ${{ inputs.auto-shutdown-at }}
EPHEMERAL: ${{ inputs.ephemeral }}
53 changes: 30 additions & 23 deletions stop/action.yml
Original file line number Diff line number Diff line change
@@ -4,28 +4,29 @@ description: Deregisters and terminates a self-hosted GitHub actions repository

inputs:
aws-region:
description: AWS region
description: AWS region, e.g. eu-west-1
required: true
aws-access-key-id:
description: AWS access key ID
required: true
description: AWS access key ID (pass via GitHub secret). Required only if **not** using OIDC
required: false
aws-secret-access-key:
description: AWS secret access key
required: true
description: AWS secret access key (pass via GitHub secret). Required only if **not** using OIDC
required: false
aws-role-to-assume:
description: AWS IAM role to assume
description: AWS IAM role (ARN) to assume, for terminating the instance. Required if using OIDC (AssumeRoleWithWebIdentity)
required: false
github-token:
description: GitHub auth token (PAT with repo scope)
required: true
description: GitHub auth token (PAT with repo scope, pass via GitHub secret). Optional if using ephemeral runners
required: false
github-repo:
description: Github repository, e.g. ghost/example
required: true
description: GitHub repository, e.g. ghost/example. Optional, defaults to the repository where the workflow is run
default: ${{ github.repository }}
required: false
runner-id:
description: GitHub repository runner id
required: true
description: GitHub repository runner ID, e.g. 53. Optional if using ephemeral runners
required: false
instance-id:
description: EC2 instance id
description: EC2 instance id, e.g. i-0ab3a789090bd84c7
required: true

runs:
@@ -45,19 +46,25 @@ runs:
- id: main
shell: bash
run: |
echo "Deregistering repository runner: $RUNNER_ID ..."
gh api -X DELETE -H 'Accept: application/vnd.github.v3+json' \
"repos/$GITHUB_REPO/actions/runners/$RUNNER_ID" || echo "WARN: Failed to deregister runner"
echo "Terminating EC2 instance: $INSTANCE_ID ..."
aws ec2 terminate-instances --instance-ids "$INSTANCE_ID" > /dev/null
# We do not fail the job on missing runner ID, because offline and ephemeral runners are automatically deregistered
if [ -n "$RUNNER_ID" ]; then
echo "Deregistering GitHub repository runner: $RUNNER_ID ..."
gh api -X DELETE "repos/$GH_REPO/actions/runners/$RUNNER_ID" \
|| echo "WARN: Failed to deregister GitHub repository runner (GitHub API call failed)"
fi
echo "Repository runner stopped"
# Failure to terminate the EC2 instance will incur unnecessary costs, so we fail the job to ensure the user notices
if [ -z "$INSTANCE_ID" ]; then
echo "ERROR: Unable to terminate EC2 instance (instance ID not available)"
exit 1
else
echo "Terminating EC2 instance: $INSTANCE_ID ..."
aws ec2 terminate-instances --instance-ids "$INSTANCE_ID" > /dev/null
echo "Repository runner stopped"
fi
env:
GH_TOKEN: ${{ inputs.github-token }}
AWS_DEFAULT_REGION: ${{ inputs.aws-region }}
RUNNER_ID: ${{ inputs.runner-id }}
GITHUB_REPO: ${{ inputs.github-repo }}
GH_REPO: ${{ inputs.github-repo }}
INSTANCE_ID: ${{ inputs.instance-id }}