7 minute read

Amazon Elastic File System (EFS) is a fully managed, scalable file storage service designed to be shared across multiple compute resources including Amazon EC2, Amazon ECS, Amazon EKS, AWS Lambda, and AWS Fargate. Unlike Amazon EBS, which is typically attached to a single instance (with limited multi-attach support), EFS is built from the ground up to support multiple EC2 instances mounting the same filesystem simultaneously.

In this article, i’ll demonstrate how we can mount EFS to multiple instances.

Architecture Overview

The following architecture is used to mount EFS to multiple EC2.

Foo
Mount EFS to multiple instances

Prerequisites

  • VPC, Three subnet (In here i will use public subnet), 1 IGW, 1 RTB
  • 3 Security Group. ALB SG (Allow HTTP, HTTPS), EC2 SG (Allow HTTP from ALB SG, SSH), EFS SG (Allow NFS from EC2 SG)
  • Three EC2 Instance in different AZ (You can provision only two also)
  • 1 EFS Volume

Terraform Implementation

VPC, Subnet IGW, and RTB


# ====================================================================================================
# Multi-Attaching Elastic File System (EFS) Volumes to EC2 with Application Load Balancers
# ====================================================================================================

provider "aws" {
  region  = "ap-southeast-1"
  profile = "default"

  default_tags {
    tags = {
      Owner   = "nugroho-L1"
      Project = "Testing"
    }
  }
}


# ====================================================================================================
# VPC and Network Infra
# ====================================================================================================

resource "aws_vpc" "main" {
  cidr_block           = "192.168.0.0/16"
  instance_tenancy     = "default"
  enable_dns_hostnames = true

  tags = {
    Name = "nugroho-vpc"
  }
}

# Subnets
resource "aws_subnet" "a" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "192.168.1.0/24"
  availability_zone = "ap-southeast-1a"

  tags = {
    Name = "Public-Subnet-A-nugroho"
  }
}

resource "aws_subnet" "b" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "192.168.2.0/24"
  availability_zone = "ap-southeast-1b"

  tags = {
    Name = "Public-Subnet-B-nugroho"
  }
}

resource "aws_subnet" "c" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "192.168.3.0/24"
  availability_zone = "ap-southeast-1c"

  tags = {
    Name = "Public-Subnet-C-nugroho"
  }
}

# Internet Gateway
resource "aws_internet_gateway" "gw" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "main-igw-nugroho"
  }
}

# Route Table
resource "aws_route_table" "main" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0" # Route to all external IPs
    gateway_id = aws_internet_gateway.gw.id
  }

  tags = {
    Name = "main-route-table-nugroho"
  }
}

# Route Table Associations
resource "aws_route_table_association" "a" {
  subnet_id      = aws_subnet.a.id
  route_table_id = aws_route_table.main.id
}

resource "aws_route_table_association" "b" {
  subnet_id      = aws_subnet.b.id
  route_table_id = aws_route_table.main.id
}

resource "aws_route_table_association" "c" {
  subnet_id      = aws_subnet.c.id
  route_table_id = aws_route_table.main.id
}

Security Group


# ====================================================================================================
# Security Groups
# ====================================================================================================

# Security Group for ALB
resource "aws_security_group" "alb-sec-group" {
  name        = "allow_http_https"
  description = "Allow HTTP and HTTPS inbound traffic from anywhere"
  vpc_id      = aws_vpc.main.id

  tags = {
    Name = "alb-sec-group-nugroho"
  }
}

# Allow inbound HTTP traffic (port 80) from anywhere
resource "aws_security_group_rule" "allow_http_alb" {
  type              = "ingress"
  from_port         = 80
  to_port           = 80
  protocol          = "tcp"
  security_group_id = aws_security_group.alb-sec-group.id
  cidr_blocks       = ["0.0.0.0/0"]
}

# Allow inbound HTTPS traffic (port 443) from anywhere
resource "aws_security_group_rule" "allow_https_alb" {
  type              = "ingress"
  from_port         = 443
  to_port           = 443
  protocol          = "tcp"
  security_group_id = aws_security_group.alb-sec-group.id
  cidr_blocks       = ["0.0.0.0/0"]
}

# Allow all outbound traffic
resource "aws_security_group_rule" "allow_all_outbound_alb" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  security_group_id = aws_security_group.alb-sec-group.id
  cidr_blocks       = ["0.0.0.0/0"]
}


# Security Group for EC2 or Target Group
resource "aws_security_group" "ec2-sec-group" {
  name        = "allow_from-alb"
  description = "Allow HTTP traffic from ALB and SSH from specific IP"
  vpc_id      = aws_vpc.main.id

  tags = {
    Name = "ec2-sec-group-nugroho"
  }
}

