nektos/act を使って、ローカルからGithub Actionsを動かしてみる

# 概要
以前[別の記事](https://qiita.com/eno49conan/items/6d3e98df2ac82613c3b3)で、少し触ってみたという記載しました。

私自身、Github Actionsに対する知識はまだまだ少ないですが、
この機会で nektos/act に触れながら、Github Actionsに対する理解も深められればと思って始めました。

## どういうもの？
Github Actionsのworkflowをローカルで動作させるために使用するライブラリです。

Github Actionsの挙動確認のために、都度git add → commit → push→workflow確認、という操作を何度も実行するのは手間、だと思います。
**ターミナル上でworkflowの挙動が確認出来ると便利ではないでしょうか！**
ただ新しいライブラリ、という訳ではありません！（初回リリース4年前）

https://github.com/nektos/act

## 知ったきっかけ
以下のスライドで紹介されてました。

https://speakerdeck.com/tsubakimoto_s/github-actionsto-zhong-liang-kunaru-tamenolian-xi-fang-fa

### どうやって動く？（公式より）
> When you run act it reads in your GitHub Actions from .github/workflows/ and determines the set of actions that need to be run. It uses the Docker API to either pull or build the necessary images, as defined in your workflow files and finally determines the execution path based on the dependencies that were defined. Once it has the execution path, it then uses the Docker API to run containers for each action based on the images prepared earlier.

`.github/workflows/`フォルダに配置したyamlファイルを読み込むところから始まります。
Docker APIを使用して、workflowに定義したイメージのpullとbuild実施し、定義された依存関係に基づいて実行パスを決定します。
各アクションのコンテナを実行して、workflowの処理を行っていきます。

## セットアップ
さっそくですが、作業を進めます。
#### インストール
brewコマンドを使います。他は[README.md](https://github.com/nektos/act#installation-through-package-managers)をご確認ください。
```
brew install act
```

#### コマンド初回実行時
Dockerイメージのサイズを聞かれます。`Large`が特に大きいです。[こちらの記事](https://zenn.dev/kawataku/articles/508c0abd50482b)も見て、`Medium`で進めることにしました。

```shell
$ act
? Please choose the default image you want to use with act:

  - Large size image: +20GB Docker image, includes almost all tools used on GitHub Actions (IMPORTANT: currently only ubuntu-18.04 platform is available)
  - Medium size image: ~500MB, includes only necessary tools to bootstrap actions and aims to be compatible with all actions
  - Micro size image: <200MB, contains only NodeJS required to bootstrap actions, doesn't work with all actions

Default image and other options can be changed manually in ~/.actrc (please refer to https://github.com/nektos/act#configuration for additional information about file structure)  [Use arrows to move, type to filter, ? for more help]
  Large
> Medium
  Micro
```

具体的な作業に入っていきます。
今回はpushイベント時のworkflowとします。また以下の2つに関するworkflowを作成しました。
- アプリケーションに対するテスト実行
- AWSサービスとの連携（ECR・Lambda）

## アプリケーションのテスト
最初は軽めのものとして、FastAPIアプリケーションの簡単なテストを実施してみました。
python3.12のインストール（デフォルトでは3.10）、必要なライブラリインストール、テスト実行の順に実施します。
```yaml
  - name: Install Python
    uses: actions/setup-python@v4
    with:
      python-version: "3.12"
  - name: Install dependencies
    run: |
      python -m pip install --upgrade pip
      pip install -r requirements.txt
  - name: Run app tests
    run: |
      pytest ./tests/unit/test.py
```
<details><summary>アプリケーションとテストのコード</summary>

```python:main.py
from fastapi import FastAPI, status
from mangum import Mangum
from pydantic import BaseModel
from typing import Any

EventAny = Any

env = os.getenv("ENV")
if env is None:
    app = FastAPI()
else:
    app = FastAPI(
        root_path="/dev",
    )

class HealthCheck(BaseModel):
    """Response model to validate and return when performing a health check."""

    status: str = "OK"

@app.get(
    "/health",
    status_code=status.HTTP_200_OK,
    response_model=HealthCheck,
)
def get_health() -> HealthCheck:
    """Calculate speed as distance divided by time."""
    
    return HealthCheck(status="OK")

def handler(event: EventAny, context: EventAny):
    asgi_handler = Mangum(app)
    return asgi_handler(event, context)
```
`/health`というエンドポイントにアクセスし、戻り値とHTTPステータスコードが期待値通りか確認するテストです。
```python:test.py
from fastapi.testclient import TestClient
from fastapi import status
from src.main import app

client = TestClient(app)

def test_health():
    response = client.get("/health")
    assert response.status_code == status.HTTP_200_OK
    assert response.json() == {"status": "OK"}
```
</details>

コマンドを実行して、actでのテストが成功しました。(pushイベントの場合は、特に引数不要)
```shell
act
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/586521f9-cbd2-7806-2f0f-1766931ee892.png)

## AWSを用いたworkflowで挙動確認
少し規模を大きくします。以下workflowが、actで動作するか確かめてみました。
1. 認証情報設定
1. ECRへのログイン
1. DockerイメージのBuildとECRへのPush
1. Dockerイメージを使用したLambda関数の更新

:::note warn
**デフォルトの挙動**
コマンド実行時にオプションを付けないと、実際にAWSリソースは更新されますので、ご注意ください。
更新したくない場合は、公式のREADME.mdにも記載の通り、`n`オプションを設定してください。
> Run in dry-run mode:
```
act -n
```
:::

### 認証情報の設定
```yaml
  - name: Set up AWS
    run: |
      aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID
      aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY
      aws configure set region $REGION 
```

actで実行すると、以下のように怒られます。
```text
command not found, please refer to https://github.com/nektos/act/issues/107 for more information
```
actには、**AWS CLIがインストールされていない**ようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/3d057263-5358-9ce4-b153-e08f3322cb90.png)

[Github Actions(Ubuntu22.04)](https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2204-Readme.md)は、AWS CLIがインストール済なので、問題なく動きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/7d81d76a-01d1-7912-21b1-2a89a782636f.png)

actのためにawsコマンドを使える状態にするのも変なので、
以下を使って、awsコマンドを明記せずに認証情報を設定できる方法を採用します。
- [configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials)
```yaml
  - name: Set AWS credentials
    uses: aws-actions/configure-aws-credentials@v4  
    with:
      aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
      aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      aws-region: ap-northeast-1
```

actで実行すると、上記の実装で成功しました。
ただ私の場合、**実行前の段階で、ターミナル上で`aws configure`認証情報を設定したので成功していた**、と後で気付きました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/caf9a67d-9f05-eb8a-61c9-cb57bb48b0a6.png)

awsの認証情報は設定していない体で、もう少し詳しく確認してみました。

[actのドキュメント](https://nektosact.com/beginner/index.html#envsecrets-files-structure)の通り、Github Actions上で設定したsecrets値を、読み込むために新たにファイルを用意します。
```text:.secrets
export AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXXXXXXX
export AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
```
作成したシークレット値用のファイルを読み込むオプションを追加して実行すると成功しました。
```shell
act --secret-file .secrets
```

何度か実行する中でわかりましたが、
`aws configure`コマンドで設定した値 → `.env`ファイル → `.secrets`ファイル、この順に認証情報があるか確認しているようです。
いずれにも正しい認証情報がない場合、以下のエラーが表示されて、Jobが失敗しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/0b499008-cb95-bc56-6b88-0229264a8279.png)

### ECRへのログイン
awsコマンドは、actでは使えないと、認証情報設定の段階で判明したので、ECRログイン時によく使われるであろう以下のコマンドは使わず進めてみます。
```shell
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin
```
以下のリポジトリを使用して、ログインします。
- [amazon-ecr-login](https://github.com/aws-actions/amazon-ecr-login)

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

以下を実行して、成功しました。
```
act --secret-file .secrets -s GITHUB_TOKEN="$(gh auth token)"
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/004d70c5-6c51-df37-9b8e-b0c380be5512.png)

### DockerイメージのBuildとECRへのPush

以下2つのリポジトリの機能を使って、Pushまで行います。
- [metadata-action@v5](https://github.com/docker/metadata-action)
- [docker/build-push-action@v5](https://github.com/docker/build-push-action)
```yaml
  - name: Set Docker Images and Tags
    id: metadata
    uses: docker/metadata-action@v5
    with:
      images: ${{ steps.login-ecr.outputs.registry }}/${{ vars.FASTAPI_LAMBDA_REPOSITORY_NAME }}
      flavor: latest=true
  - name: Docker build and push to ECR         
    uses: docker/build-push-action@v5
    with:
      context: .
      file: ./Dockerfile          
      push: true
    　tags: ${{ secrets.AWS_REGISTRY_URL }}:latest
```
- `${{ steps.login-ecr.outputs.registry }}`：XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.comが設定されます。
- login-ecr部分は、`amazon-ecr-login@v2`で設定したid名です。

actで実行すると、失敗しました。
v4に下げても同じエラーが表示されたので、バージョンは関係なさそうです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/0859f304-5784-f9c4-d99a-e17b7a7ae50a.png)

[こちらの記事](https://earthly.dev/blog/using-github-actions-locally)に、以下の記載がありました。
```
act -s GITHUB_TOKEN="$(gh auth token)"
```
> This value is provided to GitHub Actions workflows by the GitHub Actions Runner automatically on the cloud

クラウドでは自動的に与えられている値のようです。ローカルで発行するために、`gh`コマンドを使います。

https://github.com/cli/cli

`gh auth token`を実行してトークンを確認します。
作成済でなければ、`no oauth token`となるはずです。
`gh auth login`を実行して、いくつか質問に回答して、トークンを発行します。
ワンタイムパスワードが表示され、Enterキーを押すとブラウザが起動します。
ワンタイムパスワードを入力すると、ターミナル側で`Logged in as XXXXXXX`と表示されます。

<details><summary>ターミナルとブラウザの状態</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/e875f39a-152d-4de0-f7d1-53362e79c8f0.png)
ワンタイムパスワードを入力すると、ターミナル側で`Logged in as XXXXXXX`と表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/89243a8e-f250-4677-d4ab-2dbab5d1d30c.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/fdb02e56-4e46-e431-b2f0-1c68fbbb0ce9.png)

</details>

完了したら、再度`gh auth token`を実行します。`gho_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`が表示されたので、これでトークンを用意できました。

再度実行します。
```
act --secret-file .secrets -s GITHUB_TOKEN="$(gh auth token)"
```
`metadata-action@v5`の処理は成功しましたが、`docker/build-push-action@v5`の処理でエラーがありました。イメージタグに設定したシークレット値が読み込めていませんでした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/bd11e9ed-acf3-a255-6d22-849b392cd471.png)
> ERROR: invalid tag ":latest": invalid reference format

認証情報設定の段階で記載した通り、シークレットの値をファイルに設定します。`AWS_REGISTRY_URL`を追記しました。
```text:.secrets
export AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXXXXXXX
export AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
export AWS_REGISTRY_URL=XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/repo-name
```
再度実行すると、すべて成功しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/b8214a75-2496-3a9c-ebaf-9db70956a86e.png)

