Shared Storage on AWS with EFS

Shared Storage on AWS with EFS

Shared Storage on AWS with EFS and CloudFormation — NAXS Labs

Shared Storage on AWS
with EFS

A two-instance VPC with a shared EFS filesystem — public and private subnet, NAT gateway for outbound access, and IAM-authenticated encrypted mounts.

All posts

This post walks through a CloudFormation template that sets up two EC2 instances sharing an EFS filesystem — one in a public subnet, one in a private subnet. The goal is a working shared mount across both instances, with encryption in transit enforced and access controlled through IAM.

Full template on GitLab: https://git.naxslabs.com/cf/efs

Architecture

ResourcePurpose
USEASTA (10.10.1.0/24)Public subnet — internet facing, hosts InstanceEastA
USEASTB (10.10.2.0/24)Private subnet — no public IP, hosts InstanceEastB
NAT GatewayOutbound internet access for the private subnet
EFS FileSystemShared NFS filesystem mounted on both instances

Networking

MyIGW:
  Type: AWS::EC2::InternetGateway

MyIGWAttachment:
  Type: AWS::EC2::VPCGatewayAttachment
  Properties:
    VpcId: !Ref MyVPC
    InternetGatewayId: !Ref MyIGW

MyEIP:
  Type: AWS::EC2::EIP
  Properties:
    Domain: vpc

MyNatGW:
  Type: AWS::EC2::NatGateway
  DependsOn: MyIGWAttachment
  Properties:
    SubnetId: !Ref USEASTA
    AllocationId: !GetAtt MyEIP.AllocationId

The internet gateway handles inbound and outbound traffic for the public subnet. The NAT gateway sits in USEASTA with an Elastic IP and handles outbound-only traffic for USEASTB — the private instance can reach the internet to install packages but nothing from the internet can reach it directly.

MyRouteTable:
  Type: AWS::EC2::RouteTable
  Properties:
    VpcId: !Ref MyVPC

MyRoute:
  Type: AWS::EC2::Route
  DependsOn: MyIGWAttachment
  Properties:
    RouteTableId: !Ref MyRouteTable
    DestinationCidrBlock: "0.0.0.0/0"
    GatewayId: !Ref MyIGW

MyRTAssociation:
  Type: AWS::EC2::SubnetRouteTableAssociation
  Properties:
    SubnetId: !Ref USEASTA
    RouteTableId: !Ref MyRouteTable

PrivateRouteTable:
  Type: AWS::EC2::RouteTable
  Properties:
    VpcId: !Ref MyVPC

PrivateRoute:
  Type: AWS::EC2::Route
  Properties:
    RouteTableId: !Ref PrivateRouteTable
    DestinationCidrBlock: "0.0.0.0/0"
    NatGatewayId: !Ref MyNatGW

PrivateRTAssociationB:
  Type: AWS::EC2::SubnetRouteTableAssociation
  Properties:
    SubnetId: !Ref USEASTB
    RouteTableId: !Ref PrivateRouteTable

Two route tables — one public pointing to the IGW, one private pointing to the NAT gateway. USEASTA and USEASTB are explicitly associated with their respective tables. The private subnet’s default route goes to the NAT gateway rather than the IGW, which is what makes it private.

Security Groups

MySecurityGroup:
  Type: AWS::EC2::SecurityGroup
  Properties:
    GroupDescription: Allow SSH Access
    GroupName: naxs-ssh-sg
    SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 22
        ToPort: 22
        CidrIp: 0.0.0.0/0
    VpcId: !Ref MyVPC

WebSG:
  Type: AWS::EC2::SecurityGroup
  Properties:
    GroupDescription: Web tier - internet facing
    GroupName: naxs-web-sg
    VpcId: !Ref MyVPC
    SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 443
        ToPort: 443
        CidrIp: 0.0.0.0/0
      - IpProtocol: tcp
        FromPort: 80
        ToPort: 80
        CidrIp: 0.0.0.0/0

AppSG:
  Type: AWS::EC2::SecurityGroup
  Properties:
    GroupDescription: App tier - web tier access only
    GroupName: naxs-app-sg
    VpcId: !Ref MyVPC
    SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 8080
        ToPort: 8080
        SourceSecurityGroupId: !Ref WebSG
      - IpProtocol: tcp
        FromPort: 22
        ToPort: 22
        SourceSecurityGroupId: !Ref WebSG

EfsSG:
  Type: AWS::EC2::SecurityGroup
  Properties:
    GroupDescription: EFS access
    GroupName: naxs-efs-sg
    VpcId: !Ref MyVPC
    SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 2049
        ToPort: 2049
        SourceSecurityGroupId: !Ref WebSG
      - IpProtocol: tcp
        FromPort: 2049
        ToPort: 2049
        SourceSecurityGroupId: !Ref AppSG

