constructor()

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);
  }