ようへいの日々精進XP

よかろうもん

2018 年 09 月 12 日 (水)

ジョギング

日課

  • おやすみ

ビアガーデン

某勉強会の懇親会で久しぶりにビアガーデンに行った.

暑くもなく寒くもない丁度良いビアガーデン日和でお酒が進むつもりだったけど, 体調がイマイチなのかあまり呑めなかった.

今日のツイート

明日は JAWS-UG 福岡のもくもく会.

ビアガーデンの〆の一言を求められて...

2018 年 09 月 11 日 (火)

ジョギング

  • 香椎浜 x 2 周
  • 右足の違和感は少し和らいだ感がある

日課

  • (腕立て x 50 + 腹筋 x 50) x 3

9.11

ほとんど報道されていなかったけど, アメリ同時多発テロから 17 年が経った. この日を堺に世界のパワーバランスが崩れてしまったと考えている. まだ金町に住んでいて, 弟と同居していた頃だったなあ. 家でテレビを見ていた弟から「アメリカで大変なことが起きてるよ」という電話をもらったことを今も覚えている. その後, セブンイレブンでおでんを買って帰る途中に 4, 5 人の警察官に取り囲まれて職務質問されたことをこの日が来る度に思い出す.

テロによって命を落とされた方に対して心の中で合掌すると共に戦争も無い, 当然核兵器等も無い平和な世界になることを切に願うばかりである.

今日のツイート

同じ文章で三回くらい間違ってしまった. 疲れているのかな...

バーベキューには良い季節になってきたと思う.

今日のハイライトだった.

2018 年 09 月 09 日 (日)

ジョギング

  • お休み

日課

  • お休み

東京インテリア

  • 新居の家具を見繕いに東京インテリアに行ってきた
  • ソファもベッドの展示数も半端ない感じ, カフェもあったりして一日楽しめるよなあと思った
  • 某家具メーカーの営業女子と意気投合して奥さんと三人で女子のイチャラブ話で盛り上がってしまったけど元気を頂いた

さて

来週の火曜日は引っ越しであるが, 自分の準備の進捗はイマイチ

独り Terraform 研究所 (2) 〜 簡単な HTTP Backend を実装して Terraform の挙動を確認する 〜

どうも

独り Terraform 研究所, 所長兼研究員のかっぱです.

Backends

前回に引き続き, Backend の処理について少しだけ深掘りをしてみたいと思います.

inokara.hateblo.jp

今回は, 簡単な HTTP Backend を実装して Terraform の挙動を確認していきたいと思います.

HTTP Backend の実装

仕様

  • RESTful な API サーバーであること
  • 状態を GET メソッドで取得, POST メソッドで登録と更新, DELETE メソッドで削除できれば良い
  • locking をサポートする場合には, LOCK メソッド, UNLOCK メソッド (WebDav で利用されているメソッド) が要求される
  • locking については, オプションで LOCKUNLOCK メソッド以外のメソッドについても利用可能

上記を満たすサーバーを Sinatra を使って書いていきたいと思います. 尚, 実装にあたり, 以下の HTTP バックエンドを参考にさせて頂いております.

github.com

有難うございます. この HTTP バックエンドは Go で実装されていて, HTTPS もサポートしています.

最初の実装

Web アプリケーションのコード

とりあえず, 以下のようなシンプルに.

require 'sinatra'

get '/state' do
  status 200
end

delete '/state' do
  status 200
end

post '/state' do
  request.body.read.to_s
end

尚, 以下のような環境でこの Web アプリケーション (以後, 俺の HTTP バックエンド) を動かします.

$ ruby --version
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin17]

$ bundle exec gem list | grep sinatra
sinatra (2.0.3)
sinatra-contrib (2.0.3)

俺の HTTP バックエンドは以下のように起動しておきます.

$ bundle exec ruby app.rb &
[1] 51133
$ [2018-09-09 10:05:49] INFO  WEBrick 1.4.2
[2018-09-09 10:05:49] INFO  ruby 2.5.1 (2018-03-29) [x86_64-darwin17]
== Sinatra (v2.0.3) has taken the stage on 4567 for development with backup from WEBrick
[2018-09-09 10:05:49] INFO  WEBrick::HTTPServer#start: pid=51133 port=4567

Terraform の設定

以下のように Backend に http を利用するように定義します. address俺の HTTP バックエンドのエンドポイントを定義します.

terraform {
  backend "http" {
    address = "http://localhost:4567/state"
  }
}

resource "docker_container" "hoge" {
  image    = "${docker_image.centos.latest}"
  name     = "hoge-${terraform.workspace}"
  hostname = "hoge-${terraform.workspace}"
  command  = ["/bin/sh", "-c", "while true ; do sleep 1; hostname -s ; done"]
}

resource "docker_image" "centos" {
  name = "centos:6"
}

定義した後, init コマンドを実行しておきます.

$ terraform init --reconfigure

Initializing the backend...

Successfully configured the backend "http"! Terraform will automatically
use this backend unless the backend configuration changes.

Terraform の操作

plan

Lock, Unlock が未実装である為, -lock=false オプションをつけて実行すると, 特に問題なく plan の実行は終了します.

