Our image will be derived from an ubuntu:jammy
base, for target architecture type, linux/amd64
.
After building the image, we’ll push it to an ECR repository.
Image Artifacts
Create a location to store the image files:
$ mkdir $HOME/gha-jit-fargate-runner
Add the files required.
$HOME/gha-jit-fargate-runner/Dockerfile
FROM ubuntu:jammy
ENV LC_ALL=C
ENV DEBIAN_FRONTEND=noninteractive
ENV DEBCONF_NONINTERACTIVE_SEEN=true
ENV RUNNER_TOOL_CACHE=/opt/hostedtoolcache
ENV AGENT_TOOLSDIRECTORY=${RUNNER_TOOL_CACHE}
ARG RUNNER_USER
ARG RUNNER_USER_UID=1000
ARG RUNNER_USER_GID=$RUNNER_USER_UID
ARG RUNNER_VERSION
ARG DUMB_INIT_VERSION=1.2.5
ENV RUNNER_USER=${RUNNER_USER}
ENV RUNNER_HOME=/home/${RUNNER_USER}/actions_runner
ENV HOME=/home/${RUNNER_USER}
ENV TZ="UTC"
ENV RUNNER_VERSION=${RUNNER_VERSION}
ENV TARGETPLATFORM=linux/amd64
ENV RUNNER_ARCH=x64
ENV RUNNER_MANUALLY_TRAP_SIG=1
RUN apt-get update -y \
&& apt-get upgrade -y \
&& apt-get install -y --no-install-recommends \
apt-utils \
curl \
awscli \
tzdata \
jq \
ca-certificates \
apt-transport-https \
software-properties-common \
wget \
git \
gnupg \
zip \
unzip \
lsb-release \
util-linux \
&& 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 ${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} --shell /bin/bash --gecos "" --gid ${RUNNER_USER_GID} --disabled-password ${RUNNER_USER}
# https://github.com/actions/setup-python/issues/459#issuecomment-1182946401
RUN mkdir -p ${RUNNER_TOOL_CACHE} \
&& chown ${RUNNER_USER}:${RUNNER_USER} ${RUNNER_TOOL_CACHE} \
&& chmod g+rwx ${RUNNER_TOOL_CACHE}
RUN LSB_RELEASE=$(lsb_release -rs) \
&& wget -q https://packages.microsoft.com/config/ubuntu/$LSB_RELEASE/packages-microsoft-prod.deb \
&& dpkg -i packages-microsoft-prod.deb \
&& rm packages-microsoft-prod.deb \
&& apt-get update -y \
&& apt-get upgrade -y \
&& apt-get install -y powershell \
&& apt-get autoremove -y \
&& apt-get autoclean -y \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN export ARCH=$(dpkg --print-architecture) \
&& 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=$(dpkg --print-architecture) \
&& if [ "$ARCH" = "amd64" ] || [ "$ARCH" = "x86_64" ] || [ "$ARCH" = "i386" ]; then export ARCH=x64 ; fi \
&& mkdir -p "$AGENT_TOOLSDIRECTORY" \
&& mkdir -p "$RUNNER_HOME" \
&& cd "$RUNNER_HOME" \
&& export runner_download_url=$(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 ".assets[]|select(.name | startswith(\"actions-runner-linux-$ARCH\")).browser_download_url") \
&& curl -fLo runner.tar.gz ${runner_download_url} \
&& tar xzf ./runner.tar.gz \
&& rm -f runner.tar.gz \
&& ./bin/installdependencies.sh \
&& apt-get install -y libyaml-dev \
&& apt-get autoremove -y \
&& apt-get autoclean -y \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
&& chown -R ${RUNNER_USER}:${RUNNER_USER} \
"$AGENT_TOOLSDIRECTORY" \
"$RUNNER_HOME" \
&& chmod -R g+rwx ${RUNNER_TOOL_CACHE} "$RUNNER_HOME"
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
RUN arch="$(dpkg --print-architecture)" \
&& cli_latest_version="$(curl -s -L -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" https://api.github.com/repos/cli/cli/releases/latest | jq -r '.tag_name' | cut -c 2-)" \
&& wget "https://github.com/cli/cli/releases/download/v${cli_latest_version}/gh_${cli_latest_version}_linux_${arch}.deb" -O gh_${cli_latest_version}_linux_${arch}.deb \
&& dpkg -i "gh_${cli_latest_version}_linux_${arch}.deb" \
&& rm "gh_${cli_latest_version}_linux_${arch}.deb"
COPY ./entrypoint.sh ./logger.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh /usr/bin/logger.sh
RUN mkdir -p ${RUNNER_HOME} \
&& chown -R ${RUNNER_USER}:${RUNNER_USER} ${RUNNER_HOME} ${HOME} $AGENT_TOOLSDIRECTORY
RUN echo "ImageOS=ubuntu-jammy" >> /etc/environment
USER ${RUNNER_USER}
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
CMD ["bash", "-c", "exec /usr/bin/entrypoint.sh"]
$HOME/gha-jit-fargate-runner/entrypoint.sh
#!/usr/bin/env bash
source logger.sh
RUNNER_USER=$(whoami)
RUNNER_HOME=${RUNNER_HOME:-/home/${RUNNER_USER}/actions-runner}
RUNNER_ARCH=${RUNNER_ARCH:-x64}
RUNNER_CONFIG_RETRIES=${RUNNER_CONFIG_RETRIES:-10}
RUNNER_CONFIG_RETRY_INTERVAL_SECONDS=${RUNNER_CONFIG_RETRY_INTERVAL_SECONDS:-10}
SQS_QUEUE_NAME="${SQS_QUEUE_NAME:-git-actions-wf-job-queue}"
aws_acct=$(aws sts get-caller-identity --query Account --output text)
aws_region=$(aws configure get region)
QUEUE_URL="https://sqs.$aws_region.amazonaws.com/$aws_acct/$SQS_QUEUE_NAME"
trap 'kill -INT -$PID;
echo "Exiting runner";
rm -fr ${RUNNER_HOME}/.runner;
${RUNNER_HOME}/config.sh remove' INT TERM
MSG=$(aws sqs receive-message --queue-url "$QUEUE_URL")
rc=$?
if [[ -n "$MSG" ]] && [[ $rc -eq 0 ]]; then
labels=$(echo "$MSG" | jq -r '.Messages[]| .ReceiptHandle' |
(xargs -I {} aws sqs delete-message --queue-url "$QUEUE_URL" --receipt-handle {}) &&
echo "$MSG" | jq -c '.Messages[].Body|fromjson|.detail.workflow_job.labels')
log.notice "Received runner labels ${labels}."
sqs_queue_msg_id=$(echo "$MSG" | jq -r '.Messages[]|[.MessageId]|("SQS Queue Message Id: "+.[])')
log.notice "Processing ${sqs_queue_msg_id}."
else
echo "Error: problem with reading sqs queue/labels"
exit 1
fi
job_run_ids=$(echo "$MSG" | jq -r '.Messages[]|.Body|fromjson|
[("job_id" + ": " + (.detail.workflow_job.id|tostring)),
("run_id" + ": " + (.detail.workflow_job.run_id|tostring))]|join(", ")')
log.notice "Processing for ${job_run_ids}."
job_id=$(echo "$MSG" | jq -r '.Messages[]|.Body|fromjson|.detail.workflow_job.id|tostring')
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"
if [[ -n "${REPOSITORY}" ]]; then
rest_api_ep="${github_api_pfx}/repos/${ORGANIZATION}/${REPOSITORY}"
fi
if [[ -n "${ORGANIZATION}" ]]; then
rest_api_ep="${github_api_pfx}/orgs/${ORGANIZATION}"
fi
if [[ -n "${ENTERPRISE}" ]]; then
rest_api_ep="${github_api_pfx}/enterprises/${ENTERPRISE}"
fi
rest_api_ep_runners_pfx=${rest_api_ep}/actions/runners
log.notice "Will use GitHub Rest API JIT config prefix ${rest_api_ep_runners_pfx}."
runner_version_maintenance() {
# Check the official actions/runner public repository for the latest release version tag.
# Strictly speaking, an ACCESS_TOKEN is not required for for "curling" the runner agent public repository,
# however, without a token, we are more likely to hit the GitHub API rate limit
remote_ver=$(curl -s -L \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "X-GitHub-Api-Version: 2022-11-28" https://api.github.com/repos/actions/runner/releases/latest |
jq -r '.tag_name' | cut -c 2-)
current_ver="$("$RUNNER_HOME"/config.sh --version)"
log.notice "Runner agent version bundled in our image is ${current_ver}, latest official release version is ${remote_ver}"
if [ "${current_ver}" != "${remote_ver}" ]; then
log.notice "Upgrading runner agent from version ${current_ver}, to latest official release ${remote_ver}"
rm -fr "$RUNNER_HOME/{"*",".*"}"
runner_download_url=$(curl -s -L \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "X-GitHub-Api-Version: 2022-11-28" https://api.github.com/repos/actions/runner/releases/latest |
jq -r ".assets[]|select(.name | startswith(\"actions-runner-linux-$RUNNER_ARCH\")).browser_download_url")
curl -fLo "$RUNNER_HOME"/runner.tar.gz "$runner_download_url"
tar xzf "$RUNNER_HOME"/runner.tar.gz -C "$RUNNER_HOME"
rm -f "$RUNNER_HOME"/runner.tar.gz
"$RUNNER_HOME"/bin/installdependencies.sh
log.notice "Unpacked upgraded runner from version ${current_ver} to ${remote_ver}"
export RUNNER_VERSION="${remote_ver}"
else
log.notice "Installed and latest available runner versions are identical:- installed: ${current_ver}, latest release: ${remote_ver} ... update not required."
fi
}
if ! cd "${RUNNER_HOME}"; then
log.notice "Runner home dir ${RUNNER_HOME} is missing, creating it..."
mkdir -p "${RUNNER_HOME}"
chown -R "${RUNNER_USER}":"${RUNNER_USER}" "$RUNNER_HOME"
else
rm -fr "$RUNNER_HOME/{"*",".*"}"
runner_version_maintenance
fi
log.notice "Registering..."
retries_left=${RUNNER_CONFIG_RETRIES}
while [[ ${retries_left} -gt 0 ]]; do
log.debug 'Configuring the runner.'
jit_json_parms="{\"name\":\"${job_id}\",\"runner_group_id\":1,\"labels\":${labels},\"work_folder\":\"_work\"}"
data=$(echo -en "$jit_json_parms" | jq -c '.')
log.notice "Will generate JIT runner config using the following options:"
log.notice "${data}"
jit_runner_config=$(curl -sL -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "X-GitHub-Api-Version: 2022-11-28" "${rest_api_ep_runners_pfx}"/generate-jitconfig \
-d "${data}")
rc=$?
if [[ $rc -eq 0 ]]; then
encoded_jit_config=$(echo -en "$jit_runner_config" | jq -cr '.encoded_jit_config')
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
# unset the following to prevent them from being displayed/echoed from the runner environment
unset REPOSITORY ACCESS_TOKEN ORGANIZATION ENTERPRISE STARTUP_DELAY_IN_SECONDS jit_runner_config
log.notice "Spawning runner..."
mapfile -t env </etc/environment
env -- "${env[@]}" ./run.sh --jitconfig "${encoded_jit_config}" &
PID=$!
wait $PID
returnCode=$?
log.notice "Runner background process completed with retun code: $returnCode."
log.notice "Exiting with return code: $returnCode."
exit $returnCode
$HOME/gha-jit-fargate-runner/logger.sh
#!/usr/bin/env bash
##########################################################################################
# This script was sourced from :
# - https://github.com/actions/actions-runner-controller/blob/master/runner/logger.sh
##########################################################################################
# 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
Build and Tag
Assuming the following ECR repository has been created,
<aws-acct-id>.dkr.ecr.<region>.amazonaws.com/gha-jit-fargate-runner
build and tag the image for uploading to the ECR:
$ cd $HOME/gha-jit-fargate-runner
$ docker build -t <aws-acct-id>.dkr.ecr.<region>.amazonaws.com/gha-jit-fargate-runner:latest \
--build-arg RUNNER_USER="ubuntu" \
--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-)" .
Push to ECR
Authenticate to target registry:
$ aws ecr get-login-password --region <region> | docker login --username AWS --password-stdin <aws-acct-id>.dkr.ecr.<region>.amazonaws.com
Push the image to ECR:
$ docker push <aws-acct-id>.dkr.ecr.<region>.amazonaws.com/gha-jit-fargate-runner:latest