# Allow inbound HTTP traffic (port 80) from ALB only
resource "aws_security_group_rule" "allow_http_from_alb" {
  type                     = "ingress"
  from_port                = 80
  to_port                  = 80
  protocol                 = "tcp"
  security_group_id        = aws_security_group.ec2-sec-group.id
  source_security_group_id = aws_security_group.alb-sec-group.id
}

# Allow inbound SSH traffic (port 22) from a specific IP
resource "aws_security_group_rule" "allow_ssh" {
  type              = "ingress"
  from_port         = 22
  to_port           = 22
  protocol          = "tcp"
  security_group_id = aws_security_group.ec2-sec-group.id
  cidr_blocks       = ["0.0.0.0/0"] # Replace with your actual public IP
}

# Allow all outbound traffic
resource "aws_security_group_rule" "allow_all_outbound_ec2" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  security_group_id = aws_security_group.ec2-sec-group.id
  cidr_blocks       = ["0.0.0.0/0"]
}


# Security Group for EFS
resource "aws_security_group" "efs-sec-group" {
  name        = "allow_from-ec2"
  description = "Allow TCP traffic on port 2049 from EC2 instances"
  vpc_id      = aws_vpc.main.id

  tags = {
    Name = "efs-sec-group-nugroho"
  }
}

# Allow inbound 2049 from EC2 security group only
resource "aws_security_group_rule" "allow_nfs_from_ec2" {
  type                     = "ingress"
  from_port                = 2049
  to_port                  = 2049
  protocol                 = "tcp"
  security_group_id        = aws_security_group.efs-sec-group.id
  source_security_group_id = aws_security_group.ec2-sec-group.id
}

# Allow all outbound traffic
resource "aws_security_group_rule" "allow_all_outbound_efs" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  security_group_id = aws_security_group.efs-sec-group.id
  cidr_blocks       = ["0.0.0.0/0"]
}

EC2 Instances


# ====================================================================================================
# Launch an EC2 Instance
# ====================================================================================================

# Get latest Ubuntu AMI
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"] # The official Ubuntu AMI owner ID, if the owner using amazon, then it can't connect.

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-*-amd64-server-*"]
  }
}

# Define a map of subnets and availability zones dynamically using locals
locals {
  subnets = {
    a = aws_subnet.a.id
    b = aws_subnet.b.id
    c = aws_subnet.c.id
  }
}

