- Architecture Overview and Infrastructure Components
- ECR Runner Image Repository
- Self-hosted GitHub Runner(s) Registration Token
- Hosting the Runner Docker Artifacts on CodeCommit
- Build/Push Runner Image using CodeBuild
- Scalable ECS Cluster
- EventBus and Schema Discover for Webhook Events
- ECS Runner Task Definition
- Lambda Function URL
- GitHub Webhook
- EventBridge Rule
- Testing the Final Infrastructure
The self-hosted runner agent supports a variety of processor architectures and operating systems. This post covers the preparation/testing of a debian:stable-slim based OS runner, and pushing them to a CodeCommit repo.
CodeCommit Repository
Create Repository
Run the following CLI command to create the repository named: github-actions-self-hosted-runner-debian. The AWS profile, my_dev_profile, in this example, is associated with IAM user runner-dev-user.
aws --profile my_dev_profile codecommit create-repository \
--repository-name github-actions-self-hosted-runner-debian \
--repository-description "Repository contains code for build Debian based GitHub runner image"
Sample output:
{
"repositoryMetadata": {
"accountId": "xxxxxxxxxxxx",
"repositoryId": "g6dgteg-r46r-5555-1111-iiiiiiii",
"repositoryName": "github-actions-self-hosted-runner-debian",
"repositoryDescription": "Repository contains code for build Debian based GitHub runner image",
"lastModifiedDate": "2023-01-05T16:02:30.188000+11:00",
"creationDate": "2023-01-05T16:02:30.188000+11:00",
"cloneUrlHttp": "https://git-codecommit.us-east-1.amazonaws.com/v1/repos/github-actions-self-hosted-runner-debian",
"cloneUrlSsh": "ssh://git-codecommit.us-east-1.amazonaws.com/v1/repos/github-actions-self-hosted-runner-debian",
"Arn": "arn:aws:codecommit:us-east-1:xxxxxxxxxxxx:github-actions-self-hosted-runner-debian"
}
}
Note down the repository’ HTTPS cloneUrlHttp:
https://git-codecommit.us-east-1.amazonaws.com/v1/repos/github-actions-self-hosted-runner-debian
Grant IAM User Access
In order for an IAM user to be able to perform operations on the repository, such as git pull/push, an appropriate policy needs to be attached to their user profile by an Administrator
Attach the PowerUser CodeCommit Managed policy to IAM user with name runner-dev-user:
aws --profile admin_profile iam attach-user-policy --user-name runner-dev-user --policy-arn arn:aws:iam::aws:policy/AWSCodeCommitPowerUser
Configure Credential Helper
The following commands would allow runner-dev-user (aws profile my_dev_user) to issue commands with privileges inherited from IAM policy, AWSCodeCommitPowerUser.
git config --global --add credential.helper '!aws --profile my_dev_profile my_dev_profile credential-helper $@'
git config --global --add credential.UseHttpPath true
Clone the Repository
User runner-dev-user can now clone and configure the repository.
cd ~
git config user.name runner-dev-user
git config user.email myemail@somehost.com
Output:
Cloning into 'github-actions-self-hosted-runner-debian'...
warning: You appear to have cloned an empty repository.
Create GitHub Runner Docker Build Files
With the repository and access now in place, the next step is to prepare and test the artifacts required for building the GitHub runner docker image.
Note: Some files listed below include code snippets sourced from the actions-runner-controller repository.
Create a local directory structure:
$ cd $HOME
$ mkdir -p github-actions-self-hosted-runner-debian/debian_stable_slim/hooks/job-completed.d
$ mkdir -p github-actions-self-hosted-runner-debian/debian_stable_slim/hooks/job-started.d
Create the files:
$ cd $HOME/github-actions-self-hosted-runner-debian
$ tree --dirsfirst
.
├── debian_stable_slim
│ ├── hooks
│ │ ├── job-completed.d
│ │ │ └── update-status
│ │ ├── job-started.d
│ │ │ └── update-status
│ │ ├── job-completed.sh
│ │ └── job-started.sh
│ ├── Dockerfile
│ ├── daemon.json
│ ├── docker-exec.sh
│ ├── entrypoint.sh
│ ├── graceful-stop.sh
│ ├── logger.sh
│ ├── startup.sh
│ ├── update-status
│ └── wait.sh
└── buildspec.yml
./debian_stable_slim/hooks/job-completed.d/update-status
#!/usr/bin/env bash
set -u
exec update-status Idle
./debian_stable_slim/hooks/job-started.d/update-status
#!/usr/bin/env bash
set -u
exec update-status Running "Run $GITHUB_RUN_ID from $GITHUB_REPOSITORY"
./debian_stable_slim/hooks/job-completed.sh
#!/usr/bin/env bash
set -Eeuo pipefail
# shellcheck source=runner/logger.sh
source logger.sh
log.debug "Running actions runner Job Completed Hooks"
for hook in /etc/actions-runner/hooks/job-completed.d/*; do
log.debug "Running hook: $hook"
"$hook" "$@"
done
./debian_stable_slim/hooks/job-started.sh
#!/usr/bin/env bash
set -Eeuo pipefail
# shellcheck source=runner/logger.sh
source logger.sh
log.debug "Running Job Started Hooks"
for hook in /etc/actions-runner/hooks/job-started.d/*; do
log.debug "Running hook: $hook"
"$hook" "$@"
done
./debian_stable_slim/Dockerfile
FROM debian:stable-slim
# Supported platforms https://github.com/actions/runner
# Release downloads https://github.com/actions/runner/releases
# Dockerfile contains code snippets inspired by resources found at: https://github.com/actions/actions-runner-controller/blob/master/runner/
ENV LC_ALL C
ENV DEBIAN_FRONTEND=noninteractive
ENV DEBCONF_NONINTERACTIVE_SEEN=true
ENV RUNNER_TOOL_CACHE=/opt/hostedtoolcache
ARG CONTEXT_ROOT_PATH
ARG RUNNER_USER
ARG RUNNER_USER_UID=1000
ARG RUNNER_USER_GID=$RUNNER_USER_UID
ARG RUNNER_VERSION
ARG CHANNEL=stable
ARG DOCKER_VERSION=24.0.7
ARG DOCKER_GROUP_GID=993
ARG DUMB_INIT_VERSION=1.2.5
ENV RUNNER_USER=${RUNNER_USER}
ENV RUNNER_HOME=/${RUNNER_USER}
ENV HOME=/home/${RUNNER_USER}
ENV TARGETPLATFORM="linux/amd64"
ENV TZ="UTC"
ENV RUNNER_VERSION=${RUNNER_VERSION}
ENV RUNNER_ASSETS_DIR=/runnertmp
RUN apt-get update -y \
&& apt-get upgrade -y \
&& apt-get install -y --no-install-recommends \
curl \
tzdata \
jq \
ca-certificates \
apt-utils \
apt-transport-https \
software-properties-common \
build-essential \
wget \
git \
iptables \
ssh \
gnupg \
zip \
unzip \
lsb-release \
sudo \
&& apt-get autoremove -y \
&& apt-get autoclean -y \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Download latest git-lfs version
RUN curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash && \
apt-get install -y --no-install-recommends git-lfs
RUN if [ "$(getent group ${DOCKER_GROUP_GID} | cut -d: -f3)" = "${DOCKER_GROUP_GID}" ] ; then \
groupdel $(getent group ${DOCKER_GROUP_GID} | cut -d: -f1) ; \
fi \
&& groupadd -g ${DOCKER_GROUP_GID} docker
RUN if [ "$(getent group ${RUNNER_USER_GID} | cut -d: -f3)" = "${RUNNER_USER_GID}" ] ; then \
groupdel $(getent group ${RUNNER_USER_GID} | cut -d: -f1) ; \
fi \
&& addgroup --system --gid ${RUNNER_USER_GID} ${RUNNER_USER}
RUN adduser --system --home ${HOME} --uid ${RUNNER_USER_GID} --gecos "" --gid ${RUNNER_USER_GID} --disabled-password ${RUNNER_USER} \
&& usermod -aG sudo ${RUNNER_USER} \
&& usermod -aG docker ${RUNNER_USER} \
&& echo "%sudo ALL=(ALL:ALL) NOPASSWD:ALL" >> /etc/sudoers \
&& echo "Defaults env_keep += \"DEBIAN_FRONTEND\"" >> /etc/sudoers
RUN export ARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) \
&& if [ "$ARCH" = "arm64" ]; then export ARCH=aarch64 ; fi \
&& if [ "$ARCH" = "amd64" ] || [ "$ARCH" = "i386" ]; then export ARCH=x86_64 ; fi \
&& curl -fLo /usr/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v${DUMB_INIT_VERSION}/dumb-init_${DUMB_INIT_VERSION}_${ARCH} \
&& chmod +x /usr/bin/dumb-init
RUN export ARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) \
&& if [ "$ARCH" = "amd64" ] || [ "$ARCH" = "x86_64" ] || [ "$ARCH" = "i386" ]; then export ARCH=x64 ; fi \
&& mkdir -p "$RUNNER_ASSETS_DIR" \
&& cd "$RUNNER_ASSETS_DIR" \
&& curl -fLo runner.tar.gz https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-${ARCH}-${RUNNER_VERSION}.tar.gz \
&& tar xzf ./runner.tar.gz \
&& rm -f runner.tar.gz \
&& ./bin/installdependencies.sh \
# libyaml-dev is required for ruby/setup-ruby action.
# It is installed after installdependencies.sh and before removing /var/lib/apt/lists
# to avoid rerunning apt-update on its own.
&& apt-get install -y libyaml-dev \
&& apt-get autoremove -y \
&& apt-get autoclean -y \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# https://github.com/actions/setup-python/issues/459#issuecomment-1182946401
RUN mkdir -p ${RUNNER_TOOL_CACHE} \
&& chown ${RUNNER_USER}:docker ${RUNNER_TOOL_CACHE} \
&& chmod g+rwx ${RUNNER_TOOL_CACHE}
RUN LSB_RELEASE=$(lsb_release -sr 2>/dev/null) \
&& wget -q https://packages.microsoft.com/config/debian/$LSB_RELEASE/packages-microsoft-prod.deb \
&& dpkg -i packages-microsoft-prod.deb \
&& rm packages-microsoft-prod.deb
RUN curl -fsSLO https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip \
&& unzip awscli-exe-linux-x86_64.zip \
&& ./aws/install -i /usr/local/aws -b /usr/local/bin \
&& rm awscli-exe-linux-x86_64.zip
# Include support for docker-in-docker
# Add Docker's official GPG key:
RUN install -m 0755 -d /etc/apt/keyrings \
&& curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
&& chmod a+r /etc/apt/keyrings/docker.gpg
# Add the repository to Apt sources:
RUN echo \
"deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
"$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null \
&& apt-get update -y \
&& apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin \
&& apt-get autoremove -y \
&& apt-get autoclean -y \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN mkdir -p /etc/docker
COPY ${CONTEXT_ROOT_PATH}/daemon.json /etc/docker/daemon.json
COPY ${CONTEXT_ROOT_PATH}/entrypoint.sh ${CONTEXT_ROOT_PATH}/startup.sh ${CONTEXT_ROOT_PATH}/logger.sh ${CONTEXT_ROOT_PATH}/graceful-stop.sh ${CONTEXT_ROOT_PATH}/wait.sh ${CONTEXT_ROOT_PATH}/update-status /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh /usr/bin/startup.sh /usr/bin/logger.sh /usr/bin/graceful-stop.sh /usr/bin/wait.sh /usr/bin/update-status
COPY ${CONTEXT_ROOT_PATH}/docker-exec.sh /usr/local/bin/docker
RUN chmod +x /usr/local/bin/docker
# Configure hooks folder structure.
COPY ${CONTEXT_ROOT_PATH}/hooks /etc/actions-runner/hooks/
RUN chmod -R +x /etc/actions-runner/hooks
VOLUME /var/lib/docker
ENV PATH="${PATH}:${HOME}/.local/bin:${RUNNER_HOME}"
ENV ImageOS=debian-stable
RUN echo "PATH=${PATH}" >> /etc/environment \
&& echo "ImageOS=${ImageOS}" >> /etc/environment
USER ${RUNNER_USER}
ENTRYPOINT ["/bin/bash", "-c"]
CMD ["entrypoint.sh"]
./debian_stable_slim/daemon.json
{
"hosts": [
"unix:///var/run/docker.sock"
]
}
./debian_stable_slim/docker-exec.sh
#!/usr/bin/env bash
set -Eeuo pipefail
DOCKER=/usr/bin/docker
exec $DOCKER "$@"
./debian_stable_slim/entrypoint.sh
#!/bin/bash
source logger.sh
source graceful-stop.sh
trap graceful_stop_term TERM
dumb-init bash <<'SCRIPT' &
source logger.sh
source wait.sh
DOCKER_DAEMON_CONFIG="/etc/docker/daemon.json"
if [ -e "${DOCKER_DAEMON_CONFIG}" ]; then
printf -- '---\n' 1>&2
cat ${DOCKER_DAEMON_CONFIG} | jq . 1>&2
printf -- '---\n' 1>&2
else
log.debug 'Docker daemon config file not supplied'
fi
log.debug 'Starting Docker daemon'
sudo service docker start &
log.debug 'Waiting for processes to be running...'
processes=(dockerd)
for process in "${processes[@]}"; do
if ! wait_for_process "$process"; then
log.error "$process is not running after max time"
exit 1
else
log.debug "$process is running"
fi
done
startup.sh
SCRIPT
RUNNER_INIT_PID=$!
log.notice "Runner init started with pid $RUNNER_INIT_PID"
wait $RUNNER_INIT_PID
log.notice "Runner init exited. Exiting this process with code 0."
trap - TERM
./debian_stable_slim/graceful-stop.sh
#!/bin/bash
RUNNER_GRACEFUL_STOP_TIMEOUT=${RUNNER_GRACEFUL_STOP_TIMEOUT:-30}
graceful_stop_term() {
log.notice "Executing actions-runner's SIGTERM handler."
log.notice "Ensuring dockerd is still running."
if ! docker ps -a; then
log.warning "Detected configuration error: dockerd should be running but is already nowhere. This is wrong. Ensure that your init system to NOT pass SIGTERM directly to dockerd!"
fi
# The below procedure atomically removes the runner from GitHub Actions service,
# to ensure that the runner is not running any job.
# This is required to not terminate the actions runner agent while running the job.
# If we didn't do this atomically, we might end up with a rare race where
# the runner agent is terminated while it was about to start a job.
# `pushd`` is needed to run the config.sh successfully.
# Without this the author of this script ended up with errors like the below:
# Cannot connect to server, because config files are missing. Skipping removing runner from the server.
# Does not exist. Skipping Removing .credentials
# Does not exist. Skipping Removing .runner
if ! pushd ${RUNNER_HOME}; then
log.error "Failed to pushd ${RUNNER_HOME}"
exit 1
fi
# wait for the registration first.
log.notice "Waiting for the runner to register first."
j=0
while [[ $j -lt $RUNNER_GRACEFUL_STOP_TIMEOUT ]]; do
sleep 1
if [ ! -f ${RUNNER_HOME}/.runner ]; then
log.notice "still waiting for runner to register"
else
log.notice "Observed that the runner has been registered."
break
fi
j=$((j + 1))
done
if ! ${RUNNER_HOME}/config.sh remove --token "${runner_remove_token}"; then
i=0
log.notice "Waiting for RUNNER_GRACEFUL_STOP_TIMEOUT=$RUNNER_GRACEFUL_STOP_TIMEOUT seconds until the runner agent to stop by itself."
while [[ $i -lt $RUNNER_GRACEFUL_STOP_TIMEOUT ]]; do
sleep 1
if ! pgrep Runner.Listener >/dev/null; then
log.notice "The runner agent stopped before RUNNER_GRACEFUL_STOP_TIMEOUT=$RUNNER_GRACEFUL_STOP_TIMEOUT"
break
fi
i=$((i + 1))
done
fi
if ! popd; then
log.error "Failed to popd from ${RUNNER_HOME}"
exit 1
fi
if pgrep Runner.Listener >/dev/null; then
# The below procedure fixes the runner to correctly notify the Actions service for the cancellation of this runner.
# It enables you to see `Error: The operation was canceled.` in the worklow job log, in case a job was still running on this runner when the
# termination is requested.
#
# Note though, due to how Actions work, no all job steps gets `Error: The operation was canceled.` in the job step logs.
# Jobs that were still in the first `Stet up job` step` seem to get `Error: A task was canceled.`
runner_listener_pid=$(pgrep Runner.Listener)
log.notice "Sending SIGTERM to the actions runner agent ($runner_listener_pid)."
kill -TERM "$runner_listener_pid"
log.notice "SIGTERM sent. If the runner is still running a job, you'll probably see \"Error: The operation was canceled.\" in its log."
log.notice "Waiting for the actions runner agent to stop."
while pgrep Runner.Listener >/dev/null; do
sleep 1
done
fi
# This message is supposed to be output only after the runner agent output:
# YYYY-MM-DD HH:MM:SS: Job ttxxxx completed with result: Canceled
# because this graceful stopping logic is basically intended to let the runner agent have some time
# needed to "Cancel" it.
# At the times we didn't have this logic, the runner agent was even unable to output the Cancelled message hence
# unable to gracefully stop, hence the workflow job hanged like forever.
log.notice "The actions runner process exited."
if [ "$RUNNER_INIT_PID" != "" ]; then
log.notice "Holding on until runner init (pid $RUNNER_INIT_PID) exits, so that there will hopefully be no zombie processes remaining."
# We don't need to kill -TERM $RUNNER_INIT_PID as the init is supposed to exit by itself once the foreground process(=the runner agent) exists.
wait "$RUNNER_INIT_PID" || :
fi
log.notice "Graceful stop completed."
}
./debian_stable_slim/logger.sh
#!/usr/bin/env bash
# We are not using `set -Eeuo pipefail` here because this file is sourced by
# other scripts that might not be ready for a strict Bash setup. The functions
# in this file do not require it, because they are not handling signals, have
# no external calls that can fail (printf as well as date failures are ignored),
# are not using any variables that need to be set, and are not using any pipes.
# This logger implementation can be replaced with another logger implementation
# by placing a script called `logger.sh` in `/usr/local/bin` of the image. The
# only requirement for the script is that it defines the following functions:
#
# - `log.debug`
# - `log.notice`
# - `log.warning`
# - `log.error`
# - `log.success`
#
# Each function **MUST** accept an arbitrary amount of arguments that make up
# the (unstructured) logging message.
#
# Additionally the following environment variables **SHOULD** be supported to
# disable their corresponding log entries, the value of the variables **MUST**
# not matter the mere fact that they are set is all that matters:
#
# - `LOG_DEBUG_DISABLED`
# - `LOG_NOTICE_DISABLED`
# - `LOG_WARNING_DISABLED`
# - `LOG_ERROR_DISABLED`
# - `LOG_SUCCESS_DISABLED`
# The log format is constructed in a way that it can easily be parsed with
# standard tools and simple string manipulations; pattern and example:
#
# YYYY-MM-DD hh:mm:ss.SSS $level --- $message
# 2022-03-19 10:01:23.172 NOTICE --- example message
#
# This function is an implementation detail and **MUST NOT** be called from
# outside this script (which is possible if the file is sourced).
__log() {
local color instant level
color=${1:?missing required <color> argument}
shift
level=${FUNCNAME[1]} # `main` if called from top-level
level=${level#log.} # substring after `log.`
level=${level^^} # UPPERCASE
if [[ ! -v "LOG_${level}_DISABLED" ]]; then
instant=$(date '+%F %T.%-3N' 2>/dev/null || :)
# https://no-color.org/
if [[ -v NO_COLOR ]]; then
printf -- '%s %s --- %s\n' "$instant" "$level" "$*" 1>&2 || :
else
printf -- '\033[0;%dm%s %s --- %s\033[0m\n' "$color" "$instant" "$level" "$*" 1>&2 || :
fi
fi
}
# To log with a dynamic level use standard Bash capabilities:
#
# level=notice
# command || level=error
# "log.$level" message
#
# @formatter:off
log.debug() { __log 37 "$@"; } # white
log.notice() { __log 34 "$@"; } # blue
log.warning() { __log 33 "$@"; } # yellow
log.error() { __log 31 "$@"; } # red
log.success() { __log 32 "$@"; } # green
# @formatter:on
./debian_stable_slim/startup.sh
#!/bin/bash
source logger.sh
RUNNER_ASSETS_DIR=${RUNNER_ASSETS_DIR:-/runnertmp}
RUNNER_HOME=${RUNNER_HOME:-/runner}
RUNNER_CONFIG_ARGS=${RUNNER_CONFIG_ARGS}
RUNNER_LABELS=${RUNNER_LABELS}
REPOSITORY=${REPOSITORY}
ENTERPRISE=${ENTERPRISE}
ORGANIZATION=${ORGANIZATION}
RUNNER_CONFIG_RETRIES=${RUNNER_CONFIG_RETRIES:-10}
RUNNER_CONFIG_RETRY_INTERVAL_SECONDS=${RUNNER_CONFIG_RETRY_INTERVAL_SECONDS:-10}
RUNNER_ADMIN_TOKEN=${RUNNER_ADMIN_TOKEN}
DOCKER_ENABLED=${DOCKER_ENABLED:-"true"}
DISABLE_WAIT_FOR_DOCKER=${DISABLE_WAIT_FOR_DOCKER:-"false"}
RUNNER_VERSION=${RUNNER_VERSION}
EVENTS_DETAIL_WORKFLOW_JOB=${EVENTS_DETAIL_WORKFLOW_JOB}
# The scripts are automatically executed when the runner has the following
# environment variables containing an absolute path to the script:
# ACTIONS_RUNNER_HOOK_JOB_STARTED: The script defined in this environment variable
# is triggered when a job has been assigned to a runner,
# but before the job starts running.
# ACTIONS_RUNNER_HOOK_JOB_COMPLETED: The script defined in this environment variable is triggered at the end of the job,
# after all the steps defined in the workflow have run.
export ACTIONS_RUNNER_HOOK_JOB_STARTED=/etc/actions-runner/hooks/job-started.sh
export ACTIONS_RUNNER_HOOK_JOB_COMPLETED=/etc/actions-runner/hooks/job-completed.sh
if [ -n "${STARTUP_DELAY_IN_SECONDS}" ]; then
log.notice "Delaying startup by ${STARTUP_DELAY_IN_SECONDS} seconds"
sleep "${STARTUP_DELAY_IN_SECONDS}"
fi
github_api_pfx="https://api.github.com"
github_tgt_pfx="https://github.com"
if [[ "${REPOSITORY:-}" != '' ]]; then
rest_api_ep="${github_api_pfx}/repos/${ORGANIZATION}/${REPOSITORY}"
registration_url="${github_tgt_pfx}/${ORGANIZATION}/${REPOSITORY}"
fi
if [[ "${ORGANIZATION:-}" != '' ]]; then
rest_api_ep="${github_api_pfx}/orgs/${ORGANIZATION}"
registration_url="${github_tgt_pfx}/${ORGANIZATION}"
fi
if [[ "${ENTERPRISE:-}" != '' ]]; then
rest_api_ep="https://api.github.com/enterprises/${ENTERPRISE}"
registration_url="${github_tgt_pfx}/${ENTERPRISE}"
fi
rest_api_ep_runners_pfx=$(echo ${rest_api_ep}/actions/runners)
rest_api_ep_add_token=$(echo ${rest_api_ep_runners_pfx}/registration-token)
rest_api_ep_remove_token=$(echo ${rest_api_ep_runners_pfx}/remove-token)
if [ ! -d "${RUNNER_HOME}" ]; then
log.notice "Runner home dir ${RUNNER_HOME} is missing, creating it..."
sudo mkdir -p "${RUNNER_HOME}"
sudo chown -R ${RUNNER_USER}:docker "$RUNNER_HOME"
shopt -s dotglob
cp -r "$RUNNER_ASSETS_DIR"/* "$RUNNER_HOME"/
shopt -u dotglob
else
log.error "$RUNNER_HOME should be an emptyDir mount"
exit 1
fi
if ! cd "${RUNNER_HOME}"; then
log.error "Failed to cd into ${RUNNER_HOME}"
exit 1
fi
update-status "Registering"
retries_left=${RUNNER_CONFIG_RETRIES}
while [[ ${retries_left} -gt 0 ]]; do
log.debug 'Configuring the runner.'
runner_reg_token=$(echo $(curl -L -X POST -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${RUNNER_ADMIN_TOKEN}" -H "X-GitHub-Api-Version: 2022-11-28" ${rest_api_ep_add_token} | jq .token --raw-output))
runner_remove_token=$(echo $(curl -L -X POST -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${RUNNER_ADMIN_TOKEN}" -H "X-GitHub-Api-Version: 2022-11-28" ${rest_api_ep_remove_token} | jq .token --raw-output))
event_detail_wf_job=`echo ${EVENTS_DETAIL_WORKFLOW_JOB} | jq`
log.notice "Event detail workflow job json: ${event_detail_wf_job}"
if [[ "${RUNNER_CONFIG_ARGS:-}" != '' ]] && [[ "${RUNNER_LABELS:-}" != '' ]]; then
labels=`echo ${EVENTS_DETAIL_WORKFLOW_JOB} | jq -r '(.labels | join(","))'`
${RUNNER_HOME}/config.sh --url ${registration_url} --token ${runner_reg_token} ${RUNNER_CONFIG_ARGS} --labels "${labels}"
else
${RUNNER_HOME}/config.sh --url ${registration_url} --token ${runner_reg_token} ${RUNNER_CONFIG_ARGS}
fi
if [ -f ${RUNNER_HOME}/.runner ]; then
log.debug 'Runner successfully configured.'
break
fi
log.debug 'Configuration failed. Retrying'
retries_left=$((${RUNNER_CONFIG_RETRIES} - 1))
log.debug "Number of retries left before giving up -> ${retries_left} "
sleep ${RUNNER_CONFIG_RETRY_INTERVAL_SECONDS}
done
if [ ! -f ${RUNNER_HOME}/.runner ]; then
# couldn't configure and register the runner
log.error 'Configuration failed!'
exit 2
fi
cat ${RUNNER_HOME}/.runner
# Note: the `.runner` file's content should be something like the below:
# ----------------------------------------------------------------------
# $ cat ${RUNNER_HOME}/.runner
# {
# "agentId": 426,
# "agentName": "somerunner",
# "poolId": 1,
# "poolName": "some-runnergroup-name",
# "disableUpdate": true,
# "serverUrl": "https://pipelinesghubeus13.actions.githubusercontent.com/rZU6ldCEexQKjJTKI0qZFIEgV6GOKzbnLqm0X8GE7HIsQYIzpI/",
# "gitHubUrl": "https://github.com/some-organization",
# "workFolder": "_work"
# }
runner_id=$(cat ${RUNNER_HOME}/.runner | jq -r .agentId)
runner_name=$(cat ${RUNNER_HOME}/.runner | jq -r .agentName)
runner_group_id=$(cat ${RUNNER_HOME}/.runner | jq -r .poolId)
runner_group_name=$(cat ${RUNNER_HOME}/.runner | jq -r .agentName)
runner_work_folder=$(cat ${RUNNER_HOME}/.runner | jq -r .workFolder)
log.debug 'Waiting for processes to be running...'
WAIT_FOR_DOCKER_SECONDS=${WAIT_FOR_DOCKER_SECONDS:-120}
if [[ "${DISABLE_WAIT_FOR_DOCKER}" != "true" ]] && [[ "${DOCKER_ENABLED}" == "true" ]]; then
log.debug 'Docker enabled runner detected and Docker daemon wait is enabled'
log.debug "Waiting until Docker is available or the timeout of ${WAIT_FOR_DOCKER_SECONDS} seconds is reached"
if ! timeout "${WAIT_FOR_DOCKER_SECONDS}s" bash -c 'until docker ps ;do sleep 1; done'; then
log.notice "Docker has not become available within ${WAIT_FOR_DOCKER_SECONDS} seconds. Exiting with status 1."
exit 1
fi
else
log.notice 'Docker wait check skipped. Either Docker is disabled or the wait is disabled, continuing with entrypoint'
fi
# Unset entrypoint environment variables so they don't leak into the runner environment
unset EVENTS_DETAIL_WORKFLOW_JOB RUNNER_LABELS RUNNER_CONFIG_ARGS REPOSITORY RUNNER_ADMIN_TOKEN ORGANIZATION ENTERPRISE STARTUP_DELAY_IN_SECONDS DISABLE_WAIT_FOR_DOCKER runner_reg_token runner_remove_token
update-status "Idle"
exec env -- "${env[@]}" ./run.sh
./debian_stable_slim/update-status
#!/usr/bin/env bash
set -Eeuo pipefail
if [[ ${1:-} == '' ]]; then
# shellcheck source=runner/logger.sh
source logger.sh
log.error "Missing required argument -- '<phase>'"
exit 64
fi
./debian_stable_slim/wait.sh
#!/bin/bash
function wait_for_process() {
local max_time_wait=30
local process_name="$1"
local waited_sec=0
while ! pgrep "$process_name" >/dev/null && ((waited_sec < max_time_wait)); do
log.debug "Process $process_name is not running yet. Retrying in 1 seconds"
log.debug "Waited $waited_sec seconds of $max_time_wait seconds"
sleep 1
((waited_sec = waited_sec + 1))
if ((waited_sec >= max_time_wait)); then
return 1
fi
done
return 0
}
./buildspec.yml
version: 0.2
env:
shell: bash
phases:
pre_build:
commands:
- echo Logging in to Amazon ECR....
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
build:
commands:
- echo Building the Docker image...
- docker pull $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
- |
#!/bin/bash
INSTALLED_AGENT_VERSION=$(docker inspect $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG -f '{{range .Config.Env}}{{printf "%s\n" .}}{{end}}' | grep RUNNER_VERSION | cut -d= -f2)
echo $INSTALLED_AGENT_VERSION
LATEST_RELEASE_VERSION=$(curl -s -L -H 'Accept: application/vnd.github+json' -H 'X-GitHub-Api-Version: 2022-11-28' https://api.github.com/repos/actions/runner/releases/latest | jq -r '.tag_name' | cut -c 2-)
echo $LATEST_RELEASE_VERSION
if [[ "$INSTALLED_AGENT_VERSION" != "$LATEST_RELEASE_VERSION" ]]; then
docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG --build-arg RUNNER_VERSION=$LATEST_RELEASE_VERSION --build-arg RUNNER_USER=runner --build-arg CONTEXT_ROOT_PATH=debian_stable_slim -f debian_stable_slim/Dockerfile .
docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
else
echo Latest GitHub Agent latest release version: $LATEST_RELEASE_VERSION is the same as version bundled installed in the ECR image: $INSTALLED_AGENT_VERSION
echo Nothing to do
fi
post_build:
commands:
- echo Build completed on `date`
Test Docker Image Build
The Dockerfile accepts the following build arguments to be passed:
Argument | Value |
---|---|
RUNNER_VERSION | GitHub agent runner version number, as published at https://github.com/actions/runner/tags. For example, if a release in this repository is tagged v2.309.0, then 2.309.0 would be passed as the arg value. |
RUNNER_USER | Username to use for the container |
CONTEXT_ROOT_PATH | Relative path to the Dockerfile |
Run a local test build:
$ cd $HOME/github-actions-self-hosted-runner-debian
$ docker build -t \
github-actions-self-hosted-runner-debian:latest \
--build-arg RUNNER_VERSION="$(curl -s -L -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" https://api.github.com/repos/actions/runner/releases/latest | jq -r '.tag_name' | cut -c 2-)" \
--build-arg RUNNER_USER="runner" \
--build-arg CONTEXT_ROOT_PATH="debian_stable_slim" \
-f debian_stable_slim/Dockerfile .
If the build succeeds, we can move onto pushing the local repository to the remote target.
Commit/Push Local Repository to Remote
To commit/push our local repo to CodeCommit:
cd $HOME/github-actions-self-hosted-runner-debian
git add .
git commit -m "upload app code"
git push origin master
Check contents of remote:
