Terraform入門 #2 Terraformはこわくない!!

Terraform入門 #1の続きです。

Terraformを始めるのに一番の障壁

  • 既存のインフラコードで試してみたいけどアクセスキーの準備とかアカウント申請が面倒だし、なんか壊しそうで怖い:scream:
  • 自分でやるにしても、AWS契約しないと試せないんでしょと思って面倒になる:frowning2:
  • もし自分のアカウントでやったときに高額請求されたらどうしよう:worried:

この怖さがTerraformへの障壁をあげてしまってる気がします。少なくともぼくはそうでした。

f:id:masato47744:20170707194957p:plain

簡単に試せるやつあるよ

terraformはAWSのものじゃないと、Terraform入門 #1で言いましたが、他にも適用できるものが用意されてます。もちろん、GCPやAzureなどもそうですが、GitHubなどのサービスに対しても用意されてます。気になった人は、他にはどんなProviderがあるか確認してみましょうHTTPリクエストしたときのresponse bodyを取得するなんてものもあります!!

要はAPI呼び出して何か作成するということをするサムシングがあればそれはterraform化することができると言っても過言ではありません。 その中で簡単に試せるのがDockerです。Macなら簡単にdocker環境は入れられます。みなさんもちろん入ってますよね:eye::eye:?

f:id:masato47744:20170707195051p:plain

DockerでTerraformを試してみよう!

Dockerの初歩的な知識と、Docker for Macはinstallされてる前提で始めます。terraformもhomebrewでさくっと入れておいてください。

それでは早速設定を書いて試していきましょう。

AWSや、GCPの場合でもそうですが、バックエンドにAPI呼び出しをするための設定情報を書きます。これはproviderというディレクティブに書いていきます。 providerさえ書いたらあなたはterraformの世界に入ったと思ってください:tada: Dockerの場合の書き方はTerraformのDocsをみればのっています。ここは最低限のMacのDockerに対して行う設定を書いてみます。

provider "docker" {
  host = "unix:///var/run/docker.sock"
}

Docker for Macの場合、unixドメインソケットで接続してあげるというわけですね。

:point_up:Tips: 最初につまづくのがファイルの命名規則。たいてい、main.tfとか、core.tf、common.tfとかあるんだけど、どうすりゃいいんだと悩みますが、正解はtfがついてればなんでもオーケー。試しにファイル名を好きなものにしてみましょう。

terraform plan

さぁ、あとは好きなだけterraform planコマンドを打ってみましょう! planというのが要はdry runです。絶対に環境を壊すことはないので安心してください。

terraformは現在の状態を保持するといいましたが、planを打つことでその差分を確認できます。 今の状態でterraform planを打ってみましょう。とりあえず何かメッセージが出てきたと思います。providerを書いただけなので、何も差分は出ていないはずです。

$ 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.

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, Terraform
doesn't need to do anything.

Terraformでnginxをたててみる

localhostで8080でdockerでnginxを動かしたい場合、こんな感じでdocker runで実行しますよね。

$ docker run -d --name my-nginx -p 8080:80 nginx:latest
a2c05579e456b51227d7878e6e05754fd76d13b8f7345f39ff97cdeb68458f12

runしたあとはlocalhost:8080にアクセスするとnginxのページが見れます。

$ curl localhost:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

terraform化

このdockerのコマンドの内容をTerraformで表現するとこうなります。先ほどのコマンドがどんなresourceに対応してるかは、公式Docを見れば分かります。

# 最新のnginxイメージ
resource "docker_image" "nginx" {
  name = "nginx:latest"
}

# コンテナ起動
resource "docker_container" "nginx" {
  name  = "my-nginx"
  image = "${docker_image.nginx.latest}"
  ports {
    internal = 80
    external = 8080
  }
}

早速planしてみましょう

こんな感じの出力になったと思います。

$ terraform plan
.
.
+ docker_container.nginx
    bridge:                    "<computed>"
    gateway:                   "<computed>"
    image:                     "${docker_image.nginx.latest}"
    ip_address:                "<computed>"
    ip_prefix_length:          "<computed>"
    log_driver:                "json-file"
    must_run:                  "true"
    name:                      "my-nginx"
    ports.#:                   "1"
    ports.3862886908.external: "8080"
    ports.3862886908.internal: "80"
    ports.3862886908.ip:       ""
    ports.3862886908.protocol: "tcp"
    restart:                   "no"

+ docker_image.nginx
    latest: "<computed>"
    name:   "nginx:latest"


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

それぞれ、+がついてるresourceは新しく追加されるreourceを表してます。他には、-が削除、~が変更、-/+というのは削除して新しく作り直すという記号があります。

