GitHub ActionsはGitHubのCI/CDプラットフォームです。ワークフローを定義し、ビルドやテストを自動化できます。GitHub Actionsのワークフローを実行する環境をrunnerと呼び、runnerを動かす方法は大きく2種類あります。GitHub提供のrunnerと、自分で用意(セルフホスト)したrunnerです。この記事では以下の方法を解説します。

  • Amazon ECSでセルフホストrunnerを動かす
  • GitHub ActionsでコンテナイメージをビルドしAmazon ECRにプッシュする

環境構築に使用したDockerfileやTerraformはこちらのmoriryota62/selfhosted-runner-on-ecsで公開しています。諸事情あってterraformはus-east-2前提ですがそのままap-northeast-1でも使えるはずです。全体の構成は以下の様になります。

VPC,ECSクラスタの準備

VPCおよびECSクラスタを用意してください。また、今回はワークフローでコンテナイメージのビルドを行います。ECSタスク内でdockerを使用するためにホストの/var/run/docker.sockをマウントします。そのため、AWS Fargateは使えずEC2タイプでタスクを実行します。AutoScalling等でEC2のコンテナインスタンスを用意してください。EC2に使用するAMIはECS最適化AMIが便利です。

ECRの準備

セルフホストrunnerのコンテナイメージはGitHub公式から提供されていないようです。そのため自分でコンテナイメージをビルドします。また、GitHub Actionsでビルドしたコンテナイメージを格納するターゲットのリポジトリも作成します。

今回は以下2つのリポジトリを作成しました。

  • selfhosted-github-actions-runner
  • build-test

セルフホストrunner用コンテナイメージの作成

イメージはこちらのmyoung34/github-runnerでも提供されています。しかし、信頼して良いか判断できなかったため自分でビルドすることにします。その際はこちらのブログDeploying Self-Hosted GitHub Actions Runners with Dockerを参考にしていくつか改良を加えました。

Dockerfile

主にECS最適化AMIで利用するために改良しています。runnerのECSタスクでdockerを使用するためホストの/var/run/docker.sockをマウントします。ECS最適化AMIの/var/run/docker.sockのパーミッションはroot:docker 660となっており、dockerグループのgidは994でした。セルフホストrunnerコンテナ内のdockerユーザーが/var/run/docker.sockを使用できるように、コンテナ内にもgid:994でdockerグループを作成し、dockerユーザーを所属させています。

また、コンテナ内でdockerコマンドを使用したいため、docker-ce-cliのインストールを追加しています。

# base
FROM ubuntu:bionic-20220427

# set the github runner version
ARG RUNNER_VERSION="2.293.0"

# update the base packages and add a non-sudo user
RUN apt-get update -y \
    && apt-get upgrade -y \
    && groupadd -g 994 docker \
    && useradd -m -g 994 docker

# install python and the packages the your code depends on along with jq so we can parse JSON
# add additional packages as necessary
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
    curl jq build-essential libssl-dev libffi-dev python3 python3-venv python3-dev python3-pip \
    ca-certificates gnupg lsb-release

# install Docker CLI
RUN mkdir -p /etc/apt/keyrings \
    && curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
    && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \
    && apt-get update \
    && apt-get install -y docker-ce-cli

# cd into the user directory, download and unzip the github actions runner
RUN cd /home/docker && mkdir actions-runner && cd actions-runner \
    && curl -O -L https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz \
    && tar xzf ./actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz

# install some additional dependencies
RUN chown -R docker ~docker && /home/docker/actions-runner/bin/installdependencies.sh

# copy over the start.sh script
COPY start.sh start.sh

# make the script executable
RUN chmod +x start.sh

# since the config and run script for actions are not allowed to be run by root,
# set the user to "docker" so all subsequent commands are run as the docker user
USER docker

# set the entrypoint to the start.sh script
ENTRYPOINT ["./start.sh"]

start.sh

セルフホストrunnerはOrganizationまたは特定リポジトリのrunnerとして登録できます。参考元のスクリプトはOrganizationのrunnerとしてしか登録できなかったため、特定リポジトリのrunnerとしても使えるようにしています。Organizationのrunnerとして登録した場合はコンテナの環境変数ORGANIZATIONに組織名を設定します。特定リポジトリのrunnerとして登録したい場合は環境変数OWNERにリポジトリの所有者(組織)名、環境変数REPOにリポジトリ名を設定します。

#!/bin/bash

ACCESS_TOKEN=$ACCESS_TOKEN
ORGANIZATION=$ORGANIZATION
OWNER=$OWNER
REPO=$REPO

