Terraform

Terraformのリファクタリング: リモートのアカウントID参照

Terraformのリファクタリング楽しいですね。ついついいろんなハードコードを変数化するのに熱中してしまいます。

AWSをTerraformで構築・運用するときAWSのアカウントIDが必要になることがあります。 特にVPC Peering、Private Link, SNS, S3のBucket Policyなど、クロスアカウントで利用できるリソースなどは別アカウントのIDが欲しくなる事があります。 一つならまだしも2つ以上アカウントIDと連携したりすると混乱するので、ここにハードコードしたIDではなくわかりやすい変数名、しかもリモートステートで別アカウントのIDが参照できれば良いなと思いました。

自アカウントであれば、aws_caller_identityを利用すれば簡単に取得することができます。

1
data "aws_caller_identity" "self" { }

どこかに上記のような記述をすると、下記のような形でアカウントIDを参照できます。

1
"${data.aws_caller_identity.self.account_id}"

これにリモートステートの仕組みをプラスすることで、別アカウントのIDを変数化できました。 参照される側のアカウントをA, 参照する側のアカウントをBとしています。

参照される側のアカウントの設定

まずは参照されるアカウントAの設定です。 別アカウントBに変数を公開するにはoutputとして登録する必要があります。 今回は変数名をaccount_idとして、AWSのアカウントIDを公開しています。

1
2
3
4
5
data "aws_caller_identity" "self" {}

output "account_id" {
  value = "${data.aws_caller_identity.self.account_id}"
}

