エクサウィザーズ Engineer Blog

株式会社エクサウィザーズのエンジニアチームブログ

GitHub Actions の self-hosted runners を AWS ECS で動かして、CI / CD パイプラインを作る

f:id:tadashi-nemoto0713:20201019135904p:plain

DevOps エンジニアの 根本 征 です。

7月からエクサウィザーズ にジョインし、CI / CD パイプラインの改善や自動テストの布教などを行っています。

今回は GitHub Actions の self-hosted runners を AWS ECS 上に構築し運用してみたので、その試行錯誤について紹介したいと思います。

GitHub Actions と self-hosted runners

GitHub Actions は GitHub ユーザーであれば現在多くの方がご存知・ご活用されているかと思います。

GitHub Marketplace で公開されている Actions と組み合わせることによって、簡単に様々な CI / CD パイプラインを構築することができます。

そんな GitHub Actions ですが、GitHubが提供する Runner を使う代わりに自前で用意することもできます(self-hosted runners)。

自己ホストランナーについて - GitHub Docs

GitHub が提供する Runner で GitHub Actionsを利用する場合、無料利用枠 + 従量課金の課金体制になります。

しかし、self-hosted runners で GitHub Actions を利用する場合には別途料金がかかることはありません。

Repository または Organization の設定からself-hosted runners をセットアップすることができます(Linux / MacOS / Windows 毎に手順が示されます)。

f:id:tadashi-nemoto0713:20201016201036p:plain

セットアップが完了すると、Runner として追加されていることが確認できます。

Repository で設定した Runner はその Repository で、そして Organization で設定した Runner はその Organization 内の全ての Repository で実行することができます。

f:id:tadashi-nemoto0713:20201016201348p:plain

最後に GitHub Actions の設定ファイルにおいて、self-hosted runners で実行することを記述します。

jobs:
  build:
    runs-on: self-hosted

self-hosted runners はマシン自体は自分たちで調達・メンテナンスをしないといけないですが、ワークフローを管理する部分(Jenkins でいう master)を自前で用意せず無料で利用できる点は魅力的だと感じます。

self-hosted runners を Docker で動かす

self-hosted runners 自体はオープンソースとして公開されていますが、現在 Docker イメージは提供されていません。

調べたところ、Docker・Kubernetes で self-hosted runners 動かすためにオープンソースプロジェクトで様々な試行錯誤が行われているみたいです。


本記事では AWS ECS をメインに下記の手順で解説します。

  1. self-hosted runners を Docker で動かす

  2. self-hosted runners を AWS ECS で動かす

  3. その self-hosted runners を用い、アプリケーションを AWS ECS にデプロイするパイプラインを作る


まず下記の Docker イメージを使い、手元で self-hosted runners を立ち上げてみます。

github.com

Organization に対して Runner を立ち上げるためには下記 docker コマンドを実行します。

docker run -d --restart always --name github-runner \
  -e RUNNER_NAME_PREFIX="myrunner" \
  -e ACCESS_TOKEN="footoken" \
  -e RUNNER_WORKDIR="/tmp/github-runner-your-repo" \
  -e ORG_RUNNER="true" \
  -e ORG_NAME="octokode" \
  -e LABELS="my-label,other-label" \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /tmp/github-runner-your-repo:/tmp/github-runner-your-repo \
  myoung34/github-runner:latest

GitHub Actions では Docker の操作(ビルドなど)を行うため、このコンテナ内でも Docker コマンドが使えることが必要になります。

そのため、ホストマシン上の Docker daemon を共有することで解決しています(-v /var/run/docker.sock:/var/run/docker.sock の部分、Docker outside of Docker)

Using Docker-in-Docker for your CI or testing environment? Think twice.

self-hosted runners を AWS ECS で動かす

次に先ほどの Docker イメージを使って、AWS ECSで動かしてみます。

今回は AWS CDK(Typescript) をベースに3つのポイントに絞って解説します。


1つ目に AWS ECS で Docker outside of Docker(DooD) をどう実現するかについてです(-v /var/run/docker.sock:/var/run/docker.sockの部分)。

AWS ECS だと Task 側に Volume を追加し、Container 側にその Volume をマウントすることで解決することができます。

taskDefinition.addVolume({
  name: "docker_sock",
  host: {
    sourcePath: "/var/run/docker.sock"
  }
})
containerDefinition.addMountPoints({
  containerPath: "/var/run/docker.sock",
  sourceVolume: "docker_sock",
  readOnly: true
})

