ちょっと話題の記事

GitHub Actionsのローカル実行ツール「act」を使う事でCI/CDコンフィグとローカルでのタスクランナーを1つにする

actというローカル端末でのタスクランナーとしても使える、GitHub Actionsのローカル実行ツールを紹介します。 合わせて私のCI/CDのローカル実行ツールが必要だと思う理由、タスクランナーに対して感じている課題を紹介します。
2020.07.09

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

おはようございます、加藤です。CI/CDツールとしてGitHub Actionsが2019年11月にリリースされてから一気に広まり、私もファーストチョイスとしてGitHub Actionsを検討・提案する機会が最近増えてきました。

先日、下記のTweetでactというGitHub Actionsのローカル実行ツールがあり、タスクランナーとしても使えるという事を知りとても興味を持ったのでブログにまとめました。

GitHub Actionsのローカル実行ツールの存在はいくつか確認していたのですが、CI/CDの設定をローカル実行させる事でタスクランナーを兼ねるという発想は目からウロコでした。

CI/CDローカル実行ツールの必要性

一般的にCI/CDはローカル端末上ではなくクラウド上で行われる事が多いです。私はリポジトリにPush前にUnitテストは全部回すタイプです。しかし、テストが増えてくると時間がかかり、開発者の体験を悪化させるのでローカル端末上でのテストを強制させたくはありません。

リポジトリへPush、Pull Requestが作成された際にテストが行われる仕組みを作ることでこの問題は解決できます。

さて、ここまでの説明はローカルでの実行ツールの存在を否定する内容です。なのに、私がローカル実行ツールを必要だと考えている理由は、CI/CDの設定を1回で完成させる事が非常に難しいからです。(少なくとも自分にとっては) 今まで、Circle CI、AWS CodeBuildといったCI/CDツールを使って来ましたが1発で設定ファイルを書けたことが無いです。これはリファクタリングの余地があるという意味でなく、構文エラーで実行されないや意図したとおりに実行されないという意味です。

そのため、チマチマとCommitを繰り返しながら修正するのですがCommitログが汚くなり、段々とCommitメッセージが雑になってしまいます。(ごめんなさい)

過去に使った設定を流用する事で、まず動く状態を用意してからプロジェクトに合わせて設定を変更する事で軽減する事はできますが根本的な解決策にはなりません。

このとき、ローカル実行ツールがあれば手元で実際に動作させて挙動を確認する事ができます。これが私が考えるローカル実行ツールの必要性です。端的に言うとコードと同様にCI/CDの設定も手元で動作チェックしたいよねという話です。

CI/CD設定とタスクランナーが共存する際の課題

Node.jsを使ったフロントエンドおよびバックエンドの開発を例に私が感じている課題について説明します。

私は、Node.jsを使った開発の場合、タスクランナーにNPMのScriptsを使うことが多いです。タスクランナーにはテスト、ビルド、リントなどのタスクを設定しローカル端末上で実行する事が多いです。これのタスクはCI/CD時にも行いたい場合があります。その時はCI/CDの設定で直接コマンドを記載するのではなくNPM Scriptsを呼び出す事で実現させています。これは情報を重複させない為です。(DRY原則)

しかし、NPM Scriptsで1つのタスクに複数のコマンドを実行させる様に設定すると、可読性が低下する問題がありました。

package.json

{
  "scripts": {
    "watch": "tsc -w",
    "cdk": "cdk",
    "prebuild:frontend": "API_URL=$(npm run --silent task:getparam /BackendStack/ApiUrl) && echo \"NODE_ENV='production'\nVUE_APP_API_BASE_URL='${API_URL}'\" > ./src/frontend/.env",
    "build": "tsc",
    "build:backend": "GO111MODULE=off go get -v -t -d ./src/backend/persons/... && GOOS=linux GOARCH=amd64 go build -o ./src/backend/persons/persons ./src/backend/persons/**.go",
    "build:frontend": "cd ./src/frontend/ && npm run --silent build",
    "deploy": "npm run --silent build && npm run --silent build:backend && npm run --silent deploy:backend && npm run --silent prebuild:frontend && npm run --silent build:frontend && npm run --silent deploy:frontend",
    "deploy:backend": "cdk deploy BackendStack",
    "deploy:frontend": "cdk deploy FrontendStack",
    "task:getparam": "aws ssm get-parameter --query Parameter.Value --output text --name "
  },
}

