JenkinsからGitHub Actionsへの移行（検討）

## はじめに
株式会社LITALICO SREグループの石井です。
この記事は[LITALICO Engineers Advent Calendar 2021](https://qiita.com/advent-calendar/2021/litalico) その2の17日目の記事です。
弊社のB2B向けサービスではJenkinsを使用してアプリケーションのデプロイを行っています。
Jenkinsは設定次第で色々なことが出来てとても便利なのですが、運用する中で管理面を中心に様々な課題や辛いところも出てきました。
同じような構成の新規サービス立ち上げを機に上記の課題をGitHub Actionsに移行することで解決出来ないか検討したので、こちらについて書こうと思います。

## 前提

### サービスの構成
アプリケーションはAPI・UIともにマイクロサービスで、APIは20個前後、UIは10個前後に分割されています。インフラはAPI部分はFargate、UI部分はS3＋CloudFrontという構成です。

### 今回のデプロイの責務
移行を検討する対象はアプリケーションのデプロイ部分のみでビルドやDBマイグレーションは含みません。
DockerイメージやUIのビルドは事前に実行されていて、成果物はそれぞれECRのリポジトリ、成果物格納用バケットに格納済みという前提です。
そのためGitHub Actionsで実行させたい具体的な処理としては、ecspressoのようなツールを実行してECSサービスを更新できることや、事前にS3（成果物格納用バケット）に配置されたUIの成果物を公開用バケットにsyncさせることが出来ればokという感じです。

## 現状
### Jenkinsを使用している理由
ECSサービスを更新することやS3バケット間のsyncを行うだけであれば色々な方法があると思うのですが、B2B向けサービスという特性や監査、テストのフローなど様々な理由や制約により、デプロイでは以下の条件を満たしている必要があったため、Jenkinsが使われていました。

```
1. 手動で実行できること
2. 特定のバージョン（ブランチ・コミットID）を指定してデプロイできること
3. 複数のアプリケーションをまとめてデプロイできること
4. SREチーム以外もデプロイ操作が出来ること
5. 実行者を制限できること
6. 証跡を取れること
```

### Jenkinsの運用上の課題
色々な記事でJenkinsを運用する上での課題が挙げられていますが、管理する必要があるものが多く、それらをメンテナンスし続けるのが辛いというのが運用していて感じた一番大きな課題でした。
以下はJenkinsをキチンと管理しよう考えたときに整理した管理対象一覧です(他にもあるかもです)。

```
- Jenkinsをホストするサーバ・OS周り
- Jenkins自体
- 並列数等の設定
- プラグイン
- ユーザ
- ユーザの権限
- ジョブ
- ビュー
```


これらの管理が疎かになると以下のような問題が発生したりします。

```
- 不意にプラグインをアップデートしたらエラーで動かなくなった
- ジョブがいつの間にか修正されていて実行に失敗した。修正されていたことに気づかなかった&変更履歴がないので原因究明に時間を要した
- ジョブで実行するスクリプトを修正したのにジョブ側で実行時に渡す引数を変更し忘れていて失敗した
- 上記のようなことがあるので設定をいじったりジョブを実行するのが不安
```

### Jenkinsの課題を克服出来ないのか
[こちらの記事](https://www.m3tech.blog/entry/2020/11/27/110000)を参考にさせていただき、JenkinsをDocker化した上でコード管理出来ないかなとも考えたのですが、中々大変そうなので他にも良い方法がないかとチーム内で検討した結果、出てきた候補がGitHub Actionsでした。
GitHub Actionsにすると基本的にはワークフローを定義したyamlファイルだけで完結するのでシンプルにコードとして管理することができ、その他環境の管理面を含めても格段に楽になりそうだったので移行を考えることにしました。

## 本題の前にGitHub Actionsの仕様確認
そもそも基本的な仕様に問題がないかをまず確認しました。
結論として今回のケースで使う分には特に問題はなさそうでした。

### ジョブ実行環境のスペック<br>
Linuxの仮想環境だと以下の通りです。

```
2コアCPU
7GBのRAMメモリ
14GBのSSDディスクスペース
```
  
参考： [Supported runners and hardware resources](https://docs.github.com/ja/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources)

### 並列数
プランによって異なりますがFreeプランでアカウントでの最大並列数が20です。
参考: [Usage limits](https://docs.github.com/ja/actions/learn-github-actions/usage-limits-billing-and-administration#usage-limits)

### タイムアウト
ワークフローは最大72時間、ワークフロー内の各ジョブは最大6時間実行可能です。
ワークフロー・ジョブともに最大実行時間が長いので、意図しない長時間実行を防ぐため適切に`timeout-minutes`を指定することが望ましいです。
参考: [Usage limits](https://docs.github.com/ja/actions/learn-github-actions/usage-limits-billing-and-administration#usage-limits)



### 料金
パブリックリポジトリだと無料です。
プライベートリポジトリだとOSによって異なりますがLinux仮想環境で1分あたり`$0.008`かかります。
また、月単位で無料枠が設定されており、Freeプランだと月2000分は無料で使うことが出来ます。
参考: [GitHub Actionsの支払いについて](https://docs.github.com/ja/billing/managing-billing-for-github-actions/about-billing-for-github-actions)

## JenkinsをGithub Actionsに移行できるか
本題です。[Jenkinsを使用している理由](#Jenkinsを使用している理由)で挙げた条件をGitHub Actionsでも満たせるかを検討しました。

### 条件1. 手動で実行できること
`workflow_dispatch`を指定することで手動実行にすることが出来ます。

```yaml
on:
  workflow_dispatch:
```

### 条件2. デプロイ時にブランチ・コミットIDが指定できること
`inputs`でパラメータを指定可能です。
ブランチやコミットIDをパラメータとして受け取れるようにすれば良さそうです。

```yaml
on:
  workflow_dispatch:
    branches:
      - main
    inputs:
      branch:
        description: 'デプロイしたいbranchを指定してください'
        required: false
        default: ''
```

### 条件3&4. 複数のアプリケーションをまとめてデプロイできること、SREチーム以外もデプロイ操作が出来ること


Jenkinsでのデプロイ実行時のイメージ画面です。
デプロイ対象のアプリケーションをチェックボックスで選択する方式です。指定する値が分かりやすくツールにあまり慣れていない方でも使いやすい作りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283016/1fa3fa24-120b-6c05-246c-883b51461ced.png)



これをGitHub Actionsに置き換えると以下のようになります。
選択する値や方法に変化はないので今まで通り使ってもらえそうです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283016/4082b2fc-8ed7-6605-c7fd-e4bb88db4e9a.png)


inputsのtypeを`boolean`にするとチェックボックス形式でパラメータを入れることが出来ます。

```yaml
    inputs:
      application_1:
        description: ''
        type: boolean
        required: false
        default: 'false'
```



**<font color="Red">ここで問題が、、、</font>**
この記事用に10個以上の`inputs`を定義したワークフローを用意したところ以下のようなエラーが出ました。

```
you may only define up to 10 `inputs` for a `workflow_dispatch` event
```
inputsで指定出来る最大数が10とのことでした。。


### 条件5. 実行者を制限できること
**こちらの実装はSREチームのメンバーがやってくれました:pray::pray::pray:**

リポジトリの権限があれば本番環境などに誰でもデプロイ出来てしまうというのはよろしくないので実行者の制限をかける必要があります。
[OpenID Connectを使用してGitHub ActionsでAssumeRoleが出来るようになった](https://github.blog/changelog/2021-10-27-github-actions-secure-cloud-deployments-with-openid-connect/)ので、ワークフローではこの方法で引き受けた権限を使用しています。こちらを利用してIAMロール側で制限をかけることにしました。
最初に`aws-actions/configure-aws-credentials@v1`を使用して認証を行う際に、ロールのセッション名（`role-session-name`）を渡すことが出来るので、セッション名としてワークフローの実行者（[GITHUB_ACTOR](https://docs.github.com/ja/actions/learn-github-actions/environment-variables)）を指定します。


```workflow.yml
    - name: Configure AWS Credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-region: us-east-1
        role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
        role-session-name: ${{ github.actor }}
```

次にIAMロール側のTrust RelationshipのConditionでロールのセッション名が特定の場合（以下だと`operator-1`、`operator-2`、`operator-3`）のみ許可するという条件を追加します。

```trust_relationship.json
{
    "Version": "2008-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::xxxxxxxxxxxx:oidc-provider/token.actions.githubusercontent.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringLike": {
                    "token.actions.githubusercontent.com:sub": "repo:<organization_name>/*",
                    "sts:RoleSessionName": [
                        "operator-1",
                        "operator-2",
                        "operator-3",
                    ]
                }
            }
        }
    ]
}
```

このようにすることでIAMロール側で設定したセッション名（≒ワークフローの実行者）の場合のみAssumeRole出来るという制限をかけることが出来ました。
ただこちらの方法でもワークフローファイルが変更され、`role-session-name`に入る値を改ざんされるといったケースには対応出来ません。
2021年12月15日時点では未対応ですが、AWSが公式にGithubアカウント用のIDPを用意してくれて、OIDCキーでactorのキーが使えるようになればそちらを使うのがより良いかと思います。

※補足

- `aws-actions/configure-aws-credentials@v1`を使用する際に`aws-region`を`us-east1`としているのは、STSを無効化出来ないリージョンだからです。`us-east-1`とするとCloudTrailのログがバージニアリージョンに出力されるなど、運用上の複雑性が増すという面もあるので、東京リージョンを指定する方が良いかもしれません。
- IAMロールの`Trust Relationship`で`"token.actions.githubusercontent.com:sub": "repo:<organization_name>/*"`としていますが、この場合は指定したGitHubのOrganizationのすべてのリポジトリからの呼び出しが可能です。弊社ではAWS OraganizationとStackSetsを使用して、全AWSアカウントでIAMロールを自動作成しており、かつ実行者の制限もしているため、Oraganization全体を許可する形としています。利用するリポジトリを厳密に指定可能であれば、そちらの方がよりセキュリティ的に望ましいと思います。

### 条件6. 証跡を取れること
パブリックリポジトリでは最大90日間ログを保存してくれるようです。
プライベートリポジトリではOraganization内の設定を変えることで最大400日まで保存できるみたいです。
参考: [Organization 内の GitHub Actions アーティファクトとログの保持期間を設定する](https://docs.github.com/ja/organizations/managing-organization-settings/configuring-the-retention-period-for-github-actions-artifacts-and-logs-in-your-organization)

## まとめ
記事を書いている最中に思わぬ制限にぶつかりましたが最初に整理した条件をほぼ満たせそうなことが分かりました。

| 条件 | 検討結果 |
| ---- | :----: |
| 条件1. 手動で実行できること | 〇 |
| 条件2. デプロイ時にブランチ・コミットIDが指定できること  | 〇 |
| 条件3. 複数のアプリケーションをまとめてデプロイできること | 〇<br>※指定出来るパラメータの数の制限付き |
| 条件4. SREチーム以外もデプロイ操作が出来ること | 〇 |
| 条件5. 実行者を制限できること | 〇 |
| 条件6. 証跡を取れること | 〇※400日まで |

まだまだ詰める必要がある部分はありますが諸々の管理からの解放、GitHubとの親和性などGitHub Actionsへの移行によるメリットは大きいと思うので、今後はGitHub側でinputsの制限が緩和されるのを期待しつつ、打開策を検討しながら実装を進めていきたいと思います。

## おまけ
GitHub Actionsについて調べて他にもいろいろ学びがあったので少しだけ書こうと思います。

### 複合ステップアクション

複合ステップアクションはアクションを一つにまとめて必要なところで呼び出して再利用することが出来る機能です（CircleCIでの`commands`に近いイメージです）。
以前までは公開アクションは複合ステップアクション化出来ず、出来るのはスクリプトで記述された部分のみでした。
しかしアップデートにより上記の制限がなくなり、公開アクションも複合ステップアクションに出来るようになりました。今まで共通化出来なかった処理をまとめることが可能になり、よりワークフローファイルを簡潔に書きやすくなったと思います。
参考: [GitHub Actions: Reduce duplication with action composition](https://github.blog/changelog/2021-08-25-github-actions-reduce-duplication-with-action-composition/)

### セルフホストランナー
自前で実行環境を用意することで、実行時間やワークフロー実行にかかる料金を気にすることなくGitHub Actionsを使うことが出来る機能です（実行環境分の費用はかかります）。
今回はGitHubホストランナーで特に問題がないためセルフホストランナーを使うことはありませんでしたが、試してみたところ思ってた以上に使い勝手が良さそうだったので所感を箇条書きします。

良いところ

- secretsや公開アクション等、GitHubホストランナーで使用できる機能が遜色なく使える
- ホストのセットアップが簡単（エージェント入れてスクリプトを動かすだけ）
- エージェント側からGitHubに対してポーリングする形式なのでインバウンドの許可設定が不要
- t2.microサイズのEC2インスタンスでも動く
- ホストランナーにIAMロールを付けることでGitHub側で認証を渡す設定をなくせそう（OIDCを使えない場合とかは特に良さそう）

実際使うとなると気になるところ

- 1ジョブで1ホスト占有するようなので並列化するにはホストの台数を増やす必要がある
- ホストを起動し続けるのはコスト効率が悪いのでAutoScalingやワークフロー実行時のみホストを起動するような仕組みが求められる

## おわりに
明日は@mihotoyama さんの記事です！お楽しみに！