$ terraform plan -lock=false
...
Plan: 2 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.

実行すると, 以下のように俺の HTTP バックエンドのログが出力されます.

::1 - - [09/Sep/2018:10:08:10 +0900] "GET /state HTTP/1.1" 200 - 0.0011
::1 - - [09/Sep/2018:10:08:10 JST] "GET /state HTTP/1.1" 200 0
- -> /state

apply

引き続き, apply を実行してリソースを作成します.

$ terraform apply -lock=false

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create
...
docker_container.hoge: Creation complete after 1s (ID: 97ced319ed6ee961d461c7760be50d7731a78ba60059d244577f1471ded3ce50)

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

# 作成されたリソースを確認
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
97ced319ed6e        b5e5ffb5cdea        "/bin/sh -c 'while t…"   2 minutes ago       Up 2 minutes                                 hoge-default

実行すると, 以下のように俺の HTTP バックエンド のログが出力されます.

::1 - - [09/Sep/2018:10:13:18 +0900] "GET /state HTTP/1.1" 200 - 0.0007
::1 - - [09/Sep/2018:10:13:18 JST] "GET /state HTTP/1.1" 200 0
- -> /state
::1 - - [09/Sep/2018:10:13:27 +0900] "POST /state HTTP/1.1" 200 2651 0.0134
::1 - - [09/Sep/2018:10:13:27 JST] "POST /state HTTP/1.1" 200 2651
- -> /state

apply 時には, 一度, 状態を取得してからリソースを作成し, リソースの作成が完了した段階で状態を登録 (POST) するようです.

ここまでではめっちゃ簡単に HTTP Backend が動いたので拍子抜けしてしまいました.

destroy

作成したリソースを削除したいと思います.

$ terraform destroy -lock=false
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

# アレ...
Destroy complete! Resources: 0 destroyed.

# 削除されたはずのリソースを確認
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
97ced319ed6e        b5e5ffb5cdea        "/bin/sh -c 'while t…"   5 minutes ago       Up 5 minutes                                 hoge-default

destroy を実行すると, 俺の HTTP バックエンド は以下のようなログを出力しています.

::1 - - [09/Sep/2018:10:18:57 +0900] "GET /state HTTP/1.1" 200 - 0.0007
::1 - - [09/Sep/2018:10:18:57 JST] "GET /state HTTP/1.1" 200 0
- -> /state
::1 - - [09/Sep/2018:10:19:01 +0900] "POST /state HTTP/1.1" 200 317 0.0006
::1 - - [09/Sep/2018:10:19:01 JST] "POST /state HTTP/1.1" 200 317
- -> /state

apply の時と同様に, 状態を取得してからリソースを削除し, 削除が完了した段階で改めて状態を登録しているようですが...なぜか, リソースが削除されていないようです.

リソースが削除されていないのは, そもそも状態のデータそのものがどこにも登録されていないことが原因のようです. 実際に curl を使ってエンドポイントにアクセスすると, 以下のように何もレスポンスが返ってきていません.

$ curl -XGET localhost:4567/state
$

ということで, 状態を保存して永続化する為のストレージを用意してあげる必要がありそうです.

永続化層の追加

Redis を利用する

ということで, 永続化層として状態を Redis に保存するような実装を俺の HTTP バックエンドに追加していきたいと思います.

require 'sinatra'
require 'redis'

redis = Redis.new host: '127.0.0.1', port: '6379'

get '/state' do
  body = redis.get('state')
  body
end

delete '/state' do
  redis.del('state')
end

post '/state' do
  body = request.body.read.to_s
  redis.set 'state', body
end

Redis は docker イメージを利用してコンテナとしてあげておきます.

改めて, Terraform の操作

apply

事前に Redis 側にデータが何も登録されていないことを確認しておきます.

127.0.0.1:6379> keys *
(empty list or set)

引き続き apply します.

$ terraform apply -lock=false

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:
...
docker_container.hoge: Creation complete after 0s (ID: 074f29992a2083ef7ee0c8abdfe05f55b9b63cbaa47566f83559231068a684f5)

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

# リソースが作成されていることを確認
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS                    NAMES
074f29992a20        b5e5ffb5cdea        "/bin/sh -c 'while t…"   About a minute ago   Up About a minute                            hoge-default

Redis に状態が登録されていることを確認します.

127.0.0.1:6379> keys *
1) "state"
127.0.0.1:6379> get state
"{\n    \"version\": 3,\n    \"terraform_version\": \"0.11.8\",\n    \"serial\": 1,\n    \"lineage\": \"3b3166f1-2662-3c2b-b0fd-7c707170f7af\",\n    \"modules\": [\n        {\n            \"path\": [\n                \"root\"\n
 ],\n            \"outputs\": {},\n            \"resources\": {\n                \"docker_container.hoge\": {\n                    \"type\": \"docker_container\",\n                    \"depends_on\": [\n                        \"docker_image.centos\"\n                    ],\n                    \"primary\": {\n                        \"id\": \"2898f8b99470b977e6f215f16df5bc185c37fb8781b38f4671ce0ba1df8ee40c\",\n                        \"attributes\": {\n
          \"bridge\": \"\",\n                            \"command.#\": \"3\",\n                            \"command.0\": \"/bin/sh\",\n                            \"command.1\": \"-c\",\n                            \"command.2\": \"while true ; do sleep 1; hostname -s ; done\",\n                            \"gateway\": \"172.17.0.1\",\n                            \"hostname\": \"hoge-default\",\n                            \"id\": \"2898f8b99470b977e6f215f16df5bc185c37fb8781b38f4671ce0ba1df8ee40c\",\n                            \"image\": \"sha256:b5e5ffb5cdea24c637511e05e1bbe2c92207ae954559d4c7b32e36433035c368\",\n                            \"ip_address\": \"172.17.0.3\",\n
