AWS CDK で API Gateway を使った、プライベートな API を構築してみました。
作成する API は、VPC エンドポイントからのみ接続できる API です。インターネットからのアクセスやその他のリソースからのアクセスはできません。また、VPC に EC2 を作成し、API に接続できるか動作確認してみます。
構築するリソースのイメージは以下となります。
動作環境
- Node.js:
v16.15.1
- TypeScript:
4.9.4
- AWS CDK:
2.55.1
CDK でリソースを作成
CDK を使って必要なリソースを定義していきます。以下のリソースを作成します。
- VPC
- セキュリティグループ
- 動作確認用の EC2 用のセキュリティグループ
- VPC エンドポイント用のセキュリティグループ
- VPC エンドポイント
- 動作確認用の EC2 インスタンス
- 動作確認はセッションマネージャを使って確認
- API のバックエンドに利用する Lambda 関数
- API Gateway
- プライベートな API
cdk-private-api-sample-stack.ts
...(省略)...
// VPC
const vpc = new ec2.Vpc(this, "Vpc", {
subnetConfiguration: [
{
cidrMask: 24,
name: "public-subnet",
subnetType: ec2.SubnetType.PUBLIC,
}
],
});
// セキュリティグループ
const ec2SecurityGroup = new ec2.SecurityGroup(this, "ec2SecurityGroup", {
vpc,
});
const vpcEndpointSecurityGroup = new ec2.SecurityGroup(this, "vpcEndpointSecurityGroup", {
vpc,
});
vpcEndpointSecurityGroup.addIngressRule(ec2SecurityGroup, ec2.Port.allTraffic());
// VPC エンドポイント
const privateApiVpcEndpoint = new ec2.InterfaceVpcEndpoint(this, "privateApiVpcEndpoint", {
vpc,
service: ec2.InterfaceVpcEndpointAwsService.APIGATEWAY,
subnets: {subnets: vpc.publicSubnets},
securityGroups: [vpcEndpointSecurityGroup],
open: false,
});
const ec2Instance = new ec2.Instance(this, "ec2Instance", {
vpc,
instanceType: ec2.InstanceType.of(
ec2.InstanceClass.T4G,
ec2.InstanceSize.NANO
),
machineImage: new ec2.AmazonLinuxImage({
generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
cpuType: ec2.AmazonLinuxCpuType.ARM_64,
}),
securityGroup: ec2SecurityGroup,
role: new iam.Role(this, "ec2Role", {
managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore")], // セッションマネージャーを利用するために必要なポリシー
assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
}),
});
// Lambda
const lambdaFunction = new nodejs.NodejsFunction(this, "lambdaFunction", {
runtime: lambda.Runtime.NODEJS_16_X,
entry: "lib/lambda.ts",
architecture: lambda.Architecture.ARM_64,
memorySize: 256,
});
// API Gateway
const privateApi = new apigateway.LambdaRestApi(this, 'privateApi', {
endpointTypes: [apigateway.EndpointType.PRIVATE],
handler: lambdaFunction,
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": privateApiVpcEndpoint.vpcEndpointId
}
}
}),
new iam.PolicyStatement({
principals: [new iam.AnyPrincipal],
actions: ['execute-api:Invoke'],
resources: ['execute-api:/*'],
effect: iam.Effect.ALLOW
})
]
})
});
...(省略)...
プライベート API を特定の VPC や VPC エンドポイントからのみアクセスを許可するように、API Gateway のリソースポリシーを設定する必要がある点にご注意ください。詳しくは公式ドキュメントを参照ください。
API実行時の処理は、動作確認用の Lambda が固定のメッセージを返します。
lambda.ts
export const handler = async (event: any): Promise<any> => {
const responseBody = {
"message": "Private API executed!!"
};
return {
statusCode: "200",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(responseBody),
};
};
デプロイすると、このようなリソースが作成されました。
動作確認
環境の構築ができたので、プライベート API の動作確認をしてみます。
なお API のエンドポイントは、CloudFormation の 出力
を確認するか、API Gateway の ステージ
から確認することができます。
ローカル環境からインターネット経由でアクセス
ローカル環境から curl コマンドを使って、プライベート API にアクセスしてみます。
$ curl https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/
curl: (6) Could not resolve host: xxx.execute-api.ap-northeast-1.amazonaws.com
ホスト名が解決できなくて、接続できませんでした。今回作成されたエンドポイントは、インターネットからだとアクセスできないようでしたので、意図した動きになっています。
VPC 内に作成した EC2 からアクセス
続いて、VPC 内に作成した EC2 から、VPC エンドポイントを経由して、アクセスしてみます。EC2 への接続は、セッションマネージャーで接続して、動作確認してみます。
セッションマネジャーで接続後、コマンドを実行してみます。
$ curl https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod/
{"message":"Private API executed!!"}
API のエンドポイントを実行し、Lambda からのレスポンスを取得することができました。VPC エンドポイントを経由したアクセスだと接続することができたので、意図した動きになっていますね。
さいごに
VPC 内のリソース内からのアクセスのみに制限した API を CDK を利用して実装することができました。L2 Construct を利用して定義することができるので、実装が少なくて済むのがいいですね。
プライベート API には、カスタムドメインが利用できないことなど、いくつか考慮事項がありますので、事前に要件にマッチするかを確認すると良いと思います。
今回実装したサンプルのソースコードは、こちらのリポジトリで確認することができます。どなたかの参考になれば幸いです。