AWS ECS では起動タイプとして Fargate と EC2 がありますが、現状 Fargate で上記を行うことがサポートされていなかったため、今回はEC2起動タイプを選択しました。


2つ目に ECS Task に与える Role についてです。

今回の GitHub Actions 上でのパイプラインでは、Docker ビルド ・ECRへのイメージのアップロード・ECSへのデプロイまで行おうとしています。

もちろん GitHub 側に AWS アクセスキーを保存し、それを GitHub Action に渡すことで上記を実現することができます。

しかし、self-hosted runners のコンテナ自体に上記に必要な Role を与えることによって、GitHub側に AWS アクセスキーを保存する必要自体をなくすことができます。

const taskRole = new iam.Role(this, 'GitHubActionsSelfhostRunnerTaskRole', {
  assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
  managedPolicies: [
    iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2ContainerServiceFullAccess'),
    iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2ContainerRegistryPowerUser'),
  ],
})

const taskDefinition = new ecs.Ec2TaskDefinition(
  this,
  `GitHubActionsTaskDefinition`, {
    taskRole: taskRole
  }
);


3つ目に、スポットインスタンスについてです。

self-hosted runners は CI / CD として使うため、スポットインスタンスを活用してコストを抑えることができます。

AWS CDK の場合、spotInstanceDraining プロパティを true にすることでスポットインスタンスを利用することができます。

cluster.addCapacity('GitHubActionsCapacity', {
  instanceType: new ec2.InstanceType("t3.small"),
  spotInstanceDraining: true
});

アプリケーションを AWS ECS へデプロイする Workflow を作る

今回はこの GitHub Actions と self-hosted runners を活用して、アプリケーションを AWS ECS へデプロイする Workflow を作りたいと思います。

同じ AWS ECS なのでややこしくなってしまいますが、self-hosted runners を動かすクラスタとアプリケーションを動かすクラスタは別という想定です。

f:id:tadashi-nemoto0713:20201020153820p:plain

具体的には下記のような手順になります。

Docker build して ECR にPushする
↓
Task Definition ファイルを編集する(Dockerイメージの部分を新しくする)
↓
Task Definition を新たに登録し、Service が更新されるまで待つ

AWSで提供されている GitHub Actions の Step と組み合わせると下記のようになります。

name: Deploy to ECS

on:  
  push:
    branches:
      - master

jobs:
  build:
    runs-on: self-hosted

    steps:
    - uses: actions/checkout

    - name: Configure AWS Credentials
      uses: aws-actions/configure-aws-credentials
      with:
        aws-region: ap-northeast-1

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

    - name: Build, tag, and push image to Amazon ECR
      id: build-image
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        ECR_REPOSITORY: ${{ secrets.AWS_ECR_REPO_NAME }}
      run: |
        docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
        echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"

    - name: Fill in the new image ID in the Amazon ECS task definition
      id: task-def
      uses: aws-actions/amazon-ecs-render-task-definition
      with:
        task-definition: task-definition.json
        container-name: hoge-container
        image: ${{ steps.build-image.outputs.image }}

    - name: Deploy Amazon ECS task definition
      uses: aws-actions/amazon-ecs-deploy-task-definition
      with:
        task-definition: ${{ steps.task-def.outputs.task-definition }}
        service: hoge-service
        cluster: hoge-cluster
        wait-for-service-stability: true

先ほども述べましたが、本来は aws-actions/configure-aws-credentials の Step で下記のように AWS アクセスキーを GitHub に保存し渡してあげる必要があります。

- name: Configure AWS Credentials
   uses: aws-actions/configure-aws-credentials
   with:
     aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
     aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
     aws-region: ap-northeast-1

しかし、今回は self-hosted runners に必要な権限を渡してあげているのでこの必要はありません。

おわりに

今回はGitHub Actions の self-hosted runners を AWS ECS 上に構築してみましたが、下記のような状況でメリットがあると考えています。

  • GitHub Action を使ってアプリケーションをAWSへデプロイする際、GitHub 側に不必要に AWS アクセスキーを渡したくない

  • スポットインスタンスや活用することによって、コストパフォーマンスよく CI / CD 環境を運用したい(特に無料枠を大幅に超えることが予想される場合)

  • GitHub が提供する Runner よりスペックの高いマシンで CI / CD 環境を運用したい


今後、この GitHub Actions をベースにより良い CI / CD パイプラインが作れたら良いと考えています。


hrmos.co