タスクランナーの定番としてはMakefileがありますが、シェル記法の様で微妙に異なるMakefileは何度使っても自分には合わず採用を避けていました。他のタスクランナーもいくつか試しましたが気に入るものが見つからず、上記のような可読性が低いNPM Scriptsで妥協していました。

actのGitHub Actionsをローカルで実行可能にする事で、タスクランナーを代替するという発想はこの課題に対して適切な解決策でした。

やってみた

インストール

actには各プラットフォーム用のワン・コマンドでのインストール方法が用意されています。

  • Mac: brew install nektos/tap/act
  • Win
    • Choco: choco install act-cli
    • Scoop: scoop install act
  • Manual: curl [https://raw.githubusercontent.com/nektos/act/master/install.sh](https://raw.githubusercontent.com/nektos/act/master/install.sh) | sudo bash

これら以外にもArch LinuxやNix向けのコマンドが公開されていました。

nektos/act

検証用リポジトリの用意

公開できるGitHub Actionsの設定が無かったので、適当に実行用のリポジトリを用意します。

AWS CDKをCI/CD対象と見立てます。作成したCDKは下記のリポジトリで公開しています。

https://github.com/intercept6/test-act

また、Actionsの設定ファイルは下記の様に作成しました。

.github/workflows/cdk.yml

name: cdk

on:
  push:
    branches:
        - master
  pull_request:

env:
  AWS_DEFAULT_REGION: 'ap-northeast-1'
  AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

jobs:
  aws_cdk:
    runs-on: ubuntu-18.04
    steps:
      - name: Checkout
        uses: actions/checkout@v1

      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: '12.x'

      - name: Setup dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Unit tests
        if: contains(github.event_name, 'pull_request')
        run: npm run test

      - name: CDK Deploy
        if: contains(github.event_name, 'push')
        run: npm run cdk:deploy

actを使いタスクを実行してみる

CDKを実行するということはAWSアカウントへのアクセスが必要なので、アクセスキーを設定する必要があります。

actでは --secret or -sでActionsのSecretを設定できます。コマンドオプション上で値を設定するだけではなく環境変数を引き継ぐ事ができます。

# MY_SECRETにsomevalueという値を設定する
act -s MY_SECRET=somevalue
# 同名の環境変数MY_SECRET1とMY_SECRET2の値をActionsのシークレットに設定する
act -s MY_SECRET1 -s MY_SECRET2

これを利用して下記の様に実行することで、PushイベントとしてActionsが実行されます。(actコマンドの引数でeventを指定しなかった場合、pushイベントのタスクが実行されます。)

act -s AWS_ACCOUNT_ID -s AWS_ACCESS_KEY_ID -s AWS_SECRET_ACCESS_KEY

result language=push

[cdk/aws_cdk] ?  Start image=node:12.6-buster-slim
[cdk/aws_cdk]   ?  docker run image=node:12.6-buster-slim entrypoint=["/usr/bin/tail" "-f" "/dev/null"] cmd=[]
[cdk/aws_cdk]   ?  docker cp src=/Users/kato.ryo/ghq/github.com/intercept6/test-act/. dst=/github/workspace
[cdk/aws_cdk] ⭐  Run Checkout
[cdk/aws_cdk]   ✅  Success - Checkout
[cdk/aws_cdk] ⭐  Run Setup Node
[cdk/aws_cdk]   ☁  git clone 'https://github.com/actions/setup-node' # ref=v1
[cdk/aws_cdk]   ?  docker cp src=/Users/kato.ryo/.cache/act/actions-setup-node@v1 dst=/actions/
[cdk/aws_cdk]   ?  ::debug::isExplicit: 
[cdk/aws_cdk]   ?  ::debug::explicit? false
[cdk/aws_cdk]   ?  ::debug::evaluating 0 versions
[cdk/aws_cdk]   ?  ::debug::match not found
[cdk/aws_cdk]   ?  ::debug::evaluating 399 versions
[cdk/aws_cdk]   ?  ::debug::matched: v12.18.2
[cdk/aws_cdk]   ?  ::debug::isExplicit: 12.18.2
[cdk/aws_cdk]   ?  ::debug::explicit? true
[cdk/aws_cdk]   ?  ::debug::checking cache: /opt/hostedtoolcache/node/12.18.2/x64
[cdk/aws_cdk]   ?  ::debug::not found
[cdk/aws_cdk]   ?  ::debug::Downloading https://nodejs.org/dist/v12.18.2/node-v12.18.2-linux-x64.tar.gz
[cdk/aws_cdk]   ?  ::debug::Destination /tmp/6d770b42-9191-40e7-9c8d-c468aab7667c
[cdk/aws_cdk]   ?  ::debug::download complete
[cdk/aws_cdk]   ?  ::debug::Checking tar --version
[cdk/aws_cdk]   ?  ::debug::tar (GNU tar) 1.30%0ACopyright (C) 2017 Free Software Foundation, Inc.%0ALicense GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.%0AThis is free software: you are free to change and redistribute it.%0AThere is NO WARRANTY, to the extent permitted by law.%0A%0AWritten by John Gilmore and Jay Fenlason.
| [command]/bin/tar xz --warning=no-unknown-keyword -C /tmp/14be2d41-e6cd-4512-a6dd-e1d792905987 -f /tmp/6d770b42-9191-40e7-9c8d-c468aab7667c
[cdk/aws_cdk]   ?  ::debug::Caching tool node 12.18.2 x64
[cdk/aws_cdk]   ?  ::debug::source dir: /tmp/14be2d41-e6cd-4512-a6dd-e1d792905987/node-v12.18.2-linux-x64
[cdk/aws_cdk]   ?  ::debug::destination /opt/hostedtoolcache/node/12.18.2/x64
[cdk/aws_cdk]   ?  ::debug::finished caching tool
[cdk/aws_cdk]   ⚙  ::add-path:: /opt/hostedtoolcache/node/12.18.2/x64/bin
| [command]/opt/hostedtoolcache/node/12.18.2/x64/bin/node --version
| v12.18.2
| [command]/opt/hostedtoolcache/node/12.18.2/x64/bin/npm --version
| 6.14.5
[cdk/aws_cdk]   ❓  ##[add-matcher]/actions/actions-setup-node@v1/.github/tsc.json
[cdk/aws_cdk]   ❓  ##[add-matcher]/actions/actions-setup-node@v1/.github/eslint-stylish.json
[cdk/aws_cdk]   ❓  ##[add-matcher]/actions/actions-setup-node@v1/.github/eslint-compact.json
[cdk/aws_cdk]   ✅  Success - Setup Node
[cdk/aws_cdk] ⭐  Run Setup dependencies

| > core-js-pure@3.6.5 postinstall /github/workspace/node_modules/core-js-pure
| > node -e "try{require('./postinstall')}catch(e){}"
| 
| Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!
| 
| The project needs your help! Please consider supporting of core-js on Open Collective or Patreon: 
| > https://opencollective.com/core-js 
| > https://www.patreon.com/zloirock 
| 
| Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)
| 
added 872 packages in 18.337s
[cdk/aws_cdk]   ✅  Success - Setup dependencies
[cdk/aws_cdk] ⭐  Run Build
| 
| > test-act@0.1.0 build /github/workspace
| > tsc
| 
[cdk/aws_cdk]   ✅  Success - Build
[cdk/aws_cdk] ⭐  Run CDK Deploy
| 
| > test-act@0.1.0 cdk:deploy /github/workspace
| > cdk deploy
| 
| TestActStack: deploying...
| 
|  ✅  TestActStack (no changes)
| 
| Stack ARN:
| arn:aws:cloudformation:ap-northeast-1:***:stack/TestActStack/41c8c8f0-c17a-11ea-b11f-06c1e14cac70
[cdk/aws_cdk]   ✅  Success - CDK Deploy

