Terraformを1年間運用して学んだトラブルパターン4選

この記事は Eureka Advent Calendar 2016 15日目 の記事です。
14日目は丹 俊貴さんの「iOSアプリのUX改善! FacebookのAsyncDisplayKitで60FPSのハイパフォーマンスなiOSアプリを作る」でした。

はじめに

こんにちは! エウレカのインフラを担当しております恩田です。
今回はTerraformの運用で学んだトラブルパターンと解決方法について書きたいと思います。

TerraformによるInfrastructure as Codeの実践

エウレカではサービスのインフラ基盤としてAWSを全面採用しており、リソース管理のコード化を推進しています。メリットや手法については以前のInfrastructure as Code実践についての記事に譲りますが、今回はそのなかでも中心的役割を果たすTerraformの概要説明と運用中のトラブルパターンについて書きます。

Terraformで管理するファイルと役割

Terraformはインフラの構築・変更・バージョン管理を安全かつ効率的に行うためのツールです。AWSリソースをコードで記述、作成や変更、削除が行えます。また、バイナリを実行パスに置いておけば動くので導入も簡単です。dry runの機能もあり、かつ開発が早いため比較的最新のAWSコンポーネントにも機能が追従しています。エウレカではPairsのAWSリソースは(ほぼ)全てTerraformを
使って管理しています。以下、エウレカでの使用例ですが、Terraformのファイルは環境ごとに大きく3つのファイルが存在します。

  • 〇〇.tf
  • terraform.tfstate
  • variable.tf

〇〇.tf

  • terraformで実行されるレシピ本体です。
  • vpc.tfといった具合に、AWSのコンポーネントごとにファイルを分けています。
  • 同一ディレクトリ内に複数ファイルが存在する場合、一斉に実行されます。

terraform.tfstate

  • terraformレシピを実行した結果が記載されてるファイルです。
  • 作成されたリソース情報 + serial(通算変更回数)が保存されます。

variables.tf

  • デフォルトで読まれる変数ファイルです。

以下、VPCを1つ作成するレシピとファイル記述例です。VPCを作成するレシピ + リージョンやネットマスクを定義する変数ファイル + 実行結果(リソース実体)を表したファイルの3つとなります。

# variable.tf
variable "vpc" {
  default = {
        region   = "ap-northeast-1"             
    vpc_cidr = "10.0.0.0/16"
  }
}
# vpc.tf
provider "aws" {
    region = "${var.vpc.region}"
}

resource "aws_vpc" "vpc" {
    cidr_block           = "${var.vpc.vpc_cidr}"
    instance_tenancy     = "default"
    enable_dns_support   = "true"
    enable_dns_hostnames = "true"
}
# terraform.tfstate
{
    "version": 3,
    "terraform_version": "0.7.10",
    "serial": 1,
    "lineage": "6e3ebbf6-0e6d-4afd-957b-e7f08ff55fc3",
    "modules": [
        {
            "path": [
                "root"
            ],
            "outputs": {},
            "resources": {
                "aws_vpc.vpc": {
                    "type": "aws_vpc",
                    "depends_on": [],
                    "primary": {
                        "id": "vpc-aaaaaa",
                        "attributes": {
                            "cidr_block": "10.0.0.0/16",
                            "default_network_acl_id": "acl-aaaaaa",
                            "default_route_table_id": "rtb-aaaaaa",
                            "default_security_group_id": "sg-aaaaa",
                            "dhcp_options_id": "dopt-aaaaaa",
                            "enable_classiclink": "false",
                            "enable_dns_hostnames": "true",
                            "enable_dns_support": "true",
                            "id": "vpc-aaaaaa",
                            "instance_tenancy": "default",
                            "main_route_table_id": "rtb-aaaaa",
                            "tags.%": "3",
                            "tags.Name": "Pairs-jp",
                            "tags.env": "prod",
                            "tags.region": "jp"
                        },
                        "meta": {},
                        "tainted": false
                    },
                    "deposed": [],
                    "provider": ""
                }
            },
            "depends_on": []
        }
    ]
}

トラブルシューティング編

これまでの経験上、Terraformのトラブルは以下4つに分類できることが多いです。

  • リソース作成に失敗する
  • リソース削除(変更)に失敗する
  • レシピを変更するとリソースが意図せず削除される
  • レシピを変更してないのにリソース削除(変更)される

それぞれ、順番にあるある発生ケースと原因を見ていきます。

リソース作成に失敗する

AWS側の制約で失敗するケースです。具体的には、リソースの作成上限にひっかかる。重複した名前のリソースがある、などです。よくある例だと、手動でリソースを作成、後でコード管理下に入れようとして同名でTerraformでリソース作成して失敗するってケースです。ELBやDNSなど、重複したリソースをAWS側が認めない箇所でよく発生します。この場合、Terraform側のリソース名を変更する or AWS側のリソースを削除する必要があります。

リソース削除(変更)に失敗する

これもAWS側の制約で失敗するケースです。例えば、紐付いてるインスタンスが存在する状態でELBを削除しようとする。EBSのボリュームがビジー状態でデタッチ不可の状態でEBSごとEC2を削除しようとする、紐づくSubnetが存在する状態でVPCを削除する、といった場合です。このような場合、紐づくリソースも同時に削除する必要があります。

レシピを変更するとリソースが削除される

既にTerraformで作成済みのリソースに対して変更を加える場合、変更するパラメタによって、

  • modify
  • re-create(再作成)

この2種類の動きをします。前者はAWSコンソールから変更可能なもの(例:EC2のタグ名、SGの許可プロトコル、Cidrなど)です。後者はAWSコンソールから変更不可なもの(例:EC2のAMI ID、ELBのNameやDescription、VPCやSubnetのCIDRなど)です。後者のリソースに対してTerraformレシピで変更を加えて実行すると、Terraformは一度既存のリソースを破棄して別リソースとして作成し直します。

terraform plan実行時、表示される差分の左側に実行されるアクションが表示されるので、これが再作成マーク( -/+ )になっていたら要注意です。

# ex:instanceのami_idを変更して実行してみる
# ami_idを変更
$ git diff ec2.tf

