Skip to main content

AWS Elastic File System (EFS): Terraform Configuration, Mount EFS into EC2 Instance

1620 words·
AWS AWS CLI Terraform Elastic File System EFS EFS Utils Mount Targets NFS Virtual Private Cloud (VPC) EC2 Mermaid Flowchart
Table of Contents
GitHub Repository Available

Overview
#

Mermaid Flowchart
#

graph LR %% Region 1 subgraph Region2["AWS Region us-east-1"] %% VPC subgraph VPC["VPC 10.10.0.0/16"] %% Region-AZ1 subgraph Subnet1["Subnet / AZ - us-east-1a"] MountTarget1["EFS Mount Target"] EC2-1["EC2 Instance 1"] end %% Region-AZ2 subgraph Subnet2["Subnet / AZ - us-east-2a"] MountTarget2["EFS Mount Target"] EC2-2["EC2 Instance 2"] end SG-EFS["SG EFS"] --> MountTarget1 SG-EFS["SG EFS"] --> MountTarget2 SG-EC2["SG EC2"] -->|Allow inbound TCP 2049
from SG EC2| SG-EFS EC2-1 -->|Allow outbound TCP 2049| SG-EC2 EC2-2 -->|Allow outbound TCP 2049| SG-EC2 end EFS["EFS File System"] end %% Connections MountTarget1 -.-> EFS MountTarget2 -.-> EFS %% Dashed Border classDef BoarderDash1 stroke-dasharray:5 5; class VPC,Subnet1,Subnet2 BoarderDash1 %% Node Colors style EFS fill:#d1f7d6,stroke:#333,stroke-width:2px style MountTarget1 fill:#d1f7d6,stroke:#333,stroke-width:2px style MountTarget2 fill:#d1f7d6,stroke:#333,stroke-width:2px style EC2-1 fill:#f7d6d6,stroke:#333,stroke-width:2px style EC2-2 fill:#f7d6d6,stroke:#333,stroke-width:2px



Prerequisites
#

Install AWS CLI
#

# Update packages
sudo apt update

# Unstall zip tool
sudo apt install unzip -y

# Download AWS CLI zip file
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"

# Unzip
unzip awscliv2.zip

# Install
sudo ./aws/install
# Verify installation / check version
/usr/local/bin/aws --version

Configure AWS CLI
#

# Start AWS CLI configuration
aws configure

Install Terraform
#

# Install the HashiCorp GPG key
wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg > /dev/null

# Verify the GPG key fingerprint
gpg --no-default-keyring --keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg --fingerprint

# Add the official HashiCorp repository 
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list

# Install Terraform
sudo apt update && sudo apt-get install terraform
# Verify installation / check version
terraform version



Terraform EFS Stack
#

File and Folder Structure
#

The file and folder structure of the Terraform project looks like this:

aws-efs-ec2
├── ec2.tf
├── efs_filesystem.tf
├── outputs.tf
├── security-groups.tf
├── terraform.tf
├── variables.tf
└── vpc.tf

Terraform Configuration Files
#

Project Folder & Terraform Provider
#

# Create Terraform project folder
TF_PROJECT_NAME=aws-efs-ec2
mkdir $TF_PROJECT_NAME && cd $TF_PROJECT_NAME

  • terraform.tf
# Terraform Provider
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# Provider AWS Region
provider "aws" {
  alias  = "aws_region"
  region = var.aws_region
}

Variables
#

  • variables.tf
## AWS Region & Availability Zones

variable "aws_region" {
  description = "AWS Region"
  type        = string
  default     = "us-east-1"
}

variable "availability_zone_1" {
  description = "The availability zone for the subnet"
  type        = string
  default     = "us-east-1a"
}

variable "availability_zone_2" {
  description = "The availability zone for the subnet"
  type        = string
  default     = "us-east-1b"
}


## VPC: CIDR Blocks

variable "vpc_cidr" {
  description = "VPC CIDR block"
  type        = string
  default     = "10.10.0.0/16"
}

variable "vpc_subnet_cidr_1" {
  description = "Subnet CIDR block"
  type        = string
  default     = "10.10.0.0/24"
}

variable "vpc_subnet_cidr_2" {
  description = "Subnet CIDR block"
  type        = string
  default     = "10.10.1.0/24"
}


## VPC Name

variable "vpc_name" {
  description = "The name of the VPC"
  type        = string
  default     = "Example-VPC"
}


## SSH Key and EC2 Image

# SSH key pair name
variable "key_name" {
  default = "us-east-1-pc-le" # Define key pair name
}

# EC2 Image ID
variable "ami_id" {
  default = "ami-0e2c8caa4b6378d8c" # Define EC2 AMI ID
}

VPC and Subnets
#

  • vpc.tf
# VPC
resource "aws_vpc" "vpc" {
  provider = aws.aws_region
  cidr_block           = var.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name       = "${var.vpc_name}"
    Env        = "Production"
  }
}