最後の1行にも、2つのresourceが追加されるよと書いてありますね。

terraform applyしてみよう

実行すると、Applyが成功した出力がでてきたと思います。 ※ apply前に、さっきdocker runしたコンテナはkillしてrm -fしておきましょう。

$ terraform apply
docker_image.nginx: Creating...
  latest: "" => "<computed>"
  name:   "" => "nginx:latest"
docker_image.nginx: Creation complete (ID: sha256:c246cd3dd41d35f9deda43609cdeaa9a...658f9c5e38aad25c4ea5efee10nginx:latest)
docker_container.nginx: Creating...
  bridge:                    "" => "<computed>"
  gateway:                   "" => "<computed>"
  image:                     "" => "sha256:c246cd3dd41d35f9deda43609cdeaa9aaf04d3658f9c5e38aad25c4ea5efee10"
  ip_address:                "" => "<computed>"
  ip_prefix_length:          "" => "<computed>"
  log_driver:                "" => "json-file"
  must_run:                  "" => "true"
  name:                      "" => "my-nginx"
  ports.#:                   "" => "1"
  ports.3862886908.external: "" => "8080"
  ports.3862886908.internal: "" => "80"
  ports.3862886908.ip:       "" => ""
  ports.3862886908.protocol: "" => "tcp"
  restart:                   "" => "no"
docker_container.nginx: Creation complete (ID: 7d5ccd716c3de242af221588ea303b4382dd3ab789f4841f888e2b9b549eda76)

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

実際にコンテナがたってるかみてみましょう。動いてますね。

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                  NAMES
7d5ccd716c3d        c246cd3dd41d        "nginx -g 'daemon ..."   7 seconds ago       Up 6 seconds        0.0.0.0:8080->80/tcp   my-nginx
$ curl localhost:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

tfstateができています

applyすると、terraform.tfstateというファイルがterraformによって作成されます。その中身がどうなってるか確認してみましょう。jsonファイルにいろんな情報が追加されてますね。この中に自分で指定したresourceの名前などが入ってることを確認してみましょう。

