【AWS CDK v2】IPを固定化したLambdaの作成方法


 IP制限がかかっているサーバーにLambdaからアクセスする必要がある場合など、LambdaのIP固定化が必要な際にAWS CDK v2でどのように実装するかを記載します。

## 構成

VPCを作成し、その中でLambdaとEIPを紐付けてIPを固定化し、インターネットにアクセスする構成です。
Lambdaの中身はTypescriptで実装し、[axios](https://github.com/axios/axios)を利用してAPIを実行します。

![archtecture.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2412171/5e842cc7-7e9a-6e76-b7cc-9751bf9caebd.png)

## 環境
```bash
cdk --version
2.21.1 (build a6ee543)

node_modules/.bin/tsc --v
Version 4.8.3
```

## 完成系

```vpc-lambda-stack.ts
import { Duration, Stack, StackProps } from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as nodejs from "aws-cdk-lib/aws-lambda-nodejs";
import { Construct } from "constructs";

export class VpcLambdaStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // VPCの作成
    const vpc = new ec2.Vpc(this, "vpc", {
      maxAzs: 2,
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: "PublicSubnet",
          subnetType: ec2.SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: "PrivateSubnet",
          subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
        },
      ],
    });

    // セキュリティグループの作成
    const securityGroup = new ec2.SecurityGroup(this, "SecurityGroup", {
      vpc: vpc,
      allowAllOutbound: true,
    });

    // Lambdaの作成
    new nodejs.NodejsFunction(this, "VpcLambdaFunction", {
      entry: "src/api-test/handler/handler.ts",
      runtime: lambda.Runtime.NODEJS_16_X,
      timeout: Duration.seconds(10),
      vpc: vpc,
      securityGroups: [securityGroup],
      vpcSubnets: vpc.selectSubnets({
        subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
      }),
      allowPublicSubnet: true,
    });
  }
}
```

### VPC
まずは基盤となるVPCを作成します(公式リファレンスは[こちら](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.Vpc.html))。
このVPCを作成する際に`サブネット`や`EIP`などの関連するリソースも一緒に作成できます。

ここではデフォルトのリソース以外に、オプションを利用することで2つの`アベイラビリティゾーン`に対して`パプリックサブネット`と`プライベートサブネット`をそれぞれ作成しています。

```typescript
    const vpc = new ec2.Vpc(this, "vpc", {
      maxAzs: 2,
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: "PublicSubnet",
          subnetType: ec2.SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: "PrivateSubnet",
          subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
        },
      ],
    });
```

### Security Group

次にLambdaに紐づけるセキュリティーグループを作成します(公式リファレンスは[こちら](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.SecurityGroup.html))。
セキュリティーグループの`vpc`には先ほど作成したものを指定します。

```typescript
    // セキュリティグループの作成
    const securityGroup = new ec2.SecurityGroup(this, "SecurityGroup", {
      vpc: vpc,
      allowAllOutbound: true,
    });
```

### Lambda

最後にVPCに配置するLambdaを作成します(公式リファレンスは[こちら](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs.NodejsFunction.html))。

`securityGroups`には先ほど作成したものを、`vpcSubnets`にはvpcの`selectSubnets`メソッドによってsubnetTypeが`PRIVATE_WITH_NAT`になっているものを渡します。
Lambda関数はインターネットにアクセスするため、`allowPublicSubnet`を`true`にする設定も必要です。

```typescript
    // Lambdaの作成
    new nodejs.NodejsFunction(this, "VpcLambdaFunction", {
      entry: "src/api-test/handler/handler.ts",
      runtime: lambda.Runtime.NODEJS_16_X,
      timeout: Duration.seconds(10),
      vpc: vpc,
      securityGroups: [securityGroup],
      vpcSubnets: vpc.selectSubnets({
        subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
      }),
      allowPublicSubnet: true,
    });
  }
```

## 固定化されたIPの確認

作成された`EIP`はAWSコンソールの`EC2 -> ネットワーク＆セキュリティ -> Elastic IP`から確認できます。

今回は`maxAzs`を2に指定しているため、`EIP`も2つ作成されます。アクセス制限のあるサーバーなどにアクセスする場合はこちらのIPアドレスに対する許可が必要です。

<span style="color:#d70910;">※注意：デフォルトでは、すべてのAWSアカウントで1リージョンあたり5つのElastic IPアドレスまでしか作成できない制限があります。</span>参考：[Elastic IP アドレス](https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/elastic-ip-addresses-eip.html)

![mosaic_20220928161244.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2412171/f4fcb86b-768a-16fe-c17d-3d126f872886.png)

## まとめ

LambdaのIPを固定化させるにはVPCを細かく設定しないといけないと思いきや、必要なリソースはVPCが良い感じに作成してくれるため、意外と簡単に実装が可能なことがわかりました。

ただし、1アカウントに作成できるEIPは5個までの制限があるため、複数環境を利用する場合は環境毎にアカウントを分けるなどの対応が必要かと思います。

IP制限のあるアクセスは何かと多いと思うので、この記事が参考になれば幸いです。

## 参考資料

[【AWS/Lambda】lambdaに固定IPをつける/VPCに所属させる](https://qiita.com/shim-hiko/items/d085f05e8fdded648e1d)
[AWS CDKを使ってLambda 関数URL(Function URLs)を設定してみた。(L1 Constractで)](https://zenn.dev/keni_w/articles/80ff147f0e57f6)

## おまけ

### Lambdaの中身の実装

Lambdaの中身はTypeScriptで実装し、[axios](https://github.com/axios/axios)を利用してAPIを実行します。
以下のサンプルでは簡単なPOSTリクエストを送信できます。

```handler.ts
import { APIGatewayProxyEvent } from "aws-lambda";
import axios from "axios";
import * as log from "lambda-log";

/**
 * apiを実行するLambda
 * @param event
 * @returns
 */
export const handler = async (event: APIGatewayProxyEvent) => {
  log.info("event row data", { event });

  const endpoint = "https://example.com/";
  const headers = {
    "Content-Type": "application/json",
  };
  const body = {
    id: "XXXXXXXXXX",
  };

  // APIを実行
  try {
    return await axios
      .post(endpoint, body, {
        headers: headers,
      })
      .then((response) => {
        log.info("raw response data", { data: response.data });
      })
      .catch(async (reason) => {
        log.error("Executing api failed.", { reason });
        throw reason;
      });
  } catch (e) {
    log.error(`Error executing api, reason=${(e as Error).message}`);
  }
};
```