Go to list of users who liked
More than 5 years have passed since last update.
Terraform を使うと、EFS を作成して EC2 にマウントさせておくなんてことが簡単にできます。
Autoscaling 環境で Web ドキュメントルートを共有したい時とかに便利なんで、みんな使えばいいと思うよ。
なお、この記事の想定読者は AWS はダッシュボードからポチポチしてインスタンス立てたりしてるけど、そろそろインフラをコードで管理したいな。Terraform とか便利そうだねー使ってみたいねーって人です。
てわけで、単純に EC2 立ち上げても面白くないので EFS をマウントさせてみました。
そもそも、Terraform ってなんだ?って人は、以下のページとか参考になると思います。
Terraform簡易チュートリアル on AWS
実際の設定
Terraform は、特定のディレクトリ下にある拡張子が .tf なファイルを全部読み込んでいい感じにリソースを起動してくれます。なので、機能別に .tf 作成していってみましょう。
メイン設定
まず、メインの設定を作成。
プロバイダーとか、設定ファイル内で使用する変数とか設定していってみましょうか。
# 今回のプロジェクト名variable "project" {}variable "domain" {}# AWS リソースを起動するリージョンとかの情報variable "region" { default = "us-west-2" }variable "azs" { default { "a" = "us-west-2a" "b" = "us-west-2b" "c" = "us-west-2c" }}# AMI ID (Amazon Linux)variable "ami" { default { "us-west-2" = "ami-8ca83fec" }}# EC2 接続用の SSH 鍵の公開鍵variable "ssh_public_key" {}provider "aws" { region = "${var.region}"}
variable で設定した値は tf ファイル内で${var.region}
のようにして参照可能です。
また、terraform の各種コマンドを実行する際に以下のようにパラメータとして変数を渡して上書きすることもできます。
$terraform plan\-var'project=example'\-var'domain=example.com'
同じディレクトリ内にterraform.tfvars
というファイルがあれば、それを読み込んで値が上書きされたりします。この辺の詳細は以下を参照してください。
Input Variables - Terraform by HashiCorp
provider "aws"
は、aws を使いますよって宣言です。
以下のように アクセスキーを書いておくこともできますが、それやるとうっかり github とかに公開した時とかに切ない目にあうのでやめたほうが吉でしょう。
provider "aws" { access_key = "__ACCESS_KEY__" secret_key = "__SECRET_KEY__" region = "us-west-2"}
環境変数AWS_ACCESS_KEY_ID
,AWS_SECRET_ACCESS_KEY
を読み込んでいい感じでやってくれるので、僕はdirenv 使って作業ディレクトリ内で環境変数を変更することで対応してます。
(もちろん、この場合でも.gitignore
に.envrc
を含めておいて間違って公開しないようにしないと切ない目にあうので注意)
VPC の作成
こんな感じの .tf ファイルで VPC と subnet が作成できます。
## VPCresource "aws_vpc" "app" { cidr_block = "172.31.0.0/16" enable_dns_hostnames = true enable_dns_support = true instance_tenancy = "default" tags { "Name" = "${var.project}" }}## Subnetresource "aws_subnet" "a" { vpc_id = "${aws_vpc.app.id}" cidr_block = "172.31.0.0/20" availability_zone = "${lookup(var.azs,"a")}" map_public_ip_on_launch = true tags { "Name" = "${var.project}-subnet-a" }}resource "aws_subnet" "b" { vpc_id = "${aws_vpc.app.id}" cidr_block = "172.31.16.0/20" availability_zone = "${lookup(var.azs,"b")}" map_public_ip_on_launch = true tags { "Name" = "${var.project}-subnet-b" }}resource "aws_subnet" "c" { vpc_id = "${aws_vpc.app.id}" cidr_block = "172.31.32.0/20" availability_zone = "${lookup(var.azs,"c")}" map_public_ip_on_launch = true tags { "Name" = "${var.project}-subnet-c" }}
resource "aws_subnet"
の中に${aws_vpc.app.id}
ってのが出てきましたね。
Terraform の中では、管理下にあるリソースの情報を他のリソースの設定でも参照することが可能です。
各リソースで使用できる値が異なってくるので、その辺は公式ドキュメント読みましょう。
例えばaws_vpc
で使用できる値はaws_vpc を参照すればわかります。
また、${lookup(var.azs,"a")}
ってのも出てきましたね。
これは Terraform の組み込み関数です、lookup
は配列の中からキーをもとに値を探す関数です。
詳しくはBuilt-in Functions を読んでください。
ついでに Internet Gateway と Route Table も設定しておきましょう。
## Internet Gatewayresource "aws_internet_gateway" "igw" { vpc_id = "${aws_vpc.app.id}"}## Route Tableresource "aws_route_table" "rtb" { vpc_id = "${aws_vpc.app.id}" route { cidr_block = "0.0.0.0/0" gateway_id = "${aws_internet_gateway.igw.id}" }}resource "aws_route_table_association" "route_a" { subnet_id = "${aws_subnet.a.id}" route_table_id = "${aws_route_table.rtb.id}"}resource "aws_route_table_association" "route_b" { subnet_id = "${aws_subnet.b.id}" route_table_id = "${aws_route_table.rtb.id}"}resource "aws_route_table_association" "route_c" { subnet_id = "${aws_subnet.c.id}" route_table_id = "${aws_route_table.rtb.id}"}
IAM ロールの作成
次に EC2 に割り当てるための IAM ロールを作ってみましょう。
ポリシーは、AWS が用意している AmazonEC2RoleforDataPipelineRole と、EC2 から CloudwatchLogs にログを送信するためのカスタムポリシーを作ってアタッチしてみます。
## For EC2 instance Roleresource "aws_iam_role" "instance_role" { name = "instance_role" path = "/" assume_role_policy = <<POLICY{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "ec2.amazonaws.com" }, "Action": "sts:AssumeRole" } ]}POLICY}## AmazonEC2RoleforDataPipelineRoleresource "aws_iam_role_policy_attachment" "data-pipeline" { role = "${aws_iam_role.instance_role.name}" policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforDataPipelineRole"}## PutCloudwatchLogsresource "aws_iam_policy" "put-cloudwatch-logs" { name = "AmazonEC2PutCloudwatchLogs" description = "" policy = <<POLICY{ "Version": "2012-10-17", "Statement": [ { "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Effect": "Allow", "Resource": "*" } ]}POLICY}resource "aws_iam_role_policy_attachment" "put-cloudwatch-logs" { role = "${aws_iam_role.instance_role.name}" policy_arn = "${aws_iam_policy.put-cloudwatch-logs.arn}"}
aws_iam_role
のassume_role_policy
のところと、aws_iam_policy
のpolicy
のところでヒアドキュメントが出てきましたね。
こんな風に複数行にわたるインラインポリシーはヒアドキュメントで記述することが可能です。
また、以下のように別ファイルにしておいて読み込ませることも可能です。
管理しやすい方でやってください。
resource "aws_iam_role" "instance_role" { name = "instance_role" path = "/" assume_role_policy = "${file("data/instance_role_assume_policy.json")}"}
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}
セキュリティグループの作成
EC2 から EFS へのアクセスは 2049 番ポートを介して行われるので、EFS が所属するセキュリティグループに穴を開けないといけません。
EC2 は 80, 443, 22 を解放してみます。
## For EC2resource "aws_security_group" "ec2" { name = "${var.project}-EC2" description = "for ${var.project} EC2" vpc_id = "${aws_vpc.app.id}" ingress = [ { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] }, { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] }, { from_port = 22 to_port = 22 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } ] egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] }}## For EFSresource "aws_security_group" "efs" { name = "${var.project}-EFS" description = "for ${var.project} EFS" vpc_id = "${aws_vpc.app.id}" ingress { from_port = 2049 to_port = 2049 protocol = "tcp" security_groups = ["${aws_security_group.ec2.id}"] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] }}
EFS の作成
こんな感じで EFS が作成できます。
各サブネットごとにマウントターゲットを作成して、そいつをセキュリティグループに所属させる形ですね。
resource "aws_efs_file_system" "app" { tags { "Name" = "${var.domain}" }}resource "aws_efs_mount_target" "app-a" { file_system_id = "${aws_efs_file_system.app.id}" subnet_id = "${aws_subnet.a.id}" security_groups = ["${aws_security_group.efs.id}"]}resource "aws_efs_mount_target" "app-b" { file_system_id = "${aws_efs_file_system.app.id}" subnet_id = "${aws_subnet.b.id}" security_groups = ["${aws_security_group.efs.id}"]}resource "aws_efs_mount_target" "app-c" { file_system_id = "${aws_efs_file_system.app.id}" subnet_id = "${aws_subnet.c.id}" security_groups = ["${aws_security_group.efs.id}"]}
EC2 の作成
さて、いよいよ EC2 です。
ここでは、user-data を使って、初回ローンチ時に EFS をマウントさせてしまいます。
さらにマウントした EFS 内にhtml/
ってディレクトリを作成して、そいつを/var/www/html
にシンボリックリンクしてみましょうか。
と言っても、こんな感じで大丈夫です。
## IAM Instance Profileresource "aws_iam_instance_profile" "instance_role" { name = "instance_role" role = "${aws_iam_role.instance_role.name}"}## SSH Keyresource "aws_key_pair" "deployer" { key_name = "${var.project}" public_key = "${var.ssh_public_key}"}## EC2resource "aws_instance" "app" { ami = "${lookup(var.ami,var.region)}" availability_zone = "${aws_subnet.a.availability_zone}" ebs_optimized = false instance_type = "t2.micro" monitoring = true key_name = "${aws_key_pair.deployer.key_name}" subnet_id = "${aws_subnet.a.id}" vpc_security_group_ids = ["${aws_security_group.ec2.id}"] associate_public_ip_address = true source_dest_check = true iam_instance_profile = "${aws_iam_instance_profile.instance_role.id}" disable_api_termination = false user_data = <<USERDATA# !/bin/bashaz="${aws_subnet.a.availability_zone}"efs_region="${var.region}"efs_id="${aws_efs_file_system.app.id}"efs_mount_target="${aws_efs_mount_target.app-a.dns_name}:/"efs_mount_point="/mnt/efs/$${efs_id}/$${az}"web_doc_root="/var/www/html"# EFS Mount/usr/bin/yum -y install nfs-utils || /usr/bin/yum -y update nfs-utilsif [ ! -d $${efs_mount_point} ]; then mkdir -p $${efs_mount_point}ficp -pi /etc/fstab /etc/fstab.$(date "+%Y%m%d")echo "$${efs_mount_target} $${efs_mount_point} nfs4 defaults" | tee -a /etc/fstabmount $${efs_mount_point}# create Web document rootif [ -d $${web_doc_root} ]; then rm -rf $${web_doc_root}fiif [ ! -d $${efs_mount_point}/html ]; then mkdir $${efs_mount_point}/html chown ec2-user:ec2-user $${efs_mount_point}/htmlfiln -s $${efs_mount_point}/html $${web_doc_root}chown -h ec2-user:ec2-user $${web_doc_root}USERDATA root_block_device { volume_type = "gp2" volume_size = 10 delete_on_termination = true } tags { "Name" = "${var.domain}" }}
user_data は長めのシェルスクリプトなので、可読性が悪いから${file("data/user_data.sh")}
とかってやって別ファイルで管理したいですよね。
でも待ってください、ヒアドキュメントでやってるのは理由があるのです。
ヒアドキュメントで書くと、user_data 用のシェルスクリプトの中で Terraform の変数が使えます。
マウントするには EFS の ID とか、マウントターゲットの dns_name とか必要になってきますが、それを作成前に知らなくてもこのように書いておけるのです。便利ですね。
その代わり、user_data 用のシェルスクリプト内でローカルな環境変数を使いたい場合は$${efs_mount_point}
のように書いてあげてくださいね。
ざっと、こんな感じです。
慣れちゃえば、tf ファイルを使い回しできるので便利ですよ。
また、すでに作成済みの AWS リソースを Terraform 管理下に置きたい場合は
$terraform import aws_instance.app${instance_id}
のようにして管理下に置くことができます。
管理されているリソースはterraform.tfstate
というファイルに書き込まれます。
さらに別プロダクトになるのですがTerraforming と言うツールを使用すると、既存の AWS リソースから Terraform 用の tf ファイルを作成したり、terraform.tfstate を作成したりもできるので便利です。
Terraforming については、Terraforming で既存のインフラを Terraform 管理下におく を参考にしてください。
実際にリソース作成してみる
tf ファイル書いたら
$terraform plan
で、設定ファイルに誤りがないか?既存のリソースへの影響はどの程度あるのかが確認できます。
実際に反映させたい時は
$terraform apply
で、おっけ。
では、良い Terraform を!
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme