go-taskでストレスフリーな開発体験

この記事は、Retty Advent Calendar 2021 Part1の15日目の記事です!

Part1:

adventar.org

Part2:

adventar.org

本記事は、 [非公式] Go Reject Con 2021で発表した、「go-taskでストレスフリーな開発体験」に追加説明をしたものです。

speakerdeck.com

はじめに

開発していると、開発用に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を記述します。

Taskfile.ymlの書き方
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で varsenv で変数を定義することでタスク内からvarsとenvの値を利用できます。varsとenvはGoのテンプレート記述( {{.変数名}} )で呼び出せますし、envで定義した変数はシェルスクリプトで変数を呼び出す形( "$変数名" )でも値を利用できます。

以下はvarsとenvに定義した GLOBAL_VARGLOBAL_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

上の画像はデータベースを操作するTaskfile.ymlです。Taskfile.yml内で定義されている変数やタスクの説明は以下の通りです。

  • vars
    • MIGRATE_DATABASE_CONFIG
      • sql-migrateの設定ファイルのパスの変数
  • env
    • DATASOURCE_HOST
      • データベースのホストの変数
      • 環境変数DATASOURCE_HOST が定義されていない場合は 127.0.0.1 に接続する
    • DATASOURCE_PORT
      • データベースのポート番号の変数
      • 環境変数DATASOURCE_PORT が定義されていない場合は 3306 に接続する
    • DATASOUCE_ENDPOINT
      • 環境変数から DATASOURCE_USER(データベースに接続するユーザー名)、DATASOURCE_PASSWORD(データベースに接続するパスワード)、DATASOURCE_DATABASE(データベース名)、前述のDATASOURCE_HOSTDATASOURCE_PORT の値を利用してsql-migrateの接続先の変数を定義する
  • tasks
    • database:seed
      • データベースに初期データを追加するタスク
      • database:dropとmigrate:upのタスクを実行してから、データベースに値を追加する
    • database:drop
      • データベースを削除するタスク
      • mysqlコマンドで DATASOURCE_DATABASE で定義されたデータベース名を削除する
    • migrate:up

ソースコードをクローンしてきた方は、以下のコマンドで動作の確認ができるので是非試してみてください。動かすには以下のツールや環境変数が必要です。

  • 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テーブルが追加されていて、データも追加されていることが確認できます。

f:id:rettydev:20211215151120p:plain

これをシェルスクリプトで表現すると下の画像のようになると思います。

データベースを操作するシェルスクリプト
データベースを操作するシェルスクリプト

シェルスクリプトと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を使って快適な開発体験を得られることを願っています。