\"ip_prefix_length\": \"16\",\n                            \"log_driver\": \"json-file\",\n                            \"must_run\": \"true\",\n                            \"name\": \"hoge-default\",\n                            \"restart\": \"no\"\n                        },\n                        \"meta\": {},\n                        \"tainted\": false\n                    },\n                    \"deposed\": [],\n                    \"provider\": \"provider.docker\"\n                },\n                \"docker_image.centos\": {\n                    \"type\": \"docker_image\",\n                    \"depends_on\": [],\n                    \"primary\": {\n                        \"id\": \"sha256:b5e5ffb5cdea24c637511e05e1bbe2c92207ae954559d4c7b32e36433035c368centos:6\",\n                        \"attributes\": {\n                            \"id\": \"sha256:b5e5ffb5cdea24c637511e05e1bbe2c92207ae954559d4c7b32e36433035c368centos:6\",\n
                           \"latest\": \"sha256:b5e5ffb5cdea24c637511e05e1bbe2c92207ae954559d4c7b32e36433035c368\",\n                            \"name\": \"centos:6\"\n                        },\n                        \"meta\": {},\n
                      \"tainted\": false\n                    },\n                    \"deposed\": [],\n                    \"provider\": \"provider.docker\"\n                }\n            },\n            \"depends_on\": []\n        }\n
   ]\n}\n"
127.0.0.1:6379>

以下のように curl を利用してデータを JSON として取得することも出来ます.

$ curl -XGET localhost:4567/state
{
    "version": 3,
    "terraform_version": "0.11.8",
    "serial": 1,
    "lineage": "3b3166f1-2662-3c2b-b0fd-7c707170f7af",
    "modules": [
        {
            "path": [
                "root"
            ],
            "outputs": {},
            "resources": {
                "docker_container.hoge": {
                    "type": "docker_container",
                    "depends_on": [
                        "docker_image.centos"
                    ],
                    "primary": {
                        "id": "2898f8b99470b977e6f215f16df5bc185c37fb8781b38f4671ce0ba1df8ee40c",
                        "attributes": {
                            "bridge": "",
                            "command.#": "3",
                            "command.0": "/bin/sh",
                            "command.1": "-c",
                            "command.2": "while true ; do sleep 1; hostname -s ; done",
                            "gateway": "172.17.0.1",
                            "hostname": "hoge-default",
                            "id": "2898f8b99470b977e6f215f16df5bc185c37fb8781b38f4671ce0ba1df8ee40c",
                            "image": "sha256:b5e5ffb5cdea24c637511e05e1bbe2c92207ae954559d4c7b32e36433035c368",
                            "ip_address": "172.17.0.3",
                            "ip_prefix_length": "16",
                            "log_driver": "json-file",
                            "must_run": "true",
                            "name": "hoge-default",
                            "restart": "no"
                        },
                        "meta": {},
                        "tainted": false
                    },
                    "deposed": [],
                    "provider": "provider.docker"
                },
                "docker_image.centos": {
                    "type": "docker_image",
                    "depends_on": [],
                    "primary": {
                        "id": "sha256:b5e5ffb5cdea24c637511e05e1bbe2c92207ae954559d4c7b32e36433035c368centos:6",
                        "attributes": {
                            "id": "sha256:b5e5ffb5cdea24c637511e05e1bbe2c92207ae954559d4c7b32e36433035c368centos:6",
                            "latest": "sha256:b5e5ffb5cdea24c637511e05e1bbe2c92207ae954559d4c7b32e36433035c368",
                            "name": "centos:6"
                        },
                        "meta": {},
                        "tainted": false
                    },
                    "deposed": [],
                    "provider": "provider.docker"
                }
            },
            "depends_on": []
        }
    ]
}

state list, state show

以下のように state コマンドも実行してみます.

# state list を実行
$ terraform state list
docker_container.hoge
docker_image.centos

# state show を実行
$ terraform state show docker_image.centos
id     = sha256:b5e5ffb5cdea24c637511e05e1bbe2c92207ae954559d4c7b32e36433035c368centos:6
latest = sha256:b5e5ffb5cdea24c637511e05e1bbe2c92207ae954559d4c7b32e36433035c368
name   = centos:6

永続化された状態が取得出来ていることが判ります.

destroy

destroy も実行してみます.

$ terraform destroy -lock=false
docker_image.centos: Refreshing state... (ID: sha256:b5e5ffb5cdea24c637511e05e1bbe2c92207ae954559d4c7b32e36433035c368centos:6)
docker_container.hoge: Refreshing state... (ID: 2898f8b99470b977e6f215f16df5bc185c37fb8781b38f4671ce0ba1df8ee40c)

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


Destroy complete! Resources: 2 destroyed.

# リソースが削除されていることを確認
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES

Redis に登録されていた状態は以下のように変わって (更新されて) いることを確認します.

127.0.0.1:6379> get state
"{\n    \"version\": 3,\n    \"terraform_version\": \"0.11.8\",\n    \"serial\": 2,\n    \"lineage\": \"3b3166f1-2662-3c2b-b0fd-7c707170f7af\",\n    \"modules\": [\n        {\n            \"path\": [\n                \"root\"\n            ],\n            \"outputs\": {},\n            \"resources\": {},\n            \"depends_on\": []\n        }\n    ]\n}\n"
127.0.0.1:6379>

先程と同様に curl でも確認してみます.

$ curl -XGET localhost:4567/state
{
    "version": 3,
    "terraform_version": "0.11.8",
    "serial": 2,
    "lineage": "3b3166f1-2662-3c2b-b0fd-7c707170f7af",
    "modules": [
        {
            "path": [
                "root"
            ],
            "outputs": {},
            "resources": {},
            "depends_on": []
        }
    ]
}

合わせて state コマンドでも確認してみます.

$ terraform state show docker_image.centos
$ 

先程とは異なり状態の情報は出力されないようになりました.

ロックの実装

ロック処理

まずは, ロックの処理がどのように行われるかを見る為にロック用のエンドポイントを既存のコードに追加します.

post '/lock' do
  status 200
end

delete '/lock' do
  status 200
end

そして, main.tf についても, 以下のようにロック用の定義を追加します.

terraform {
  backend "http" {
    address        = "http://localhost:4567/state"
    lock_address   = "http://localhost:4567/lock"
    lock_method    = "POST"
    unlock_address = "http://localhost:4567/lock"
    unlock_method  = "DELETE"
  }
}

デフォルトでは LOCKUNLOCK メソッドが利用されますが, lock_methodunlock_method に任意のメソッドを定義することが出来ます.

この状態で Terraform の操作を行ってみたいと思います.

以下, plan 実行時の俺の HTTP バックエンドが出力するログです.

::1 - - [10/Sep/2018:00:36:59 +0900] "POST /lock HTTP/1.1" 200 - 0.0053
::1 - - [10/Sep/2018:00:36:59 JST] "POST /lock HTTP/1.1" 200 0
- -> /lock
::1 - - [10/Sep/2018:00:36:59 +0900] "GET /state HTTP/1.1" 200 318 0.0029
::1 - - [10/Sep/2018:00:36:59 JST] "GET /state HTTP/1.1" 200 318
- -> /state
::1 - - [10/Sep/2018:00:36:59 +0900] "DELETE /lock HTTP/1.1" 200 - 0.0060
::1 - - [10/Sep/2018:00:36:59 JST] "DELETE /lock HTTP/1.1" 200 0
- -> /lock

また, 以下は apply 実行時に出力されるログです.

::1 - - [10/Sep/2018:07:27:46 +0900] "POST /lock HTTP/1.1" 200 - 0.0253
::1 - - [10/Sep/2018:07:27:46 JST] "POST /lock HTTP/1.1" 200 0
- -> /lock
::1 - - [10/Sep/2018:07:27:46 +0900] "GET /state HTTP/1.1" 200 318 0.0171
::1 - - [10/Sep/2018:07:27:46 JST] "GET /state HTTP/1.1" 200 318
- -> /state
::1 - - [10/Sep/2018:07:29:00 +0900] "POST /state?ID=db1efcb5-0565-d31f-aa96-fe45b83327f1 HTTP/1.1" 200 2 0.0015
::1 - - [10/Sep/2018:07:29:00 JST] "POST /state?ID=db1efcb5-0565-d31f-aa96-fe45b83327f1 HTTP/1.1" 200 2
- -> /state?ID=db1efcb5-0565-d31f-aa96-fe45b83327f1
::1 - - [10/Sep/2018:07:29:00 +0900] "DELETE /lock HTTP/1.1" 200 - 0.0004
::1 - - [10/Sep/2018:07:29:00 JST] "DELETE /lock HTTP/1.1" 200 0
- -> /lock

ログを見る限りだと, ロック処理は以下のような挙動となるようです.

  • plan の時
    1. ロックする
    2. 状態を取得
    3. 1 をアンロックする
  • apply 及び, destroy の時
    1. ロックする
    2. 状態を取得する
    3. 新しい状態で更新する (パラメータにロック ID を付与している)
    4. 1 をアンロックする

複雑なことやってるんだろうなあと思っていたんですが, 意外にシンプルな感じなので驚きました.

ロック処理を追加したアプリケーション

驚いたところで, ロック処理を追加した俺の HTTP バックエンドは以下のようになりました.

require 'sinatra'
require 'redis'

redis = Redis.new host: '127.0.0.1', port: '6379'

get '/state' do
  state = redis.get('state')
  state 
end

delete '/state' do
  redis.del('state')
end

post '/state' do
  @lock_id = params[:ID]
  if @lock_id
    lock_id = JSON.parse(redis.get('lock'))['ID']
    # Conflict
    halt 409 unless @lock_id == lock_id
  end

  body = request.body.read.to_s
  redis.set 'state', body