# Public Subnet "10.10.0.0/24"
resource "aws_subnet" "vpc_subnet_public1" {
  provider = aws.aws_region
  vpc_id                  = aws_vpc.vpc.id
  cidr_block              = var.vpc_subnet_cidr_1
  availability_zone       = var.availability_zone_1
  map_public_ip_on_launch = true

  tags = {
    Name      = "${var.vpc_name} Subnet-Public-1"
    Env       = "Production"
  }
}

# Public Subnet "10.11.0.0/24"
resource "aws_subnet" "vpc_subnet_public2" {
  provider = aws.aws_region
  vpc_id                  = aws_vpc.vpc.id
  cidr_block              = var.vpc_subnet_cidr_2
  availability_zone       = var.availability_zone_2
  map_public_ip_on_launch = true

  tags = {
    Name      = "${var.vpc_name} Subnet-Public-2"
    Env       = "Production"
  }
}


# Internet Gateway
resource "aws_internet_gateway" "vpc_igw" {
  provider = aws.aws_region
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name        = "${var.vpc_name} IGW"
    Env         = "Production"
  }
}


# Public Routing Table
resource "aws_route_table" "vpc_public_routetable" {
  provider = aws.aws_region
  vpc_id = aws_vpc.vpc.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.vpc_igw.id
  }

  tags = {
    Name        = "${var.vpc_name} Public Route Table"
    Env         = "Production"
  }
}


# Associate Routes with Subnets
resource "aws_route_table_association" "vpc_subnet_public1_ra" {
  provider = aws.aws_region
  subnet_id      = aws_subnet.vpc_subnet_public1.id
  route_table_id = aws_route_table.vpc_public_routetable.id
}

# Associate Routes with Subnets
resource "aws_route_table_association" "vpc_subnet_public2_ra" {
  provider = aws.aws_region
  subnet_id      = aws_subnet.vpc_subnet_public2.id
  route_table_id = aws_route_table.vpc_public_routetable.id
}

Security Groups
#

  • security-groups.tf
