EKSでGitHub Self-Hosted Runnerを立てる

# この記事の目的
- クラウドネイティブなシステムを作るのに、CI/CDは欠かせない仕組みです。
- CIパイプラインを動かすツールとして有名なGitHub Actionsを、Self-hosted RunnerとしてEKSクラスタ上で動かすため設計の注意点や導入方法について記載しています。
- 公式が英語で、DeepLにコピるのが面倒な方の一助になれば幸いです。

## この記事でわかること
- EKSでのGitHub Self-hosted Runnerの立て方
- 設計上の注意ポイント

## この記事で分からないこと
- GitHub Actionsでのワークフローの作り方
    - 動作確認のため簡単なワークフローは動かしていますが、GitHub Acitonsのワークフローに関する設定について深く言及しません。
- AzureやGCP特有の設定
    - AWS上で動かすことにフォーカスしています。k8sではあるもののパブリッククラウドごとに微妙に制約が変わってくると思います。
- Terraformについて
    - テスト用にTerraformコードを動かしていますが、内容について深く言及しません。

# GitHub Actionsとは
この記事を興味をもって読んでいただいてる方には不要かもしれませんが、一応、GitHub Actionsの説明を公式から引っ張りました。

`GitHub Actionsで、ソフトウェア開発ワークフローをリポジトリの中で自動化し、カスタマイズし、実行しましょう。 CI/CDを含む好きなジョブを実行してくれるアクションを、見つけたり、作成したり、共有したり、完全にカスタマイズされたワークフロー中でアクションを組み合わせたりできます。`
[GitHub Actions公式から引用](https://docs.github.com/ja/actions)

GitHubリポジトリに配置したワークフローを、PR発行時やmainブランチへのマージ時など、任意のタイミングで実行・結果の出力をしてくれるツールとなります。

# GitHub Self-Hosted Runnerとは
このGitHub Actionsは、実行主体となるRunnerの稼働場所として、2種類の選択肢があります。

- **GitHub-hosted Runner**
    - GitHub上にホストされた仮想マシン上でのRunnerを稼働させる
    - GitHubにおけるデフォルト構成であり、特に何も考えなくてもワークフローのファイルを置けばRunnerが実行される
- **Self-hosted Runner**
    - 自身の環境に専用Runnerを構築する
    - GitHubリポジトリと認証連携を行うことで、ワークフローのファイルをRunnerで実行することが可能

GitHub-hosted Runnerは、素早いワークフロー構築やRunner性能を気にしなくていい点など非常に便利ですが、以下点が懸念になります。
・Runnerがインターネット側にあり、かつGitHubで利用しているグローバルIPが流動的なため、自身の環境への接続が必要な場合はインターネットからの通信許可を大きく開ける必要がある
・Runnerが利用しているコンテナイメージのカスタマイズができない

パブリッククラウドを利用するとしても、企業のシステムを構築する以上、オンプレで作る場合と同様にセキュリティ面を考慮したり、リソース管理を行いたいという要件が発生すると思うので、その場合はSelf-hosted Runnerを利用することになります。

### Actions Runner Controller
Self-hosted Runnerは、EC2インスタンスなどの稼働マシンで稼働させることもできますが、せっかくクラウドネイティブな環境を作っているのに不要な仮想マシンは増やしたくありません。
その場合、EKSクラスタ上にPodとしてRunnerを起動できる、**Actions Runner Controller**を利用します。
[Actions Runner Controller公式](https://github.com/actions-runner-controller/actions-runner-controller)

Actions Runner Controllerはコンテナ環境でのGitHub ActionsのRunner管理を行う機能を提供するリソースで、OSSとして公開されています。
大まかに、以下の機能を提供しています。
- リポジトリもしくはOrgaizationsに対するRunnerのセッティング
- Runnerのスケーリング
- Runnerへの権限付与

Actions Runner Controllerによって、Runnerがデプロイされますが、Runnerは`Deployment`もしくは`ReplicaSet`の形でデプロイが可能です。今回は`RunnerDeployment`でデプロイします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981495/f071c56c-960b-0aff-00e3-904f5192fe1e.png)

# EKSにRunnerを立ててみる
という事で、EKSクラスタにAtcions Runner Controllerをデプロイして、実際にワークフローを動かしてみます。
構成はこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981495/7a5647f7-5d09-89a4-e306-8b230bee4b4a.png)

