Skip to main content

· 2 min read
Shunsuke Suzuki

小ネタです。

dd-time を使って CircleCI の run を使ったコマンドの実行時間をどう計測したらいいのかちょっと考えました。

以前、コマンドの実行時間を DataDog に送るツール dd-time を作りました。

これは基本的に以下のように引数として -- 以降に実行するコマンドを指定します。

$ dd-time -m dd_time.execution_time -t command:docker-build -- docker build .

実行するスクリプトを標準入力で渡したい場合はこうします。

$ curl https://example.com/install.sh | dd-time -m dd_time.execution_time -- sh

もちろんシェルスクリプトである必要はなくて例えば Python だったらこうなります。

$ curl https://example.com/setup.py | dd-time -m dd_time.execution_time -- python

CircleCI の run では shell オプションで shell を指定できます。

https://circleci.com/docs/2.0/configuration-reference/#run

なので command 全体の時間を計測したい場合は、 shell を次のようにします。

- run:
name: test dd-time
shell: /usr/local/bin/dd-time -m dd_time.test -- sh -eo pipefail
command: |
echo start
sleep 5
echo end

こうすると shell 以外を弄ることなく実行時間を計測して DataDog に送ることが出来ます。

この shell のカスタマイズは dd-time に限らず使えるかも知れないですね。

  • ログをどっかに送ったりとか
  • コマンドが失敗したらエラーを握りつぶしつつどっかに通知したりとか
  • etc

以上、小ネタでした。

· 6 min read
Shunsuke Suzuki

CircleCI の組み込みの command checkout の注意点について書きます。

なお、ここに書かれている内容は 2020/04/24 時点のものであり、予告なしに checkout の挙動が変わる可能性があります。

また、今回は話を簡略化するため、 checkout 実行時点で .git がない(つまりキャッシュしていない)ものとします。

最初に結論

先に結論を書くと

  • CircleCI ではローカルのデフォルトブランチを参照しないほうが良い($CIRCLE_BRANCH がデフォルトブランチである場合は除く)
    • 履歴が origin と異なり、 $CIRCLE_BRANCH と同様になっているため
  • 代わりに origin のデフォルトブランチを参照したほうが良い
  • git branch -f <デフォルトブランチ> origin/<デフォルトブランチ> を実行してデフォルトブランチの履歴を修正するのもあり

checkout がなにをやっているか

checkout でなにをやっているかは実際に使ってみて CircleCI の job の詳細画面(?) から確認できます。

サンプル: https://app.circleci.com/pipelines/github/suzuki-shunsuke/test-circleci/73/workflows/5611059c-d6b1-4a34-91b5-45d6f149d408/jobs/96

ここでは checkout の全てについては触れません。一部抜粋します。

elif [ -n "$CIRCLE_BRANCH" ]
then
git reset --hard "$CIRCLE_SHA1"
git checkout -q -B "$CIRCLE_BRANCH"
fi

git reset --hard などをしています。

なぜこんなことをしているのか?

なんでこんなことをしているのでしょうか?単に git checkout $CIRCLE_BRANCH とかじゃだめなんでしょうか?

本当のところは CircleCI の中の人に聞かないとわかりませんが、自分なりに考えてみました。

昔実行した job を rerun することを思い浮かべてみましょう。 rerun するタイミングではすでに remote branch が削除されているかもしれません。そうなれば単に git checkout では失敗します。 また、branch があったとしても $CIRCLE_SHA1 がその branch の HEAD とも限りません。 force push で履歴が変更され、 $CIRCLE_SHA1 が CIRCLE_BRANCH の履歴上にないかもしれません。

そういったケースを考慮し、このような実装になっているのだと思います。

注意点

しかし、 git reset --hard していることにより、一つ注意が必要です。 git reset --hard は branch を $CIRCLE_BRANCH に切り替える前に実行しているため、 実行しているブランチ(基本的にデフォルトブランチ)の履歴が変更されています。

ローカルで再現してみる

CircleCI の rerun with SSH でも確認できますが、 ローカルの適当なリポジトリで同様のコマンドを叩いてみることで簡単に再現できます。

$ mkdir sample
$ cd sample
$ git init
$ git commit --allow-empty -m "master first commit"
$ export CIRCLE_BRANCH=feature/hello
$ git checkout -b $CIRCLE_BRANCH
$ git commit --allow-empty -m "$CIRCLE_BRANCH first commit"
$ git log
$ git checkout master # clone 直後の状態
$ export CIRCLE_SHA1=$(git rev-parse $CIRCLE_BRANCH)

# ここから CircleCI 同様のコマンドを叩いてみる
$ git reset --hard "$CIRCLE_SHA1"
$ git checkout -q -B "$CIRCLE_BRANCH"

ここで git log してみると master branch の履歴が $CIRCLE_BRANCH と同様になっているのが分かると思います。

何が困るのか

デフォルトブランチ を参照しなければ特に困ることはないでしょう。 一方でデフォルトブランチとの差分を検知して変更があったものだけビルドするとかそういうことをやっている場合には注意が必要です。

$ git diff --name-only master $CIRCLE_BRANCH

こうすると差分がないことになってしまいます。

回避方法

master の代わりに origin/master を参照すれば良いでしょう。

$ git diff --name-only origin/master $CIRCLE_BRANCH

ただ、うっかりローカルのデフォルトブランチを参照してしまうことは十分考えられる上に別にエラーは起こらないので間違いに気づきにくいです。

そこで checkout 直後にローカルのデフォルトブランチの履歴を origin と強制的に同じにしてしまうというテクニック(?)が考えられます。

DEFAULT_BRANCH=master
if [ "${CIRCLE_BRANCH:-}" != "$DEFAULT_BRANCH" ]; then
git branch -f "$DEFAULT_BRANCH" "origin/$DEFAULT_BRANCH"
fi

こうすれば間違えてローカルのデフォルトブランチを参照してしまっても安心です。 とはいえ、コードを部分的に移植したりする際にも危険(間違いに気づきにくい)なので、 origin を参照することを推奨します。

· 7 min read
Shunsuke Suzuki

自作の CLI ツール skaffold-generator の紹介です。 プロトタイピングみたいなノリで半日くらいで割と手早く作れました。 名前が長くて適当なのでもっと良い名前ないかなと思ってます。

Skaffold に欲しい機能がないので補完する感じで作ったのですが、「それ〇〇で出来るよ」とかあったら(GitHub issue とか Twitter で)教えていただけると幸いです。

どんなツールか

設定ファイル skaffold-generator.yaml を監視して変更があったら skaffold.yaml を生成するツールです。設定ファイルでサービスの依存関係を定義できたり、コマンドライン引数で指定したサービス及びそれが依存するサービスに関連した設定だけを使って skaffold.yaml を更新します。 このツールは skaffold.yaml を生成するだけなので実際にアプリケーションをビルド・デプロイするには skaffold と組み合わせて使います。

なぜ作ったか

元々ローカルでアプリケーションを動かしながら開発するために Docker Compose を使ってるリポジトリがあるのですが、それを skaffold に移行出来ないか検証しています。 まだ skaffold を触り始めたばかりで理解が浅いのですが、 本番環境は k8s で動いてるからローカルも k8s で動かせるといいかなと思ったり、あとは変更を検知して自動でビルド・デプロイしてくれたりして便利そうかなと思いました。 まぁ結果的に移行しないことになったとしても、 Skaffold と現状の仕組みについて理解が深まればいいかなくらいのつもりです。

検証の過程で、 以下のようなことが Docker Compose だと出来るけど Skaffold だと難しそうだと思いました。

  • サービスの依存関係を定義すること
    • Skaffold というより k8s の問題かとは思いますが
    • Docker Compose だと依存するものを自動で起動してくれて便利
  • コマンドライン引数で指定したサービスだけ起動すること
    • Skaffold だと skafffold.yaml で定義したものすべてがビルド・デプロイされるという認識

サービスの数が少なければ全部ビルド・デプロイでもいいですが、 マイクロサービスをモノレポで管理しているような場合、 すべてのマイクロサービスをビルド・デプロイするのは無駄が大きかったりします。

そこで skaffold.yaml の元となる設定ファイルを用意し、コマンドライン引数でサービスを指定して必要最小限の skaffold.yaml を生成するツールを作ってみました。

インストール

Go のバイナリをダウンロードしてきてください。 https://github.com/suzuki-shunsuke/skaffold-generator/releases

使い方

使い方は簡単です。サブコマンドもありません。 リポジトリにサンプルがあるのでそれを見ましょう。

まずは skaffold-generator.yaml を用意します。

skaffold-generator.yamlbaseservices からなります。

base は生成される skaffold.yaml のベースとなるものです。 deploy.kubectl.manifestsbuild.artifacts は上書きされるので指定しないでください。

services ではサービスのリストを定義します。 各サービスは以下の属性を持ちます。

  • name: サービス名。コマンドライン引数と depends_on でサービスを指定するのに使う。ユニークにする
  • manifests: skaffold.yaml の deploy.kubectl.manifests
  • artifacts: skaffold.yaml の build.artifacts
  • depends_on: サービスが依存するサービス名のリスト

用意したら skaffold-generator を実行します。 skaffold.yaml が生成(既にあれば上書き)され、 skaffold-generator.yaml の変更を監視した状態になります。

$ skaffold-generator
2020/04/05 18:19:37 start to watch skaffold-generator.yaml

コマンドライン引数でサービス名を指定しない場合、すべてのサービスが skaffold.yaml に反映されます。 別のターミナルで skaffold dev を実行すれば 生成された skaffold.yaml を使ってアプリケーションをビルド・デプロイ出来ます。

$ skaffold dev

skaffold-generator.yaml を変更すれば、その変更を検知し skaffold.yaml が更新され、そして skaffold devskaffold.yaml の変更を検知しアプリケーションがビルド・デプロイされます。

引数無しですべてのサービスをデプロイするとこのツールの意味がないので、コマンドライン引数でサービス名を指定しましょう。

$ skaffold-generator api

こうするとサービス apiapi が依存するサービス(依存関係は再帰的に処理されます)だけが skaffold.yaml に反映されます。 依存関係は循環してても大丈夫です。

使い方は以上です。

Docker Compose みたいにできないこと

Docker Compose みたいに依存関係を定義できるようになりましたが、 Docker Compose みたいにデプロイの順序は考慮されません。 まぁこのツールは skaffold.yaml を生成するだけなので仕方ないですね。

最後に

以上、 skaffold-generator の紹介でした。 まだ作ったばっかで自分でも使えてないので本当に使い物になるのかは分かりませんが、 興味ある人は触ってみてください。

· 40 min read
Shunsuke Suzuki