# vpc: Security Group for SSH Access and Ping
resource "aws_security_group" "ec2_sg" {
  provider = aws.aws_region
  name        = "SG"
  description = "Security group for SSH access and ping"
  vpc_id      = aws_vpc.vpc.id

  ingress {
    description = "Allow SSH from anywhere"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    description = "Allow ping"
    from_port   = 8
    to_port     = -1
    protocol    = "icmp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    description = "Allow all outbound traffic"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  depends_on = [aws_vpc.vpc]

  tags = {
    Name = "SG"
    Env  = "Production"
  }
}


# Security Group for EFS Mount Targets
resource "aws_security_group" "efs_sg" {
  provider    = aws.aws_region
  name        = "EFS-SG"
  description = "Security group for EFS access"
  vpc_id      = aws_vpc.vpc.id

  ingress {
    description     = "Allow NFS traffic from EC2 instances"
    from_port       = 2049
    to_port         = 2049
    protocol        = "tcp"
    security_groups = [aws_security_group.ec2_sg.id]
  }

  egress {
    description = "Allow all outbound traffic"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  depends_on = [aws_vpc.vpc]

  tags = {
    Name = "EFS SG"
    Env  = "Production"
  }
}

EFS File System & Mount Targets
#

Mount targets must be created in every availability zone (AZ) where the EFS file system needs to be mounted on EC2 instances.

  • efs_filesystem.tf
# EFS File System
resource "aws_efs_file_system" "efs_filesystem" {
  creation_token = "efs-ec2"
  performance_mode = "generalPurpose"
  throughput_mode  = "elastic"

  lifecycle_policy {
    transition_to_ia = "AFTER_30_DAYS"
  }

  depends_on = [aws_vpc.vpc]

  tags = {
    Name = "EFS for EC2"
  }
}


# Mount Target in Subnet 1
resource "aws_efs_mount_target" "efs_mount_target1" {
  file_system_id  = aws_efs_file_system.efs_filesystem.id
  subnet_id       = aws_subnet.vpc_subnet_public1.id
  security_groups = [aws_security_group.efs_sg.id]

  depends_on = [
    aws_vpc.vpc,
    aws_subnet.vpc_subnet_public1,
    aws_security_group.efs_sg,
    aws_efs_file_system.efs_filesystem
    ]
}

# Mount Target in Subnet 2
resource "aws_efs_mount_target" "efs_mount_target2" {
  file_system_id  = aws_efs_file_system.efs_filesystem.id
  subnet_id       = aws_subnet.vpc_subnet_public2.id
  security_groups = [aws_security_group.efs_sg.id]

  depends_on = [
    aws_vpc.vpc,
    aws_subnet.vpc_subnet_public2,
    aws_security_group.efs_sg,
    aws_efs_file_system.efs_filesystem
    ]
}

EC2 Instances
#

  • ec2.tf
# EC2 Instance in Public Subnet 1
resource "aws_instance" "ec2_public_subnet1" {
  provider = aws.aws_region
  ami                    = var.ami_id
  instance_type          = "t3.small"
  subnet_id              = aws_subnet.vpc_subnet_public1.id
  key_name               = var.key_name
  vpc_security_group_ids = [aws_security_group.ec2_sg.id]

  depends_on = [
    aws_vpc.vpc,
    aws_security_group.ec2_sg,
    aws_efs_file_system.efs_filesystem,
    aws_efs_mount_target.efs_mount_target1
    ]

  tags = {
    Name = "public-subnet-vm-1"
    Env  = "Production"
  }
}


# EC2 Instance in Public Subnet 2
resource "aws_instance" "ec2_public_subnet2" {
  provider = aws.aws_region
  ami                    = var.ami_id
  instance_type          = "t3.small"
  subnet_id              = aws_subnet.vpc_subnet_public2.id
  key_name               = var.key_name
  vpc_security_group_ids = [aws_security_group.ec2_sg.id]

  depends_on = [
    aws_vpc.vpc,
    aws_security_group.ec2_sg,
    aws_efs_file_system.efs_filesystem,
    aws_efs_mount_target.efs_mount_target2
    ]

  tags = {
    Name = "public-subnet-vm-2"
    Env  = "Production"
  }
}

Outputs
#

  • outputs.tf
# EC2 Instance public IP
output "Public_Instance1_Public_IP" {
  description = "Public IP address of the EC2 instance in the public subnet"
  value       = aws_instance.ec2_public_subnet1.public_ip
}

# EC2 Instance public IP
output "Public_Instance2_Public_IP" {
  description = "Public IP address of the EC2 instance in the public subnet"
  value       = aws_instance.ec2_public_subnet2.public_ip
}


# EFS File System ID
output "EFS_File_System_ID" {
  value = aws_efs_file_system.efs_filesystem.id
  description = "The ID of the EFS file system"
}



Configuration Deployment
#

Initialize Terraform Project
#

This will download and install the AWS Terraform provider defined in the terraform.tf file with “hashicorp/aws”, as well as setting up the configuration files in the project directory.

# Initialize the Terraform project
terraform init

Validate Configuration Files
#

# Validates the syntax and structure of Terraform configuration files
terraform validate

# Shell output:
Success! The configuration is valid.

Plan the Deployment
#

# Dry run / preview changes before applying them
terraform plan

Apply the Configuration
#

# Create network stack
terraform apply -auto-approve

# Shell output:
Apply complete! Resources: 14 added, 0 changed, 0 destroyed.

Outputs:

EFS_File_System_ID = "fs-0bfe5e35bcbb57b44"
Public_Instance1_Public_IP = "54.210.85.168"
Public_Instance2_Public_IP = "44.193.7.244"



Verify Resources via Management Console
#

EFS Mount Targets
#


Show Attach Instructions
#

  • Open the “Attach” section from the top right corner



Mount EFS
#

SSH Into EC2 Instance
#

# SSH into EC2 instance 1
ssh -i /home/ubuntu/.ssh/us-east-1-pc-le.pem ubuntu@54.210.85.168

# SSH into EC2 instance 2
ssh -i /home/ubuntu/.ssh/us-east-1-pc-le.pem ubuntu@44.193.7.244

Install EFS-Utils
#

# Install requirements
sudo apt update && sudo apt -y install git binutils rustc cargo pkg-config libssl-dev gettext

# Clone efs-utils git repository
git clone https://github.com/aws/efs-utils && cd efs-utils

# Build & install amazon-efs-utils
./build-deb.sh
sudo apt-get -y install ./build/amazon-efs-utils*deb
# Verify Installation
amazon-efs-mount-watchdog --version

# Shell output
/usr/bin/amazon-efs-mount-watchdog Version: 2.2.0

Mount EFS
#

Optional, adapt the AWS region in EFS configuration:

# Open the EFS configuration file
sudo vi /etc/amazon/efs/efs-utils.conf
# Uncomment and define AWS EFS Region
[mount]
region = us-east-1
# Create the mount point directory
sudo mkdir -p /mnt/efs

# Mount EFS file system: Define EFS ID
sudo mount -t efs -o tls fs-0bfe5e35bcbb57b44:/ /mnt/efs
# List mounted filesystems
df -h

# Shell
Filesystem       Size  Used Avail Use% Mounted on
/dev/root        6.8G  3.5G  3.3G  52% /
tmpfs            956M     0  956M   0% /dev/shm
tmpfs            383M  968K  382M   1% /run
tmpfs            5.0M     0  5.0M   0% /run/lock
efivarfs         128K  3.6K  120K   3% /sys/firmware/efi/efivars
/dev/nvme0n1p16  881M   76M  744M  10% /boot
/dev/nvme0n1p15  105M  6.1M   99M   6% /boot/efi
tmpfs            192M   12K  192M   1% /run/user/1000
127.0.0.1:/      8.0E     0  8.0E   0% /mnt/efs

Verify EFS Share
#

# Create a new file on the EFS share via EC2 instance 1
sudo touch /mnt/efs/file1
# Verify the file from the EC2 instance 2
ls /mnt/efs/

# Shell output:
file1



Links #

# GitHub Repository
https://github.com/jueklu/terraform-aws-efs-ec2
# EFS Utils Installation
https://github.com/aws/efs-utils?tab=readme-ov-file#on-other-linux-distributions