- AWS上に必要なリソースを作成します。
    - EKSクラスタ
    - Terraform用S3バケット、DynamoDB
        - tfstateの配置、排他制御に利用
    - ServiceAccount用IAMロール
        - AWS全体のReadOnlyAccess権限、S3/DynamoDBへの接続権限
        - Runner実行時の権限を付与
- GitHubにテスト用のTerraformリポジトリを用意します。
- Actions Runner Controllerをデプロイし、リポジトリ専用のRunnerをデプロイします。
    - サービスアカウントを利用して、IAMロールを紐づけ
- Runnerの挙動確認をするため、`terrafom plan`コマンドを実行するワークフローを実行します。

その他バージョン・関連情報は、以下の通りです。
```
AWSリージョン：東京(ap-northeast-1)
EKSクラスタ：1.24
Actions Runner Controller : 0.21.1
Terraform : 1.3.0
```

## AWSリソースの準備
事前に必要となるAWSリソースを作成します。
リソース作成にはIAMユーザなどに権限が必要だったりアクセスキーの設定だったりが必要ですが、ここでは設定済みの前提で進めます。

### EKSクラスタ

- EKSクラスタが簡単に構築できる`eksctl`コマンドを利用します。

```shell
$  eksctl create cluster \
--name test-cluster \
--region ap-northeast-1 \
--timeout 40m \
--version 1.24 \
--nodegroup-name test-node \
--node-type t3.large
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981495/b819eaef-4e1b-dbbe-01a9-e1c4ff9a56a7.png)

- ローカル環境からEKSクラスタに接続できるようにしておきます。
```shell
# 作成したEKSクラスタをローカルに登録
$ aws eks --region ap-northeast-1 update-kubeconfig --name test-cluster
Added new context arn:aws:eks:ap-northeast-1:XXXXXXXXXX:cluster/test-cluster to C:\Users\xxxxxx\.kube\config

# 接続確認
$ kubectl get svc
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.100.0.1   <none>        443/TCP   60m
```

### S3バケットとDynamoDBの作成
マネジメントコンソールから作成します。
作成したリソースの名前はこの後、IAMロールやTerraformコード内で利用します。
- S3バケット
![S3バケット.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981495/fc88f848-ac04-3335-1c8d-843bdfcc7aa8.png)

- DynamoDB
![DynamoDB.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981495/b4da5b27-2f58-3470-4385-89051276ce2b.png)

### OIDCプロバイダの作成
以下2つの用途で、OpenIDConnect(OIDC)プロバイダを作成する必要があります。
- EKSクラスタ
    - **サービスアカウントへのIAMロール紐づけ**
    - 対象者：`sts.amazonaws.com`
    - プロバイダ：`oidc.eks.ap-northeast-1.amazonaws.com/id/<乱数>`
        - EKSクラスタの情報から取得
![OIDCプロバイダ(EKSクラスタ).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981495/467d2b7e-b4b2-7ac8-f005-e3c02b4cae1f.png)

- GitHub Actions
    - **Actions実行時の認証**
    - 対象者：`sts.amazonaws.com`
    - プロバイダ：`token.actions.githubusercontent.com`
![OIDCプロバイダ(GitHubアクション).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981495/8bdef719-f110-85b4-f760-53dc06cb1200.png)

### Self-hosted Runner用のIAMロール作成
Runner実行時に利用するIAMロールを作成します。
`terraform plan`を実行するため、IAMロールには以下の権限を付与します。
- ReadOnlyAccess
- 作成したDynamoDBへのアクセス権
![IAMロール.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981495/e9545ae9-d3c0-27be-a881-d63c95e11f79.png)

```json:DynamoDBアクセス権
{
    "Statement": [
        {
            "Action": [
                "dynamodb:PutItem",
                "dynamodb:GetItem",
                "dynamodb:DeleteItem"
            ],
            "Effect": "Allow",
            "Resource": "arn:aws:dynamodb:ap-northeast-1:xxxxxxx:table/<作成したDynamoDBの名前>",
            "Sid": ""
        }
    ],
    "Version": "2012-10-17"
}
```

また、先ほど作成したOIDCプロバイダとの信頼関係を設定しています。
Runner用のサービスアカウントと、Runnerを稼働するリポジトリを対象に設定します。
![IAMロール_信頼関係.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981495/ae06b78e-39f7-2e4f-5303-d1bc2defe58a.png)

```json:信頼関係の設定
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::XXXXXXX:oidc-provider/oidc.eks.ap-northeast-1.amazonaws.com/id/<乱数>"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "oidc.eks.ap-northeast-1.amazonaws.com/id/<乱数>:sub": "system:serviceaccount:<Runnerのnamespace>:<RunnerのServiceAccout名>"
                }
            }
        },
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::XXXXXXX:oidc-provider/token.actions.githubusercontent.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringLike": {
                    "token.actions.githubusercontent.com:sub": "repo:<Runnerを稼働するリポジトリ名>:*"
                }
            }
        }
    ]
}
```

- OIDCプロバイダの設定とIAMロールの設定を行うことで、GitHub Actionsに対してシークレットキーなどの秘匿情報を保管する必要がなくなります。

これで、AWSリソースの準備は完了です。

## GitHubリポジトリの準備

GitHubリポジトリ側の準備を行います。
- リポジトリと実行コードの作成
- GitHub Appsの作成

### リポジトリと実行コードの作成 
適当なTerraformリポジトリを作成し、`terrafom plan`コマンドを実行時に利用するテストコードを配置します。
VPCとサブネットを作成する簡単コードを書いています。planを実行するためだけなので実際にデプロイはされません。

```terraform:test.tf
terraform {
  # Terraformバージョン指定
  required_version = "~> 1.3.0" 

  # AWSプロバイダのバージョン指定
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.17.0"
    }
  }

  # tfstateファイルの配置場所をS3バケットに指定
  backend "s3" {
    bucket         = "qiita-terraform-tfstate" # S3バケット名
    key            = "test_vpc" # tfstateに格納する際のキー名 
    region         = "ap-northeast-1"
    # S3への処理を排他制御するDynamoDBを指定
    dynamodb_table = "Qiita-dynamoDB" # 作成したDynamoDB名
  }
}

# VPCを作成するコード
resource "aws_vpc" "main" {
  cidr_block                       = "192.168.0.0/16"
}

# サブネットを作成するコード
resource "aws_subnet" "main" {
  vpc_id                           = aws_vpc.main.id
  cidr_block                       = "192.168.100.0/24"
  availability_zone                = "ap-northeast-1a"
}

```

### GitHub Appsの作成

Self-hosted RunnerがGitHubリポジトリのRunnerとして稼働するために、GitHubに対する認証が必要になります。
GitHubアカウントのPAT(Personal Account Token)を発行する方法がありますが、セキュリティ的にあまりよろしくないので、今回はGitHub Appsを利用します。

- GitHubアカウントもしくはOrganizationsのSerttingから`Developer_settings`を選択します。
![Developer_settings.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981495/e90c6433-4d42-2b9e-88e3-ee3d8a43bb77.png)

- GitHub Appsを選択し、`New GitHub Apps`から新規作成します。
![GitHub Apps.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981495/67b2d947-0d75-5dfc-1e5f-e06ce4b119af.png)

- 以下設定でGitHubAppsを作成します。

| No. | 項目 | 必須 | 値 |
| :-- | :-- | :-- | :-- |
| 1 | GitHub App name  | 〇 | GitHub Appsの名前(任意) |
| 2 | Description | - | GitHub Appsの説明 |
| 3 | Homepage URL | 〇 | GitHub AppsのURL<br>※今回は利用しないので適当な名前でOK |
| 4 | WebHook | - | 今回は利用しないので、チェックを外す |

- 設定時に併せてリポジトリに対する権限を付与します。
Repository permissions

| 項目 | 権限 |
| :-- | :-- |
| Actions | Read-Only | 
| Administration | |Read and Write |
| Checks | Read-Only |
| Metadata | Read-Only |

Organizarion permissions

| 項目 | 権限 |
| :-- | :-- |
| Self-hosted runners | Read and Write | 

- 作成されたGitHub Appsで`Private Keys`を発行し手元に保管します。(この後利用します。)

![Github PrivateKey.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981495/714de47e-3869-c2cb-c622-f23172c2882d.png)

- 作成したGitHub AppsをTerraformリポジトリに対してインストールします。
![GitHub Apps インストール.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981495/75be969e-3d9f-d5a9-db98-29a8502eeed8.png)
![GitHub Apps インストール_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981495/47b6a523-08cd-2ddb-039e-61ee44a62696.png)

- インストールが完了したら、GitHub Appsの準備完了です。

以上で、GitHubリポジトリの準備が完了です。

## Actions Runner ControllerおよびRunnerのデプロイ
事前準備が完了したので、Actions Runner ControllerとRunnerをデプロイしていきます。

- 認証用に作成したGitHub Appsの情報をsecretとしてEKSにデプロイします。

```yaml:github-apps-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: github-actions-runner-secret
  namespace: default
data: # すべてbase64でエンコード
  github_app_id: <GitHub Appsのapp ID>
  github_app_installation_id: <リポジトリにインストールした際のID(URLから取得)>
  github_app_private_key: <発行したPrivate Keys>
```

```shell
$  kubectl apply -f github-apps-secret.yaml 
secret/github-actions-runner-secret created

$  kubectl get secret github-actions-runner-secret
NAME                           TYPE     DATA   AGE
github-actions-runner-secret   Opaque   3      15s
```

- GitHub ActionsのCA管理に利用する、cert-managerをHelmチャートを利用してデプロイします。

```shell
# Helmリポジトリの取得
$ helm repo add cert-manager https://charts.jetstack.io
$ helm repo upgrade 

# CRDのインストール
$ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.10.1/cert-manager.crds.yaml
customresourcedefinition.apiextensions.k8s.io/clusterissuers.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/challenges.acme.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/certificaterequests.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/issuers.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/certificates.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/orders.acme.cert-manager.io created

# cert-managerインストール(Helm)
$ helm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --version v1.10.1
NAME: cert-manager
LAST DEPLOYED: Tue Dec 13 21:16:05 2022
NAMESPACE: cert-manager
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
cert-manager v1.10.1 has been deployed successfully!

In order to begin issuing certificates, you will need to set up a ClusterIssuer
or Issuer resource (for example, by creating a 'letsencrypt-staging' issuer).

More information on the different types of issuers and how to configure them
can be found in our documentation:

https://cert-manager.io/docs/configuration/

For information on how to configure cert-manager to automatically provision
Certificates for Ingress resources, take a look at the `ingress-shim`
documentation:

https://cert-manager.io/docs/usage/ingress/

# 稼働確認
$ kubectl get pod -n cert-manager
NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager-5fdbd97fb5-82kj4              1/1     Running   0          59s
cert-manager-cainjector-7c44879bc4-gtfhn   1/1     Running   0          59s
cert-manager-webhook-5db84854c8-98pp4      1/1     Running   0          59s
```

- Actions Runner ControllerをHelmでデプロイします。
```shell
# Helmリポジトリの取得
$ helm repo add actions-runner-controller https://actions-runner-controller.github.io/actions-runner-controller
$ helm repo upgrade 