end

post '/lock' do
  has_lock_key = redis.exists('lock')
  # Locked
  halt 423 if has_lock_key

  body = request.body.read.to_s
  redis.set 'lock', body
  status 200
end

delete '/lock' do
  redis.del('lock')
  status 200
end

これを起動して簡単に動作確認してみたいと思います.

改めてロック処理の確認

destroy 時にリソースを本当に削除して良いかというメッセージが出力され, yes 又は no を入力しなければいけない状態で...

$ terraform destroy
docker_image.centos: Refreshing state... (ID: sha256:b5e5ffb5cdea24c637511e05e1bbe2c92207ae954559d4c7b32e36433035c368centos:6)
docker_container.hoge: Refreshing state... (ID: 0f39fe1210a431706da1247d3df69750a0d0d6d828004134ab07ce1a42f184b6)

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

Terraform will perform the following actions:

  - docker_container.hoge

  - docker_image.centos


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:

別の端末から plan を実行してみます.

$ terraform plan

Error: Error locking state: Error acquiring the state lock: HTTP remote state already locked, failed to unmarshal body

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.

ロックされている旨のメッセージが出力されて plan すら失敗します. また, この時に俺の HTTP バックエンド のログを見ると以下のようにステータスコード 423 が返ってきます.

::1 - - [10/Sep/2018:08:13:23 +0900] "POST /lock HTTP/1.1" 423 - 0.0017
::1 - - [10/Sep/2018:08:13:23 JST] "POST /lock HTTP/1.1" 423 0
- -> /lock

また, このタイミングでロック用のエンドポイントに curl を使ってアクセスすると, 以下のような JSON が登録されていることを確認出来ます.

$ curl -s -XGET localhost:4567/lock | jq .
{
  "ID": "c50961ec-b0b4-9d6f-d439-4cc10688356a",
  "Operation": "OperationTypeApply",
  "Info": "",
  "Who": "ahokappa",
  "Version": "0.11.8",
  "Created": "2018-09-09T23:11:42.524759374Z",
  "Path": ""
}

destroy しようとしているのに OperationOperationTypeApply となっているのに若干の違和感を感じますが, ロック ID 等が情報として登録されていることが判ります.

現場からは以上です

ということで, 今回は超スーパーウルトラ簡単な HTTP バックエンドを実装して, 状態がどのように保存されるか, ロックの制御がどのように行われるかを研究してみましたが, 意外にも状態の保存もロックの制御もシンプルに実装されていることが判りました.

ということで, 引き続き, Terraform について研究を続けていきたいと思います.

2018 年 09 月 08 日 (土)

ジョギング

  • 香椎浜 x 2 周
  • 冷たい雨の中で走った

日課

  • お休み

12 ヶ月点検

  • 車の 12 ヶ月点検だったので, ディーラーに持って行った
  • ニューモデルが出ているとのことだったので, 試乗させてもらってけど, なんとなく今の車の方がしっくりきたので当分買い替えは無いだろう

引越し準備

  • 引っ越しが好きという奥さんに任せっきりだったけど, この週末は本腰入れて頑張ろうと思う
  • 香椎に引っ越してきて 4 年以上経って一度も日の目を見ていない雑多なモノは基本的に廃棄というポリシーで進めている

Terraform

  • ギョームの引き継ぎで Terraform を少し触る必要があったので, Terraform についてあれこれやり直している
  • Provider がプラグイン機構になっていたり, バックエンドの種類が増えていたり, State Locking が実装されていたりとなかなか面白い

独り Terraform 研究所 (1) 〜 Backend についてドキュメントを読んだり, チュートリアルしたり 〜

どうも

独り Terraform 研究所, 所長兼研究員のかっぱです.

以下のような Terraform に関する記事を書いてから, 2 年以上が経過して, すごく久しぶりに Terraform を触る機会を得たので 2 年のブランクを少しでも埋めるべく Terraform について, 自分が気になる部分を掻い摘んで研究していきたいと思います.

inokara.hateblo.jp

尚, 本記事で利用している環境は以下の通りとなります.

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.13.6
BuildVersion:   17G65

$ terraform --version
Terraform v0.11.8

また, 英文の翻訳には Google 翻訳を利用しておりますが, 一部には自分による意訳が含まれている為, 実際の内容とは異なる可能性がある旨, 何卒ご了承の程よろしくお願い致します.

Backends

ドキュメント

www.terraform.io

状態の管理

  • Terraform で構築したインフラの状態 (State) を管理する
  • Backend 自体は抽象レイヤーで, 実際にはリモートのオブジェクトストレージ等を選択して状態を保存することが出来る
  • デフォルトの設定は local Backend で状態はローカル PC に保存される

メリット

Backend のメリットについては, ドキュメントのおいては以下のように言及されています.

  • リモートに状態を保存した上で, その状態をロックすることで, 状態の破損を防ぐことが出来る
  • リモートストレージを利用することで, 機密情報をローカルディスクに保存する必要がなくなる
  • いくつかの Backend については, 遠隔操作が可能となる為, apply して放置ということが出来る (リモートストレージとロックを組み合わせる)