if [ -n "$ORGANIZATION" ]; then
  echo "get Organization token"
  REG_TOKEN=$(curl -sX POST -H "Authorization: token ${ACCESS_TOKEN}" https://api.github.com/orgs/${ORGANIZATION}/actions/runners/registration-token | jq .token --raw-output)
  cd /home/docker/actions-runner
  echo "setup Organization Runner"
  ./config.sh --url https://github.com/${ORGANIZATION} --token ${REG_TOKEN}
else
  echo "get Repository token"
  REG_TOKEN=$(curl -sX POST -H "Authorization: token ${ACCESS_TOKEN}" https://api.github.com/repos/${OWNER}/${REPO}/actions/runners/registration-token | jq .token --raw-output)
  cd /home/docker/actions-runner
  echo "setup Repository Runner"
  ./config.sh --url https://github.com/${OWNER}/${REPO} --token ${REG_TOKEN}
fi

cleanup() {
    echo "Removing runner..."
    ./config.sh remove --unattended --token ${REG_TOKEN}
}

trap 'cleanup; exit 130' INT
trap 'cleanup; exit 143' TERM

./run.sh & wait $!

イメージ作成

上記、Dockerfileとstart.shを配置したディレクトリで以下コマンドを実行します。ECRへのログインおよびプッシュコマンドはECRのマネジメントコンソール画面でも確認できます。

aws ecr get-login-password --region <region> | docker login --username AWS --password-stdin <account id>.dkr.ecr.<region>.amazonaws.com
docker build -t <account id>.dkr.ecr.<region>.amazonaws.com/selfhosted-github-actions-runner:20220615 .
docker push <account id>.dkr.ecr.<region>.amazonaws.com/selfhosted-github-actions-runner:20220615

GitHubアクセストークンの作成

セルフホストrunnerからGitHubに接続するためのアクセストークンをGitHubで作成します。

セルフホストrunnerを登録する対象の組織またはリポジトリに参加済のアカウントでGitHubにログインします。ログイン後、画面右上のアイコンからSettingsを選びます。

 

左メニューからDeveloper settingsを選びます。

Personal access tokensを開き、Generate new tokenを選びます。

NoteExpirationは任意の値を設定します。scopeはrepoworkflowadmin:orgを選びます。画面下部のGenerate tokenでトークンを作成します。表示されるトークンは控えておいてください。ECSタスク定義の設定に使います。

セルフホストrunnerタスクの実行

セルフホストrunnerをECSで動かします。まずはタスク定義を用意し、その後サービスでタスクを実行します。

タスク定義

セルフホストrunnerタスクの設定を行います。コンテナイメージは先程ECRへプッシュしたセルフホストrunnerイメージを指定します。環境変数ACCESS_TOKEN、OWNER、REPOを設定します。ACCESS_TOKENには先程GitHubで発行したアクセストークンを設定します。Organizaionのruunerにする場合はOWNERREPOの代わりにORGANIZATIONを設定してください。最後にホストの/var/run/docker.sockをコンテナの/var/run/docker.sockにマウントするように設定しています。

なお、今回はワークフローでECRへのプッシュを行いたいため、タスクのIAMロール(taskRoleArnで指定するIAMロール)にはAmazonEC2ContainerRegistryFullAccessのポリシーも追加しておきます。

{
    "containerDefinitions": [
        {
            "environment": [
                {
                    "name": "ACCESS_TOKEN",
                    "value": "ghp_cHaNgEYourgiTHUbAccessTOKEn123456789"
                },
                {
                    "name": "OWNER",
                    "value": "moriryota62"
                },
                {
                    "name": "REPO",
                    "value": "selfhosted-runner-on-ecs"
                }
            ],
            "mountPoints": [
                {
                    "readOnly": null,
                    "containerPath": "/var/run/docker.sock",
                    "sourceVolume": "docker"
                }
            ],
            "cpu": 256,
            "memory": 512,
            "image": "<account id>.dkr.ecr.<region>.amazonaws.com/selfhosted-github-actions-runner:20220615",
            "name": "github-actions-runner",
            "essential": true
        }
    ],
    "cpu": "256",
    "memory": "512",
    "executionRoleArn": "arn:aws:iam::<account id>:role/selfhosted-runner-EcsTaskExecuteRole",
    "taskRoleArn": "arn:aws:iam::<account id>:role/selfhosted-runner-GitHubActionsRole",
    "family": "selfhosted-runner",
    "requiresCompatibilities": [
        "EC2"
    ],
    "inferenceAccelerators": [],
    "volumes": [
        {
            "name": "docker",
            "host": {
                "sourcePath": "/var/run/docker.sock"
            }
        }
    ],
    "placementConstraints": [],
    "tags": []
}

サービス

タスクを実行するサービスを設定します。起動タイプはEC2を指定し、タスク定義は上記で作成したものを指定してください。

登録確認

サービスでタスクを実行したらGitHubにrunnerが登録されているか確認します。対象リポジトリまたは組織のSettingsを開き、Actions -> Runnersを表示します。self-hostedのタグがついたidleのrunnerがあれば登録できています。

GitHub Actionsによるイメージビルド

runnerの設定ができたのでコンテナイメージのビルドおよびECRへのプッシュができるか確認します。ワークフローを実行したいリポジトリのルートに.github/workflows/workflow.ymlを作成します。workflow.ymlは以下の内容にします。このワークフローはmainブランチ更新時に実行するようにしており、DeployというJobを定義しています。指定したリージョンのECRにログインし、リポジトリのルートにあるDockerfileを使用してdocker buildします。イメージのリポジトリ名はbuild-test、タグはコミットハッシュとしています。

name: Deploy to ECR

on:
  push:
    branches: [main]

jobs:
  deploy:
    name: Deploy
    runs-on: self-hosted

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-region: <region>

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      - name: Build, tag, and push image to Amazon ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: build-test
        run: |
          # Build a docker container and
          # push it to ECR so that it can
          # be deployed to ECS.
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:${{ github.sha }} .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:${{ github.sha }}

上記.github/workflows/workflow.ymlと適当なDockerfileをリポジトリのルートに作成し、mainブランチを更新してください。GitHubで対象リポジトリのActionsを開くとワークフローの状態を確認できます。さらにJobを選択するとJobで実行した各ステップのログも確認できます。

ワークフローが正常に完了するとECRにイメージがプッシュされています。

参考記事

本ドキュメントは以下の文書を基に作成しました。

Deploying Self-Hosted GitHub Actions Runners with Docker https://testdriven.io/blog/github-actions-docker/

Self-hosted GitHub Actions on ECS https://msyea.medium.com/self-hosted-github-actions-on-ecs-3959267c3177

GitHub Actions https://docs.github.com/ja/actions