in packages/aws-rfdk/lib/core/lib/pad-efs-storage.ts [152:337]
constructor(scope: Construct, id: string, props: PadEfsStorageProps) {
super(scope, id);
/*
Implementation:
This is implemented as an AWS Step Function that implements the following
algorithm:
try {
du = diskUsage(<efs access point directory>)
while (du != desiredPadding) {
if (du < desiredPadding) {
<grow padding by adding up to 20 1GB numbered files to the filesystem.>
} else if (du > desiredPadding) {
<delete 1GB numbered files from the filesystem to reduce the padding to the desired amount>
// Note: We break here to prevent two separate invocations of the step function (e.g. accidental manual
// invocations) from looping indefinitely. Without a break, one invocation trying to grow while another
// tries to shrink will infinitely loop both -- the diskUsage will never settle on the value that either
// invocation wants.
break;
}
du = diskUsage(<efs access point directory>)
}
return success
} catch (error) {
return failure
}
*/
const diskUsageTimeout = Duration.minutes(5);
const paddingTimeout = Duration.minutes(15);
// Location in the lambda environment where the EFS will be mounted.
const efsMountPoint = '/mnt/efs';
let desiredSize;
try {
desiredSize = props.desiredPadding.toGibibytes({rounding: SizeRoundingBehavior.FAIL});
} catch (err) {
Annotations.of(this).addError('Failed to round desiredSize to an integer number of GiB. The size must be in GiB.');
}
const securityGroup = props.securityGroup ?? new SecurityGroup(this, 'LambdaSecurityGroup', {
vpc: props.vpc,
allowAllOutbound: false,
});
const lambdaProps: any = {
code: Code.fromAsset(path.join(__dirname, '..', '..', 'lambdas', 'nodejs')),
runtime: Runtime.NODEJS_18_X,
logRetention: RetentionDays.ONE_WEEK,
// Required for access point...
vpc: props.vpc,
vpcSubnets: props.vpcSubnets ?? {
subnetType: SubnetType.PRIVATE_WITH_EGRESS,
},
securityGroups: [ securityGroup ],
filesystem: LambdaFilesystem.fromEfsAccessPoint(props.accessPoint, efsMountPoint),
};
const diskUsage = new LambdaFunction(this, 'DiskUsage', {
description: 'Used by RFDK PadEfsStorage to calculate disk usage of an EFS access point',
handler: 'pad-efs-storage.getDiskUsage',
timeout: diskUsageTimeout,
memorySize: 128,
...lambdaProps,
});
// Implicit reference should have been fine, but the lambda is unable to mount the filesystem if
// executed before the filesystem has been fully formed. We shouldn't have the lambda created until
// after the EFS is created.
diskUsage.node.addDependency(props.accessPoint);
const doPadding = new LambdaFunction(this, 'PadFilesystem', {
description: 'Used by RFDK PadEfsStorage to add or remove numbered 1GB files in an EFS access point',
handler: 'pad-efs-storage.padFilesystem',
timeout: paddingTimeout,
// Execution requires about 70MB for just the lambda, but the filesystem driver will use every available byte.
// Larger sizes do not seem to make a difference on filesystem write performance.
// Set to 256MB just to give a buffer.
memorySize: 256,
...lambdaProps,
});
// Implicit reference should have been fine, but the lambda is unable to mount the filesystem if
// executed before the filesystem has been fully formed. We shouldn't have the lambda created until
// after the EFS is created.
doPadding.node.addDependency(props.accessPoint);
// Build the step function's state machine.
const fail = new Fail(this, 'Fail');
const succeed = new Succeed(this, 'Succeed');
const diskUsageTask = new LambdaInvoke(this, 'QueryDiskUsage', {
lambdaFunction: diskUsage,
comment: 'Determine the number of GB currently stored in the EFS access point',
timeout: diskUsageTimeout,
payload: {
type: InputType.OBJECT,
value: {
'desiredPadding.$': '$.desiredPadding',
'mountPoint': efsMountPoint,
},
},
resultPath: '$.diskUsage',
});
const growTask = new LambdaInvoke(this, 'GrowTask', {
lambdaFunction: doPadding,
comment: 'Add up to 20 numbered 1GB files to the EFS access point',
timeout: paddingTimeout,
payload: {
type: InputType.OBJECT,
value: {
'desiredPadding.$': '$.desiredPadding',
'mountPoint': efsMountPoint,
},
},
resultPath: '$.null',
});
const shrinkTask = new LambdaInvoke(this, 'ShrinkTask', {
lambdaFunction: doPadding,
comment: 'Remove 1GB numbered files from the EFS access point to shrink the padding',
timeout: paddingTimeout,
payload: {
type: InputType.OBJECT,
value: {
'desiredPadding.$': '$.desiredPadding',
'mountPoint': efsMountPoint,
},
},
resultPath: '$.null',
});
const choice = new Choice(this, 'BranchOnDiskUsage')
.when(Condition.numberLessThanJsonPath('$.diskUsage.Payload', '$.desiredPadding'), growTask)
.when(Condition.numberGreaterThanJsonPath('$.diskUsage.Payload', '$.desiredPadding'), shrinkTask)
.otherwise(succeed);
diskUsageTask.next(choice);
diskUsageTask.addCatch(fail, {
// See: https://docs.aws.amazon.com/step-functions/latest/dg/concepts-error-handling.html
errors: ['States.ALL'],
});
growTask.next(diskUsageTask);
growTask.addCatch(fail, {
errors: [ 'States.ALL' ],
});
shrinkTask.next(succeed);
shrinkTask.addCatch(fail, {
errors: [ 'States.ALL' ],
});
const statemachine = new StateMachine(this, 'StateMachine', {
definition: diskUsageTask,
});
// ==========
// Invoke the step function on stack create & update.
const invokeCall: AwsSdkCall = {
action: 'startExecution',
service: 'StepFunctions',
apiVersion: '2016-11-23',
region: Stack.of(this).region,
physicalResourceId: PhysicalResourceId.fromResponse('executionArn'),
parameters: {
stateMachineArn: statemachine.stateMachineArn,
input: JSON.stringify({
desiredPadding: desiredSize,
}),
},
};
const resource = new AwsCustomResource(this, 'Default', {
installLatestAwsSdk: true,
logRetention: RetentionDays.ONE_WEEK,
onCreate: invokeCall,
onUpdate: invokeCall,
policy: AwsCustomResourcePolicy.fromSdkCalls({
resources: [ statemachine.stateMachineArn ],
}),
});
resource.node.addDependency(statemachine);
// Add RFDK tags to the construct tree.
tagConstruct(this);
}