Terraformを使ってEC2でEFSをマウントする方法

## はじめに
[TerraformでEC2を作成する時に初回起動時にスクリプトを実行したい場合のUser Dataの記載方法](https://qiita.com/S_Hassan/items/57e4619174e24f12a8c0)の引継ぎになりますが、
今回は、Terraformを使ってEC2でEFSをマウントする方法について解説します。Terraformを使用して、手軽に環境を構築し、EFSをEC2にマウントする手順をご紹介します。


## 目次
- 作成するリソース
- 作成するファイル
- 解説

### 作成するリソース
- VPC
- サブネット
- ルートテーブル
- セキュリティーグループ
- インターネットゲートウェイ
- EFS
- EC2

### 作成するファイル
- variables.tf
- main.tf
- script.sh
- outputs.tf


#### variables.tf

```terraform: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

```terraform: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

```bash: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

```terraform: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の基本的なセットアップが完了しました。