EfsSG controls access to the EFS mount targets on port 2049 (NFS). Only instances holding WebSG or AppSG can reach it. The EfsSG is attached to the mount targets, not the instances — the instances initiate outbound to port 2049 and the mount target’s security group decides whether to allow it.

Lab only

SSH open to the world on MySecurityGroup is intentional for demo purposes. In production restrict this to a known CIDR or use SSM Session Manager.

IAM Role

NXSEfsRole:
  Type: AWS::IAM::Role
  Properties:
    RoleName: NXSEfsRole
    AssumeRolePolicyDocument:
      Version: "2012-10-17"
      Statement:
        - Effect: Allow
          Principal:
            Service: ec2.amazonaws.com
          Action: sts:AssumeRole
    Policies:
      - PolicyName: EfsAccess
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
            - Effect: Allow
              Action:
                - elasticfilesystem:ClientMount
                - elasticfilesystem:ClientWrite
                - elasticfilesystem:ClientRootAccess
              Resource: "*"

NXSEfsInstanceProfile:
  Type: AWS::IAM::InstanceProfile
  Properties:
    Roles:
      - !Ref NXSEfsRole

The EFS filesystem is encrypted, which means the mount helper needs IAM credentials to authenticate. The role grants ClientMount, ClientWrite, and ClientRootAccess on EFS. The instance profile wraps the role so it can be attached to EC2 instances — you can’t attach a role directly to an instance, only through a profile.

The trust policy’s Principal: ec2.amazonaws.com is what allows EC2 to assume this role. Without it the role exists but no service can use it.

EFS Filesystem and Mount Targets

MyEFS:
  Type: AWS::EFS::FileSystem
  Properties:
    Encrypted: true

EfsMountTargetA:
  Type: AWS::EFS::MountTarget
  Properties:
    FileSystemId: !Ref MyEFS
    SubnetId: !Ref USEASTA
    SecurityGroups:
      - !Ref EfsSG

EfsMountTargetB:
  Type: AWS::EFS::MountTarget
  Properties:
    FileSystemId: !Ref MyEFS
    SubnetId: !Ref USEASTB
    SecurityGroups:
      - !Ref EfsSG

The EFS filesystem itself is a single resource. Mount targets are the network endpoints that instances use to connect to it — one per subnet. Each mount target gets an IP address in its subnet and has EfsSG attached to control access. Instances connect to the mount target in their own subnet rather than crossing subnet boundaries.

EC2 Instances

InstanceEastA:
  Type: AWS::EC2::Instance
  Properties:
    SubnetId: !Ref USEASTA
    InstanceType: t2.micro
    ImageId: "ami-0ea87431b78a82070"
    IamInstanceProfile: !Ref NXSEfsInstanceProfile
    KeyName: "myectwos"
    SecurityGroupIds:
      - !Ref MySecurityGroup
      - !Ref WebSG
    MetadataOptions:
      HttpTokens: required
      HttpEndpoint: enabled
    BlockDeviceMappings:
      - DeviceName: /dev/xvda
        Ebs:
          VolumeSize: 8
          VolumeType: gp3
          Encrypted: true

InstanceEastB:
  Type: AWS::EC2::Instance
  Properties:
    SubnetId: !Ref USEASTB
    InstanceType: t2.micro
    ImageId: "ami-0ea87431b78a82070"
    IamInstanceProfile: !Ref NXSEfsInstanceProfile
    KeyName: "myectwos"
    SecurityGroupIds:
      - !Ref AppSG
    MetadataOptions:
      HttpTokens: required
      HttpEndpoint: enabled
    BlockDeviceMappings:
      - DeviceName: /dev/xvda
        Ebs:
          VolumeSize: 8
          VolumeType: gp3
          Encrypted: true

Both instances get the EFS instance profile so the mount helper can authenticate. InstanceEastA is the public entry point — it gets both MySecurityGroup for SSH and WebSG for HTTP/HTTPS. InstanceEastB sits in the private subnet with AppSG only, reachable by SSH from the web tier and on port 8080.

IMDSv2 is enforced on both instances via HttpTokens: required and EBS volumes are encrypted at rest.

Mounting EFS

Once the stack is deployed, install the EFS mount helper on each instance and mount the filesystem:

sudo yum install -y amazon-efs-utils
sudo mkdir /mnt/efs
sudo mount -t efs -o tls fs-xxxxxxxxx:/ /mnt/efs

The -o tls flag enforces encryption in transit. The mount helper picks up the IAM credentials from the instance profile automatically — no credentials file needed.

To persist the mount across reboots add it to /etc/fstab:

fs-xxxxxxxxx:/ /mnt/efs efs _netdev,tls 0 0
Cost

The NAT gateway is the main cost driver here — it charges hourly plus per GB of data processed. Delete the stack when you’re done with the lab. EFS charges per GB stored so an empty filesystem costs essentially nothing.


Full template: https://git.naxslabs.com/cf/efs

NAXS Labs
Logo