# Create the Ubuntu EC2 Web server
resource "aws_instance" "web" {
  depends_on                  = [aws_efs_file_system.efs_file_system]
  for_each                    = local.subnets
  ami                         = data.aws_ami.ubuntu.id
  instance_type               = "t3.micro"
  key_name                    = "nugroho-msikp"
  subnet_id                   = each.value
  associate_public_ip_address = true
  vpc_security_group_ids      = [aws_security_group.ec2-sec-group.id]

  tags = {
    Name = "web-server-apache-${each.key}-nugroho"
  }

  user_data = <<-EOF
  #!/bin/bash
  sudo apt-get update -y
  sudo apt-get install apache2 -y
  sudo systemctl start apache2
  sudo systemctl enable apache2

  sudo apt-get install -y amazon-efs-utils

  # Create the directory for EFS mount
  sudo mkdir /efs

  # Mount the EFS
  sudo mount -t efs -o tls ${aws_efs_file_system.efs_file_system.id}:/ /efs

  # Add EFS entry to /etc/fstab for auto-mount on reboot
  echo "${aws_efs_file_system.efs_file_system.id}:/ /mnt/efs efs _netdev,tls 0 0" | sudo tee -a /etc/fstab

  # Get EC2 metadata
  instanceId=$(curl http://169.254.169.254/latest/meta-data/instance-id)
  instanceAZ=$(curl http://169.254.169.254/latest/meta-data/placement/availability-zone)
  pubHostName=$(curl http://169.254.169.254/latest/meta-data/public-hostname)
  pubIPv4=$(curl http://169.254.169.254/latest/meta-data/public-ipv4)
  privHostName=$(curl http://169.254.169.254/latest/meta-data/local-hostname)
  privIPv4=$(curl http://169.254.169.254/latest/meta-data/local-ipv4)

  # Output instance metadata and EFS information
  echo "<font face = 'Verdana' size = '5'>"                               > /var/www/html/index.html
  echo "<center><h1>AWS Ubuntu VM Deployed with Terraform</h1></center>"   >> /var/www/html/index.html
  echo "<center> <b>EC2 Instance Metadata</b> </center>"                  >> /var/www/html/index.html
  echo "<center> <b>Instance ID:</b> $instanceId </center>"                      >> /var/www/html/index.html
  echo "<center> <b>AWS Availability Zone:</b> $instanceAZ </center>"             >> /var/www/html/index.html
  echo "<center> <b>Public Hostname:</b> $pubHostName </center>"                 >> /var/www/html/index.html
  echo "<center> <b>Public IPv4:</b> $pubIPv4 </center>"                         >> /var/www/html/index.html
  echo "<center> <b>Private Hostname:</b> $privHostName </center>"               >> /var/www/html/index.html
  echo "<center> <b>Private IPv4:</b> $privIPv4 </center>"                       >> /var/www/html/index.html
  echo "<center><b>EFS File System ID:</b> ${aws_efs_file_system.efs_file_system.id}</center>" >> /var/www/html/index.html
  echo "</font>"                                                          >> /var/www/html/index.html
  EOF


}


Application Load Balancer and Target Group


# ====================================================================================================
# Application Load Balancer
# ====================================================================================================
resource "aws_lb" "aws-application_load_balancer" {
  name                       = "app-load-balancer-nugroho"
  internal                   = false
  load_balancer_type         = "application"
  security_groups            = [aws_security_group.alb-sec-group.id]
  subnets                    = values(local.subnets) # All subnets
  enable_deletion_protection = false

  tags = {
    Name = "Application-Load-Balancer-nugroho"
  }
}


# ====================================================================================================
# Target Group
# ====================================================================================================

resource "aws_lb_target_group" "app_tg" {
  name        = "app-target-group-nugroho"
  port        = 80     # Target Group port
  protocol    = "HTTP" # Protocol to communicate with targets
  vpc_id      = aws_vpc.main.id
  target_type = "instance" # Target type (can be "ip" or "lambda" as well)

  health_check {
    enabled             = true
    interval            = 30 # Check every 30 seconds
    path                = "/"
    timeout             = 5
    matcher             = 200
    healthy_threshold   = 5 # Mark healthy after 2 successes
    unhealthy_threshold = 5 # Mark unhealthy after 2 failures
  }
  lifecycle {
    create_before_destroy = true
  }
}

# Attach EC2 instances to the Target Group
resource "aws_lb_target_group_attachment" "app_tg_attachment" {
  for_each = aws_instance.web # Loop over all instances

  target_group_arn = aws_lb_target_group.app_tg.arn
  target_id        = each.value.id # Attach each instance by ID
  port             = 80            # Port for the target
}


# ====================================================================================================
# Listener
# ====================================================================================================

resource "aws_lb_listener" "http_listener" {
  load_balancer_arn = aws_lb.aws-application_load_balancer.id
  port              = 80 # Listener port
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.app_tg.arn
  }
}

Elastic File System


# ====================================================================================================
# Configuring Elastic File System (EFS)
# ====================================================================================================

# Create EFS
resource "aws_efs_file_system" "efs_file_system" {
  creation_token   = "efs-test"
  performance_mode = "generalPurpose"
  throughput_mode  = "bursting"
  lifecycle_policy {
    transition_to_ia = "AFTER_30_DAYS"
  }
}

# Create EFS Mount Targets for each subnet
resource "aws_efs_mount_target" "mount_targets" {
  count           = 3
  file_system_id  = aws_efs_file_system.efs_file_system.id
  subnet_id       = [aws_subnet.a.id, aws_subnet.b.id, aws_subnet.c.id][count.index] # Mount target per subnet
  security_groups = [aws_security_group.efs-sec-group.id]
}

Outputs


# ====================================================================================================
# Outputs
# ====================================================================================================
# Output the DNS of ALB
output "alb_dns_name" {
  value       = aws_lb.aws-application_load_balancer.dns_name
  description = "DNS name of the Application Load Balancer"
}

# Output public IPs of all EC2 instances
output "instance_public_ips" {
  value = {
    for key, instance in aws_instance.web : key => instance.public_ip
  }
  description = "Public IPs of the EC2 instances"
}

# Output private IPs of all EC2 instances
output "instance_private_ips" {
  value = {
    for key, instance in aws_instance.web : key => instance.private_ip
  }
  description = "Private IPs of the EC2 instances"
}

Test Create File

If you access the EC2 and create a file in one of the EC2. And then, try acessing the file in another ec2, you’ll find the same file.

Common Errors and Troubleshooting

Can’t access the browser

If you can’t access the instance in the browser, please check the security group that you use, make sure that the security group allow inbound HTTP/HTTPS from anywhere.

AttachmentLimitExceeded error

Btw there are number of limit when attach ebs volume to the instance, it depends on the instance type.

References:

  • https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/volume_limits.html

Tags: ,

Categories:

Updated: