閉じこもり (Proxy) 環境の EKS で actions-runner-controller を動かしたい

2022/12/29 Kubernetes 1.24 に対応するよう情報を更新しました。

## 本記事の構成
やりたいことは主に以下の 2 点です。

* プライベートアクセス専用の EKS クラスターで actions-runner-controller を動作させたい
* GitHub API との通信はオンプレミスのプロキシサーバーを経由する

以下の構成で検証しています。

* Amazon EKS: Kubernetes 1.24
* actions-runner-controller: v0.26.0
* cert-manager v1.10.1
* eksctl: 0.124.0
* kubectl: v1.24.7
* AWS CLI: 2.9.11

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/70b5e39a-a9f9-e2f5-46b6-57ceadd83c7b.png)

## actions-runner-controller とは
Kubernetes クラスター上で GitHub Actions の self-hosted runners を操作するためのコントローラーです。

https://github.com/actions/actions-runner-controller

以降はプライベートアクセス専用の EKS クラスターで actions-runner-controller を構築する際の流れを記載します。複数箇所でプロキシサーバーの接続情報の設定が必要になるため、そのあたりを中心に補足しています。

## 構築の流れ
### Private EKS クラスターの構築
事前に作業環境に kubectl, eksctl, AWS CLI をセットアップしておきます。詳細な手順は割愛します。

https://kubernetes.io/ja/docs/tasks/tools/install-kubectl/

https://docs.aws.amazon.com/eks/latest/userguide/eksctl.html

https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html

eksctl によるプライベートクラスターの作成は以下のドキュメントを参照してください。

https://eksctl.io/usage/eks-private-cluster/

今回は以下のような Config File を使用して EKS クラスターを作成しました。

```private-cluster.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: actions-runner
  region: ap-northeast-1
  version: "1.24"

privateCluster:
  enabled: true
  skipEndpointCreation: true

vpc:
  subnets:
    private:
      ap-northeast-1a:
        id: subnet-xxxxxxxxxxxxxxxxx
      ap-northeast-1c:
        id: subnet-yyyyyyyyyyyyyyyyy
      ap-northeast-1d:
        id: subnet-zzzzzzzzzzzzzzzzz
```

プライベートアクセス専用のクラスターの場合、以下の VPC エンドポイントが必要になります。

* ECR (`ecr.api`, `ecr.dkr`)
* S3 (Gateway 型 `s3`)
* EC2 (`ec2`)
* STS (`sts`)
* CloudWatch Logs (`logs`)

eksctl でプライベートクラスターを有効化 (`privateCluster.enabled`) した場合、自動的にこれらのエンドポイントが作成されます。 今回は Direct Connect で接続済みの VPC を使用したいという要件があり、既存の VPC とエンドポイントを利用するため、`privateCluster.skipEndpointCreation` でエンドポイントの作成はスキップさせています。

:::note warn
既存の VPC を指定する場合、eksctl はルートテーブルを編集します。
:::

ただしサブネットがメインのルートテーブルに紐づいている場合、eksctl はルートテーブルを編集しません (クラスターの作成に失敗します)。そのため明示的なルートテーブルの作成と紐づけを行なっておく必要があります。

eksctl はプロキシサーバーを介して EKS の AWS API と通信します。ただしプライベート EKS クラスターのエンドポイントに対する通信はプロキシを使用しないため、以下のような環境変数をあらかじめ設定しておきます。

```shell-session
export https_proxy=http://xxx.xxx.xxx.xxx:yyyy
export no_proxy=.eks.amazonaws.com
```

:::note info
2022/12/19 に EKS が PrivateLink をサポートしたため、 AWS API に対する通信も VPC エンドポイント経由とすることができるようになりました。
:::

https://aws.amazon.com/jp/about-aws/whats-new/2022/12/amazon-eks-supports-aws-privatelink/

以下のコマンドでクラスターを作成します。プライベートクラスターは通常のクラスターの作成より時間がかかります。今回の構成では 22 分程度でしたが、VPC や VPC エンドポイントを作成する場合はさらに時間がかかるので必要に応じてタイムアウト値を調整してください。

```shell
$ eksctl create cluster -f private-cluster.yaml

# タイムアウト値を伸ばす場合
$ eksctl create cluster --timeout 60m -f private-cluster.yaml
```

### マネージドノードグループの登録
マネージドノードグループで使用する起動テンプレートに UserData を追加し、containerd がプロキシを使用するように構成します。
以下のブログを参考にさせていただきました。ありがとうございます。

https://yomon.hatenablog.com/entry/2020/10/eks_docker_inside_proxy#%E3%83%9E%E3%83%8D%E3%83%BC%E3%82%B8%E3%83%89%E5%9E%8B%E3%83%8E%E3%83%BC%E3%83%89%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%97%E3%81%ABProxy%E8%A8%AD%E5%AE%9A

こちらのブログの場合、Docker デーモンに対するプロキシを構成していますが、Kubernetes 1.24 以降では dockershim が削除されているため、以下のように containerd に対する設定に変更する必要があります。

```shell
Content-Type: multipart/mixed; boundary="==BOUNDARY=="
MIME-Version:  1.0

--==BOUNDARY==
Content-Type: text/cloud-boothook; charset="us-ascii"

# Set the proxy hostname and port
PROXY=${ProxyIP}:${ProxyPort}
MAC=$(curl -s http://169.254.169.254/latest/meta-data/mac/)
VPC_CIDR=$(curl -s http://169.254.169.254/latest/meta-data/network/interfaces/macs/$MAC/vpc-ipv4-cidr-blocks | xargs | tr ' ' ',')

# Create the containerd systemd directory
mkdir -p /etc/systemd/system/containerd.service.d

# Configure containerd with the proxy
cloud-init-per instance containerd_proxy_config tee <<EOF /etc/systemd/system/containerd.service.d/http-proxy.conf >/dev/null
[Service]
Environment="HTTP_PROXY=http://$PROXY"
Environment="HTTPS_PROXY=http://$PROXY"
Environment="NO_PROXY=172.20.0.1,$VPC_CIDR,localhost,127.0.0.1,169.254.169.254,.internal,s3.amazonaws.com,.s3.ap-northeast-1.amazonaws.com,api.ecr.ap-northeast-1.amazonaws.com,dkr.ecr.ap-northeast-1.amazonaws.com,ec2.ap-northeast-1.amazonaws.com,ap-northeast-1.eks.amazonaws.com"
EOF

# Reload the daemon and restart docker to reflect proxy configuration at launch of instance
cloud-init-per instance reload_daemon systemctl daemon-reload 
cloud-init-per instance enable_containerd systemctl enable --now --no-block containerd
--==BOUNDARY==

```


起動テンプレートを作成したら以下のような Config File でマネージドノードグループを作成します。

```nodegroup.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: actions-runner
  region: ap-northeast-1

managedNodeGroups:
- name: t3s-proxy
  desiredCapacity: 1
  privateNetworking: true
  launchTemplate:
    id: lt-xxxxxxxxxxxxxxxxx
    version: "1"
```

create nodegroup でマネージドノードグループを作成します。

```shell-session
$ eksctl create nodegroup -f nodegroup.yaml
```

