はじめに
TerraformでEC2を作成する時に初回起動時にスクリプトを実行したい場合のUser Dataの記載方法の引継ぎになりますが、
今回は、Terraformを使ってEC2でEFSをマウントする方法について解説します。Terraformを使用して、手軽に環境を構築し、EFSをEC2にマウントする手順をご紹介します。
目次
- 作成するリソース
- 作成するファイル
- 解説
作成するリソース
- VPC
- サブネット
- ルートテーブル
- セキュリティーグループ
- インターネットゲートウェイ
- EFS
- EC2
作成するファイル
- variables.tf
- main.tf
- script.sh
- outputs.tf
variables.tf
#network関連
variable "region" {
default = "us-east-1" # Change this to your desired AWS region
}
variable "vpc_cidr" {
default = "10.0.0.0/16" # Change this to your desired VPC CIDR block
}
variable "public_subnet_cidrs" {
default = ["10.0.1.0/24", "10.0.2.0/24"] # Change these to your desired public subnet CIDR blocks
}
variable "private_subnet_cidrs" {
default = ["10.0.3.0/24", "10.0.4.0/24"] # Change these to your desired private subnet CIDR blocks
}
#EFS関連
variable "subnets" {
description = "List of subnet IDs where EFS mount targets will be created."
type = list(string)
default = ["subnet-12345678", "subnet-87654321"] # Replace with your actual subnet IDs
}
variable "efs_name" {
description = "Name of the Amazon EFS file system."
type = string
default = "your-efs-name" # Replace with your desired EFS name
}
variable "tag" {
description = "Tag for the Amazon EFS file system."
type = string
default = "your-tag" # Replace with your desired tag
}
variable "security_group_id" {
description = "ID of the security group for EFS mount targets."
type = string
default = "sg-0123456789abcdef0" # Replace with your actual security group ID
}
#EC2関連
variable "instance_type" {
default = "t2.micro" # Replace with your desired EC2 instance type
}
variable "availability_zone" {
default = "us-east-1a" # Replace with your desired availability zone
}
variable "subnets" {
default = aws_subnet.private[*].id
}
variable "security_group_ids" {
default = [aws_security_group.ec2.id] # Replace with your actual security group ID
}
variable "key_pair_name" {
default = "YOUR_KEY_PAIR_NAME" # Replace with your actual key pair name
}
variable "efs_dns_name" {
#efsのモジュールのoutputから${var.efs_dns_name} 経由で参照させる
}
main.tf
#network関連
# Create VPC
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "main-vpc"
}
}
# Create internet gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "main-igw"
}
}
# Create public subnets
resource "aws_subnet" "public" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidrs[count.index]
availability_zone = element(data.aws_availability_zones.available.names, count.index)
map_public_ip_on_launch = true
tags = {
Name = "public-subnet-${count.index + 1}"
}
}
# Create private subnets
resource "aws_subnet" "private" {
count = length(var.private_subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.private_subnet_cidrs[count.index]
availability_zone = element(data.aws_availability_zones.available.names, count.index)
map_public_ip_on_launch = false
tags = {
Name = "private-subnet-${count.index + 1}"
}
}
# Create route tables
resource "aws_route_table" "public" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "public-route-table-${count.index + 1}"
}
}
resource "aws_route_table" "private" {
count = length(var.private_subnet_cidrs)
vpc_id = aws_vpc.main.id
tags = {
Name = "private-route-table-${count.index + 1}"
}
}
# Associate subnets with route tables
resource "aws_route_table_association" "public" {
count = length(var.public_subnet_cidrs)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public[count.index].id
}
resource "aws_route_table_association" "private" {
count = length(var.private_subnet_cidrs)
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private[count.index].id
}
# Create security groups
resource "aws_security_group" "ec2" {
vpc_id = aws_vpc.main.id
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Add other ingress/egress rules as needed for your EC2 instances
tags = {
Name = "ec2-security-group"
}
}
resource "aws_security_group" "efs" {
vpc_id = aws_vpc.main.id
ingress {
from_port = 2049
to_port = 2049
protocol = "tcp"
security_group_ids = [aws_security_group.ec2.id] # Allow EC2 instances to access EFS
}
# Add other ingress/egress rules as needed for your EFS
tags = {
Name = "efs-security-group"
}
}
resource "aws_security_group" "load_balancer" {
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Add other ingress/egress rules as needed for your load balancer
tags = {
Name = "load-balancer-security-group"
}
}
#EFS関連
# Amazon EFS file system
resource "aws_efs_file_system" "efs" {
performance_mode = "generalPurpose" # Replace with your desired performance mode
throughput_mode = "bursting" # Replace with your desired throughput mode
encrypted = true
lifecycle_policy {
transition_to_ia = "AFTER_7_DAYS" # Replace with your desired lifecycle transition setting
}
tags = {
Name = "YOUR_EFS_NAME" # Replace with your desired EFS name
Tag = "YOUR_TAG" # Replace with your desired tag
}
}
# Amazon EFS backup policy
resource "aws_efs_backup_policy" "policy" {
file_system_id = aws_efs_file_system.efs.id
backup_policy {
status = "ENABLED" # Replace with "DISABLED" if you don't want to enable backups
}
}
# Amazon EFS mount targets
resource "aws_efs_mount_target" "efs-mount" {
count = length(var.subnets)
file_system_id = aws_efs_file_system.efs.id
subnet_id = element(var.subnets, count.index)
security_groups = [aws_security_group.ec2.id] # Replace with your actual security group ID
}
# Amazon EFS file system policy
resource "aws_efs_file_system_policy" "efa_policy" {
file_system_id = aws_efs_file_system.efs.id
policy = <<-EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["elasticfilesystem:ClientMount"],
"Principal": {
"AWS": "*"
}
}
]
}
EOF
}
#EC2関連
# Create an EC2 instance
data "template_file" "user_data" {
template = "${file("./script.sh")}"
#template = "${file("${path.module}/script.sh")}" # 必要に応じてmoduleディレクトリに対し、shellスクリプトが存在するパスを変えてください。
vars = {
efs_dns_name = "${var.efs_dns_name}"
}
}
resource "aws_instance" "ec2" {
ami = data.aws_ssm_parameter.ec2_ami_id.value
instance_type = var.instance_type
availability_zone = var.availability_zone
disable_api_termination = false
ebs_optimized = false
associate_public_ip_address = true
iam_instance_profile = aws_iam_instance_profile.InstanceProfile.name
hibernation = false
subnet_id = element(var.subnets, 0) # Assuming only one private subnet is used
vpc_security_group_ids = var.security_group_ids
key_name = var.key_pair_name
# EBS block device configuration
ebs_block_device {
device_name = "/dev/sdm"
volume_size = 50
volume_type = "gp2"
encrypted = true
delete_on_termination = true
}
# Tags for the EC2 instance
tags = {
Name = "YOUR_INSTANCE_NAME" # Replace with your desired instance name
Tag = "YOUR_TAG" # Replace with your desired tag
}
user_data = data.template_file.user_data.rendered
}
# IAM Role for the EC2 instance
resource "aws_iam_role" "InstanceRole" {
name = "YOUR_ROLE_NAME" # Replace with your desired IAM role name
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Action = "sts:AssumeRole",
Effect = "Allow",
Principal = {
Service = "ec2.amazonaws.com"
}
}
]
})
managed_policy_arns = [
"arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy",
"arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore",
"arn:aws:iam::aws:policy/AmazonElasticFileSystemFullAccess"
]
tags = {
Tag = "YOUR_TAG" # Replace with your desired tag
}
}
# IAM Instance Profile for the EC2 instance
resource "aws_iam_instance_profile" "InstanceProfile" {
name = "YOUR_INSTANCE_PROFILE_NAME" # Replace with your desired instance profile name
role = aws_iam_role.InstanceRole.name
path = "/"
tags = {
Tag = "YOUR_TAG" # Replace with your desired tag
}
}
script.sh
#!/bin/bash
# アップデートのインストール
sudo yum update -y
# /efs ディレクトリが存在するか確認し、存在しない場合は作成する
if [ ! -d "/efs" ]; then
sudo mkdir /efs
sudo chown ec2-user:ec2-user /efs
fi
# Amazon EFS ユーティリティがインストールされているか確認し、インストールされていない場合はインストールする
if ! command -v mount.efs &> /dev/null; then
sudo yum install -y amazon-efs-utils
fi
# /efs が既にマウントされているか確認し、されていない場合はマウントする
if ! mountpoint -qd /efs; then
# EFS DNS 名をefsのモジュールから${var.efs_dns_name} 経由で参照させる
mount_output=$(sudo mount -t efs ${var.efs_dns_name}:/ /efs/ 2>&1)
if [ $? -eq 0 ]; then
# マウント成功時のメッセージとコマンドの出力をファイルに書き込む
echo -e "EFS successfully mounted on $(date)\n\nMount Command Output:\n$mount_output" | sudo tee /efs/mount_confirmation.txt
else
# マウント失敗時のメッセージとコマンドの出力をファイルに書き込む
echo -e "EFS mount failed on $(date). Check the mount command and EFS configuration.\n\nMount Command Output:\n$mount_output" | sudo tee /efs/mount_confirmation.txt
# もしくは、エラーログを標準エラー出力に表示する
# echo -e "EFS mount failed on $(date). Check the mount command and EFS configuration.\n\nMount Command Output:\n$mount_output" >&2
fi
fi
# ブート時に自動的にマウントされるように /etc/fstab にエントリを追加する
echo "${var.efs_dns_name}:/ /efs efs defaults,_netdev 0 0" | sudo tee -a /etc/fstab
outputs.tf
#Network関連
output "vpc_id" {
value = aws_vpc.main.id
}
output "public_subnet_ids" {
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id
}
output "ec2_security_group_id" {
value = aws_security_group.ec2.id
}
output "efs_security_group_id" {
value = aws_security_group.efs.id
}
output "load_balancer_security_group_id" {
value = aws_security_group.load_balancer.id
}
#EFS関連
output "efs_id" {
description = "ID of the created Amazon EFS file system."
value = aws_efs_file_system.efs.id
}
output "efs_dns_name" {
description = "DNS name of the created Amazon EFS file system."
value = aws_efs_file_system.efs.dns_name
}
output "efs_mount_target_ids" {
description = "IDs of the created Amazon EFS mount targets."
value = aws_efs_mount_target.efs-mount[*].id
}
解説
1. なぜ別のシェルファイル(script.sh)を使用するのか?
Terraformは、インフラストラクチャのコードを管理するためのツールですが、ユーザーデータの直接埋め込みは非効率的であり、可読性が低くなります。そのため、ユーザーデータを別ファイル(ここではscript.sh)に分離することが一般的です。これにより、Terraformのメインコードがシンプルで保守しやすくなります。
2. Bashスクリプト(script.sh)の動作
Bashスクリプト(script.sh)はEC2インスタンス内で実行され、以下の手順を実行します。
a. アップデートのインストール
EC2インスタンスのパッケージを最新の状態に更新します。
b. /efs ディレクトリの作成
/efs ディレクトリが存在しない場合、作成し、適切な権限を設定します。
c. Amazon EFS ユーティリティのインストール
mount.efs がインストールされていない場合、Amazon EFS ユーティリティをインストールします。
d. /efs のマウント
/efs がまだマウントされていない場合、指定されたEFS DNS名を使用してEFSを /efs にマウントします。また、確認ファイルも作成します。
e. ブート時の自動マウント
/etc/fstab にエントリを追加して、EC2インスタンスのブート時にEFSが自動的にマウントされるようにします。
3. Terraformコードのメインファイル(main.tf)
a. ネットワーク関連
Terraformを使用して、VPC、サブネット、ルートテーブル、セキュリティーグループ、インターネットゲートウェイを作成します。
b. EC2関連
EC2インスタンスの作成と設定を行います。Bashスクリプト(script.sh)を使用してEC2インスタンスに必要な構成を適用します。IAMロールやプロファイルも構成されます。
#####c. EFS関連
Amazon EFSファイルシステムと関連するリソース(マウントターゲット、バックアップポリシーなど)を作成します。
4. Terraformの変数(variables.tf)
a. なぜ変数を使用するのか?
変数を使用することで、コードの再利用性が向上し、パラメータを簡単に変更できます。例えば、AWSリージョンやCIDRブロック、インスタンスタイプなどを変更する際に便利です。
b. 特定の変数の注意点
var.efs_dns_name はEFSモジュールのoutputから参照され、EC2インスタンスのユーザーデータに渡されます。これにより、EFSのDNS名、ID、ARNなどの情報を手動で設定する手間が省け、Terraformが自動的にそれらの情報を挿入します。
具体的には、以下の流れがあります:
TerraformがEFSを作成し、そのDNS名が output でEFSモジュールから取得されます。
EC2インスタンスの user_data はBashスクリプト(script.sh)として定義され、その中で${var.efs_dns_name}
として参照されます。
BashスクリプトがEC2インスタンス内で実行される際、${var.efs_dns_name}
は実際のEFSのDNS名に置き換えられます。
これにより、EC2インスタンスは自動的に正しいEFSにマウントされ、手動でDNS名やIDを設定する手間が省かれます。
この仕組みにより、構築プロセスが自動化され、コードのメンテナンスが簡素化されます。手動で入力するリスクが低減し、一貫性が保たれるため、作業効率が向上します。
5. Terraformの出力(outputs.tf)
Terraformの実行後に表示される情報を定義します。例えば、作成したVPCのID、EFSのIDやDNS名、マウントターゲットのIDなどがここで出力されます。
これで、Terraformを使ってEC2でEFSをマウントするTerraformの基本的なセットアップが完了しました。