- どうも
- Backends
- HTTP Backend の実装
— 仕様
— 最初の実装
—– Web アプリケーションのコード
—– Terraform の設定
—– Terraform の操作
—– plan
—– apply
—– destroy
— 永続化層の追加
—– Redis を利用する
—– 改めて, Terraform の操作
—– apply
—– state list, state show
—– destroy
— ロックの実装
—– ロック処理
—– ロック処理を追加したアプリケーション
—– 改めてロック処理の確認 - 現場からは以上です
どうも
独り Terraform 研究所, 所長兼研究員のかっぱです.
Backends
前回に引き続き, Backend の処理について少しだけ深掘りをしてみたいと思います.
inokara.hateblo.jp
今回は, 簡単な HTTP Backend を実装して Terraform の挙動を確認していきたいと思います.
HTTP Backend の実装
仕様
- RESTful な API サーバーであること
- 状態を
GET
メソッドで取得,POST
メソッドで登録と更新,DELETE
メソッドで削除できれば良い - locking をサポートする場合には,
LOCK
メソッド,UNLOCK
メソッド (WebDav で利用されているメソッド) が要求される- ステータスコード
423
で Locked - ステータスコード
409
で Conflict - ステータスコード
200
でロックの成功
- ステータスコード
- locking については, オプションで
LOCK
やUNLOCK
メソッド以外のメソッドについても利用可能
上記を満たすサーバーを 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" } }
デフォルトでは LOCK
や UNLOCK
メソッドが利用されますが, lock_method
や unlock_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
しようとしているのに Operation
が OperationTypeApply
となっているのに若干の違和感を感じますが, ロック ID 等が情報として登録されていることが判ります.
現場からは以上です
ということで, 今回は超スーパーウルトラ簡単な HTTP バックエンドを実装して, 状態がどのように保存されるか, ロックの制御がどのように行われるかを研究してみましたが, 意外にも状態の保存もロックの制御もシンプルに実装されていることが判りました.
ということで, 引き続き, Terraform について研究を続けていきたいと思います.
元記事はこちら
「独り Terraform 研究所 (2) 〜 簡単な HTTP Backend を実装して Terraform の挙動を確認する 〜」