{
    "version": 3,
    "terraform_version": "0.9.9",
    "serial": 0,
    "lineage": "5ecb5960-880c-4d99-88b8-6495ad61c6ea",
    "modules": [
        {
            "path": [
                "root"
            ],
            "outputs": {},
            "resources": {
                "docker_container.nginx": {
                    "type": "docker_container",
                    "depends_on": [
                        "docker_image.nginx"
                    ],
                    "primary": {
                        "id": "7d5ccd716c3de242af221588ea303b4382dd3ab789f4841f888e2b9b549eda76",
                        "attributes": {
                            "bridge": "",
                            "gateway": "172.17.0.1",
...

差分を発生さてさるみる

先ほどの状態から、8080から8081にportを変更してみましょう。以下の変更を加えて

-     external = 8080
+     external = 8081

planをしてみましょう。

$ terraform plan
-/+ docker_container.nginx
    bridge:                    "" => "<computed>"
    gateway:                   "172.17.0.1" => "<computed>"
    image:                     "sha256:c246cd3dd41d35f9deda43609cdeaa9aaf04d3658f9c5e38aad25c4ea5efee10" => "sha256:c246cd3dd41d35f9deda43609cdeaa9aaf04d3658f9c5e38aad25c4ea5efee10"
    ip_address:                "172.17.0.2" => "<computed>"
    ip_prefix_length:          "16" => "<computed>"
    log_driver:                "json-file" => "json-file"
    must_run:                  "true" => "true"
    name:                      "my-nginx" => "my-nginx"
    ports.#:                   "1" => "1"
    ports.1078587976.external: "" => "8081" (forces new resource)
    ports.1078587976.internal: "" => "80" (forces new resource)
    ports.1078587976.ip:       "" => ""
    ports.1078587976.protocol: "" => "tcp" (forces new resource)
    ports.3862886908.external: "8080" => "0" (forces new resource)
    ports.3862886908.internal: "80" => "0" (forces new resource)
    ports.3862886908.ip:       "" => ""
    ports.3862886908.protocol: "tcp" => "" (forces new resource)
    restart:                   "no" => "no"


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

赤い表示で、forces new resourceというものが出てきたと思います。これはどういうことかというと、該当のリソース(ここではコンテナ)を動かしたままportを変更することはできないため、作り直すよってことを言っています。 最後の1行にも英語で1つ追加して、1つ削除しますと書いてあります。

今回は手元のDockerなので作り直しばっちこいですが、もし、これがAWS上のデータベースだとしたら・・:scream:

Terraformを使っていれば、こんな感じで危険を察知できたり、次に何が起きるかが分かるようになります。

applyしてみよう

docker psでCONTAINER IDが変わっているので、再作成されたことが分かります。curlでも8081でちゃんとつながってますね。

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                  NAMES
1bf5b57aa7a4        c246cd3dd41d        "nginx -g 'daemon ..."   3 seconds ago       Up 2 seconds        0.0.0.0:8081->80/tcp   my-nginx
$ curl localhost:8081
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

tfstate.backup

新しくterraform.tfstate.backupというファイルができていると思います。これは、前回のものをbackupしたものです。このように前回の状態も自動的に保存しておいてくれます。 applyでどうしようもない誤った変更をしてしまった(いい例が思いつきませんが、、)場合に、そのtfstate.backupファイルからまたresourceを作り直すことができます。やったことないけど、完全に復元とはいかないです。

変数とインターポレーション

先ほどは、1つのコンテナでしたが複数コンテナをたてたいとします。 その場合に、同じ文字列を設定する部分は変数にしたいという思いがおきてくると思います。

例えば、各コンテナの名前は別の名前を設定する必要がありますが、my-nginxというところは同じにして、後ろに1とか2とかをつけることで別名にしたいとします。 その場合は、まずvariableというディレクティブを使って、このように変数を宣言することができます。細かい文法はここは無視してこういう風に宣言すると思ってください。

variable "name" {
  default = "my-nginx"
}

そして、containerのリソース側で呼び出すときは、var.nameでアクセスできて、${}を使って変数展開できます。shの変数展開の書き方とそっくりですね。

resource "docker_container" "nginx_1" {
  name  = "${var.name}-1"
  image = "${docker_image.nginx.latest}"
  ports {
    internal = 80
    external = 8080
  }
}

resource "docker_container" "nginx_2" {
  name  = "${var.name}-2"
  image = "${docker_image.nginx.latest}"
  ports {
    internal = 80
    external = 8081
  }
}

この状態でplanをしてみましょう。nameの部分が変数展開されていることが分かるかと思います。

+ docker_container.nginx_1
.
    name:                      "my-nginx-1"
.
.
+ docker_container.nginx_2
.
    name:                      "my-nginx-2"
.

ちょっと待って:raised_hand:

このコードを見て、プログラマのみなさんならうずうずしてしまう部分があるかと思います。 そう、port指定の80808081、nameのsuffixの-1-2のようなベタ打ちはなるべく避けたいのではないのでしょうか。変数に配列、そしてforやifが使えれば・・・・

terraformでもfor文やif文のような機能がある

手続き型プログラミング言語ではないので素直にforやifと素直にはいかないですが、それと同じことを実現する手段は用意されています。

実際にコードを見てみた方が早いと思います。こちらをご覧ください。

variable "ports" {
  default = [8080, 8081, 8082, 8083]
}

variable "name" {
  default = "my-nginx"
}

resource "docker_container" "nginx" {
  count = "${length(var.ports)}"

  name  = "${count.index % 2 == 0 ? "${var.name}-a-${count.index + 1}" : "${var.name}-b-${count.index + 1}"}"
  image = "${docker_image.nginx.latest}"
  ports {
    internal = 80
    external = "${var.ports[count.index]}"
  }
}

resource "docker_container" "nginx"の下に新たにcountと書かれたところがあります。これは、countに渡した数だけresourceを作ってくれ、これがfor文相当です。 lengthというビルトイン関数を利用して、portsという配列の長さをとってきています。 そして、count.indexで該当のindexにアクセスできます。forのindex相当です。

nameのところで書いているのがif文相当です。if文は書けないので三項演算子が用意されています。この処理の場合は、偶数の場合は、aという文字を、奇数の場合はbという文字を追加しています。

この状態でplanをしてみましょう

出力された結果はnameやportが想定した値となっているでしょうか。このように共通した処理はforやifなどを使ってまとめて書くことができます。

実際は、interpolationというところにもっとビルトイン関数が用意されてます。また、変数のタイプもmapやbooleanなどがあり、variablesにまとまっています。手元のdockerなので、ぶっ壊れても安心なのでいろいろと試してみましょう!

まとめ

Dokerを通してTerraformを気軽に試せる環境について紹介しました。 Providersを見て、Terraformで管理したら便利そうだなと思うものがあれば、自分の遊びプロジェクトでで導入してみるといいかもしれませんね:heart_eyes: 次回は、実践的な環境でTerraformのコードをリファクタリングしていく内容を紹介していこうと思います。リファクタリングの過程を通して実践的な知識を学んでもらえればと思います。題材は検討中・・