Skip to content

Instantly share code, notes, and snippets.

@skorfmann
Last active November 27, 2023 00:39
Show Gist options
  • Save skorfmann/6941326b2dd75f52cb67e1853c5f8601 to your computer and use it in GitHub Desktop.
Save skorfmann/6941326b2dd75f52cb67e1853c5f8601 to your computer and use it in GitHub Desktop.
Private Api Gateway in CDK.

Private API Gateway with the AWS CDK

  • Lambda
  • Private Api Gateway
  • VPC Endpoint

NB: In order to access the Api Gateway through the public DNS of the VPC endpoint, a curl request has to have the api id as header. See also here

curl -i -H "x-apigw-api-id: <api-id>" https://vpce-<vpce-id>.execute-api.<region>.vpce.amazonaws.com/
{
"name": "private-api",
"version": "0.1.0",
"bin": {
"private-api": "bin/private-api.js"
},
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"test": "jest",
"cdk": "cdk"
},
"devDependencies": {
"@aws-cdk/assert": "^1.21.1",
"@types/jest": "^24.0.22",
"@types/node": "10.17.5",
"jest": "^24.9.0",
"ts-jest": "^24.1.0",
"aws-cdk": "^1.21.1",
"ts-node": "^8.1.0",
"typescript": "~3.7.2"
},
"dependencies": {
"@aws-cdk/aws-apigateway": "^1.21.1",
"@aws-cdk/aws-ec2": "^1.21.1",
"@aws-cdk/aws-iam": "^1.21.1",
"@aws-cdk/aws-lambda": "^1.21.1",
"@aws-cdk/core": "^1.21.1",
"source-map-support": "^0.5.16"
}
}
import * as cdk from '@aws-cdk/core';
import * as apigateway from '@aws-cdk/aws-apigateway'
import { InterfaceVpcEndpoint, Vpc, Subnet, SecurityGroup, Peer, Port } from '@aws-cdk/aws-ec2'
import * as lambda from '@aws-cdk/aws-lambda'
import * as iam from '@aws-cdk/aws-iam'
import path = require('path');
export class PrivateApiGatewayStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const vpc = Vpc.fromLookup(this, 'PrimaryVPC', {
vpcName: '<vpcid>'
})
const a = Subnet.fromSubnetAttributes(this, 'ASubnet', {
availabilityZone: 'eu-central-1a',
subnetId: 'subnet-<id>'
})
const b = Subnet.fromSubnetAttributes(this, 'BSubnet', {
availabilityZone: 'eu-central-1b',
subnetId: 'subnet-<id>'
})
const sg = new SecurityGroup(this, 'SecurityGroup', {
vpc,
allowAllOutbound: true,
securityGroupName: 'VpcEndpoint'
});
sg.addIngressRule(Peer.ipv4("<CIDR>"), Port.tcp(443))
const vpcEndpoint = new InterfaceVpcEndpoint(this, 'ApiVpcEndpoint', {
vpc,
service: {
name: 'com.amazonaws.eu-central-1.execute-api',
port: 443
},
subnets: {
subnets: [a, b]
},
privateDnsEnabled: true,
securityGroups: [sg]
})
const fn = new lambda.Function(this, 'PrivateLambda', {
runtime: lambda.Runtime.NODEJS_10_X,
handler: 'index.handler',
code: lambda.Code.fromAsset(path.join(__dirname, 'lambda')),
});
new apigateway.LambdaRestApi(this, 'PrivateLambdaRestApi', {
endpointTypes: [apigateway.EndpointType.PRIVATE],
handler: fn,
policy: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
principals: [new iam.AnyPrincipal],
actions: ['execute-api:Invoke'],
resources: ['execute-api:/*'],
effect: iam.Effect.DENY,
conditions: {
StringNotEquals: {
"aws:SourceVpce": vpcEndpoint.vpcEndpointId
}
}
}),
new iam.PolicyStatement({
principals: [new iam.AnyPrincipal],
actions: ['execute-api:Invoke'],
resources: ['execute-api:/*'],
effect: iam.Effect.ALLOW
})
]
})
})
}
}
@arash-cid
Copy link

Yep, I got it working too last week. To get the IP dynamically, you can use this getEniIpsByEniIds function like this:

// inside the construct 
// ...
        const vpcEndpoint = new InterfaceVpcEndpoint(this, 'ApiVpcEndpoint', {
          vpc,
          service: {
            name: 'com.amazonaws.ap-southeast-2.execute-api',
            port: 443,
          },
          subnets: {
            subnets: [
              vpcEndpointSubnetA,
              vpcEndpointSubnetB,
              vpcEndpointSubnetC,
            ],
          },
          privateDnsEnabled: false,
          open: true,
        });
        const vpcEndpointIps = getEniIpsByEniIds(
          this,
          vpcEndpoint.vpcEndpointNetworkInterfaceIds,
        );
        const vpcEndpointTargets = _.map(
          vpcEndpointIps,
          (ip) => new IpTarget(ip, 443),
        );

        const nlb = new NetworkLoadBalancer(this, 'Nlb', {
          loadBalancerName: `${apiGwName}-prv`,
          vpc,
          vpcSubnets: {
            subnets: [
              vpcEndpointSubnetA,
              vpcEndpointSubnetB,
              vpcEndpointSubnetC,
            ],
          },
          crossZoneEnabled: true,
          internetFacing: false,
        });
        const nlbListener = nlb.addListener(`https`, {
          port: 443,
          certificates: [certificate],
        });
        const nlbTargetGroup = nlbListener.addTargets('IpTargets', {
          targets: vpcEndpointTargets,
          port: 443,
          deregistrationDelay: Duration.seconds(30),
        });
//...

// outside construct 
export function getEniIpsByEniIds(
  scope: Construct,
  eniIds: string[],
): string[] {
  const outputPaths = eniIds.map(
    (_, index) => `NetworkInterfaces.${index}.PrivateIpAddress`,
  );
  const response = new AwsCustomResource(scope, `describe-enis`, {
    onUpdate: {
      service: 'EC2',
      action: 'describeNetworkInterfaces',
      outputPaths,
      parameters: { NetworkInterfaceIds: eniIds },
      physicalResourceId: PhysicalResourceId.of(Math.random().toString()),
    },
    policy: {
      statements: [
        new PolicyStatement({
          actions: ['ec2:DescribeNetworkInterfaces'],
          resources: ['*'],
        }),
      ],
    },
  });
  return outputPaths.map((outputPath) => response.getResponseField(outputPath));
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment