CloudFormation EC2 Volume Persistence

Fabio Gollinucci
2 min readJun 6, 2023

It’s a common requirements, for application and general instance setup, to persist a disk portion. Setting aside the solution via EFS, if not require from access multiple instance (ASG), an EBS volume does its job anyway.

AWS Infrastructure schema

Starting from a SAM template that describe an EBS volume:

DataVolume:
Type: AWS::EC2::Volume
Properties:
Size: !Ref DataVolumeSize
VolumeType: !Ref DataVolumeType
Iops: !Ref DataVolumeIops

Add the instance and relate launch template:

LaunchTemplate:
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateName: !Sub "${ProjectName}-${EnvironmentName}-mysql"
LaunchTemplateData:
IamInstanceProfile:
Arn: !GetAtt InstanceProfile.Arn
DisableApiTermination: false
KeyName: !Ref KeyPair
ImageId: !Ref ImageId
InstanceType: !Ref InstanceType
SecurityGroupIds:
- !Ref SecurityGroupId
BlockDeviceMappings:
- DeviceName: /dev/sda1
Ebs:
VolumeSize: !Ref VolumeSize
VolumeType: !Ref VolumeType

Instance:
Type: AWS::EC2::Instance
Properties:
LaunchTemplate:
LaunchTemplateId: !Ref LaunchTemplate
Version: !GetAtt LaunchTemplate.LatestVersionNumber
SubnetId: !Ref SubnetId

Attaching the volume using a AWS::EC2::VolumeAttachment:

MountPoint:
Type: AWS::EC2::VolumeAttachment
Properties:
InstanceId: !Ref Ec2Instance
VolumeId: !Ref NewVolume
Device: /dev/xvdh

result in a success creation, but when the LaunchTemplate will be updated, the VolumeAttachment will raise an error because CloudFormation does not support the update of this resource. Also the VolumeAttachment cannot be deleted because the instance need to un-mount the volume first.

The proposed solution replace the VolumeAttachment resource with a Lambda Function that attach the volume to the newly launched instance:

MountVolumeFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub "${AWS::StackName}-mount-volume"
Runtime: nodejs16.x
Handler: index.handler
InlineCode: !Sub |
const EC2 = require('aws-sdk/clients/ec2');
const ec2 = new EC2({ logger: console });
exports.handler = async (event) => ec2.attachVolume({
Device: "/dev/xvdh",
InstanceId: "${Instance}",
VolumeId: "${DataVolume}"
})
.promise()
.then(resp => {
console.log('mounted!')
})
.catch(e => {
switch (e.code) {
case 'DuplicateInstanceAttachment':
console.log('already mounted!')
return;
default:
console.error(e.code, e.message)
throw e;
}
});
Policies:
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- ec2:AttachVolume
Resource: '*'

The MountVolumeFunction will be executed when the CloudFormation Stack is update successfully:

MountVolumeFunction:
Type: AWS::Serverless::Function
Properties:
# [...]
Events:
StackCreatedOrUpdated:
Type: EventBridgeRule
Properties:
EventBusName: default
Pattern:
source:
- aws.cloudformation
detail-type:
- CloudFormation Stack Status Change
detail:
stack-id:
- !Ref AWS::StackId
status-details:
status:
- UPDATE_COMPLETE
- CREATE_COMPLETE

In order to allow volume to be attached to the new instance, when the old one still running, the MultiAttach feature need to be enabled:

DataVolume:
Type: AWS::EC2::Volume
Properties:
MultiAttachEnabled: true
Size: !Ref DataVolumeSize
VolumeType: !Ref DataVolumeType
Iops: !Ref DataVolumeIops
AvailabilityZone: !GetAtt Instance.AvailabilityZone

The DataVolumeType can be only “io1” or “io2”, the only two volume type that support the MultiAttach feature.

Credits: Cloudcraft.

Originally written on Mar 2, 2023.

--

--