【ACT】GitHub Actions のローカル実行ツール使ってみた

GitHub Actions テンプレート書いて、`プッシュ` → `実行` → `失敗` → `修正` → `プッシュ`、今までずっとこういう繰り返してきたが、とても時間かかってしまいます。 最近いいもの見つかったので、簡単で紹介します。

## [nektos/act](https://github.com/nektos/act)
`GitHub Actions` をローカルで実行できるツールです。`Windows`、`Linux`、`Mac`、メジャーなプラットフォームは全てサポートされてます。

## 簡単なサンプル
![img](https://github.com/nektos/act/wiki/quickstart/act-quickstart-2.gif)

## インストール方法
```sh
# Mac
brew install act

# Linux
curl https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash

# Windows Chocolatey
choco install act-cli

# Windows Scoop
scoop install act
```

## 環境変数
`GitHub Actions` のデフォルト [環境変数](https://docs.github.com/en/actions/reference/environment-variables#default-environment-variables) はパラメータで渡すことができます。

```sh:コマンド
act --env-file .env
```

```sh:.env
MY_ENV_VAR=MY_ENV_VAR_VALUE
MY_2ND_ENV_VAR="my 2nd env var value"
```

## シークレット変数
`GitHub Actions` 用のシークレット変数はパラメータで渡すことができます。

```sh:コマンド
# シークレットファイル
act --secret-file .secrets

# シークレットパラメータ
act -s AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE -s AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
```

## 完全な例
まず `GitHub Actions` 用のテンプレートを用意します。今回は `AWS ECS` へのデプロイ用のテンプレートとなります。

```yaml:.github/workflows/backend.yml

name: Build and Deploy to ECS Backend Service

on:
  push:
    paths:
      - backend/private/**
      - .github/workflows/backend.yml

env:
  ECR_REPOSITORY: fargate-backend
  TAKS_DEFINITION_FILE: task-def/backend.json
  SERVICE_NAME: backend
  CLUSTER_NAME: fargate-microservice

jobs:
  build:
    runs-on: ubuntu-18.04

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

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

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

      # docker build and push
      - name: Build, tag, and push image to Amazon ECR
        working-directory: backend/private
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        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"

      # update task definition
      - name: Fill in the new image ID in the Amazon ECS task definition
        id: render-web-container
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: ${{ env.TAKS_DEFINITION_FILE }}
          container-name: ${{ env.ECR_REPOSITORY }}
          image: ${{ steps.build-image.outputs.image }}

      - name: Deploy Amazon ECS task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.render-web-container.outputs.task-definition }}
          service: ${{ env.SERVICE_NAME }}
          cluster: ${{ env.CLUSTER_NAME }}
          wait-for-service-stability: true
```

```sh:コマンド
act push --secret-file .secrets
```

```sh:.secrets
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
```

```sh:実行結果
[Build .../build] 🚀  Start image=nektos/act-environments-ubuntu:18.04
[Build .../build]   🐳  docker run image=nektos/act-environments-ubuntu:18.04 entrypoint=["/usr/bin/tail" "-f" "/dev/null"] cmd=[]
[Build .../build]   🐳  docker cp src=/usr/local/src/fargate-microservice-architecture/. dst=/github/workspace
[Build .../build] ⭐  Run Checkout
[Build .../build]   ✅  Success - Checkout
[Build .../build] ⭐  Run Configure AWS credentials
[Build .../build]   ☁  git clone 'https://github.com/aws-actions/configure-aws-credentials' # ref=v1
[Build .../build]   🐳  docker cp src=/home/ec2-user/.cache/act/aws-actions-configure-aws-credentials@v1 dst=/actions/
[Build .../build]   ⚙  ::add-mask::***
[Build .../build]   ⚙  ::add-mask::***
[Build .../build]   ⚙  ::add-mask::999999999999
[Build .../build]   ⚙  ::set-output:: aws-account-id=999999999999
[Build .../build]   ✅  Success - Configure AWS credentials
[Build .../build] ⭐  Run Login to Amazon ECR
[Build .../build]   ☁  git clone 'https://github.com/aws-actions/amazon-ecr-login' # ref=v1
[Build .../build]   🐳  docker cp src=/home/ec2-user/.cache/act/aws-actions-amazon-ecr-login@v1 dst=/actions/
[Build .../build]   ⚙  ::set-output:: registry=999999999999.dkr.ecr.ap-northeast-1.amazonaws.com
[Build .../build]   ❓  ::save-state name=registries::999999999999.dkr.ecr.ap-northeast-1.amazonaws.com
[Build .../build]   💬  ::debug::'skip-logout' is  for 1 registries.
[Build .../build]   ✅  Success - Login to Amazon ECR
[Build .../build] ⭐  Run Build, tag, and push image to Amazon ECR
| Sending build context to Docker daemon  11.26kB
| Step 1/12 : FROM node:14-alpine as build
|  ---> 471e8b4eb0b2
| Step 2/12 : WORKDIR /app
|  ---> Using cache
|  ---> 99cdc798e7f7
| Step 3/12 : COPY . .
|  ---> 6ad23c2907e9
| Step 4/12 : RUN yarn install
|  ---> Running in c4952859c6d8
| yarn install v1.22.5
| warning package.json: No license field
| info No lockfile found.
| warning No license field
| [1/4] Resolving packages...
| [2/4] Fetching packages...
| [3/4] Linking dependencies...
| [4/4] Building fresh packages...
| success Saved lockfile.
| Done in 2.88s.
| Removing intermediate container c4952859c6d8
|  ---> 7d59270a7da3
| Step 5/12 : RUN yarn build
|  ---> Running in 7dd9e30c6be5
| yarn run v1.22.5
| warning package.json: No license field
| $ tsc
| Done in 2.40s.
| Removing intermediate container 7dd9e30c6be5
|  ---> 25d360ba037f
| Step 6/12 : FROM node:14-alpine
|  ---> 471e8b4eb0b2
| Step 7/12 : WORKDIR /app
|  ---> Using cache
|  ---> 99cdc798e7f7
| Step 8/12 : COPY --from=build /app/build .
|  ---> 481979ef9c8c
| Step 9/12 : COPY package.json .
|  ---> e2f9100ebeae
| Step 10/12 : RUN yarn install --production --no-lockfile
|  ---> Running in 3fec184a0e87
| yarn install v1.22.5
| warning package.json: No license field
| warning No license field
| [1/4] Resolving packages...
| [2/4] Fetching packages...
| [3/4] Linking dependencies...
| [4/4] Building fresh packages...
| Done in 2.84s.
| Removing intermediate container 3fec184a0e87
|  ---> cad29e95c4bb
| Step 11/12 : EXPOSE 8090
|  ---> Running in 57c67c1ea88b
| Removing intermediate container 57c67c1ea88b
|  ---> ff75e6a21c15
| Step 12/12 : CMD [ "yarn", "start"]
|  ---> Running in 3ae8cfe11af5
| Removing intermediate container 3ae8cfe11af5
|  ---> df3e4e5d1a05
| Successfully built df3e4e5d1a05
| Successfully tagged 999999999999.dkr.ecr.ap-northeast-1.amazonaws.com/fargate-backend:f8a3efce5d67ff5ebe5b083334a79ba1651d7cb9
| The push refers to repository [999999999999.dkr.ecr.ap-northeast-1.amazonaws.com/fargate-backend]
8ae25a3d281b: Pushed 
7c35cc8ea3b9: Pushed 
232cca45d3ab: Pushed 
2bd9c1c800cb: Layer already exists 
e5312594675f: Layer already exists 
e8ab0f5d941e: Layer already exists 
42e6ad646250: Layer already exists 
0fcbbeeeb0d7: Layer already exists 
f8a3efce5d67ff5ebe5b083334a79ba1651d7cb9: digest: sha256:cfb60be4df7f147425e809c745eec8ca0a3660c16b989832accd973a16e905fd size: 1991
[Build .../build]   ⚙  ::set-output:: image=999999999999.dkr.ecr.ap-northeast-1.amazonaws.com/fargate-backend:f8a3efce5d67ff5ebe5b083334a79ba1651d7cb9
[Build .../build]   ✅  Success - Build, tag, and push image to Amazon ECR
[Build .../build] ⭐  Run Fill in the new image ID in the Amazon ECS task definition
[Build .../build]   ☁  git clone 'https://github.com/aws-actions/amazon-ecs-render-task-definition' # ref=v1
[Build .../build]   🐳  docker cp src=/home/ec2-user/.cache/act/aws-actions-amazon-ecs-render-task-definition@v1 dst=/actions/
[Build .../build]   ⚙  ::set-output:: task-definition=/tmp/task-definition--86-Ixb7NQkEVcwv-.json
[Build .../build]   ✅  Success - Fill in the new image ID in the Amazon ECS task definition
[Build .../build] ⭐  Run Deploy Amazon ECS task definition
[Build .../build]   ☁  git clone 'https://github.com/aws-actions/amazon-ecs-deploy-task-definition' # ref=v1
[Build .../build]   🐳  docker cp src=/home/ec2-user/.cache/act/aws-actions-amazon-ecs-deploy-task-definition@v1 dst=/actions/
[Build .../build]   💬  ::debug::Registering the task definition
[Build .../build]   ⚙  ::set-output:: task-definition-arn=arn:aws:ecs:ap-northeast-1:999999999999:task-definition/fargate-backend:21
[Build .../build]   💬  ::debug::Updating the service
| Deployment started. Watch this deployment's progress in the Amazon ECS console: https://console.aws.amazon.com/ecs/home?region=ap-northeast-1#/clusters/fargate-microservice/services/Backend/events
[Build .../build]   💬  ::debug::Waiting for the service to become stable. Will wait for 30 minutes
[Build .../build]   ✅  Success - Deploy Amazon ECS task definition
```

## よくある問題

### Dockerコマンドが見つからない
`::error::Unable to locate executable file: docker`

```sh
[Build .../build]   🐳  docker cp src=/home/ec2-user/.cache/act/aws-actions-amazon-ecr-login@v1 dst=/actions/
[Build .../build]   ⚙  ::set-output:: registry=999999999999.dkr.ecr.ap-northeast-1.amazonaws.com
[Build .../build]   ❗  ::error::Unable to locate executable file: docker. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also check the file mode to verify the file is executable.
```

### 解決策
Default Runner は `node:12.6-buster-slim` の`Docker image`を使ってるため、OS に `Docker Command` が使えません。下記はデフォルトの設定です。

GitHub Runner|Docker Image
---|---
ubuntu-latest|node:12.6-buster-slim
ubuntu-20.04|node:12.6-buster-slim
ubuntu-18.04|node:12.6-buster-slim
ubuntu-16.04|node:12.6-stretch-slim
windows-latest|unsupported
windows-2019|unsupported
macos-latest|unsupported
macos-10.15|unsupported

実行用の `Docker image` を置き換えれば、解決できます。ただ、イメージファイルは **`18 GB`** 以上ありますので、最初のダウンロードは時間かかります。

```sh:解決策
act push --secret-file .secrets --platform ubuntu-18.04=nektos/act-environments-ubuntu:18.04
```

## 最後に
他のコマンドも色々ありますので、`act --help` で確認してください。