また、S3のバケットポリシーを編集して、TerraformのStateをアカウントBが閲覧できるように設定をする必要があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "ExampleStatement1",
            "Effect": "Allow",
            "Principal": {
                "AWS": [
                    "arn:aws:iam::[アカウントBのID]:root",
                ]
            },
            "Action": [
                "s3:GetBucketLocation",
                "s3:ListBucket",
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::[アカウントAのStateが入っているBucket]",
                "arn:aws:s3:::[アカウントAのStateが入っているBucket]"/*"
            ]
        }
    ]
}

参照される側のアカウントの設定

参照する側のアカウントBでは参照するアカウントAのStateを参照する設定を追加します。

1
2
3
4
5
6
7
8
9
data "terraform_remote_state" "account_a" {
  backend = "s3"

  config {
    bucket = "[アカウントAのStateが入っているBucket]"
    key    = "state"
    region = "ap-northeast-1"
  }
}

例えばアカウントAとBのVPC Peeringの設定とかで利用できます。

1
2
3
4
5
6

resource "aws_vpc_peering_connection" "acount_a_b" {
  peer_owner_id = "${data.terraform_remote_state.account_a.account_id}"
  peer_vpc_id   = "[アカウントAのVPC ID]"
  vpc_id        = "[アカウントBのVPC ID]"
}

アカウントAのVPC IDもリモートステートで取得する手もありますが、今回は割愛しました。

まとめ

Terraformを書いていていろんなアカウントと連携する設定を書くときに、アカウントID部分が数字の羅列だと混乱するので変数化してみました。 クロスアカウントの機能は充実していますし機能拡張もされ続けています。 何かと使い所はあるのではないでしょうか。

TerraformでSnapshotからAMI作って立ち上げる

独自のAMIを作ってたら、どうもうまく行かず。Try and Errorの様相を呈してきた。 何度もSnapshot作ってAMIを作ってLaunchってやり始めたので、Terraformでこの辺をやるようにした。

ただ、EBSボリュームからSnapshotを作る部分は見つけられなかったので、Snapshotを作るところは手動です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
resource "aws_ami" "origin-base" {
    name = "origin-base-20161124"
    virtualization_type = "hvm"
    root_device_name = "/dev/sda1"

    ebs_block_device {
        device_name = "/dev/sda1"
        snapshot_id = "snap-60559eee"
        volume_size = 10
    }
}

resource "aws_instance" "gside-origin" {
    ami                         = "${aws_ami.origin-base.id}"
    availability_zone           = "${aws_subnet.main.availability_zone}"
    ebs_optimized               = false
    associate_public_ip_address = false
    instance_type               = "t2.nano"
    monitoring                  = false
    key_name                    = "${aws_key_pair.gside-key.key_name}"
    vpc_security_group_ids      = ["${aws_security_group.basic.id}"]
    associate_public_ip_address = true
    disable_api_termination     = "true"
    source_dest_check           = "false"
    subnet_id 			= "${aws_subnet.main.id}"

    root_block_device {
        volume_type           = "gp2"
        volume_size           = 10
        delete_on_termination = true
    }

    tags {
        "Name" = "gside-origin"
    }
}

Terraformで追加のEBSをインスタンスにAttacheする

とあるインスタンスに追加でEBS VolumeをAttacheしたかったのですが、 現在のTerraform(Ver 0.7.9)では新規にインスタンスを作成し直さないとできないようです。 残念。

ebs_block_deviceの箇所で追加のEBSについて記載しています。 サイズとマウントポイントを記載するだけ。 簡単なんだけどやはりインスタンス作りなおしなのは惜しいですね。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
resource "aws_instance" "gside" {
    ami                         = "ami-0c11b26d"
    availability_zone           = "${aws_subnet.main.availability_zone}"
    ebs_optimized               = false
    associate_public_ip_address = false
    instance_type               = "t2.nano"
    monitoring                  = false
    key_name                    = "${aws_key_pair.gside-key.key_name}"
    vpc_security_group_ids      = ["${aws_security_group.basic.id}"]
    associate_public_ip_address = true
    private_ip                  = "10.0.1.10"
    disable_api_termination     = "true"
    source_dest_check           = "false"
    subnet_id 			= "${aws_subnet.main.id}"

    root_block_device {
        volume_type           = "gp2"
        volume_size           = 10
        delete_on_termination = true
    }

    ebs_block_device {
	device_name = "/dev/xvdb"
        volume_size           = 10
    }

    tags {
        "Name" = "gside"
    }
}

インスタンスにログインしてディスクの状況を確認します。

1
2
3
4
5
[ec2-user@ip-10-0-1-10 ~]$ lsblk
NAME    MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
xvda    202:0    0  10G  0 disk
└─xvda1 202:1    0  10G  0 part /
xvdb    202:16   0  10G  0 disk

sshの鍵をTerraformで扱う

AMIを作る時に必要なSSHのキーですが、 Terraformでは鍵のインポートのみサポートしています。

確かに秘密鍵をダウンロードするより、公開鍵をアップロードするほうが健全ですね。

鍵の作成

手元のPCで秘密鍵と公開鍵のペアを作成します。

1
2
3
4
5
6
7
8
$ ssh-keygen -t rsa -b 2048
Generating public/private rsa key pair.
Enter file in which to save the key
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in
Your public key has been saved in
The key fingerprint is:

Terraformで公開鍵をアップロード

リソースにaws_key_pairを使って、公開鍵をアップロードします。 key_nameの参照には${aws_key_pair.gside-key.key_name}を使います。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
resource "aws_key_pair" "gside-key" {
  key_name = "gside-key"
  public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCvGSftV0pe4Pu4AA6CIwZ5QwUnVmO1YZ6LnkUuY1oti0OBuwNhvKE2gJ7eELwUmXLixq5OsccItAeUyIstp8u86AJqaO4DeZBE6gHwaBlrKKG+0b0jFsNCtfFu/jFsmnTuED5I/MpggUk0NKV4BFveqX9Wi7fxaOt5XEsx4XR9mJD+RrtrVAuzSoSK3y3jJgLKpBku1TqcaPZutE6fHIE6OalPRY0JCrN9WzQmFWXL+whTe9KaPMKs6PdHeFG+KBpnY9VjQxxc+lPmMfcID1t/xAuYpi5TZQbtB+YH5Qrn4uz+v1FyL9N/GYmcX8dVz9d9HMgXDUgzgEZU3JpWd6uf gside"
}

resource "aws_instance" "gside" {
    ami                         = "ami-0c11b26d"
    availability_zone           = "${aws_subnet.main.availability_zone}"
    ebs_optimized               = false
    associate_public_ip_address = false
    instance_type               = "t2.nano"
    monitoring                  = false
    key_name                    = "${aws_key_pair.gside-key.key_name}"
    vpc_security_group_ids      = ["${aws_security_group.basic.id}"]
    associate_public_ip_address = true
    private_ip                  = "10.0.1.10"
    disable_api_termination     = "true"
    source_dest_check           = "false"
    subnet_id 			= "${aws_subnet.main.id}"

    root_block_device {
        volume_type           = "gp2"
        volume_size           = 10
        delete_on_termination = true
    }

    tags {
        "Name" = "gside"
    }
}

まとめ

これで秘密鍵をダウンロードする気持ち悪さからもおさらばです。

Route53のHealthをTerraformで設定してみる

先日入門したTerraform、Route53のURL監視も入れてみました。 ポイントとしては、Route53のCloudWatchアラームはN.Virginiaのリージョンで作る必要がある点です。 それ以外のリージョンは現在はサポートされていません。

Route53のヘルスチェックを作成する

Route53のヘルスチェックを作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
resource "aws_route53_health_check" "gside" {
  fqdn = "gside.org"
  port = 80
  type = "HTTP"
  resource_path = "/blowg/b"
  failure_threshold = "3"
  request_interval = "30"

  tags = {
    Name = "gside"
   }
}

Cloudwatchアラームを作成する

前述したように、N.VirginiaのリージョンにClouwdWatchアラームを作成します。 dimensionsには先程作成したRoute53のヘルスチェックのIDを指定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
provider "aws" {
    region = "us-east-1"
    alias = "virginia"
}

resource "aws_cloudwatch_metric_alarm" "gside-healthcheck" {
    provider      = "aws.virginia"
    alarm_name = "gside-healthcheck"
    comparison_operator = "GreaterThanThreshold"
    evaluation_periods = "1"
    metric_name = "HealthCheckStatus"
    namespace = "AWS/Route53"
    period = "60"
    statistic = "Minimum"
    threshold = "1"
    alarm_description = "This metric monitor gside url healthcheck"
    dimensions {
        HealthCheckId="${aws_route53_health_check.gside.id}"
	}
    alarm_actions = ["arn:aws:sns:xxxxxxx"]
}

まとめ

ClouwdWatchアラームをN.Virgnia以外で作成して、なかなかRoute53ヘルスチェックと関連づかずハマりましたが、 それ以外は問題なく作成できる内容でした。 ちなみにアラート時のEmail送信用SNSを作るところもTerraform化しようとしましたが、 Emailは送信者認証が入るところがTerraformのモデルに合わず未サポートだそうです。

Terraform入門してみた

まずはこのBlogをホストしている環境をTerrraorrm化してみました。 いまだにClassic EC2を使ってたので、そろそろ再構築したかったんですよね。

インストール

https://www.terraform.io/downloads.html ダウンロードしたファイルを解凍して、terraformファイルにパスを通しておきます。

構成

1つのVPCに1つのsubnetを割り振って、その中でインスタンスを起動します。 ELBは使わず、EIPを割り振ります。

実行

アクセスキーとシークレットキーを環境変数に設定します。

1
2
3
$ export AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXXXXXXXX
$ export AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
$ export AWS_DEFAULT_REGION=ap-northeast-1
1
$ terraform plan

で変更箇所を確認して、

1
$ terraform apply

で実行します。

terraform.tfstateの管理

terraformはterraform.tfstateというファイルで、対象のインフラ構成の状態を管理しています。 実際のインフラ構成の状態をこのファイルで管理しているので、重要なファイルです。

こいつはterraformを実行するどの環境でも同じ物である必要があります。 というわけで、こいつをS3で管理してみます。

1
2
3
4
5
$ aws s3 mb s3://gside-terraform-state
make_bucket: s3://gside-terraform-state/
$ terraform remote config -backend=S3 -backend-config="bucket=gside-terraform-state" -backend-config="key=terraform.tfstate"
Remote configuration updated
Remote state configured and pulled.

この設定の後は、S3でterraform.tfstateファイルを管理しつつ、特にS3を意識することなくterraformが使えます。

まとめ

拍子抜けするくらい簡単にterraform化が終わってしまいました。 既存の環境をimportする機能もついたということで、ますます利用するシーンが増えそうです。

参考に今回実行したコードを載せておきます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
resource "aws_vpc" "main" {
    cidr_block = "10.0.0.0/16"
}

resource "aws_internet_gateway" "gw" {
    vpc_id = "${aws_vpc.main.id}"

    tags {
        Name = "main"
    }
}

resource "aws_subnet" "main" {
    vpc_id = "${aws_vpc.main.id}"
    cidr_block = "10.0.1.0/24"

    tags {
        Name = "Main"
    }
}

resource "aws_route_table" "r" {
    vpc_id = "${aws_vpc.main.id}"
    route {
        cidr_block = "0.0.0.0/0"
        gateway_id = "${aws_internet_gateway.gw.id}"
    }

    tags {
        Name = "main"
    }
}

resource "aws_route_table_association" "a" {
    subnet_id = "${aws_subnet.main.id}"
    route_table_id = "${aws_route_table.r.id}"
}

resource "aws_security_group" "basic" {
    name        = "basic"
    description = "basic security group for web"
    vpc_id      = "${aws_vpc.main.id}"

    ingress {
        from_port       = 22
        to_port         = 22
        protocol        = "tcp"
        cidr_blocks     = ["0.0.0.0/0"]
    }

    ingress {
        from_port       = 80
        to_port         = 80
        protocol        = "tcp"
        cidr_blocks     = ["0.0.0.0/0"]
    }

    ingress {
        from_port       = 443
        to_port         = 443
        protocol        = "tcp"
        cidr_blocks     = ["0.0.0.0/0"]
    }


}

resource "aws_instance" "gside" {
    ami                         = "ami-c1fe26a2"
    availability_zone           = "${aws_subnet.main.availability_zone}"
    ebs_optimized               = false
    associate_public_ip_address = false
    instance_type               = "t1.micro"
    monitoring                  = false
    key_name                    = "ec2Key"
    vpc_security_group_ids      = ["${aws_security_group.basic.id}"]
    associate_public_ip_address = true
    private_ip                  = "10.0.1.10"
    disable_api_termination     = "true"
    source_dest_check           = "false"
    subnet_id 			= "${aws_subnet.main.id}"

    root_block_device {
        volume_type           = "gp2"
        volume_size           = 10
        delete_on_termination = true
    }

    tags {
        "Name" = "gside"
    }
}

resource "aws_eip" "gside" {
  vpc = true

  instance                  = "${aws_instance.gside.id}"
  associate_with_private_ip = "10.0.1.10"
}