Terraform を勉強するには実際に使ってみるのが一番手っ取り早いですが、 では手頃な題材はあるかと言われると少し難しいです。

公式の Getting Started では AWS が使われていますが、 AWS のアカウントやクレデンシャルが必要ですしお金もかかってしまいます(無料枠はありますが)。 もう少し手軽なものが欲しいところです。

そこで公式の Provider で丁度いいものはないか探したところ、 MySQL Provider が良さそうでした。 MySQL のユーザーや Database を Terraform で管理したいとは自分は思いませんが、 Terraform の入門で遊ぶにはちょうどよいでしょう。

ちなみに公式の Provider のリストはこちらです。

また、 Terraform に関しては Terraform 入門 も参照してください。

今回の作業用に適当にディレクトリを作成し、そこで作業しましょう。

以降、コマンドの実行結果は一部省略することがあります。

$ mkdir workspace
$ cd workspace

Terraform のバージョンと tfenv

Terraform を複数人で使う場合、 Terraform のバージョンを揃えるのが重要です。 理由の一つとして、 Terraform の State は State を作成した Terraform のバージョンを記録しており、それより古いバージョンの Terraform で terraform plan などを実行すると失敗するようになっていることが挙げられます(この点については後でも触れます)。 そういう意味では、 tfenv によってバージョン管理するのが良いです。

.terraform-version を作成します。

$ echo 0.12.19 > .terraform-version
$ tfenv install
$ terraform version
Terraform v0.12.19

MySQL を Docker で動かす

では MySQL を Docker で動かします。

https://hub.docker.com/_/mysql?tab=description

Docker Compose 使う場合

docker-compose.yml

---
version: "3"
services:
mysql:
image: mysql:5.7.28
ports:
- "23306:3306"
environment:
MYSQL_ROOT_PASSWORD: password
$ docker-compose up -d
$ docker-compose ps # コンテナが起動しているか確認

不要になったら削除しましょう。

$ docker-compose rm -sf mysql

これから Terraform で MySQL の Database を作成します。 作成されているか確認するために MySQL に接続しておきます。

$ docker-compose exec mysql mysql -u root -p -P 23306
mysql> show databases; # database の一覧を確認
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
4 rows in set (0.00 sec)

Docker Compose を使わない場合

基本的に Docker Compose 使う場合と変わりません。

$ docker run --name terraform-mysql -p "23306:3306" -e MYSQL_ROOT_PASSWORD=password -d mysql:5.7.28
$ docker exec -ti terraform-mysql mysql -u root -p -P 23306
$ docker rm -vf terraform-mysql

リソースの作成

次のような設定ファイルを用意します。

main.tf

provider "mysql" {
endpoint = "localhost:23306"
username = "root"
password = "password"
}

resource "mysql_database" "foo" {
name = "foo"
}

設定できる属性やその意味などはドキュメントを確認してください。

https://www.terraform.io/docs/providers/mysql/r/database.html

MySQL Provider の設定として MySQL に接続するための情報と、Terraform によって作成するデータベース foo の設定が定義されています。 password が平文で書かれているのが気になるかもしれませんが、一旦スルーしてください。

この状態で terraform plan を実行してみます。 terraform planterraform apply によるリソースの作成を DRY RUN するコマンドです。

$ terraform plan

Error: Could not satisfy plugin requirements


Plugin reinitialization required. Please run "terraform init".

Plugins are external binaries that Terraform uses to access and manipulate
resources. The configuration provided requires plugins which can't be located,
don't satisfy the version constraints, or are otherwise incompatible.

Terraform automatically discovers provider requirements from your
configuration, including providers used in child modules. To see the
requirements and constraints from each module, run "terraform providers".



Error: provider.mysql: no suitable version installed
version requirements: "(any version)"
versions installed: none

失敗しました。

Plugin reinitialization required. Please run "terraform init".

とある通り、 terraform planapply などのコマンドを実行する前に terraform init を実行する必要があります。

$ terraform init

Initializing the backend...

Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "mysql" (terraform-providers/mysql) 1.9.0...
^C
The following providers do not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.mysql: version = "~> 1.9"

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

Provider のダウンロードが行われています。 terraform init を実行すると .terraform というディレクトリが作成されます。

$ ls -A
.terraform .terraform-version main.tf

terraform init は何度でも安全に実行できます。 .terraform を削除した場合でももう一度 terraform init を実行すれば問題ありません。

次に terraform plan を実行します。

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create

Terraform will perform the following actions:

# mysql_database.foo will be created
+ resource "mysql_database" "foo" {
+ default_character_set = "utf8"
+ default_collation = "utf8_general_ci"
+ id = (known after apply)
+ name = "foo"
}

Plan: 1 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

Plan: 1 to add, 0 to change, 0 to destroy.

とある通り、リソースが 1 つ作成されるようです。 DRY RUN なのでまだ作成されてません。

では実際に作成してみましょう。

$ terraform apply
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create

Terraform will perform the following actions:

# mysql_database.foo will be created
+ resource "mysql_database" "foo" {
+ default_character_set = "utf8"
+ default_collation = "utf8_general_ci"
+ id = (known after apply)
+ name = "foo"
}

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.

Enter a value: yes

mysql_database.foo: Creating...
mysql_database.foo: Creation complete after 0s [id=foo]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

途中確認があるので yes と入力すると実際に変更が適用されます。

本当に Database が作られているか確認します。 MySQL クエリを叩きます。

mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| foo |
| mysql |
| performance_schema |
| sys |
+--------------------+
5 rows in set (0.01 sec)

mysql> select * from INFORMATION_SCHEMA.SCHEMATA where SCHEMA_NAME='foo';
+--------------+-------------+----------------------------+------------------------+----------+
| CATALOG_NAME | SCHEMA_NAME | DEFAULT_CHARACTER_SET_NAME | DEFAULT_COLLATION_NAME | SQL_PATH |
+--------------+-------------+----------------------------+------------------------+----------+
| def | foo | utf8 | utf8_general_ci | NULL |
+--------------+-------------+----------------------------+------------------------+----------+
1 row in set (0.00 sec)

ありました。

すると terraform.tfstate というファイルが作られているはずです。

$ ls
docker-compose.yml main.tf terraform.tfstate

こんな JSON ファイルになります。

{
"version": 4,
"terraform_version": "0.12.19",
"serial": 1,
"lineage": "7011a551-bfa1-96a5-4153-2c9d6f32251c",
"outputs": {},
"resources": [
{
"mode": "managed",
"type": "mysql_database",
"name": "foo",
"provider": "provider.mysql",
"instances": [
{
"schema_version": 0,
"attributes": {
"default_character_set": "utf8",
"default_collation": "utf8_general_ci",
"id": "foo",
"name": "foo"
},
"private": "bnVsbA=="
}
]
}
]
}

管理されているリソースの情報と、 Terraform のバージョンなどのメタ情報が保存されています。 このファイルは基本的に terraform によって更新されるものであり、人間が手で修正するものではありません。

古い Terraform を使ってみる

ここであえて古いバージョンの Terraform を使って terraform plan を実行してみます。

$ echo 0.12.12 > .terraform-version
$ tfenv install
$ terraform version
$ terraform plan
Error: Error locking state: Error acquiring the state lock: state snapshot was created by Terraform v0.12.19, which is newer than current v0.12.12; upgrade to Terraform v0.12.19 or greater to work with this state

Terraform acquires a state lock to protect the state from being written
by multiple users at the same time. Please resolve the issue above and try
again. For most commands, you can disable locking with the "-lock=false"
flag, but this is not recommended.

失敗しました。 State の lock に失敗したようです。 State の lock については https://techblog.szksh.cloud/terraform-state-locking/ も参照してください。

このように古いバージョンの terraform は使えません。 一方新しいバージョンを使うには問題なく使えますが、 State の terraform_version が更新され、それまでのバージョンを使ってた人が突然 terraform plan などができなくなりますので、注意が必要です。

これが前述した意味になります。

Terraform を複数人で使う場合、 Terraform のバージョンを揃えるのが重要です。 理由の一つとして、 Terraform の State は State を作成した Terraform のバージョンを記録しており、それより古いバージョンの Terraform で terraform plan などを実行すると失敗するようになっていることが挙げられます(この点については後でも触れます)。

では元のバージョンに戻しましょう。

$ echo 0.12.19 > .terraform-version
$ terraform version
$ terraform plan
No changes. Infrastructure is up-to-date.

リソースの更新

同じ調子でもう一つ Database を作ってみましょう。 Database foo の設定をコピーして terraform plan を実行します。

resource "mysql_database" "foo" {
name = "foo"
}

resource "mysql_database" "foo" {
name = "foo"
}
$ terraform plan

Error: Duplicate resource "mysql_database" configuration