Pull requestイベントとしてActionsを実行したい場合は下記の様にします。

act pull_request

result language=pull_request

[cdk/aws_cdk] ?  Start image=node:12.6-buster-slim
[cdk/aws_cdk]   ?  docker run image=node:12.6-buster-slim entrypoint=["/usr/bin/tail" "-f" "/dev/null"] cmd=[]
[cdk/aws_cdk]   ?  docker cp src=/Users/kato.ryo/ghq/github.com/intercept6/test-act/. dst=/github/workspace
[cdk/aws_cdk] ⭐  Run Checkout
[cdk/aws_cdk]   ✅  Success - Checkout
[cdk/aws_cdk] ⭐  Run Setup Node
[cdk/aws_cdk]   ☁  git clone 'https://github.com/actions/setup-node' # ref=v1
[cdk/aws_cdk]   ?  docker cp src=/Users/kato.ryo/.cache/act/actions-setup-node@v1 dst=/actions/
[cdk/aws_cdk]   ?  ::debug::isExplicit: 
[cdk/aws_cdk]   ?  ::debug::explicit? false
[cdk/aws_cdk]   ?  ::debug::evaluating 0 versions
[cdk/aws_cdk]   ?  ::debug::match not found
[cdk/aws_cdk]   ?  ::debug::evaluating 399 versions
[cdk/aws_cdk]   ?  ::debug::matched: v12.18.2
[cdk/aws_cdk]   ?  ::debug::isExplicit: 12.18.2
[cdk/aws_cdk]   ?  ::debug::explicit? true
[cdk/aws_cdk]   ?  ::debug::checking cache: /opt/hostedtoolcache/node/12.18.2/x64
[cdk/aws_cdk]   ?  ::debug::not found
[cdk/aws_cdk]   ?  ::debug::Downloading https://nodejs.org/dist/v12.18.2/node-v12.18.2-linux-x64.tar.gz
[cdk/aws_cdk]   ?  ::debug::Destination /tmp/41b5b3a0-1d8b-461b-99c8-ea39b5bc6f90
[cdk/aws_cdk]   ?  ::debug::download complete
[cdk/aws_cdk]   ?  ::debug::Checking tar --version
[cdk/aws_cdk]   ?  ::debug::tar (GNU tar) 1.30%0ACopyright (C) 2017 Free Software Foundation, Inc.%0ALicense GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.%0AThis is free software: you are free to change and redistribute it.%0AThere is NO WARRANTY, to the extent permitted by law.%0A%0AWritten by John Gilmore and Jay Fenlason.
| [command]/bin/tar xz --warning=no-unknown-keyword -C /tmp/28615f63-4d64-498d-a34c-0400a1cc683f -f /tmp/41b5b3a0-1d8b-461b-99c8-ea39b5bc6f90
[cdk/aws_cdk]   ?  ::debug::Caching tool node 12.18.2 x64
[cdk/aws_cdk]   ?  ::debug::source dir: /tmp/28615f63-4d64-498d-a34c-0400a1cc683f/node-v12.18.2-linux-x64
[cdk/aws_cdk]   ?  ::debug::destination /opt/hostedtoolcache/node/12.18.2/x64
[cdk/aws_cdk]   ?  ::debug::finished caching tool
[cdk/aws_cdk]   ⚙  ::add-path:: /opt/hostedtoolcache/node/12.18.2/x64/bin
| [command]/opt/hostedtoolcache/node/12.18.2/x64/bin/node --version
| v12.18.2
| [command]/opt/hostedtoolcache/node/12.18.2/x64/bin/npm --version
| 6.14.5
[cdk/aws_cdk]   ❓  ##[add-matcher]/actions/actions-setup-node@v1/.github/tsc.json
[cdk/aws_cdk]   ❓  ##[add-matcher]/actions/actions-setup-node@v1/.github/eslint-stylish.json
[cdk/aws_cdk]   ❓  ##[add-matcher]/actions/actions-setup-node@v1/.github/eslint-compact.json
[cdk/aws_cdk]   ✅  Success - Setup Node
[cdk/aws_cdk] ⭐  Run Setup dependencies

