protected setupLifecycleEventHandlerFunction()

in packages/aws-rfdk/lib/core/lib/staticip-server.ts [311:380]


  protected setupLifecycleEventHandlerFunction(): LambdaFunction {
    const stack = Stack.of(this);

    // The SingletonFunction does not tell us when it's newly created vs. finding a pre-existing
    // one. So, we do our own singleton Function so that we know when it's the first creation, and, thus,
    // we must attach one-time permissions.
    const functionUniqueId = 'AttachEniToInstance' + this.removeHyphens('83a5dca5-db54-4aa4-85d2-8d419cdf85ce');
    let singletonPreExists: boolean = true;
    let eventHandler = stack.node.tryFindChild(functionUniqueId) as LambdaFunction;
    if (!eventHandler) {
      const handlerCode = Code.fromAsset(path.join(__dirname, '..', '..', 'lambdas', 'nodejs', 'asg-attach-eni'), {
        exclude: ['**/*', '!index*'],
      });
      eventHandler = new LambdaFunction(stack, functionUniqueId, {
        code: handlerCode,
        handler: 'index.handler',
        runtime: Runtime.NODEJS_18_X,
        description: `Created by RFDK StaticPrivateIpServer to process instance launch lifecycle events in stack '${stack.stackName}'. This lambda attaches an ENI to newly launched instances.`,
        logRetention: RetentionDays.THREE_DAYS,
      });
      singletonPreExists = false;
    }

    // Note: We **cannot** reference the ASG's ARN in the lambda's policy. It would create a deadlock at deployment:
    //  Lambda policy waiting on ASG completion to get ARN
    //  -> lambda waiting on policy to be created
    //  -> ASG waiting on lambda to signal lifecycle continue for instance start
    //  -> back to the start of the cycle.
    // Instead we use resourcetags condition to limit the scope of the lambda.
    const tagKey = 'RfdkStaticPrivateIpServerGrantConditionKey';
    const tagValue = Names.uniqueId(eventHandler);
    const grantCondition: { [key: string]: string } = {};
    grantCondition[`autoscaling:ResourceTag/${tagKey}`] = tagValue;
    Tags.of(this.autoscalingGroup).add(tagKey, tagValue);

    // Allow the lambda to complete the lifecycle action for only tagged ASGs.
    const iamCompleteLifecycle = new PolicyStatement({
      effect: Effect.ALLOW,
      actions: [
        'autoscaling:CompleteLifecycleAction',
      ],
      resources: [
        `arn:${stack.partition}:autoscaling:${stack.region}:${stack.account}:autoScalingGroup:*:autoScalingGroupName/*`,
      ],
      conditions: {
        'ForAnyValue:StringEquals': grantCondition,
      },
    });
    eventHandler.role!.addToPrincipalPolicy(iamCompleteLifecycle);

    if (!singletonPreExists) {
      // Allow the lambda to attach the ENI to the instance that was created.
      // Referencing: https://docs.aws.amazon.com/IAM/latest/UserGuide/list_amazonec2.html
      // Last-Accessed: July 2020
      // The ec2:DescribeNetworkInterfaces, and ec2:AttachNetworkInterface operations
      // do not support conditions, and do not support resource restriction.
      // So, we only attach the policy to the lambda function once; when we first create it.
      const iamEniAttach = new PolicyStatement({
        effect: Effect.ALLOW,
        actions: [
          'ec2:DescribeNetworkInterfaces',
          'ec2:AttachNetworkInterface',
        ],
        resources: ['*'],
      });
      eventHandler.role!.addToPrincipalPolicy(iamEniAttach);
    }

    return eventHandler;
  }