on main.tf line 11:
11: resource "mysql_database" "foo" {

A mysql_database resource named "foo" was already declared at main.tf:7,1-32.
Resource names must be unique per type in each module.

失敗しました。エラーメッセージの通り、リソースパスはユニークでないといけません。 修正しましょう。

resource "mysql_database" "foo" {
name = "foo"
}

resource "mysql_database" "bar" { # "foo" を "bar" に変更
name = "foo"
}
$ terraform plan
Terraform will perform the following actions:

# mysql_database.bar will be created
+ resource "mysql_database" "bar" {
+ default_character_set = "utf8"
+ default_collation = "utf8_general_ci"
+ id = (known after apply)
+ name = "foo"
}

Plan: 1 to add, 0 to change, 0 to destroy.

作成されるようです。 apply してみましょう。

$ terraform apply
mysql_database.bar: Creating...

Error: Error 1007: Can't create database 'foo'; database exists

on main.tf line 11, in resource "mysql_database" "bar":
11: resource "mysql_database" "bar" {

失敗しました。同じ名前のデータベースは作成できないので当然です。 このように plan に成功しても apply に失敗することはあります。

では name を修正しましょう。ついでに database foo の default character set を修正します。

resource "mysql_database" "foo" {
name = "foo"
default_character_set = "utf8mb4" # default character set を修正。デフォルトは utf8
}

resource "mysql_database" "bar" {
name = "bar" # 名前を foo から bar に変更
}
$ terraform plan
Terraform will perform the following actions:

# mysql_database.bar will be created
+ resource "mysql_database" "bar" {
+ default_character_set = "utf8"
+ default_collation = "utf8_general_ci"
+ id = (known after apply)
+ name = "bar"
}

# mysql_database.foo will be updated in-place
~ resource "mysql_database" "foo" {
~ default_character_set = "utf8" -> "utf8mb4"
default_collation = "utf8_general_ci"
id = "foo"
name = "foo"
}

Plan: 1 to add, 1 to change, 0 to destroy.
$ terraform apply
mysql_database.bar: Creating...
mysql_database.foo: Modifying... [id=foo]
mysql_database.bar: Creation complete after 0s [id=bar]

Error: Error 1253: COLLATION 'utf8_general_ci' is not valid for CHARACTER SET 'utf8mb4'

on main.tf line 7, in resource "mysql_database" "foo":
7: resource "mysql_database" "foo" {

mysql_database.foo の変更に失敗しました。一方 mysql_database.bar の作成には成功しています。

mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| bar |
| foo |
| mysql |
| performance_schema |
| sys |
+--------------------+
6 rows in set (0.00 sec)

mysql> select * from INFORMATION_SCHEMA.SCHEMATA where SCHEMA_NAME='foo';
+--------------+-------------+----------------------------+------------------------+----------+
| CATALOG_NAME | SCHEMA_NAME | DEFAULT_CHARACTER_SET_NAME | DEFAULT_COLLATION_NAME | SQL_PATH |
+--------------+-------------+----------------------------+------------------------+----------+
| def | foo | utf8 | utf8_general_ci | NULL |
+--------------+-------------+----------------------------+------------------------+----------+
1 row in set (0.00 sec)

このように、一部の変更の適用に失敗にしても、その他の変更は適用されるという、ある意味中途半端に apply される場合があります。こういった場合に rollback するようなコマンドはないので気をつけましょう。適用された変更はちゃんと State に反映されています。

では mysql_database.foo の変更に失敗したので、適切に設定を変更しましょう。

resource "mysql_database" "foo" {
name = "foo"
default_character_set = "utf8mb4"
default_collation = "utf8mb4_general_ci" # utf8_general_ci から変更
}

apply に --auto-approve というオプションをつけると確認なしに適用されます。CIで実行する場合には基本これをつけることになると思います。

$ terraform apply --auto-approve
mysql_database.foo: Refreshing state... [id=foo]
mysql_database.bar: Refreshing state... [id=bar]
mysql_database.foo: Modifying... [id=foo]
mysql_database.foo: Modifications complete after 0s [id=foo]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

変更できました。

mysql> select * from INFORMATION_SCHEMA.SCHEMATA where SCHEMA_NAME='foo';
+--------------+-------------+----------------------------+------------------------+----------+
| CATALOG_NAME | SCHEMA_NAME | DEFAULT_CHARACTER_SET_NAME | DEFAULT_COLLATION_NAME | SQL_PATH |
+--------------+-------------+----------------------------+------------------------+----------+
| def | foo | utf8mb4 | utf8mb4_general_ci | NULL |
+--------------+-------------+----------------------------+------------------------+----------+
1 row in set (0.00 sec)

terraform.tfstate.backup

ところで terraform.tfstate.backup というファイルが作られています。

$ ls
docker-compose.yml main.tf terraform.tfstate terraform.state.backup

これは名前の通り terraform.tfstate のバックアップです。 terraform.tfstate が terraform のコマンドによって更新される前に自動的にバックアップが作成されます。

リソースの recreate

今度は database の名前を変更してみましょう。

resource "mysql_database" "foo" {
name = "foo2" # foo から変更
default_character_set = "utf8mb4"
default_collation = "utf8mb4_general_ci"
}
$ terraform plan
Terraform will perform the following actions:

# mysql_database.foo must be replaced
-/+ resource "mysql_database" "foo" {
default_character_set = "utf8mb4"
default_collation = "utf8mb4_general_ci"
~ id = "foo" -> (known after apply)
~ name = "foo" -> "foo2" # forces replacement
}

Plan: 1 to add, 0 to change, 1 to destroy.

先程 default_collation と default_character_set を変更した際は既存のデータベースが更新されましたが、今度は新しく作り直されるようです。 これはリソースの定義に依存します。ソースコードを確認しましょう。

https://github.com/terraform-providers/terraform-provider-mysql/blob/v1.9.0/mysql/resource_database.go#L31

属性の定義で ForceNew: true となっている場合、その属性が変更されるとリソースが作り直されます。デフォルトだと既存のリソースの更新になります。 新しく作り直されるということは、テーブルなどは消えるはずです。試しにテーブルを作っておいて、 apply してみましょう。

mysql> show tables from foo;
Empty set (0.00 sec)

mysql> create table foo.zoo (id int);
Query OK, 0 rows affected (0.03 sec)

mysql> show tables from foo;
+---------------+
| Tables_in_foo |
+---------------+
| zoo |
+---------------+
1 row in set (0.00 sec)
$ terraform apply --auto-approve
mysql_database.foo: Refreshing state... [id=foo]
mysql_database.bar: Refreshing state... [id=bar]
mysql_database.foo: Destroying... [id=foo]
mysql_database.foo: Destruction complete after 0s
mysql_database.foo: Creating...
mysql_database.foo: Creation complete after 0s [id=foo2]

Apply complete! Resources: 1 added, 0 changed, 1 destroyed.

mysql_database.foo: Destroying... [id=foo]

とあるように一度削除されています。

mysql> show tables from foo;
ERROR 1049 (42000): Unknown database 'foo'
mysql> show tables from foo2;
Empty set (0.00 sec)

新しいデータベースには先程作成したテーブルがありません。このように recreate は危険な操作なのでやるときには注意を払いましょう。

変数の利用

変数を使ってみましょう。設定を修正します。

resource "mysql_database" "foo" {
name = "foo2"
default_character_set = var.default_character_set # 変数 default_character_set を参照
default_collation = "utf8mb4_general_ci"
}
$ terraform plan

Error: Reference to undeclared input variable

on main.tf line 9, in resource "mysql_database" "foo":
9: default_character_set = var.default_character_set

An input variable with the name "default_character_set" has not been declared.
This variable can be declared with a variable "default_character_set" {}
block.

エラーになりました。変数を利用するには宣言が必要です。 variables.tf というファイルを作成しましょう。

variable "default_character_set" {
type = string
}
$ terraform plan
var.default_character_set
Enter a value:

値の入力を求められました。これは変数の値が設定されていないからです。 ここでは foo と入力してみます。

$ terraform plan
var.default_character_set
Enter a value: foo

Terraform will perform the following actions:

# mysql_database.foo will be updated in-place
~ resource "mysql_database" "foo" {
~ default_character_set = "utf8mb4" -> "foo"
default_collation = "utf8mb4_general_ci"
id = "foo2"
name = "foo2"
}

Plan: 0 to add, 1 to change, 0 to destroy.

-input=false にすると入力を求められずにエラーを返します。

$ terraform plan -input=false

Error: No value for required variable

on variables.tf line 1:
1: variable "default_character_set" {

The root module input variable "default_character_set" is not set, and has no
default value. Use a -var or -var-file command line argument to provide a
value for this variable.

terraform.tfvars というファイルを作成し、値を設定しましょう。

default_character_set = "utf8mb4"
$ terraform plan

No changes. Infrastructure is up-to-date.

terraform.tfvars は特別なファイル名で、カレントディレクトリにこのファイルがあると自動で読み込まれます。

ファイル名を変えて terraform plan をしてみましょう。

$ mv terraform.tfvars main.tfvars
$ terraform plan
var.default_character_set
Enter a value:

聞かれました。 main.tfvars が読み込まれていません。 -var-file オプションでファイルを指定すれば main.tfvars を読み込めます。

$ terraform plan -var-file=main.tfvars

もとに戻しておきます。

$ mv main.tfvars terraform.tfvars

他のリソースの属性の参照

設定を修正します。

resource "mysql_database" "foo" {
name = "foo2"
default_character_set = var.default_character_set
default_collation = "utf8mb4_general_ci"
}

resource "mysql_database" "bar" {
name = "bar"
default_character_set = mysql_database.foo.default_character_set # 他のリソースの属性を参照
default_collation = "utf8mb4_general_ci" # default_character_set に合わせて変更
}
$ terraform plan
Terraform will perform the following actions:

# mysql_database.bar will be updated in-place
~ resource "mysql_database" "bar" {
~ default_character_set = "utf8" -> "utf8mb4"
~ default_collation = "utf8_general_ci" -> "utf8mb4_general_ci"
id = "bar"
name = "bar"
}

Plan: 0 to add, 1 to change, 0 to destroy.
$ terraform apply --auto-approve
mysql_database.foo: Refreshing state... [id=foo2]
mysql_database.bar: Refreshing state... [id=bar]
mysql_database.bar: Modifying... [id=bar]
mysql_database.bar: Modifications complete after 0s [id=bar]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

terraform state rm

https://www.terraform.io/docs/commands/state/rm.html

Terraform は State によって設定ファイル中のリソースと実際のリソースをマッピングしています。 State からリソースを削除して terraform plan を実行してみると、 Terraform はそのリソースを新規で作成しようとします。

$ terraform plan
No changes. Infrastructure is up-to-date.

$ terraform state rm mysql_database.bar
Removed mysql_database.bar
Successfully removed 1 resource instance(s).

$ terraform plan
Terraform will perform the following actions:

# mysql_database.bar will be created
+ resource "mysql_database" "bar" {
+ default_character_set = "utf8mb4"
+ default_collation = "utf8mb4_general_ci"
+ id = (known after apply)
+ name = "bar"
}

Plan: 1 to add, 0 to change, 0 to destroy.

実際には同じ名前のデータベースは作成できないので apply で失敗するはずです。 terraform state rm は元々 Terraform で管理していたものを Terraform で管理するのを止めたり、あるいは手動で削除してしまったリソースを State からも削除するのに使えます。

terraform import

https://www.terraform.io/docs/import/index.html https://www.terraform.io/docs/commands/import.html

terraform import は Terraform で管理されていないリソースを Terraform で管理するために State にリソースのデータを取り込むコマンドです。 ちょうど mysql_database.bar を State から消してしまったので、 import することにしましょう。

https://www.terraform.io/docs/providers/mysql/r/database.html#import にある通り、データベースはデータベース名を指定すれば import 出来ます。

$ terraform import mysql_database.bar bar
mysql_database.bar: Importing from ID "bar"...
mysql_database.bar: Import prepared!
Prepared mysql_database for import
mysql_database.bar: Refreshing state... [id=bar]

Import successful!

The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.

Import 出来ました。 terraform plan を実行して差分がなくなっていることを確認します。

$ terraform plan
No changes. Infrastructure is up-to-date.

次に手動で Database を作成してそれを import してみましょう。

mysql> create database zoo;
Query OK, 1 row affected (0.00 sec)
$ terraform import mysql_database.zoo zoo
Error: resource address "mysql_database.zoo" does not exist in the configuration.

Before importing this resource, please create its configuration in the root module. For example:

resource "mysql_database" "zoo" {
# (resource arguments)
}

失敗しました。 mysql_database.zoo が存在しないからです。 空で良いのでリソースの設定を追加しましょう。

resource "mysql_database" "zoo" {
}
$ terraform import mysql_database.zoo zoo
mysql_database.zoo: Importing from ID "zoo"...
mysql_database.zoo: Import prepared!
Prepared mysql_database for import
mysql_database.zoo: Refreshing state... [id=zoo]

Import successful!

The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.

Import 出来たので zoo を Terraform で管理できるようになりました。 terraform plan を実行してみます。

$ terraform plan

Error: Missing required argument

on main.tf line 19, in resource "mysql_database" "zoo":
19: resource "mysql_database" "zoo" {

The argument "name" is required, but no definition was found.

zoo の設定が空で name が設定されていないので失敗しました。 修正します。

resource "mysql_database" "zoo" {
name = "zoo"
}
$ terraform plan
Terraform will perform the following actions:

# mysql_database.zoo will be updated in-place
~ resource "mysql_database" "zoo" {
~ default_character_set = "latin1" -> "utf8"
~ default_collation = "latin1_swedish_ci" -> "utf8_general_ci"
id = "zoo"
name = "zoo"
}

Plan: 0 to add, 1 to change, 0 to destroy.

まだ差分が出てしまいました。

Import は State にはリソースのデータを反映してくれますが、設定ファイルには反映してくれないので自分で反映させる必要があります。 一部の Provider では、設定ファイルに自動で反映させるためのサードパーティのツールもあります。

修正します。

resource "mysql_database" "zoo" {
name = "zoo"
default_character_set = "latin1"
default_collation = "latin1_swedish_ci"
}
$ terraform plan
No changes. Infrastructure is up-to-date.

無事差分がなくなりました。

リソースの削除

次にリソースを削除してみます。

terraform destroy もありますが、今回は設定をコメントアウトします。 ちなみに Terraform の設定ファイルの記述言語である HCL ではコメントアウトは # でも // でもどちらでも良いです。

https://github.com/hashicorp/hcl#syntax

# resource "mysql_database" "bar" {
# name = "bar"
# default_character_set = mysql_database.foo.default_character_set
# default_collation = "utf8mb4_general_ci"
# }
$ terraform plan
Terraform will perform the following actions:

# mysql_database.bar will be destroyed
- resource "mysql_database" "bar" {
- default_character_set = "utf8mb4" -> null
- default_collation = "utf8mb4_general_ci" -> null
- id = "bar" -> null
- name = "bar" -> null
}

Plan: 0 to add, 0 to change, 1 to destroy.
$ terraform apply --auto-approve
mysql_database.foo: Refreshing state... [id=foo2]
mysql_database.bar: Refreshing state... [id=bar]
mysql_database.bar: Destroying... [id=bar]
mysql_database.bar: Destruction complete after 0s

Apply complete! Resources: 0 added, 0 changed, 1 destroyed.
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| foo2 |
| mysql |
| performance_schema |
| sys |
| zoo |
+--------------------+
6 rows in set (0.00 sec)

確かに削除されています。

terraform state mv

https://www.terraform.io/docs/commands/state/mv.html

ここで リソースの名前を変えて terraform plan してみましょう。名前を変えたくなることはまぁあると思います。

resource "mysql_database" "foo2" { # foo から foo2 に変更
name = "foo2"
default_character_set = var.default_character_set
default_collation = "utf8mb4_general_ci"
}
$ terraform plan
Terraform will perform the following actions:

# mysql_database.foo will be destroyed
- resource "mysql_database" "foo" {
- default_character_set = "utf8mb4" -> null
- default_collation = "utf8mb4_general_ci" -> null
- id = "foo2" -> null
- name = "foo2" -> null
}

# mysql_database.foo2 will be created
+ resource "mysql_database" "foo2" {
+ default_character_set = "utf8mb4"
+ default_collation = "utf8mb4_general_ci"
+ id = (known after apply)
+ name = "foo2"
}

Plan: 1 to add, 0 to change, 1 to destroy.

既存のデータベースが削除されて新しいデータベースが作成されてしまいそうです。 State に記録されているリソースのパスを修正する必要があります。

$ terraform state mv mysql_database.foo mysql_database.foo2
Move "mysql_database.foo" to "mysql_database.foo2"
Successfully moved 1 object(s).
$ terraform plan
No changes. Infrastructure is up-to-date.

差分がなくなりました。

このようにコマンドによって State を更新する必要があるので、特に複数人で作業する場合は安易にリソースパスを変更するべきではありません。

terraform refresh

https://www.terraform.io/docs/commands/refresh.html

terraform refresh は実際のインフラの情報を取得して State に反映させるコマンドです。 Terraform を使わずに加えた変更を State に反映させるのに使えます。 Terraform を使ってインフラを管理している以上、 Terraform を使わずにインフラを変更するのは望ましくないですが、実際のところはよくある話かと思います。

Terraform を使わずに default_character_set を変更してみましょう。

mysql> ALTER DATABASE zoo DEFAULT CHARACTER SET utf8mb4;
Query OK, 1 row affected (0.01 sec)

mysql> select * from INFORMATION_SCHEMA.SCHEMATA where SCHEMA_NAME='zoo';
+--------------+-------------+----------------------------+------------------------+----------+
| CATALOG_NAME | SCHEMA_NAME | DEFAULT_CHARACTER_SET_NAME | DEFAULT_COLLATION_NAME | SQL_PATH |
+--------------+-------------+----------------------------+------------------------+----------+
| def | zoo | utf8mb4 | utf8mb4_general_ci | NULL |
+--------------+-------------+----------------------------+------------------------+----------+
1 row in set (0.00 sec)

default_character_set を変えると自動で DEFAULT_COLLATION_NAME も変わりました。 設定ファイルにも変更を反映させましょう。

resource "mysql_database" "zoo" {
name = "zoo"
default_character_set = "utf8mb4"
default_collation = "utf8mb4_general_ci"
}

-refresh=false をつけて terraform plan を実行してみます。

$ terraform plan -refresh=false
Terraform will perform the following actions:

# mysql_database.zoo will be updated in-place
~ resource "mysql_database" "zoo" {
~ default_character_set = "latin1" -> "utf8mb4"
~ default_collation = "latin1_swedish_ci" -> "utf8mb4_general_ci"
id = "zoo"
name = "zoo"
}

Plan: 0 to add, 1 to change, 0 to destroy.

Warning: Resource targeting is in effect

You are creating a plan with the -target option, which means that the result
of this plan may not represent all of the changes requested by the current
configuration.

The -target option is not for routine use, and is provided only for
exceptional situations such as recovering from errors or mistakes, or when
Terraform specifically suggests to use it as part of an error message.

差分が出てしまいました。これは State が更新されていないからです。 terraform.tfstate を確認してみましょう。

    {
"mode": "managed",
"type": "mysql_database",
"name": "zoo",
"provider": "provider.mysql",
"instances": [
{
"schema_version": 0,
"attributes": {
"default_character_set": "latin1",
"default_collation": "latin1_swedish_ci",
"id": "zoo",
"name": "zoo"
},
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ=="
}
]
}

-refresh=false を取ると今度は差分がなくなるはずです。

$ terraform plan
No changes. Infrastructure is up-to-date.

これは、実は terraform plan はデフォルトでは State の内容と設定ファイルを比較する前に実際のインフラの情報を取得し State の内容をインメモリで更新した上で設定ファイルと比較しているからです。 ただし、インメモリでの更新であり、実際の State が更新されているわけではないです。

今までスルーしてきましたが、そのことは terraform plan の結果でも説明されています。

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

ではなんで -refresh=false が必要になるかというと、理由の一つとして terraform plan が速くなることが挙げられます。 むしろ -refresh=false をつけないと、管理するリソースの数が増えれば増えるほど情報を取得してくるのに時間がかかるようになりますし、 場合によっては API の rate limit に引っかかるなんてこともあるかもしれません。

なので普段 -refresh=false をつけて terraform plan を実行するようにしていると Terraform を使わずに加えたインフラの修正を State に取り込むために terraform refresh を実行する必要が出てきたりします。

State は更新されていないので -refresh=false とすれば相変わらず差分は出ます。

$ terraform plan -refresh=false
Terraform will perform the following actions:

# mysql_database.zoo will be updated in-place
~ resource "mysql_database" "zoo" {
~ default_character_set = "utf8mb4" -> "latin1"
~ default_collation = "utf8mb4_general_ci" -> "latin1_swedish_ci"
id = "zoo"
name = "zoo"
}

Plan: 0 to add, 1 to change, 0 to destroy.

差分が出ました。 terraform refresh を実行すると State が更新され、差分がなくなるはずです。

$ terraform refresh
module.app.mysql_database.db: Refreshing state... [id=app]
mysql_database.foo2: Refreshing state... [id=foo2]
mysql_database.zoo: Refreshing state... [id=zoo]
    {
"mode": "managed",
"type": "mysql_database",
"name": "zoo",
"provider": "provider.mysql",
"instances": [
{
"schema_version": 0,
"attributes": {
"default_character_set": "utf8mb4",
"default_collation": "utf8mb4_general_ci",
"id": "zoo",
"name": "zoo"
},
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ=="
}
]
}
$ terraform plan -refresh=false
No changes. Infrastructure is up-to-date.

Module

https://www.terraform.io/docs/configuration/modules.html

簡単なモジュールを作ってみましょう。

$ mkdir database
vi database/database.tf
resource "mysql_database" "db" {
name = "app"
default_character_set = "utf8mb4"
default_collation = "utf8mb4_general_ci"
}

では Module を使って database を作ってみます。

module "app" {
source = "./database"
}
$ terraform plan
Error: Module not installed

on main.tf line 25:
25: module "app" {

This module is not yet installed. Run "terraform init" to install all modules
required by this configuration.

失敗しました。 terraform init する必要があります。

$ terraform init
$ terraform plan
Terraform will perform the following actions:

# module.app.mysql_database.db will be created
+ resource "mysql_database" "db" {
+ default_character_set = "utf8mb4"
+ default_collation = "utf8mb4_general_ci"
+ id = (known after apply)
+ name = "app"
}

Plan: 1 to add, 0 to change, 0 to destroy.
$ terraform apply --auto-approve
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Module パラメータ

これで Module を使って Database を作れましたが、 name が "app" 固定なので使いづらいですね。名前を変えられるようにしましょう。

database/database.tf

resource "mysql_database" "db" {
name = var.name
default_character_set = "utf8mb4"
default_collation = "utf8mb4_general_ci"
}

database/variables.tf

variable "name" {
type = string
}
$ terraform plan

Error: Missing required argument

on main.tf line 25, in module "app":
25: module "app" {

The argument "name" is required, but no definition was found.

必須パラメータを指定していないので失敗しました。デフォルト値を設定しましょう。

https://www.terraform.io/docs/configuration/variables.html

variable "name" {
type = string
default = "app"
}
$ terraform plan
No changes. Infrastructure is up-to-date.

パラメータを渡して name を変更しましょう。

module "app" {
source = "./database"
name = "app2"
}
$ terraform plan
Terraform will perform the following actions:

# module.app.mysql_database.db must be replaced
-/+ resource "mysql_database" "db" {
default_character_set = "utf8mb4"
default_collation = "utf8mb4_general_ci"
~ id = "app" -> (known after apply)
~ name = "app" -> "app2" # forces replacement
}

パラメータを渡せました。もとに戻しておきます。

module "app" {
source = "./database"
name = "app"
}

Module Output

https://www.terraform.io/docs/configuration/outputs.html

mysql_database.bar から mysql_database.foo の属性を参照したように、 module.app で作成したデータベースの属性を参照するにはどうしたらよいでしょうか? モジュール内であれば普通に参照できますが、モジュールの外から参照するには、 Module 側で明示的に公開する必要があります。不便な側面もあるかもしれませんが、カプセル化とも言えますね。

default_character_set を公開しましょう。

database/output.tf

output "default_character_set" {
value = mysql_database.db.default_character_set
}

そして参照します。

main.tf

resource "mysql_database" "zoo" {
name = "zoo"
default_character_set = module.app.default_character_set # 参照
default_collation = "utf8mb4_general_ci"
}
$ terraform plan
No changes. Infrastructure is up-to-date.

terraform destroy

https://www.terraform.io/docs/commands/destroy.html

ここまでお疲れさまでした。 ハンズオンの最後にこれまで作ったデータベースを削除してしまいましょう。 -target オプションでリソースパスを指定して特定のリソースだけ削除できます。 -target オプションは複数回指定することで複数のリソースを指定できます。

$ terraform destroy -target=module.app

Terraform will perform the following actions:

# mysql_database.zoo will be destroyed
- resource "mysql_database" "zoo" {
- default_character_set = "utf8mb4" -> null
- default_collation = "utf8mb4_general_ci" -> null
- id = "zoo" -> null
- name = "zoo" -> null
}

# module.app.mysql_database.db will be destroyed
- resource "mysql_database" "db" {
- default_character_set = "utf8mb4" -> null
- default_collation = "utf8mb4_general_ci" -> null
- id = "app" -> null
- name = "app" -> null
}

Plan: 0 to add, 0 to change, 2 to destroy.

Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.

Enter a value: yes

module.app を消そうとしたら、 mysql_database.zoo も削除されそうになっています。 これは mysql_database.zoo が module.app の属性に依存しているからです。

resource "mysql_database" "zoo" {
name = "zoo"
default_character_set = module.app.default_character_set # module.app に依存
default_collation = "utf8mb4_general_ci"
}

-target オプションで mysql_database.zoo を指定すればそれだけ削除されます。

$ terraform destroy -target=mysql_database.zoo
Terraform will perform the following actions:

# mysql_database.zoo will be destroyed
- resource "mysql_database" "zoo" {
- default_character_set = "utf8mb4" -> null
- default_collation = "utf8mb4_general_ci" -> null
- id = "zoo" -> null
- name = "zoo" -> null
}

Plan: 0 to add, 0 to change, 1 to destroy.

Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.

Enter a value:

確認されるので、 yes と入力して削除します。

  Enter a value: yes

mysql_database.zoo: Destroying... [id=zoo]
mysql_database.zoo: Destruction complete after 0s

Warning: Applied changes may be incomplete

The plan was created with the -target option in effect, so some changes
requested in the configuration may have been ignored and the output values may
not be fully updated. Run the following command to verify that no other
changes are pending:
terraform plan

Note that the -target option is not suitable for routine use, and is provided
only for exceptional situations such as recovering from errors or mistakes, or
when Terraform specifically suggests to use it as part of an error message.


Destroy complete! Resources: 1 destroyed.
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| app |
| foo2 |
| mysql |
| performance_schema |
| sys |
+--------------------+
6 rows in set (0.00 sec)

確かに消えています。 設定ファイルからは消えていないので terraform plan を実行すると Create しようとします。

$ terraform plan
Terraform will perform the following actions:

# mysql_database.zoo will be created
+ resource "mysql_database" "zoo" {
+ default_character_set = "utf8mb4"
+ default_collation = "utf8mb4_general_ci"
+ id = (known after apply)
+ name = "zoo"
}

Plan: 1 to add, 0 to change, 0 to destroy.

リソースパスを指定しないと全てのリソースを削除します。

$ terraform destroy --auto-approve
module.app.mysql_database.db: Refreshing state... [id=app]
mysql_database.foo2: Refreshing state... [id=foo2]
mysql_database.foo2: Destroying... [id=foo2]
module.app.mysql_database.db: Destroying... [id=app]
module.app.mysql_database.db: Destruction complete after 0s
mysql_database.foo2: Destruction complete after 0s

Destroy complete! Resources: 2 destroyed.
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
4 rows in set (0.00 sec)

全部消えました。

· 26 min read
Shunsuke Suzuki

参考

手を動かしたい方は Terraform ハンズオン with MySQL Provider も参考にしてください。

前提

  • 執筆時点 (2020/01/05) で Terraform の最新バージョンは v0.12.18 です

Terraform とは

Terraform は Infrastructure as Code を実現する汎用的なCLIツールです。 インフラの状態を設定ファイルに定義し、コマンドを実行することで、 実際のインフラの状態と設定ファイルの差分を検知し、設定ファイルに記述されたとおりになるようにインフラを変更(CRUD)します。

Hashicorp という企業がホストしている OSS になります。 Go で書かれています。 https://github.com/hashicorp/terraform

Terraform のインストール

Terraform は Go 製なので 1 バイナリをダウンロードしてインストールするだけです。

https://www.terraform.io/downloads.html

tfenv を使うと管理が楽です。

https://github.com/tfutils/tfenv

tfenv は Terraform のバージョン管理ツールです。 pyenv や rbenv の Terraform 版みたいなものです。

用語集

https://www.terraform.io/docs/glossary.html

Provider

https://www.terraform.io/docs/glossary.html#terraform-provider

Terraform を「汎用的な」ツールといいましたが、ここでいう「汎用的」とは、 AWS などの特定のサービス専用ではなく、様々なサービスを同じように扱うことが出来るという意味です。 実際に AWS などのインフラを操作する場合にはインフラが提供する API を利用するわけですが、 サービス固有の API 呼び出しなどの処理を Provider という形で Terraform 本体から切り出すことでこの汎用性は実現されています。 Provider は AWS や GCP などの Hashicorp 公式のものもあれば、サードパーティ製のものもありますし、自分で作ってしまうことも出来ます。

少し突っ込んだ話をすると、 Terraform 本体同様 Provider も実態は Golang で書かれた1バイナリであり、本体から Provider を RPC によって呼び出すという形でプラグイン機構を実現しています。 https://github.com/hashicorp/go-plugin

余談: Provider の自作

Provider の自作に興味のある方は https://www.terraform.io/docs/extend/writing-custom-providers.html を読むと良いでしょう。 ただし、それを読めば全てわかるということはなく、作りながら Terraform 本体や AWSなどの公式の Provider のソースコードや GoDoc を読みつつ試行錯誤することになると思います。

リソース

https://www.terraform.io/docs/glossary.html#resource

Terraform ではインフラを構成する最小の構成要素を リソース と呼びます。 例えばサーバが 3 台あればそれぞれが別々のリソースと言えます。 リソースごとに設定を記述します。 terraform apply コマンドを実行すると各リソースについて設定と実際のインフラの状態の差分が検知され、リソースごとに CRUD 処理が実行されます。

設定ファイル

設定ファイルは HCL という言語で記述します。 HCL は Hashicorp Configuration Language の略で、 Hashicorp が開発している JSON や YAML のような設定を記述する言語です。

Terraform v0.12 では HCL の v2 を使います。 v1 は古いので注意してください。

設定ファイルの拡張子は基本 .tf です。 次のような形でリソースを定義します。

# resource "リソースの種類" "リソース名"
resource "aws_instance" "example" {
# 属性名 = 属性値
ami = "ami-2757f631"
instance_type = "t2.micro"
}

余談ですが、 GFM (GitHub Flavored Markdown) では hcl でシンタックスハイライトができます。

各リソース定義はパスによって識別されます。 上記のようなシンプルなリソースの定義の場合 リソースの種類.リソース名 (aws_instance.example) です。パスは一意でなければいけません。

設定ファイルは同じ1つのディレクトリに置きます。 terraform plan などのコマンドはカレントディレクトリ直下のファイルしか見ません。

リソースの属性はリソースの種類によって異なります。 各Provider のドキュメントを参照しましょう。

例えば AWS の EC2 instance のドキュメントはこちらです。 https://www.terraform.io/docs/providers/aws/r/instance.html

リソースの属性の参照

あるリソースの設定で他のリソースの属性値を参照することが出来ます。

サンプルを示します。

resource "aws_iam_instance_profile" "example" {
# リソース aws_iam_role.example の属性 name の値を参照
role = aws_iam_role.example.name
}

State

https://www.terraform.io/docs/state/index.html

設定ファイルと実際のインフラの差分を検知するには、設定ファイルのリソース定義と実際のインフラのマッピングが必要になります。 設定ファイル上ではリソースはパスによって識別されますが、実際のインフラのリソースはインフラ固有の ID などで識別されます。 設定ファイル上のリソースのパスと、実際のインフラのリソースの ID とのマッピングを保存するストレージが State になります。 ただし State の役割はマッピングだけではありません。詳細は https://www.terraform.io/docs/state/purpose.html を参照してください。 「ストレージ」といいましたがその実態はデフォルトではただの JSON ファイルです。 デフォルトでは terraform apply などを実行すると勝手に terraform.tfstate という名前のファイルがカレントディレクトリに作成、更新されます。 State の保存先は S3 を含め色々サポートされています。

https://www.terraform.io/docs/backends/types/index.html

発展: State を持たないツールとの違い

Terraform の他にもインフラを管理するツールはありますが、その中には State のようなマッピングを持たないものもあります。 マッピングを持たないツールだと、ツールを使わずに作ったリソースがツールによって削除される可能性があります。 一方、 Terraform ではツールを使わずに作ったリソースは変更の対象になりません。 変更の対象にしたい場合はそのリソースの情報を State に追加する必要があります。 ただの JSON ファイルなので手で修正することも出来ますが、 terraform import という専用のコマンドがあるのでそれを使うほうが安全です。

https://www.terraform.io/docs/import/index.html

逆に、あまりないケースではありますが、あるリソースについて Terraform で管理するのをやめたい場合、 そのリソースを設定ファイルだけでなく State からも削除する必要があります。 単純に設定ファイルから削除するだけだと、 Terraform によって実際のインフラのリソースが削除されてしまいます。 State から削除するには terraform state rm というコマンドを使います。

その他 State を管理するためのコマンドが色々あるのでドキュメントを参照してください。

https://www.terraform.io/docs/commands/state/index.html

発展: Remote State を使うか否か

Terraform を CI/CD によって実行する前提です。 基本的に remote state を使うべきだと思います。

remote state を使わない場合、 terraform.tfstate を Git で管理することになるでしょう。 その場合、 feature branch の terraform.tfstate と実際のインフラの状態の乖離が起こりえます。 チームの規模が大きく複数の開発が並行して行われれば行われるほど、乖離の弊害が大きくなり、 remote state を使ったほうが良いということになるでしょう。

また、 CI/CD で更新された terraform.tfstate を commit & push する必要があります。 terraform apply で失敗した場合、一部のリソースの更新には成功し、State が更新されているかもしれません。 terraform apply が失敗しても即座に終了させずに terraform.tfstate を commit & push する必要があります。 これは CI/CD のコードで気をつければ問題ないのでデメリットと言うほどではないですが、昔自分は何度か commit & push し損ねて面倒くさいことになりました。

変数

設定ファイルでは変数が使えます。 変数を使うには宣言が必要です。宣言では型なども指定できます。

https://www.terraform.io/docs/configuration/types.html

変数の宣言はつぎのようにします。 .tf のどこに書いても大丈夫です。

# variable "変数名"
variable "region" {
type = "string"
default = "us-east-1"
}

変数の値の設定は次のようにします。 terraform.tfvars というファイル名に書いておくとコマンド実行時に自動で読み込まれます。

ami = {
"us-east-1" = "ami-abc123"
"us-west-2" = "ami-def456"
}

コマンドライン引数で渡すことも出来ます。

Provider の設定

Provider を使うには設定が必要です。 設定の属性は Provider によって違います。

# provider "Provider名"
provider "google" {
project = "acme-app"
region = "us-central1"
}

Module

https://www.terraform.io/docs/modules/index.html https://www.terraform.io/docs/configuration/modules.html

複数のリソースの設定をまとめて再利用可能な形でパッケージングする仕組みとして Module があります。 リソースが1つだけでも、チーム固有の設定を Module のデフォルト値として設定したり、チームでは使わないリソースの属性を隠蔽したり用途はあるかと思います。

Module の作り方は通常の Terraform の設定ファイルの記述と同様、 1つのディレクトリ直下にパッケージングするリソースの設定ファイルを記述するだけです。

Module は次のように使います。

# module "名前"
module "servers" {
# モジュールへのパス
source = "./app-cluster"

# モジュールのパラメータ
servers = 5
}

Module のパスは module.モジュール名 (module.servers) になります。

Module の source としてはローカルのディレクトリへのパス以外にも様々なものをサポートしています。

https://www.terraform.io/docs/modules/sources.html

コミュニティの Module

コミュニティによって様々な Module が提供されています。

Module 化するべきか否か

Go や Python などのプログラミング言語でのモジュール(ライブラリやパッケージ等呼び方は様々ですが)化と比べ、 Terraform の Module 化は慎重でなければなりません。 理由はいくつかありますが、

  • Module を変更すると State の変更も必要になる場合もある
  • Terraform の設定は中々パワフルだとは思いますが、プログラミング言語に比べると柔軟性が足らず、変更に弱く、複雑になるとメンテナンス性が悪くなります

偏見かもしれませんが、プログラミング言語に比べると、 Terraform に「精通」している人はそれほど多くないと思います。 初心者は直ぐ Module 化に飛びつくのはやめた方が良いと思います(尤も使ってみないと理解が深まらないという意味では使ったほうが良いですが)。

Output

https://www.terraform.io/docs/configuration/outputs.html

Module で定義したリソースの属性は基本的に外部から隠蔽されます。 リソースの属性を参照できるようにするには、次のように個別に Output として宣言する必要があります。

output "instance_ip_addr" {
value = aws_instance.server.private_ip
}

これはモジュールで定義したリソース aws_instance.server の属性 private_ip を Module の属性 instance_ip_addr として外部に公開するという意味です。 Module のパスが module.servers だとすると、 module.servers.instance_ip_addr で参照できます。

Data Source

https://www.terraform.io/docs/configuration/data-sources.html

Data source は Terraform で管理していない(あるいは他の State で管理している)リソースの属性を参照するための仕組みです。

次のように記述します。

# data "リソースの種類" "リソース名"
data "aws_ami" "example" {
# リソースを一意に識別するためのクエリ

most_recent = true

owners = ["self"]
tags = {
Name = "app-server"
Tested = "true"
}
}

Data source では属性によって実際のインフラのリソースを検索します。検索にマッチするリソースは必ず1つでなければならず、 複数マッチしたり1つもマッチしなかったりすると失敗します(厳密には Provider の実装次第ですが)。

他の State で管理されているリソースの属性を Data source として参照する

https://www.terraform.io/docs/providers/terraform/d/remote_state.html

他の State で管理されているリソースの属性を Data source を使って参照するために terraform_remote_state があります。 参照するには Module 同様 その属性が Output によって外部に公開されている必要があります。

data "terraform_remote_state" "vpc" {
backend = "remote"

config = {
organization = "hashicorp"
workspaces = {
name = "vpc-prod"
}
}
}

workspace

Terraform には workspace という機能がありますが、こちらの機能は自分は使ったことがないので簡単に触れるだけに留めます。 workspace の代表的なユースケースは production や staging などの異なる環境で同じ設定ファイルを共有しつつ State を switch することだと思います。

workspace を使わない場合環境ごとにディレクトリ及び設定ファイルを完全に分けることになると思います。

production/
ec2.tf
terraform.tfstate
...
staging/
ec2.tf
terraform.tfstate
...

workspace を使うと設定ファイルを共有できます。

ec2.tf
terraform.tfstate.d/
production/
terraform.tfstate
staging/
terraform.tfstate

環境ごとの設定の微妙な違いは設定ファイル内で分岐することになるのでしょう。

発展: workspace を使うべきか

workspace を使うべきか否かは意見が分かれている様に思えます。 恐らくユースケースやチームメンバーの Terraform への成熟度にもよるでしょう。

現状自分は「使わない」というスタンスです。

自分がこれまで関わってきたチーム事情だと Terraform に全員が精通しているというよりは、むしろ Terraform にそこまで詳しくない人も触ることが多いです。 偏見かもしれませんが、そういうチームは少なくないのではないでしょうか? そういうチーム状況では、 workspace を使って DRY になるというメリット以上に、 workspace そのものへの学習コストや環境によって設定ファイル内で分岐する学習コストを省き、 Terraform に精通していなくても理解できるくらいシンプルに保つことのほうが大事なのではないかなと思います。

特に、これまで Terraform を特定のサービス横断的なチームで管理していた状態から各サービスの担当者に ownership を委譲しようとする場合、上述の学習コスト・複雑さが弊害になるのではないかなと感じています。

ただし、繰り返しになりますが自分は workspace を使ったことがありません。 上記の自分の認識が間違っているかもしれませんし、今後 workspace を使ってみたら考えが変わるかもしれません。

Terraform command の基本的な使い方

https://www.terraform.io/docs/commands/index.html

設定ファイルを書いた上で基本的なコマンドの使い方について説明します。

terraform init

https://www.terraform.io/docs/commands/init.html

まず、 terraform init を実行する必要があります。

$ terraform init

terraform init によって、依存する Provider がインストールされたりします。

terraform fmt

https://www.terraform.io/docs/commands/fmt.html

terraform fmt によってコードを整形することが出来ます。

$ terraform fmt [-check] [-recursive]

-check をつけると、コードを整形する代わりに、コードが整形されていなかったら exit code が non 0 で終了します。 CI でコードが整形されているかのチェックに使えます。

terraform plan

https://www.terraform.io/docs/commands/plan.html

terraform plan によって terraform apply の dry run が出来ます。

$ terraform plan [-refresh=false]

実際のインフラは変更されず、 apply を実行した場合にどのような変更が行われるか出力されます。

terraform apply

https://www.terraform.io/docs/commands/apply.html

実際にインフラを設定ファイルに合わせて変更します。

$ terraform apply [-auto-approve]

デフォルトでは最初に plan の実行結果が出力されて本当に変更を適用してよいか確認があります。 -auto-approve を指定すると確認を skip 出来ます。

plan に成功しても apply には失敗する場合もあります。 簡単な例としては更新する権限がない場合です。 apply に失敗すると一部のリソースだけ更新されるということは起こりえます。 そうなってもロールバックとかは出来ない(terraform に rollback の機能はない)ので注意してください。

terraform destroy

https://www.terraform.io/docs/commands/destroy.html

Terraform で作成したインフラを削除します。

$ terraform destroy

恐らく実際の運用でこれを使うことはあまりないと思います。 削除したいなら設定ファイルからそのリソースを消して apply するでしょう。 Terraform の勉強がてら遊びで作ったものを丸っと消すとか主にそういう用途で使われる気がします。

terraform refresh

https://www.terraform.io/docs/commands/refresh.html

State を実際のインフラの状態に合わせて更新します。 インフラは更新されません。 手動でインフラを変更した場合に、その変更を State に反映させるのに使えます。

$ terraform refresh

terraform import

https://www.terraform.io/docs/commands/import.html https://www.terraform.io/docs/import/index.html

Terraform で管理されていないリソースのデータを State にインポートします。 State はリソースパスとインフラのID のマッピングを管理すると言いましたが、 import コマンドの引数ではこの2つを渡すことでマッピングできるようにします。

# リソースパス ID
$ terraform import aws_instance.example i-abcd1234

ちなみに、一部のリソースは import をサポートしてません。 サポートしているかどうかは Provider の実装に依存します。

terraform state

https://www.terraform.io/docs/commands/state/index.html

State を操作するためのコマンドです。様々なサブコマンドがあります。

Terraform の CI/CD

terraform apply は原則 CD によって実行されるべきだし、 CI によって terraform fmt -checkterraform plan は実行されるべきだと思っています。

逆に terraform planapply は CI/CD でやってるけど、 terraform stateterraform import はローカルから実行しているというチームも少なくはないのかなという気がしています。

課題となりうるのは

  • Credential の管理
  • IP制限

かなと思います。

Credential の管理

Terraform で使うクレデンシャルは強力な権限を持ちがちなので扱いに注意しないといけません。 権限を絞るのが理想ですが、結構難しかったりします。

例えば PR の CI では terraform apply を実行しないのであれば、 PR の CI 用に Read Only なクレデンシャルを用意するとかもありかもしれません。

例えば AWS のインフラ管理を Terraform + CircleCI で行う場合、 AWS のクレデンシャルを CircleCI で参照できるようにする必要があります。 CircleCI では SSH でコンテナにログインできるため、クレデンシャルを盗もうと盗めますし、 CircleCI に限らず悪意のあるコードを CI で実行して外部にクレデンシャルを送ることも出来ます。

IP 制限

CI で Terraform を使う場合、 CI の実行環境からインフラの API にアクセスできる必要があります。 IP 制限をかけている場合、 CI の実行環境からはアクセスできるようにするなどの工夫が必要です。 CI の実行環境の IP range が定まってない場合、話は更に難しくなります。

発展: 設定ファイル中で使える関数

設定ファイル内ではビルドイン関数が使えます。 公式ドキュメントを参照してください。

https://www.terraform.io/docs/configuration/functions.html

余談: awesome-terraform

https://github.com/shuaibiyy/awesome-terraform

Terraform 関連の awesome なツールのリンク集です。

· 6 min read
Shunsuke Suzuki

Terraform の State Locking という機能の概要について説明します。 ただし、自分もちゃんと理解しているわけではないので、推測も混じります。 基本的には公式ドキュメントに書いてある内容なのでそちらをご参照ください。

State Locking とは

terraform plan などのコマンドは State を変更する場合があります。 その処理は atomic ではないため、同時に複数のコマンドが State を書き換えようとすると不整合が生じる可能性があります。


例えば S3 backend の state を state rm で更新する場合を考えます。 これはコマンド内部で

  1. 現在の State を取得する (READ)
  2. 修正した State を S3 に push する (WRITE)

という処理を行っているはずであり、複数のコマンドを実行した場合、READ と WRITE の間に他のコマンドによって WRITE されると、その WRITE による変更が消えてしまいます。


そこで State Locking を使うと各コマンドで State を変更する前に lock を取り、WRITE 後に lock を解除します。

コマンドラインオプション

plan, apply, refresh, state rm, state mv, state push には次のようなオプションがあります。

-lock=true          Lock the state file when locking is supported.
-lock-timeout=0s Duration to retry a state lock.

-lock はデフォルトで true なので State Locking のことを知らなくても実は State Locking 使ってたということもありえますが、 Backend type によっては State Locking のための設定をしていないと State Locking が無効になっている可能性があります。

例えば S3 backend で State Locking をするには DynamoDB が必要であり、 DynamoDB の設定 dynamodb_table が設定されていないと State Locking は無効になります。

また、-lock=false で無効化できますが、公式的に非推奨になります。

-lock-timeout は lock の取得に失敗した場合に何秒後にリトライするかの設定になります。

force-unlock

lock の解放に失敗した場合のために、 force-unlock というコマンドがあります。 何らかのトラブルで lock が解放されない場合に使います。

$ terraform force-unlock LOCK_ID

例えば plan を実行中にキャンセルすると lock が解放されないことがあるようです。

lock が解放されていない状態で plan などを実行すると lock の取得に失敗し、次のようなエラーが起こります。

Acquiring state lock. This may take a few moments...
Error: Error locking state: Error acquiring the state lock: ConditionalCheckFailedException: The conditional request failed
status code: 400, request id: xxx
Lock Info:
ID: xxx
Path: terraform.tfstate
Operation: OperationTypePlan
Who: xxx
Version: 0.12.13
Created: 2020-01-09 09:30:37.41120929 +0000 UTC
Info:
Terraform acquires a state lock to protect the state from being written
by multiple users at the same time. Please resolve the issue above and try
again. For most commands, you can disable locking with the "-lock=false"
flag, but this is not recommended.

ここで出力される ID を force-unlock の引数として指定します。

$ terraform force-unlock xxx
Do you really want to force-unlock?
Terraform will remove the lock on the remote state.
This will allow local Terraform commands to modify this state, even though it
may be still be in use. Only 'yes' will be accepted to confirm.
Enter a value: yes
Terraform state has been successfully unlocked!
The state has been unlocked, and Terraform commands should now be able to
obtain a new lock on the remote state.

S3 backend

State Locking をサポートしているかは Backend type によりますが、 S3 の場合、 DynamoDB を使えばできます。

https://www.terraform.io/docs/backends/types/s3.html#configuration-variables

backend の設定で dynamodb_table を設定する必要があります。

https://www.terraform.io/docs/backends/types/s3.html#dynamodb_table

dynamodb_table - (Optional) The name of a DynamoDB table to use for state locking and consistency. The table must have a primary key named LockID. If not present, locking will be disabled.

data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "terraform-state-prod"
key = "network/terraform.tfstate"
region = "us-east-1"

# state locking の設定
dynamodb_table = "???"
}
}

IAMの権限としては https://www.terraform.io/docs/backends/types/s3.html#dynamodb-table-permissions が必要です。

DynamoDB のテーブルには LockID という Primary Key が必要です。 型は 文字列 です。

そして State Locking を有効にした状態で plan などを実行すると DynamoDB のテーブルにレコードが State ごとに作られるようです。

State Locking をすれば安全、というわけではない

State Locking 自体は安全性に寄与する仕組みではありますが、 State Locking さえすれば安全かというとそうではないと思います。

複数人が同時に plan や apply などを実行する環境では、別のロック機構も必要だと思います。

詳細はまた別途書こうと思いますが、CI/CD で plan, apply などを実行する場合、 apply 実行中はそれが終わるまで他の plan や apply の実行を wait するような仕組みがないと危険です。

· 4 min read
Shunsuke Suzuki

コマンドの実行時間を Datadog に送る dd-time というツールを作りました。

このツールは circle-dd-bench にインスパイアされていますが、 CircleCI 以外でも需要あると思ったり、他にも幾つか改善したい部分があったので自作することにしました。

circle-dd-bench については circle-dd-bench の作者が書いたブログ https://blog.yuyat.jp/post/circle-dd-bench/ も参考にしてください。

dd-time は Go 製なので GitHub Releases からバイナリをダウンロードしてインストールすれば使えます。

使い方はシンプルで実行時間を計測したいコマンドの前に dd-time -- をつけるだけです。 例えば Docker image のビルドの時間を計測したい場合次のような感じになります。

$ dd-time -t command:docker-build -- docker build .

Datadog の API key を環境変数 DATADOG_API_KEY として設定する必要があります。 こうすると Datadog の Post timeseries points API を使い、command_execution_time というメトリックス名(変更可能)でコマンドの実行時間が送られます。

メトリックスの名前や host, tags はそれぞれ --metric-name (-m), --host, --tag (-t) で指定できます。 --tag は複数回指定可能で、 key:value というフォーマットで指定します。

CircleCI で実行した場合、 CircleCI のビルドイン環境変数が tag として勝手に設定されますが、 CircleCI 以外でも使えます。

dd-time を作る上で意識したことは、透過的にする(元のコードにほとんど影響を与えずに使えるようにする)ということです。 具体的には以下のような点です。

  • 標準入力をそのままコマンドに渡す
  • コマンドの標準出力・標準エラー出力をそのまま出力する
  • コマンドの exit code をそのまま dd-time の exit code とする
  • Datadog への送信に失敗しても dd-time の exit code は 0 とする (option で non zero にもできるようにするのもありだが、現状はそうしてない)
  • Datadog への送信に失敗した場合のエラーメッセージをファイルに吐き出せる(コマンドの出力と混ざらないようにできる)
    • デフォルトは標準エラー出力だが、 --output (-o)--append (-a) オプションで変更できる
    • --append を指定すると追記モードで出力できる
  • 適切にシグナルハンドリングする(本当に適切と言えるかは分かりませんが)

以上、簡単ですが dd-time の紹介でした。

· 15 min read
Shunsuke Suzuki

最近モブレビュー取り入れたいと感じていて、なんで取り入れたいかなどについて書いてみました。 モブレビュー自体まだ 2, 3 回しかやってないので説得力にかけますが、ご容赦ください。

想定

  • チームは 6 人以下
  • Pull Request (以下 PR) をマージするには必ず他の誰かが approve しないといけない

目的

  • チーム内の情報共有
    • 属人化の解消
    • 退職や異動などによる情報の喪失(誰も分からない状態)を防ぐ
    • チーム外の人とのコミュニケーションにも活用できる
  • レビュー待ちの短縮
  • レビューの品質の改善
  • 仕事を評価してもらうことで承認欲求を満たす
  • オンボーディングの改善
    • レビューを通じて必要な知識を吸収してもらう
    • オンボーディングに限ったことではないが、オンボーディングにも有効ではないか
  • 最後までやりきる

箇条書した内容を補足します。

情報共有

属人化の解消は結構重要だと自分は思っていて、特定の人じゃないと出来ないこと、わからないことというのは ボトルネックや技術的負債だったり、障害対応時に致命的になりかねません(深夜に障害が起こってAさんに聞かないとわからないのに、Aさんと連絡が取れないとか最悪)。 チーム全員とはいかなくても 3 人ぐらいは出来る・分かってる必要があるかなと思います。

まぁ、当初は 3 人ぐらい分かってても、時間が経って異動やら退職やらで気づいたら分かっている人いなくなってたというのは ありえなくないので、そういったリスクをどうやって防ぐかというのは考える必要がありますが、今回の話とは外れるので割愛します。

また、チーム外の人とのコミュニケーションにも活用できると思っていて、 例えばランチや飲み会でチーム外の人とのコミュニケーションを取る際に自分のチームの他の人が対応した件とかが話題に上がったときに ちゃんと情報共有できてないと、自分は担当外なので分かりませんとなってしまうでしょう。 知っていればむしろ自分から話題にできるかもしれません。 そこから発展して更に新しいタスクの話もできるかもしれません。

レビューの改善

せっかくいい仕事をしても中々レビューしてもらえないとなると不満がたまります。 モブレビューを実施することでレビューを促進し、レビュー待ちの時間を短くできることを期待します。 レビューしたくても良くわからなくてレビューできないというパターンもあると思うので、 わからない部分をモブレビューで解消されてレビューが進むと良いですね。

また、ちゃんと複数人でレビューすることで目先の問題を解決するだけの本質的でない問題解決を防ぐことが出来ることもあると思います。 「いや、それそもそもPRの前提がおかしくない?前提となっている仕様を見直すべきなのでは」 みたいなこともあるかもしれません。

仕事を評価してもらうことで承認欲求を満たす

いい仕事したら他の人にも知ってもらいたいというのは自然なことでしょう。 いい仕事を褒めるのは良いチーム・環境であり働きやすさというところにつながるのではないでしょうか。

ちなみに現職だと Slack で他のチームにも共有して emoji で褒め称えるというのが結構やられていて 気持ちのいい環境だなと思っています。

オンボーディングへの活用

そもそもオンボーディングというものをちゃんと受けたのが現職が初めてで、 現職でもオンボーディングについてはまだまだ検討中であり、「何をもってオンボーディングは終わりと言えるのか」みたいな議論が Slack でされてたりして、 自分もよくわかってないのですが、モブレビューがオンボーディングにも活用できるんじゃないかなという気がしています。

色々なにも分かってない状態の New Joiner でも理解できるように、背景とかを説明したりすることで 会社固有のドメイン知識などを補い、戦力化を促すことが出来るんじゃないかなと思います。 会社固有の略語などが当たり前に使われてたりすると New Joiner には理解できないのですが、そういうのも含めてフォローしてあげる良い機会になると思います。 マイクロサービス化なんかをやっていると New Joiner には名前を聞いただけでは理解できないものも出てきますしね。

まぁあまり細かな話までしだすとそれはまだ New Joiner には早いということにもなるので、ケースバイケースかもしれません。

最後までやりきる

PR を投げた後、レビューしてもらえない、説明を書いているのに読んでもらえないといったときに

  • 自分はやるべきことをやっている
  • レビューしない人たちが悪い
  • 説明を読まない人たちが悪い

としてそれ以上なにもしないというのはもったいないと思います。 「結果がすべて」だと考えると説明を書いても読んでもらえなかったらそれは書いてないと同じなので モブレビューという形で読んでもらう努力をするのがより建設的なのでしょう。 モブレビューしてみたら「レビューしようと思ったけど、ここが良くわからなくてやめちゃったんだよね」ということもあったので、 自分の説明も足りてなかったんだなと見直す良い機会にもなります。

解決したい課題

  • 他の人が何やってるのかよくわからない
  • 他の人のをレビューしようとしても良くわからなくてそっとタブを閉じてしまう
    • 分からないことを理解する成長チャンスを無駄にしているのでは
  • レビュー待ちが長い
    • 細かく PR を投げたくても前のがマージされてないと先に進めない
    • マージされないまま master が更新されて逐一 rebase しないといけない
  • レビューの品質の低下
    • よくわからないけどマージしてしまえ: レビューの意味がない
  • issue や PR の説明やコメントが足りてない
    • 説明を英語で書かないといけないとかだと、説明不足になりやすい
    • モブレビューで口頭で説明してみると、 issue や PR に書いてない情報が出てくる その情報を追記することで(口頭で説明するだけではだめ。後からその場にいなかった人も見返すもの)改善できる

実施方法

定例を組むのもありですが、個人的には予定を押さえなくても場所を確保できるのなら(フリースペースとかにモニターがあってサッと出来るなら理想的)不定期がいいと思います。 やりたくなったらその旨を Slack でサッと共有し、その場で直ぐできるならやり、 できない場合カレンダー見て空いてそうな枠を見つけて予定を押さえるか、Slackのリマインダーをセットします。 モブレビューで話している内容や、質疑応答などは Slack のスレッドにでもメモっておくと良さそうです。 issue や PR の説明に書いてないことが出てきたら、説明を修正して追記すると良いでしょう。

定期的に予定をいれる懸念点

  • 定例は少ないほうが良い
  • 形式的になる
  • 定例まで待たなくて良いのでは
  • 定例までレビューしないというモチベーションが働く場合もあるのでは

やりたくなったらやる場合の懸念点

  • カレンダーをチェックするのが面倒くさい(コストがかかる)
  • 人によっては躊躇してしまう
  • 場所を確保できないかも

懸念点

  • 複数人の時間を拘束して実施することのコスト
  • 口頭で説明することに甘えてちゃんと issue や PR に書かなくなる
    • レビューの段階で指摘する
  • 場所を押さえられるか
    • 会議室足りない問題
  • モブレビューの意義を共有できるか
    • 人によっては時間の無駄と思う人がいてもおかしくない
    • 不満や提案があったときにちゃんとそれを言える環境でありたい

ちゃんと文章化するのをめんどくさがって口頭での説明で済ましたがるようになったらよろしくないですね。

自分の反省点

ここまで書いて出てきた自分の反省点としては、 issue や PR の説明でちゃんと Context や Background をもっとちゃんと書かないと駄目かなということです。 口頭で説明する際にはそのへんを最初に意識して話すようにしていますが、説明には書いてないことが少なくないのでちゃんと書くようにしようと思いました。

むすび

今回モブレビューについてあれこれ考えてみました。 まだモブレビューは 2, 3 回やった程度なので、ちゃんと取り組んでみて PDCA 回して改善出来たら良いなと思います。 例えば、今の所定期ではやりたくないと思ってますが、やってみたらちゃんと定期じゃないと駄目だったということもあるかもしれませんし、 モブレビューは目的じゃなくて手段なので、他にもっとよい手段があってモブレビュー不要だったということもあるかもしれません。

やらないと始まらないのでやれたらいいかなと思います。

· 3 min read
Shunsuke Suzuki

作ったのは 2ヶ月くらい前の話ですが、 Go の command の timeout を実装するためのライブラリを作ったので紹介します。

https://github.com/suzuki-shunsuke/go-timeout

基本的には https://github.com/Songmu/timeout をオススメしますが、これだと上手くいかないパターンがあったので自作しました。

Go の command の timeout に関しては https://junkyard.song.mu/slides/gocon2019-spring/#24 がとても参考になります。

上記のスライドでは

  • 標準ライブラリの exec.CommandContext でも停止できるが、 SIGKILL で強制的に停止することになる
    • 子プロセスが停止しない
  • 公式見解 では、SIGKILL 以外は標準ライブラリではサポートしない。サードパーティでやればよい
  • Songmu/timeout 使えば SIGKILL 以外でより安全に停止できる

ということが丁寧に説明されています。

自分は cmdx という task runner を開発していてその中で task の実行時に timeout を設定出来るようにしました。 当初 Songmu/timeout を使って実装したのですが、問題があることに気づきました。 それは、 command の中で fzf を使うと、上手く動かないというものでした。

正直この辺の挙動はちゃんと理解できていないのですが、 調べてみると Songmu/timeout だと syscall.SysProcAttr の Setpgid を true に設定していて、そうすると fzf が上手く動かないようでした。

https://junkyard.song.mu/slides/gocon2019-spring/#48

https://junkyard.song.mu/slides/gocon2019-spring/#45

には timeout の実装方式として

  • GNU timeout の場合
  • Songmu timeout の場合

の 2 通り書いてありますが、 suzuki-shunsuke/go-timeout では GNU timeout のパターンで実装しています。

https://junkyard.song.mu/slides/gocon2019-spring/#46

に書いてあるとおり、少々乱暴な気もしますが、 cmdx で使う分には特に問題ない気がします。

· 6 min read
Shunsuke Suzuki

最近自作した OSS, cmdx の紹介です。

https://github.com/suzuki-shunsuke/cmdx

cmdx は task runner です。

task runner の定義はググってもわからなかったので、 cmdx を task runner と呼ぶのが適切かわかりませんが、 ここではプロジェクト固有のタスク

  • 依存するライブラリのインストール
  • ビルド
  • テスト
  • コード整形
  • lint
  • etc

などを管理するものとします。

類似するものとしては以下のようなものがあります。

使い方

詳細は README を読んでください。

$ cmdx -i

で設定ファイルの雛形を生成します。

そして設定ファイルに task を定義していきます。 設定に関しては README を参照してください。

そうすると cmdx -l でタスクの一覧とその説明が見れます。

例えば次は cmdx のリポジトリでの実行結果です。

$ cmdx -l
init, i - setup git hooks
coverage, c - test a package (fzf is required)
test, t - test
fmt - format the go code
vet, v - go vet
lint, l - lint the go code
release, r - release the new version
durl - check dead links (durl is required)
ci-local - run the Drone pipeline at localhost (drone-cli is required)

これにより新しくプロジェクトに参画した人もどのような task があるのか直ぐわかります。 例えば test を実行したければ cmdx t を実行すればいいことがわかります。 cmdx help test とすればここのタスクのより詳細なヘルプが見れます。

ドキュメントに task について書いても、ドキュメントがちゃんと更新されずドキュメントと実態が乖離するなんてことはよくありますが、 cmdx の設定ファイルからヘルプを生成することで乖離しにくくなります(実際に使われてない task が残ってたり、task の description や usage が間違ってたら駄目ですが)。

なぜ cmdx か

自分は今まで task runner として基本的に npm scripts を使ってきていて、ブログにも書いています。

JS以外でのnpmの活用

しかし、 npm scripts に対しては以下のような不満がありました。

  • security alert が定期的に飛んできて対応が面倒くさい
    • これは husky や commitlint などを使っているのが原因なのであって、 npm scripts の問題ではないですが
  • task に対するヘルプメッセージがない
    • 今までは README に書いてたが、本来は help コマンドで自動生成・サポートされるべきだと思っている

他のツールによってこれらの不満は解消できるのですが、他のツールにもそれぞれ微妙に不満があり、 完全に自分のニーズに合うものがなかったので作ることにしました。

一例ですが、 npm scripts は

  • 設定ファイル (package.json) を探索
  • 設定ファイルのあるディレクトリでコマンドを実行

します。これにより

  • カレントディレクトリを意識する必要がない
    • 設定ファイルのパスを指定する必要がない
    • コマンドの実行ディレクトリがカレントディレクトリに依存しない(逆に言うとカレントディレクトリに依存した処理を実行しにくいという面もありますが)

という良さがあり、 これが意外と他のツールではサポートされてなく(例えば Make だったら -F オプションで Makefile のパスを指定する必要がある)、不満でした。

また、 Make や Task では task の依存関係を定義し、一回のコマンドで複数のタスクを実行できますが、 cmdx ではそのような機能はサポートしていません。 自分が普段そのような機能をあまり必要としていないからです。

cmdx では上のような npm scripts の不満を解消するだけでなく、折角なので幾つか細かな機能を追加しています。

  • シェルスクリプトだと面倒なオプション引数をサポート
  • リッチなプロンプトのサポート
  • タイムアウト
  • etc

cmdx から npm scripts に乗り換えた場合の問題点としては husky や commitlint のようなツールが使えなくなることですが、 必須のツールでもないので許容しています。