| > core-js-pure@3.6.5 postinstall /github/workspace/node_modules/core-js-pure
| > node -e "try{require('./postinstall')}catch(e){}"
| 
| Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!
| 
| The project needs your help! Please consider supporting of core-js on Open Collective or Patreon: 
| > https://opencollective.com/core-js 
| > https://www.patreon.com/zloirock 
| 
| Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)
| 
added 872 packages in 17.072s
[cdk/aws_cdk]   ✅  Success - Setup dependencies
[cdk/aws_cdk] ⭐  Run Build
| 
| > test-act@0.1.0 build /github/workspace
| > tsc
| 
[cdk/aws_cdk]   ✅  Success - Build
[cdk/aws_cdk] ⭐  Run Unit tests
| 
| > test-act@0.1.0 test /github/workspace
| > jest
| 
 PASS  test/test-act.test.ts
  ✓ Empty Stack (253ms)
| 
Test Suites: 1 passed, 1 total
| Tests:       1 passed, 1 total
| Snapshots:   0 total
| Time:        3.468s
| Ran all test suites.
[cdk/aws_cdk]   ✅  Success - Unit tests

if: contains(github.event_name, '***') を使って特定のイベント時にだけ実行される様に設定したステップも期待する通りに判定されています。

考察

GitHub Actionsのローカル実行テストツールとして使いやすい