特にチームで Terraform を触る際にこれらのメリットの恩恵を受けることが出来ると思いますが, 個人だけで触る場合でも, Backend をうまく使いこなすことができれば, どこでも, いつでも同じ状態を簡単に構築, 再現することが出来ると思います.

設定諸々

  • terraform init を実行して, Backend を初期化する必要がある (init は Terraform を始めるにあたり, 必ず実行する必要がある)
  • 設定自体は Terraform ファイルの terraform セクションに記述するか, init コマンドのオプションとして, コマンドラインからも指定することが出来る

以下は Amazon S3 を Backend として利用する場合の設定例です.

teraform {
  backend "s3" {
    bucket = "oreno-terraform-state"
    key    = "terraform.tfstate.docker"
    region = "ap-northeast-1"
  }
}
  • Backend を変更する (例えば, local から s3 に変更する) 場合にも init コマンドを使う

以下は init コマンドの実行例です.

$ terraform init
Terraform has detected you're unconfiguring your previously set "s3" backend.
Do you want to copy existing state to the new backend?
  Pre-existing state was found while migrating the previous "s3" backend to the
  newly configured "local" backend. No existing state was found in the newly
  configured "local" backend. Do you want to copy this state to the new "local"
  backend? Enter "yes" to copy and "no" to start with an empty state.

  Enter a value: yes



Successfully unset the backend "s3". Terraform will now operate locally.
...

上記の例では, 従来は S3 Backend の利用を定義していたものを削除し, terraform init を実行しています.

  • workspace を利用している場合, 各 workspace 毎に状態 (tfstate) が作成される

以下は local Backend を用いて, test1 及び test2 という workspace を作成して planapply を行った結果です.

$ tree .
.
├── main.tf
├── terraform.tfstate
└── terraform.tfstate.d
    ├── test1
    │   └── terraform.tfstate
    └── test2
        └── terraform.tfstate

3 directories, 4 files

terraform.tfstate.d ディレクトリが作成され, その直下に各 workspace 名のディレクトリが作成され, その直下にそれぞれの tfstate ファイルが作成されています.

Backend Types

二種類の Backend Type

Backend Type は大きく, 提供する機能によって以下の二種類に分けられる.

  • Standard
    • 状態の管理, 状態保存とロック機能を提供
  • Enhanced
    • Standard の機能に合わせて, リモート操作を提供

「リモート操作」については, ここでは大きくは触れないが, ドキュメントによると...

Some backends support the ability to run operations (refresh, plan, apply, etc.) remotely. Terraform will continue to look and behave as if they're running locally while they in fact run on a remote machine.

と書かれており, Enhanced タイプの Backend を利用することで, Terraform の実行をリモートで行いつつ, あたかもローカル環境で実行されているかのように振る舞う機能が利用出来るようだ.

Enhanced Backends

Enhanced Backends には以下の二種類があります.

  • local
  • remote

local はデフォルトの Backend で明示的に path を指定しない場合にはカレントディレクトリに tfstate ファイルは保存されるようです. 以下は, 明示的に path の設定を行った例です.

terraform {
  backend "local" {
    path = "relative/path/to/terraform.tfstate"
  }
}

Standard Backends

Standard Backends の中でも単体で locking をサポートするものとしないもので分けられているようです. (locking についてはこの後で触れる予定.)

タイプ locking サポート メモ
artifactory no artifactory なんて初めて聞いたサービス.
azurerm yes Azure Storage のネイティブ機能を使用した状態ロックと整合性チェックもサポートする.
consul yes Consul のキー・バリューストアを利用, 状態ロックもサポートする
etcd no etcd v2.x のパスを利用する.
etcdv3 yes etcd v3 のキー・バリューストアを利用, 状態ロックもサポートする.
gcs yes Google Cloud Storage を利用, 状態ロックもサポートする.
http optional REST クライアントを利用して状態を保存する.
manta yes manta というオブジェクトストレージを利用する. manta 内で状態ロックも行う.
s3 DynamoDB を利用 Amazon S3 を利用, バージョニングを有効にすることを推奨, DynamoDB を利用して状態ロックと整合性チェックもサポートする.
swift no オブジェクトストレージの swift を利用する. s3 同様にバージョニングを有効にすることを推奨.
terraform enterprise no 旧 Atlas を利用, locking サポートしているかと思いきや...

State Locking

ドキュメント

www.terraform.io

www.terraform.io

State Locking とは

  • Backend でサポートされている場合, Terraformは状態を書き込む可能性があるすべての操作について状態をロックする
  • 状態をロックすることにより, それぞれ別の変更を加えようとした際に tfstate が破壊されることを防ぐ
  • 状態のロックは, tfstate に書き込みが発生する可能性のあるすべての操作で自動的に行われる
  • 状態のロック自体が失敗すると, Terraform は処理を中断する

例えば...

Backend Type で Amazon S3 を選んだ場合には, S3 自体には状態をロックし, 整合性をチェックするような機能は提供されていない為, DynamoDB テーブルを利用してこれらの機能を提供することになります.

また, ロックの際に保存される情報は, 以下のコードで確認することが出来ます.