diff --git a/terraform/Pairs/prod/jp/ec2.tf b/terraform/Pairs/prod/jp/ec2.tf
index 0410a6c..5574667 100644
--- a/terraform/Pairs/prod/jp/ec2.tf
+++ b/terraform/Pairs/prod/jp/ec2.tf

 # Appサーバ
 resource "aws_instance" "web_1" {
-    ami                   = "ami-aaaaaaaa"
+    ami                   = "ami-bbbbbbbb"
     instance_type         = "${var.ec2.app.instance_type}"
     availability_zone     = "${var.vpc.region_1a}"
     security_groups       = ["${aws_security_group.app.id}"]
# 実行してみる
$ terraform plan

~~ 中略 ~~

-/+ aws_instance.web_1
    ami:                        "ami-aaaaaaaa" => "ami-bbbbbbbb" (forces new resource)
    availability_zone:          "ap-northeast-1a" => "ap-northeast-1a"
    ebs_block_device.#:         "1" => "<computed>"
    ebs_optimized:              "true" => "1"
    ephemeral_block_device.#:   "0" => "<computed>"
    iam_instance_profile:       "Pairs-ec2-instance-role" => "Pairs-ec2-instance-role"
    instance_type:              "t2.medium" => "t2.medium"
    key_name:                   "" => "<computed>"
    monitoring:                 "true" => "1"
    placement_group:            "" => "<computed>"
    private_dns:                "ip-10-0-3-10.ap-northeast-1.compute.internal" => "<computed>"
    private_ip:                 "10.0.3.10" => "<computed>"
    public_dns:                 "" => "<computed>"
    public_ip:                  "" => "<computed>"
    root_block_device.#:        "1" => "<computed>"
    security_groups.#:          "1" => "1"
    security_groups.3448936684: "sg-aaaaaa" => "sg-aaaaa"
    source_dest_check:          "true" => "1"
    subnet_id:                  "subnet-aaaaa" => "subnet-aaaaa"
    tags.#:                     "4" => "4"
    tags.Name:                  "Pairs-jp-web1" => "Pairs-jp-web1"
    tags.env:                   "prod" => "prod"
    tags.group:                 "Pairs-jp-web" => "Pairs-jp-web"
    tags.region:                "jp" => "jp"
    tenancy:                    "default" => "<computed>"
    vpc_security_group_ids.#:   "1" => "<computed>"

~ aws_route53_record.web_1
    records.#: "" => "<computed>"

-/+ aws_volume_attachment.web_1
    device_name:  "/dev/xvdf" => "/dev/xvdf"
    force_detach: "false" => "0"
    instance_id:  "i-aaaaaa" => "${aws_instance.web_1.id}" (forces new resource)
    volume_id:    "vol-aaaaaa" => "vol-aaaaaa"

この場合、インスタンスに紐づくリソースとしてEBSとDNSレコードも作成されているため、インスタンスが再作成されることによってこれら関連リソースも再作成 & 変更されてしまいます。

レシピを変更してないのにリソースが削除(変更)される

Terraformのレシピに変更を加えていないのにplan実行時に差分が表示される場合、GUI上からインフラリソースに変更が加えられているケースが考えられます。例えば、SGにコンソールからルールを追加した状態でterraform planを実行すると、terraformはレシピを正としてコンソールから追加したルールを削除しようとします。他よくある例として、コンソールからインスタンスサイズを変更して再起動したケースです。この場合、そのままレシピを実行すると既存サーバを削除して別インスタンスとしてサーバが立ち上がってしまいます。現実のインフラリソースを正とするケースが大半だと思うので、その場合は以下の作業を実施する必要があります。

  • terraform refreshで実体とtfstateを揃える
  • レシピ本体を修正
  • terraform planを実行、差分がない事を確認する
  • terraform apply実行、差分をcommit & pushする

実例を示します。下記のようにインスタンスをterraformで立ててみます。

# ex:ec2.tf
provider "aws" {
    region = "ap-northeast-1"
}

resource "aws_instance" "test" {

    ami                   = "ami-aaaaaaa"
    instance_type         = "t2.medium"
    availability_zone     = "ap-northeast-1a"
    security_groups       = ["sg-aaaaaa"]
    subnet_id             = "subnet-aaaaa"
    ebs_optimized         = "false"
    monitoring            = "true"
    count                 = 1

    tags {
        Name   = "test_terraform"
    }
}
# 実行
$ terraform apply
aws_instance.test: Creating...
  ami:                        "" => "ami-aaaaaa"
  availability_zone:          "" => "ap-northeast-1a"
  ebs_block_device.#:         "" => "<computed>"
  ebs_optimized:              "" => "0"
  ephemeral_block_device.#:   "" => "<computed>"
  instance_type:              "" => "t2.medium"
  key_name:                   "" => "<computed>"
  monitoring:                 "" => "1"
  placement_group:            "" => "<computed>"
  private_dns:                "" => "<computed>"
  private_ip:                 "" => "<computed>"
  public_dns:                 "" => "<computed>"
  public_ip:                  "" => "<computed>"
  root_block_device.#:        "" => "<computed>"
  security_groups.#:          "" => "1"
  security_groups.3448936684: "" => "sg-aaaaaa"
  source_dest_check:          "" => "1"
  subnet_id:                  "" => "subnet-aaaaaa"
  tags.#:                     "" => "1"
  tags.Name:                  "" => "test_terraform"
  tenancy:                    "" => "<computed>"
  vpc_security_group_ids.#:   "" => "<computed>"
aws_instance.test: Creation complete

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

The state of your infrastructure has been saved to the path
below. This state is required to modify and destroy your
infrastructure, so keep it safe. To inspect the complete state
use the `terraform show` command.

State path: terraform.tfstate

# リソース作成後、差分がない事を確認
$ terraform plan
Refreshing Terraform state prior to plan...

aws_instance.test: Refreshing state... (ID: i-aaaaaa)

No changes. Infrastructure is up-to-date. This means that Terraform
could not detect any differences between your configuration and
the real physical resources that exist. As a result, Terraform
doesn't need to do anything.
$ cat terraform.tfstate
{
    "version": 1,
    "serial": 1,
    "modules": [
        {
            "path": [
                "root"
            ],
            "outputs": {},
            "resources": {
                "aws_instance.test": {
                    "type": "aws_instance",
                    "primary": {
                        "id": "i-aaaaaa",
                        "attributes": {
                            "ami": "ami-aaaaaa",
                            "availability_zone": "ap-northeast-1a",
                            "ebs_block_device.#": "0",
                            "ebs_optimized": "false",
                            "ephemeral_block_device.#": "0",
                            "iam_instance_profile": "",
                            "id": "i-aaaaaa",
                            "instance_type": "t2.medium",
                            "key_name": "",
                            "monitoring": "true",
                            "private_dns": "ip-10-0-3-10.ap-northeast-1.compute.internal",
                            "private_ip": "10.0.3.10",
                            "public_dns": "",
                            "public_ip": "",
                            "root_block_device.#": "1",
                            "root_block_device.0.delete_on_termination": "true",
                            "root_block_device.0.iops": "90",
                            "root_block_device.0.volume_size": "30",
                            "root_block_device.0.volume_type": "gp2",
                            "security_groups.#": "1",
                            "security_groups.3448936684": "sg-aaaaaa",
                            "source_dest_check": "true",
                            "subnet_id": "subnet-aaaaaa",
                            "tags.#": "1",
                            "tags.Name": "test_terraform",
                            "tenancy": "default",
                            "vpc_security_group_ids.#": "1",
                            "vpc_security_group_ids.3448936684": "sg-aaaaaa"
                        },
                        "meta": {
                            "schema_version": "1"
                        }
                    }
                }
            }
        }
    ]
}

この状態で、GUIからNameをtest_terraform => test_terraform_hogeへ変更します。そしてterraform planを実行すると、terraformはレシピを正としてGUIから変更した内容を修正しようとします。

$ terraform plan
Refreshing Terraform state prior to plan...

aws_instance.test: Refreshing state... (ID: i-aaaaaa)

The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed.

Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.

~ aws_instance.test
    tags.Name: "test_terraform_hoge" => "test_terraform"

GUIからの変更を正とする場合、以下のようにrefreshを実行しtfstateを現実と揃えた上でレシピを変更する必要があります。

$ git diff
diff --git a/terraform/Pairs/prod/jp/0514_test/ec2.tf b/terraform/Pairs/prod/jp/0514_test/ec2.tf
index da22778..353fe6a 100644
--- a/terraform/Pairs/prod/jp/0514_test/ec2.tf
+++ b/terraform/Pairs/prod/jp/0514_test/ec2.tf
@@ -14,6 +14,6 @@ resource "aws_instance" "test" {
     count                 = 1

     tags {
-        Name   = "test_terraform"
+        Name   = "test_terraform_hoge"
     }
 }
diff --git a/terraform/Pairs/prod/jp/0514_test/terraform.tfstate b/terraform/Pairs/prod/jp/0514_test/terraform.tfstate
index b1cff13..121936f 100644
--- a/terraform/Pairs/prod/jp/0514_test/terraform.tfstate
+++ b/terraform/Pairs/prod/jp/0514_test/terraform.tfstate
@@ -1,6 +1,6 @@
 {
     "version": 1,
-    "serial": 2,
+    "serial": 3,
     "modules": [
         {
             "path": [
@@ -37,7 +37,7 @@
                             "source_dest_check": "true",
                             "subnet_id": "subnet-75e5b402",
                             "tags.#": "1",
-                            "tags.Name": "test_terraform",
+                            "tags.Name": "test_terraform_hoge",
                             "tenancy": "default",
                             "vpc_security_group_ids.#": "1",
                             "vpc_security_group_ids.1193347299": "sg-f708fc93"

この状態でterraform plaを実行すると差分がなくなるので、上記修正を入れた状態でapply、修正分をmasterへ入れpushします。

$ terraform plan
Refreshing Terraform state prior to plan...

aws_instance.test: Refreshing state... (ID: i-7cdf55e3)

No changes. Infrastructure is up-to-date. This means that Terraform
could not detect any differences between your configuration and
the real physical resources that exist. As a result, Terraform
doesn't need to do anything.

$ terraform apply
$ git add ./
$ git push origin master

これで現実との差分が埋まりました。緊急対応などでこうした手動変更は意外と発生したりします。またこの場合、実体が正の場合が多いので、コード側が実体へ追従する必要あります。上記の例だとtag名なので可愛いですが、例えばインスタンスサイズを2x => 4xへstop & startで手動変更とかした場合、実体のサイズとコードで定義してるサイズに乖離があるので、Terraformを実行すると実体サーバ(4x)を削除して
別インスタンスとしてEC2(2x)が立ち上がる 
= サービス環境で稼働してるEC2が突然殺される、みたいなことになります。

まとめ・運用のコツ

  • Terraformで作成したリソースは手動変更しない。
  • PaaS側の制約について把握してないとエラーシューティングでハマる。

他にも多人数開発時のstateファイル管理手法とかも書こうとしましたが、
それはまた次回へ…

次回予告

明日は海藤優弥さんの「PairsでKotlinを採用した5つの理由」です。

  • このエントリーをはてなブックマークに追加

エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!

Recommend

プロトコル指向でもっとに便利に! Alamofireを使ったAPIリクエスト設計

Couplesの開発をスムーズにしてくれるbotの紹介 ~この世の理はすなわち速さだと思いませんか?~