aws / aws-cdk

The AWS Cloud Development Kit is a framework for defining cloud infrastructure in code
https://aws.amazon.com/cdk
Apache License 2.0
11.5k stars 3.84k forks source link

(aws-lambda): Option to specify VPC to Custom Resource Lambdas #22473

Open shantgup opened 1 year ago

shantgup commented 1 year ago

Describe the feature

Creating this issue as requested.

Currently, there is no way to specify a VPC for Custom Resource Lambdas. The only solution is to use Aspects like below.

import * as cdk from '@aws-cdk/core';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as lambda from '@aws-cdk/aws-lambda';

interface VpcConfig {
    readonly SecurityGroupIds: string[];
    readonly SubnetIds: string[];
}

export class LambdaVPCAspect implements cdk.IAspect {
    private readonly vpc: ec2.Vpc;

    constructor(vpc: ec2.Vpc) {
        this.vpc = vpc;
    }

    public visit(node: cdk.IConstruct): void {
        if (node instanceof lambda.CfnFunction) {
            if (!node.vpcConfig) {
                node.addPropertyOverride('VpcConfig', this.getConfig(node, this.vpc));
            }
        }
    }

    private getConfig(node: cdk.Construct, vpc: ec2.Vpc): VpcConfig {
        let securityGroups: ec2.ISecurityGroup[];

        const securityGroup = new ec2.SecurityGroup(node, 'SecurityGroup', {
            vpc: vpc,
        });

        securityGroups = [securityGroup];
        const { subnetIds } = vpc.selectSubnets();

        return {
            SecurityGroupIds: securityGroups.map(sg => sg.securityGroupId),
            SubnetIds: subnetIds,
        };
    }
}

Requesting a feature that allows specifying VpcId or similar.

There was a GitHub Issue about this in the past too, linked below. That issue has since been closed.

https://github.com/aws/aws-cdk/issues/11340

Thank you and please let me know if I can provide any other information.

Use Case

Need to specify my own custom VPC when creating custom resource Lambdas.

Proposed Solution

Allow a property like VpcId that lets users specify the VPC for custom resource Lambdas.

Other Information

No response

Acknowledgements

CDK version used

n/a

Environment details (OS name and version, etc.)

n/a

shantgup commented 1 year ago

Found a potential solution for this.

Instead of using the L1 construct CfnCustomResource , use the L2 construct AwsCustomResource which allows configuring several useful properties including vpc and vpcSubnets.

_Requesting a yellow-box note in the top-level description of the documentation for the L1 CfnCustomResource construct here, that tells customers about the existence of the L2 construct._

In general, we recommend L2 constructs over L1 constructs whenever possible.

indrora commented 1 year ago

There absolutely should also be a way to define the default VPC for execution of lambdas in custom resources. Plausibly should be a feature flag.

ollyfg commented 1 year ago

Just a note for anyone who couldn't get the above code to work.

I found that a fair number (most?) of the custom lambda functions are not actually CfnFunctions, and some are instead created using the very generic CfnResource (eg. log-retention).

I used the following code to do the modification at the CfnResource level, which catches every lambda in my stacks:

import { IConstruct } from "constructs";
import { CfnResource, IAspect } from "aws-cdk-lib";
import { aws_ec2 as EC2 } from "aws-cdk-lib";

/** VPC config for a `CfnResource` representing a lambda */
interface VpcConfig {
  readonly securityGroupIds: string[];
  readonly subnetIds: string[];
}

/**
 * Tell us if the given construct is a lambda function.
 *
 * We look at the lowest level possible, which is the existence of a
 * `CfnResource` with a `type` of `AWS::Lambda::Function`.
 */
const isLambda = (node: IConstruct): node is CfnResource => {
  const isCfnResource = node instanceof CfnResource;
  if (isCfnResource) {
    return node.cfnResourceType === "AWS::Lambda::Function";
  }
  return false;
};

/**
 * Recursively search through this node, returning all lambda functions we
 * find.
 */
const getLambdaChildren = (node: IConstruct): CfnResource[] => {
  if (isLambda(node)) {
    return [node];
  }
  return node.node.children.flatMap((child) => getLambdaChildren(child));
};

export class LambdaVPCAspect implements IAspect {
  private readonly vpc: EC2.Vpc;
  private securityGroup?: EC2.ISecurityGroup;

  constructor(vpc: EC2.Vpc) {
    this.vpc = vpc;
  }

  public visit(node: IConstruct): void {
    // Get all of the lambdas in this construct
    const lambdas = getLambdaChildren(node);
    lambdas.forEach((resource) => {
      // Only apply if the lambda does not have it's own VPC config
      // @ts-expect-error This is private but we need to see it...
      const hasVpcConfig = !!resource.cfnProperties?.vpcConfig;
      if (!hasVpcConfig) {
        resource.addPropertyOverride("vpcConfig", this.getConfig(node));
      }
    });
  }

  /** Get the `VpcConfig` property for a lambda */
  private getConfig(node: IConstruct): VpcConfig {
    // Default security group
    this.securityGroup =
      this.securityGroup ??
      EC2.SecurityGroup.fromSecurityGroupId(
        node,
        "lambdaAspectSecurityGroup",
        this.vpc.vpcDefaultSecurityGroup
      );

    // Private subnets
    const subnetIds = this.vpc.privateSubnets.map((x) => x.subnetId);

    return {
      securityGroupIds: [this.securityGroup.securityGroupId],
      subnetIds: subnetIds,
    };
  }
}

It would be great to have a simpler way to do this, especially one without having to access a protected attribute 😬

BwL1289 commented 1 year ago

@ollyfg thanks for this. A question I have is: how do you limit the security group to also allow AWS to reach the custom resource without opening it up to the whole internet?

If you have specific ingress/egress rules for IPs, the custom resource can't be reached. Not sure where this documentation is.

Said differently: can I limit the security group to the IP address that AWS uses to hit the custom resource?

Addendum, this is helpful. TLDR; use a VPC Endpoint instead of limiting IP addresses.

If you use the VPC endpoints feature, custom resources in the VPC must have access to CloudFormation-specific Amazon Simple Storage Service (Amazon S3) buckets. Custom resources must send responses to a presigned Amazon S3 URL. If they can't send responses to Amazon S3, CloudFormation won't receive a response and the stack operation fails. For more information, see Setting up VPC endpoints for AWS CloudFormation.

Resources:

  1. https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cfn-customresource.html
  2. https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-vpce-bucketnames.html
  3. https://docs.aws.amazon.com/vpc/latest/privatelink/create-interface-endpoint.html#create-interface-endpoint