### actions-runner-controller の導入
actions-runner-controller は Admission Webhooks の証明書管理に [cert-manager](https://cert-manager.io/docs/installation/) を使用するため、事前にインストールしておきます。

```shell-session
$ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.10.1/cert-manager.yaml
```

actions-runner-controller は Helm ではなく、kubectl でデプロイする前提で記載しています。

プロキシ環境下で actions-runner-controller を使用するには、以下の 3 箇所でプロキシサーバーの情報を設定する必要があります。

1. controller-manager の manager コンテナ
1. RunnerDeployment の runner コンテナ
1. RunnerDeployment の sidecar (dind) コンテナ

1 は controller-manager の Deployment 定義に https_proxy 環境変数を設定します。
2,3 は RunnerDeployment の定義に https_proxy 環境変数を設定します。

1 については actions-runner-controller のマニフェストファイルを作業環境にダウンロードし、以下の箇所を直接追記しました。

```diff:actions-runners-controller.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
 labels:
   control-plane: controller-manager
 name: controller-manager
 namespace: actions-runner-system
spec:
 replicas: 1
 selector:
   matchLabels:
     control-plane: controller-manager
 template:
   metadata:
     labels:
       control-plane: controller-manager
   spec:
     containers:
     - args:
       - --metrics-addr=127.0.0.1:8080
       - --enable-leader-election
       command:
       - /manager
       env:
       - name: GITHUB_TOKEN
         valueFrom:
           secretKeyRef:
             key: github_token
             name: controller-manager
             optional: true
       - name: GITHUB_APP_ID
         valueFrom:
           secretKeyRef:
             key: github_app_id
             name: controller-manager
             optional: true
       - name: GITHUB_APP_INSTALLATION_ID
         valueFrom:
           secretKeyRef:
             key: github_app_installation_id
             name: controller-manager
             optional: true
       - name: GITHUB_APP_PRIVATE_KEY
         value: /etc/actions-runner-controller/github_app_private_key
+      - name: http_proxy
+        value: "http://xxx.xxx.xxx.xxx:xxxx"
+      - name: https_proxy
+        value: "http://xxx.xxx.xxx.xxx:xxxx"
+      - name: no_proxy
+        value: "172.20.0.1,*.eks.amazonaws.com"
       image: summerwind/actions-runner-controller:v0.22.3
       name: manager
```

kubectl create でデプロイします。

```shell-session
$ kubectl create -f actions-runners-controller.yaml
```

:::note warn
Kubernetes 側の既知の問題により、apply ではなく create (更新時は replace) を使用する必要があります
:::

https://github.com/actions-runner-controller/actions-runner-controller/issues/1317

GitHub API の認証には GitHub App または Personal Access Token (PAT) を使用できますが、今回は PAT 認証で検証しています。ランナーの種類に応じてトークンに [適切なスコープ](https://github.com/actions-runner-controller/actions-runner-controller#deploying-using-pat-authentication) を設定し、secret を作成します。

```shell-session
kubectl create secret generic controller-manager \
    -n actions-runner-system \
    --from-literal=github_token=${GITHUB_TOKEN}
```

PAT を使用して SAML SSO が構成されている Organization にアクセスする場合は、[PAT を事前に認可](https://docs.github.com/ja/enterprise-cloud@latest/authentication/authenticating-with-saml-single-sign-on/authorizing-a-personal-access-token-for-use-with-saml-single-sign-on) しておく必要ありますのでお忘れなく。

2,3 については以下のような定義で RunnerDeployment を作成しました。3 について補足すると GitHub Actions のジョブ内でコンテナを利用する際は、RunnerDeployment の sidecar として起動される dind の docker コンテナが使用されます。Kubernetes 1.24 での dockershim の削除は dind には影響を与えません。


```RunnderDeployment.yaml
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
  name: runner-deploy
spec:
  template:
    spec:
      enterprise: <enterprise-name>
      labels:
        - runner-container
      containers:
        - name: runner
          env:
            - name: RUNNER_FEATURE_FLAG_EPHEMERAL
              value: "true"
            - name: http_proxy
              value: "http://xxx.xxx.xxx.xxx:yyyy"
            - name: https_proxy
              value: "http://xxx.xxx.xxx.xxx:yyyy"
            - name: no_proxy
              value: "172.20.0.1,10.1.2.0/24,*.eks.amazonaws.com"
        - name: docker
          lifecycle:
            postStart:
              exec:
                command:
                  - sh
                  - -c
                  - "mkdir -p $HOME/.docker/ && echo '{ \"proxies\": { \"default\": { \"httpProxy\": \"http://xxx.xxx.xxx.xxx:yyyy\", \"httpsProxy\": \"http://xxx.xxx.xxx.xxx:yyyy\"} } }' > $HOME/.docker/config.json"
          env:
            - name: http_proxy
              value: "http://xxx.xxx.xxx.xxx:yyyy"
            - name: https_proxy
              value: "http://xxx.xxx.xxx.xxx:yyyy"
            - name: no_proxy
              value: "172.20.0.1,10.1.2.0/24,*.eks.amazonaws.com"
---
apiVersion: actions.summerwind.dev/v1alpha1
kind: HorizontalRunnerAutoscaler
metadata:
  name: runner-deployment-autoscaler
spec:
  scaleDownDelaySecondsAfterScaleOut: 300
  scaleTargetRef:
    name: runner-deploy
  minReplicas: 1
  maxReplicas: 5
  metrics:
  - type: PercentageRunnersBusy
    scaleUpThreshold: '0.75'
    scaleDownThreshold: '0.25'
    scaleUpFactor: '2'
    scaleDownFactor: '0.5'
```

マニフェストをデプロイします。

```shell-session
$ kubectl apply -f RunnerDeployment.yaml
```

Runner の pod が正常に起動していることを確認できれば、GitHub 側からも対象の Runner が見えているはずです。

```shell-session
$ kubectl get pod
NAME                            READY   STATUS    RESTARTS   AGE
pod/runner-deploy-xxxxx-xxxxx   2/2     Running   0          53s
```

controller-manager が正常に起動しない場合は 1 または GitHub 側の設定を、Runner が正常に起動しない場合は、2 または 3 の設定 を確認してみてください。

以上です。
参考になれば幸いです。