### Dockerイメージを使用したLambda関数の更新
AWS for GitHub ActionsやDockerによって提供された機能ではなく、とある方（Hidetake Iwataさん）が開発された機能のようです。今回はこちらを使わせていただきます。
- [deploy-lambda-action](https://github.com/int128/deploy-lambda-action)
```yaml
  - name: Update Docker image using Lambda
    uses: int128/deploy-lambda-action@v1
    with:
        function-name: ${{ vars.FASTAPI_LAMBDA_FUNCTION_NAME }}
        image-uri: ${{ secrets.AWS_REGISTRY_URL }}:latest
```
[公式ドキュメント](https://nektosact.com/beginner/index.html#envsecrets-files-structure)の通りですが、Github Actions上で設定したvariable値を、actでも読み込むために新たにファイルを用意します。
```text:.variables
export FASTAPI_LAMBDA_FUNCTION_NAME=fastapi-sample-container-lambda
```
以下を実行して、成功しました。
```shell
act --var-file .variables --secret-file .secrets -s GITHUB_TOKEN="$(gh auth token)"
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/e3b9b79e-4898-f725-03d2-d6bc35951246.png)

最終的なworkflowは**Github Actions側も成功です！**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/67b488cc-bfab-bca8-059c-e2f0373baf1d.png)

## 成果物と感想
決して量は多くないですが、
最後まで成功するフローがあることは確認できました。

job同士の依存関係を見返すなど、workflowも一部整理しましたが、
キャッシュを利用してないので、実行時間短縮ができていません。

そのあたりは、**このAdvent Calenderの20日目で、別途公開予定です！**

<details><summary>本記事の最終的なyamlファイル</summary>

`lint`は実際Linterを用いたチェックがありますが、今回は適当に出力する処理だけ設定しました。
```yaml
name:  FastAPI App CI/CD Pipeline
run-name: ${{ github.actor }} is testing out GitHub Actions 🚀
on: 
  push:
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
  check-app:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install Python
        uses: actions/setup-python@v4
        with:
          python-version: "3.12"
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt     
      - name: Run app tests
        run: |
          pytest ./tests/unit/test.py
  release:
    runs-on: ubuntu-latest
    needs: [lint, check-app]    
    steps:          
      - uses: actions/checkout@v4    
      - name: Set AWS credentials
        uses: aws-actions/configure-aws-credentials@v4  
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1                            
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2        
      - name: Set Docker Images and Tags
        id: metadata
        uses: docker/metadata-action@v5
        with:
          images: ${{ steps.login-ecr.outputs.registry }}/${{ vars.FASTAPI_LAMBDA_REPOSITORY_NAME }}
          flavor: latest=true
      - name: Docker build and push to ECR         
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ./Dockerfile          
          push: true
          tags: ${{ secrets.AWS_REGISTRY_URL }}:latest
      - name: Update Docker image using Lambda
        uses: int128/deploy-lambda-action@v1
        with:
            function-name: ${{ vars.FASTAPI_LAMBDA_FUNCTION_NAME }}
            image-uri: ${{ secrets.AWS_REGISTRY_URL }}:latest
      - run: echo "🍏 This job's status is ${{ job.status }}."
```
</details>

実務の場合、より多くの処理がGithub Actionsのworkflowで実装されており、それもactで代用できるの？？開発進めやすいの？？と私自身も疑問に思っています。
やってみないと分からないので、皆さんもぜひ導入してみてください！とは、まだまだ言えません。。

今回実施した範囲では、通常の流れ（add→commit→push→ブラウザで確認する流れ）よりは、開発しやすいと感じましたが、もう少し複雑なフローとなった場合はどうなるのか、気になりますね。

nektos/actについて、気になる方はぜひ触ってみてください！