github.com

   info := &LockInfo{
        ID:      id,
        Who:     fmt.Sprintf("%s@%s", userName, host),
        Version: version.Version,
        Created: time.Now().UTC(),
    }

フムフム.

Docker Providor + Backend Type S3

せっかくなので

Docker Provider を使ってコンテナを作りつつ, Backend の挙動等について確認 (という名のチュートリアル) をしてみたい.

簡単な main.tf

基本となる Terraform ファイルは以下の通りです.

resource "docker_container" "hoge" {
    image = "${docker_image.centos.latest}"
    name = "hoge-${terraform.workspace}"
    hostname = "hoge-${terraform.workspace}"
    command = ["/bin/sh", "-c", "while true ; do sleep 1; hostname -s ; done"]
}

resource "docker_container" "fuga" {
    image = "${docker_image.centos.latest}"
    name = "fuga-${terraform.workspace}"
    hostname = "fuga-${terraform.workspace}"
    command = ["/bin/sh", "-c", "while true ; do sleep 1; hostname -s ; done"]
}

resource "docker_container" "popo" {
    image = "${docker_image.centos.latest}"
    name = "popo-${terraform.workspace}"
    hostname = "popo-${terraform.workspace}"
    command = ["/bin/sh", "-c", "while true ; do sleep 1; hostname -s ; done"]
}

resource "docker_image" "centos" {
    name = "centos:6"
}

コンテナ作成

workspace を事前にいくつか作ってしまっていたので, 今回は default に戻した状態でコンテナを作成します.

$ terraform workspace select default
$ terraform plan
$ terraform apply
...
docker_container.popo: Creation complete after 0s (ID: 69c652297dd3a7cfd30c91a428f0ae026ec8611090914bf8e6a0ba0eb937c796)
docker_container.fuga: Creation complete after 0s (ID: f203969cd96e0de6e1eec8dee1dc866b87d0eeab2c3b0609304d9f575f63b0d2)
docker_container.hoge: Creation complete after 0s (ID: f4a7331dd4e436920a28fcb9c98372c453f95f21d5e7718bf68170266a8bdff2)

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

サクッと, コンテナが起動します.

$ docker ps | grep 'default'
f4a7331dd4e4        b5e5ffb5cdea        "/bin/sh -c 'while t…"   About a minute ago   Up About a minute                       hoge-default
f203969cd96e        b5e5ffb5cdea        "/bin/sh -c 'while t…"   About a minute ago   Up About a minute                       fuga-default
69c652297dd3        b5e5ffb5cdea        "/bin/sh -c 'while t…"   About a minute ago   Up About a minute                       popo-default

この時に tfstate ファイルは, main.tf が保存されているディレクトリと同じディレクトリに作成されています.

$ ls main.tf terraform.tfstate
main.tf                 terraform.tfstate

Backend を S3 に変える

この状態から Backend を Amazon S3 (以後, S3) に変更してみたいので main.cf に以下を追記します.

terraform {
  backend "s3" {
    bucket = "oreno-terraform-state"
    key    = "terraform.tfstate.docker"
    region = "ap-northeast-1"
  }
}

順番はどちらでも良いけど, S3 バケットを作成します. この S3 バケット自体も terraform で作ることが出来るようですが, 今回は AWS CLI で作成します.

# バケットを作成
$ aws s3 mb s3://oreno-terraform-state

# バケットバージョニングを有効
$ aws s3api put-bucket-versioning \
  --bucket=oreno-terraform-state \
  --versioning-configuration Status=Enabled

# バケットを暗号化する
$ aws s3api put-bucket-encryption --bucket=oreno-terraform-state \
  --server-side-encryption-configuration '{
  "Rules": [
    {
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "AES256"
      }
    }
  ]
}'

作成したら, terraform init コマンドを --reconfigure オプションをつけて実行します.

$ terraform init --reconfigure

Initializing the backend...
Do you want to migrate all workspaces to "s3"?
  Both the existing "local" backend and the newly configured "s3" backend support
  workspaces. When migrating between backends, Terraform will copy all
  workspaces (with the same names). THIS WILL OVERWRITE any conflicting
  states in the destination.

  Terraform initialization doesn't currently migrate only select workspaces.
  If you want to migrate a select number of workspaces, you must manually
  pull and push those states.

  If you answer "yes", Terraform will migrate all states. If you answer
  "no", Terraform will abort.

  Enter a value: yes


Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
...

上記のように, tfstate ファイルは S3 にコピーする旨の確認メッセージが出力され, yes を入力すると S3 にコピーが完了します. 念の為, 確認してみると以下のように S3 にアップロードされていることが判ります.

# S3 上の tfstate ファイルを確認
$ aws s3 ls s3://oreno-terraform-state/
                           PRE env:/
2018-09-08 14:51:53       5662 terraform.tfstate.docker

