AWS CDK でプライベートな API を構築してみた

AWS CDK を利用して、VPC エンドポイントからのみアクセスを許可するプライベート API を構築します。
2022.12.20

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

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 には、カスタムドメインが利用できないことなど、いくつか考慮事項がありますので、事前に要件にマッチするかを確認すると良いと思います。
今回実装したサンプルのソースコードは、こちらのリポジトリで確認することができます。どなたかの参考になれば幸いです。

参考