この記事は、Retty Advent Calendar 2021 Part1の15日目の記事です!
Part1:
Part2:
本記事は、 [非公式] Go Reject Con 2021で発表した、「go-taskでストレスフリーな開発体験」に追加説明をしたものです。
はじめに
開発していると、開発用にDBを用意したり、フォーマッタやリント、コードを生成するためにコマンドを実行することが多いと思います。ただ、大体の場合は同じコマンドを繰り返し実行するので、シェルスクリプトなどで自動化したくなります。
そういったシェルスクリプトは属人化しやすく、複雑な処理を定義している場合は本人でさえ修正できず、メンテナンスが大変なことが多いです。
この記事では、タスクランナーであるgo-taskを使ってこういった課題を解決したいと思います。
go-taskについて
本章ではgo-taskの概要と使い方について説明したいと思います。
go-taskはGNU Makeのような、いわゆるタスクランナーで、シンプルで簡単な操作性を目指していています。go-taskはタスクと呼ばれるsh/bashのコマンドで書かれた定義を実行します。タスクはGo製のシェルのインタプリタであるmdvan/shで解析されるので、sh/bashの環境がないWindowsでも動くようです。
go-task内で利用する変数やタスクをYAMLで定義することができるため、記述方法が比較的統一されやすくメンテナンスしやすいと言う特徴があります。また、タスクから別タスクを呼び出すことができ、各タスクが独立している場合は並列で実行することができます。さらに、ファイルが変更されたのを検知して、タスクを再実行するホットリロード機能がデフォルトで利用できるのもシェルスクリプトやGNU Makeにはない特徴です。そして、英語で書かれていますがドキュメントが充実しているのもgo-taskの良いところだと思います。
次の項目でgo-taskの使い方を紹介します。
go-taskでタスクを定義する
YAMLファイルを Taskfile.yml
という名前で用意すると、go-taskで指定することなく自動で読み込まれます。
タスクを定義するには以下の形でTaskfile.ymlを記述します。
以下は Hello, go-task! を出力するタスクを定義したTaskfile.ymlの例です。
version: "3" # Taskfileのバージョン3を利用する tasks: greet: # greet というタスク名でタスクを定義する cmds: - echo 'Hello, go-task!' # Hello, go-task! を出力するコマンド
task <タスク名>
でコマンドを実行すると以下のように結果が出力されます。
❯ task greet task: [greet] echo 'Hello, go-task!' Hello, go-task!
Taskfile.yml内で利用する変数を定義する
Taskfile.ymlで vars
と env
で変数を定義することでタスク内からvarsとenvの値を利用できます。varsとenvはGoのテンプレート記述( {{.変数名}}
)で呼び出せますし、envで定義した変数はシェルスクリプトで変数を呼び出す形( "$変数名"
)でも値を利用できます。
以下はvarsとenvに定義した GLOBAL_VAR
と GLOBAL_ENV
を出力するTaskfile.ymlの例です。
version: "3" vars: GLOBAL_VAR: globally variable env: GLOBAL_ENV: globally environment variable tasks: echo-var: cmds: - echo {{.GLOBAL_VAR}} echo-env: cmds: - echo {{.GLOBAL_ENV}} - echo "$GLOBAL_ENV"
定義したタスクの実行結果が以下になります。
envで定義したGLOBAL_ENVの値をGoのテンプレート記述で呼び出すと、taskコマンド実行時に出力されてしまうので注意が必要です。なので、パスワードなどの秘匿したい値はenvで定義して "$変数名"
で呼び出すと安全です。
# varsで定義したGLOBAL_VARを出力するタスクを実行する ❯ task echo-var task: [echo-var] echo globally variable globally variable # envで定義したGLOBAL_ENVを出力するタスクを実行する ❯ task echo-env task: [echo-env] echo globally environment variable # GLOBAL_ENVの値が出力される globally environment variable task: [echo-env] echo "$GLOBAL_ENV" globally environment variable
データベースを操作するタスク
前項でタスクと変数の定義方法を説明したので、実際に業務でデータベースを操作するタスクを定義したいと思います。 実際のコードはpyama2000/sample-go-taskにあるので、ご利用ください。 今回の例では、データベースにMySQLを利用し、sql-migrateを使ってテーブルの追加や変更などを記述したマイグレーションファイルを適用させます。
上の画像はデータベースを操作するTaskfile.ymlです。Taskfile.yml内で定義されている変数やタスクの説明は以下の通りです。
- vars
- MIGRATE_DATABASE_CONFIG
- sql-migrateの設定ファイルのパスの変数
- MIGRATE_DATABASE_CONFIG
- env
- tasks
ソースコードをクローンしてきた方は、以下のコマンドで動作の確認ができるので是非試してみてください。動かすには以下のツールや環境変数が必要です。
- Docker
- sql-migrate
- MySQLクライアント
- 環境変数
- DATASOURCE_USER: sample-go-task
- DATASOURCE_PASSWORD: sample-go-task
- DATASOURCE_DATABASE: sample_go_task
# DockerでMySQLサーバを用意する ❯ docker compose up -d # データベースにテーブルが入っていないことを確認する ❯ mysql --user "$DATASOURCE_USER" --host 127.0.0.1 --port 3306 -p"$DATASOURCE_PASSWORD" --database "$DATASOURCE_DATABASE" mysql> show tables; Empty set (0.01 sec) # database:seedのタスクを実行する ❯ task application:database:seed task: [application:database:seed] read -p "Do you want to initialize database? (yes/[no])" ANSWER; \ if [[ "$ANSWER" != 'yes' ]]; then exit 1; fi Do you want to initialize database? (yes/[no])yes task: [application:database:drop] mysql \ --user "$DATASOURCE_USER" \ --host "$DATASOURCE_HOST" \ --port "$DATASOURCE_PORT" \ -p"$DATASOURCE_PASSWORD" \ --database "$DATASOURCE_DATABASE" \ -e "DROP DATABASE $DATASOURCE_DATABASE; CREATE DATABASE $DATASOURCE_DATABASE;" mysql: [Warning] Using a password on the command line interface can be insecure. task: [application:database:drop] sleep 1 task: [application:migrate:up] sql-migrate up -config=config/database/dbconfig.yml --dryrun ==> Would apply migration 20211113221714-create_test.sql (up) CREATE TABLE `restaurant` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=25000 DEFAULT CHARSET=utf8; task: [application:migrate:up] sql-migrate up -config=config/database/dbconfig.yml Applied 1 migration task: [application:database:seed] mysql \ --user "$DATASOURCE_USER" \ --host "$DATASOURCE_HOST" \ --port "$DATASOURCE_PORT" \ -p"$DATASOURCE_PASSWORD" \ --database "$DATASOURCE_DATABASE" \ -e 'source config/database/data/data.sql'
database:seedタスクを実行後にデータベースを確認するとrestaurantテーブルが追加されていて、データも追加されていることが確認できます。
これをシェルスクリプトで表現すると下の画像のようになると思います。
シェルスクリプトとTaskfile.ymlを比較すると、Taskfile.ymlのほうがシンプルに記述できているのでメンテナンス性が高いと感じるでしょう。
Rettyでの使われ方
RettyではモバイルオーダーシステムであるRettyOrderの開発時にgo-taskを使っています。 以下はTaskfile.ymlに定義されているタスクの一部です。
- google/wire(DIライブラリ)のコードを生成する
- GraphQLのスキーマからGoのコードを生成する
- golangci-lintによるコードの静的解析 & コードの自動修正をする
- 開発に必要なデータベースを用意する
- sql-migrateを利用してマイグレートする
- データベースからGORMの構造体を生成する
おわりに
本記事では、go-taskのメリットと使い方、データベースを操作するタスクについてと、Rettyでの使われ方を紹介しました。
シェルスクリプトで試行錯誤している方やメンテナンスがされていないシェルスクリプトを使っている方が、go-taskを使って快適な開発体験を得られることを願っています。