# オブジェクト (tfstate ファイル) がバージョニングされていることを確認
$ aws s3api list-object-versions --bucket=oreno-terraform-state
{
    "Versions": [
        {
            "ETag": "\"64b530af2d748c77a5d68b5448ac76de\"",
            "Size": 5649,
            "StorageClass": "STANDARD",
            "Key": "env:/test1/terraform.tfstate.docker",
            "VersionId": "2.eNdgnznHlbxkc_hMJ6NEz9AIcibcwO",
            "IsLatest": true,
            "LastModified": "2018-09-08T05:51:57.000Z",
            "Owner": {
                "DisplayName": "kappaahoaho",
                "ID": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
            }
        }
...
        

# オブジェクト (tfstate ファイル) が暗号化されていることを確認
$ aws s3api head-object --bucket=oreno-terraform-state --key=terraform.tfstate.docker
{
    "AcceptRanges": "bytes",
    "LastModified": "Sat, 08 Sep 2018 05:51:53 GMT",
    "ContentLength": 5662,
    "ETag": "\"355017b22a2af13dff9581b638461897\"",
    "VersionId": "tAciP72ATbpem.sDjNueU3sw7iJswg0p",
    "ContentType": "application/json",
    "ServerSideEncryption": "AES256",
    "Metadata": {}
}

そして, ローカルの tfstate ファイルを見てみると...

$ ls -l terraform.tfstate
-rw-r--r--  1 user  group  0  9  8 14:51 terraform.tfstate

空になっていることが判ります.

この状態で 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.

docker_image.centos: Refreshing state... (ID: sha256:b5e5ffb5cdea24c637511e05e1bbe2c92207ae954559d4c7b32e36433035c368centos:6)
docker_container.hoge: Refreshing state... (ID: f4a7331dd4e436920a28fcb9c98372c453f95f21d5e7718bf68170266a8bdff2)
docker_container.popo: Refreshing state... (ID: 69c652297dd3a7cfd30c91a428f0ae026ec8611090914bf8e6a0ba0eb937c796)
docker_container.fuga: Refreshing state... (ID: f203969cd96e0de6e1eec8dee1dc866b87d0eeab2c3b0609304d9f575f63b0d2)

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

No changes. Infrastructure is up-to-date.

This means that Terraform did not detect any differences between your
configuration and real physical resources that exist. As a result, no
actions need to be performed.

いい感じです.

現場から以上です

超駆け足ですが, Terraform の Backends について研究しつつ, 簡単なチュートリアルをやってみました. 状態ロックを併用することでチーム内で Terraform をいじる際に強力にサポートしてくれるものであることが解りました. また, Backend の実装にも興味が出てきましたので, HTTP のバックエンドサーバーを実装してみたいと考えています.

2018 年 09 月 07 日 (金)

ジョギング

  • 香椎浜 x 2 周
  • だいぶん涼しくて走り易くなった
  • 相変わらず, 右足股関節の違和感は続いている

日課

  • お休み

JAWS-UG 福岡もくもく会とリモートで勉強会

JAWS-UG もくもく会会場から「リモートで勉強会」に参加させて頂いた.

remote-study.connpass.com

jaws-ug-kyushu.doorkeeper.jp

リモートで参加出来る勉強会なので場所を選ばず, このような試みが出来ることに各勉強会に参加されている皆さんに感謝.

ちなみに「リモートで勉強会」で exercism.io について LT した.

speakerdeck.com

いきなり資料がめくれないという, リモートならでは (?) の謎トラブルに見舞われるも, 参加者の皆さんのフォローでなんとか乗り切ることが出来た. そして, ライブコーディング (w) まで披露することが出来た.

その後, もくもく会では AWS の話しは勿論, テストの話し等で盛り上がることが出来た.

今回, 2 つの勉強会に参加して感じたこととしては...

  • リモート経由で発表する時には資料の事前配布はほぼ必須
  • デモやライブコーディングは失敗前提でそれすら楽しむくらいでやった方が良い
  • 機能テスト ≒ feature test (ずっと, ふゅーちゃーテストって読んでいたけど, フィーチャーテストが正しい呼び方だよ!!)
  • 機能テストは何をテストしたいかを決めた上で, レスポンスに必要な情報は地道に用意する

そして, 自分にはなんか色々と足りていないことを改めて痛感, 何が足りてないのか, どのようにそれらを補完していくかを少し落ち着いて考えたい.

2018 年 09 月 06 日 (木)

ジョギング

  • 香椎浜 x 2 周
  • だいぶん涼しくて走りやすかった
  • まだ, 右股関節から右足にかけての違和感がある

日課

  • (腕立て x 50) x 3
  • 腹筋はまだ腰が痛くなりそうなので止めてる

だいぶん

  • 涼しくなった感
  • ただ, 今頃になって夏バテのような倦怠感があって辛い

2018 年 09 月 05 日 (水)

ジョギング

  • 香椎浜 x 2 周
  • 右腰, 臀部周りに強い張りは引き続きなんだけど, 両足がズシッと重い感じがしていて辛い

日課

  • (腕立て x 50) x 3
  • 腹筋はまだ腰が痛くなりそうなので止めてる

夕飯

  • パスタを作る
  • 研究に近い感じになってきた

swagger → OpenAPI

swagger という REST API を構築するためのフレームワーク. ギョームで実装した API のリクエストやパラメータ等のインターフェースを swagger spec というファイルに書き出すようなことをやっていたんだけど, 今日, 久しぶりに swagger-php を触っていたら, swagger そのものが OpenAPI という名前に変わっていて驚いた.

swagger.io