# Actions Runner Controllerのデプロイ(Helm)
$ helm install -f values.yaml --wait --namespace default actions-runner-controller actions-runner-controller/actions-runner-controller
NAME: actions-runner-controller
LAST DEPLOYED: Tue Dec 13 21:23:42 2022
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
1. Get the application URL by running these commands:
  export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=actions-runner-controller,app.kubernetes.io/instance=actions-runner-controller" -o jsonpath="{.items[0].metadata.name}")
  export CONTAINER_PORT=$(kubectl get pod --namespace default $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
  echo "Visit http://127.0.0.1:8080 to use your application"
  kubectl --namespace default port-forward $POD_NAME 8080:$CONTAINER_PORT

# 稼働確認
$ kubectl get pod
NAME                                         READY   STATUS    RESTARTS   AGE
actions-runner-controller-6dcc598777-nkkrb   2/2     Running   0          35s
```

```yaml:values.yaml
# Actions Runner Controllerのレプリカ数
replicaCount: 1

# GitHub Apps認証に利用するSecretの指定(先ほど作成)
authSecret:
  enabled: true
  create: false
  name: "github-actions-runner-secret"

# RBACの有効化
rbac:
  allowGrantingKubernetesContainerModePermissions: true

# Webhookは利用しないため無効化
githubWebhookServer:
  enabled: false
```

- Runnerが利用するサービスアカウントをデプロイします。
```yaml:serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  # IAMロール作成時に指定したサービスアカウント名と合わせる
  name: terraform-runner
  namespace: default
  annotations:
    # 作成したIAMロールを割り当てる(ARN)
    eks.amazonaws.com/role-arn: arn:aws:iam::234159168154:role/Qiita-terraform-Github
```

```shell
$ kubectl apply -f serviceaccount.yaml
serviceaccount/terraform-runner created
```

- Terraformリポジトリ専用のRunnerDeploymentをデプロイします。
```yaml:runnerdeployment.yaml
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
  name: terraform-runner
  namespace: default
spec:
  # Runnerのレプリカ数
  replicas: 1
  template:
    spec:
      # GitHubリポジトリ名を指定
      repository: aokiYosuke/terraform-test
      # 利用するサービスアカウント名
      serviceAccountName: terraform-runner
      securityContext:
        fsGroup: 1000
```

```shell
$ kubectl apply -f runnerdeployment.yaml
runnerdeployment.actions.summerwind.dev/terraform-runner created
$ kubectl get pod | grep terraform
NAME                                         READY   STATUS    RESTARTS   AGE
terraform-runner-kftwb-pzkzr                 2/2     Running   0          37m
```

- 問題なくRunnerが起動していれば、GitHubリポジトリ側のRunnerにも情報が出てきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981495/bcffb3c3-9239-9d17-eb08-38b1e13601f9.png)

これで、Self-hosted Runnerのデプロイが完了しました。

## ワークフロー挙動確認
Self-hosted Runnerが正常に稼働するか確認するため、テスト用のワークフローを動かします。

- GitHubリポジトリにワークフローファイルを配置します。
```yaml:.github/workflow/terraform-ci.yaml
name: 'Terraform'

on:
  push:
    paths:
      - .github/workflows/terraform-ci.yml
  pull_request:

permissions:
  contents: read

jobs:
  terraform:
    name: 'Terraform'
    # Self-hosted Runnerを利用するように明示的に指定
    runs-on: self-hosted
    environment: Qiita-test

    defaults:
      run:
        shell: bash

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

    # Terraformコマンドのインストール
    - name: Setup Terraform
      uses: hashicorp/setup-terraform@v2
      with:
       terraform_version: 1.3.0
    # NodeJSのインストール(Terraformコマンド実行に必要)
    - name: Setup Nodejs
      uses: actions/setup-node@v1

    # terraform fmtコマンド実行
    - name: Terraform fmt
      id: fmt
      run: terraform fmt -check
      continue-on-error: true

    # terraform initコマンド実行
    - name: Terraform Init
      id: init
      run: terraform init

    # terraform validateコマンド実行 
    - name: Terraform Validate
      id: validate
      run: terraform validate -no-color

    # terraform planコマンド実行
    - name: Terraform Plan
      id: plan
      run: terraform plan -no-color
      continue-on-error: true
```

- コードをpushし、Actionsを実行させます。
![wokflow_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981495/05da6899-e712-9dd1-e227-ba8f267c5916.png)

- 無事アクションが完了し、plan結果がログに出力されました。
![wokflow_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981495/d6c3e710-c2c7-a60d-822c-c2e49ff6d21d.png)
![wokflow_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981495/4c13e2cc-389f-f21f-62a4-e9e2be194944.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2981495/deedcb6d-5636-4f9b-5ebb-7b9c479bf67b.png)

# まとめ
Actions Runner Controllerを利用し、EKSクラスタ上にSelf-hosted Runnerをデプロイする方法について手順をざっと確認しました。
これによって、
- リポジトリに対して専用のSelf-hosted Runnerデプロイ
- 認証情報をGitHubに渡さなくてもIAM権限を利用したRunnerの実行

あたりが実現できています。

これ以外にも、
- Runnerをリポジトリ専用にするか？Organizations全体で共有するか？
- Runnerのスケーリング方式(Horizonal Runner Autoscaler)
- Runnerが利用するリソース割り当て設計

など、ちゃんとシステム構築に利用するものとする場合に考えることはまだあります。
より知りたい方は、DeepL片手にActions Runner ControllerのGitHubを覗いてみてください。
[Action Runner Controller公式](https://github.com/actions-runner-controller/actions-runner-controller/blob/master/docs/detailed-docs.md#deploying-using-github-app-authentication)