ブログ内ではシークレットの受け渡し機能しか紹介していませんが、イベントペイロードとして任意のJSONファイルの指定やディレクトリのマウント機能、ファイル変更を検知しての再実行など様々な機能があります。導入もワン・コマンドで可能で非常に使いやすいツールでした。

NPM Scriptsを一切使わずactに一本化はできない

2つの問題からNode.jsを使っているプロジェクトでNPM Scriptsの利用を辞められないという結論に至りました。

1つ目は、パスの問題です。aws-cdkの様なNPMで公開されているCLIツールはグローバルインストールかdevDependenciesでローカルインストールか2つの選択肢があり、バージョン管理やローカル端末を汚染したくないという思想から私は後者を好んでいます。しかし、ローカルインストールしたCLIツールはパスが通っていなので、そのままでは実行できずNPM Scripts内から実行する必要があります。一応、CI/CD時にPATHに ./node_modules を追加する方法や相対パスで指定する方法があります。

2つ目は、NPM Scriptsに定義されたタスクはVS CodeやIntelliJの様なIDEだと簡単に実行できるようにGUIでサポートがされています。私は普段IntelliJを使って開発していますが、設定するとショートカットキーからテストを実行できて非常に便利です、これを失いたくないです。

1つ目だけだったら相対パスで指定して環境変数にセットする判断をしたと思いますが2番目もあったので、NPM Scriptsとactを併用してタスク管理を実現しようと思います。

まとめ

actは効率よくActionsの設定を行えるように支援とローカル端末上で複雑なタスクを実行したい場合にActionsと設定を共有する事でメンテナンス負荷を軽減してくれます。また個人的な感想ですがMakefileと比べこちらの方が可読性が高いです。

今後、CI/CDを行う必要がある際は積極的にGitHub Actionsとactを活用しいきます!

参考