ALB のターゲットとして VPC Lambda を指定する構成を CDK でやってみた

はじめに

テントの中から失礼します、CX事業本部のてんとタカハシです!

ALB のターゲットに VPC Lambda を指定する構成を CDK でやってみたので、記事にしようと思います。

この構成は、Lambda から RDS に繋げたり、固定IP を持った Nat Gateway 経由で外部サービスにアクセスするなどの用途で使えそうです。

ソースコードは下記のリポジトリにも置いてあります。

GitHub - iam326/alb-vpc-lambda-sample-cdk

環境

環境は下記の通りです。

$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.15.7
BuildVersion:	19H2

$ cdk --version
1.85.0 (build 5f44668)

$ yarn --version
1.22.10

$ node --version
v12.19.0

単純な構成

構成図

構成は下記の通りです。ALB と VPC Lambda を単純に紐づけているだけの構成です。

実装

スタックの実装は下記の通りです。ターゲットグループのターゲットとして VPC Lambda を指定します。

import * as cdk from '@aws-cdk/core';
import { Vpc, SubnetType, SecurityGroup, Peer, Port } from '@aws-cdk/aws-ec2';
import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2';
import { LambdaTarget } from '@aws-cdk/aws-elasticloadbalancingv2-targets';
import * as lambda from '@aws-cdk/aws-lambda';

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

    const prefix: string = this.node.tryGetContext('projectName');

    const vpc = new Vpc(this, 'Vpc', {
      cidr: '10.0.0.0/16',
      enableDnsHostnames: true,
      enableDnsSupport: true,
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'PublicSubnet',
          subnetType: SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: 'PrivateSubnet',
          subnetType: SubnetType.PRIVATE,
        },
      ],
      natGateways: 2,
      maxAzs: 2,
    });

    const securityGroup = new SecurityGroup(this, 'SecurityGroup', {
      securityGroupName: `${prefix}-sg`,
      vpc,
    });
    securityGroup.addIngressRule(Peer.anyIpv4(), Port.tcp(80));

    const alb = new elbv2.ApplicationLoadBalancer(this, 'Alb', {
      vpc,
      vpcSubnets: vpc.selectSubnets({ subnetGroupName: 'PublicSubnet' }),
      loadBalancerName: `${prefix}-alb`,
      internetFacing: true,
      securityGroup,
    });

    const helloWorldFunction = new lambda.Function(this, 'HelloWorldFunction', {
      code: lambda.Code.fromAsset('dist/hello-world'),
      functionName: `${prefix}-hello-world`,
      handler: 'index.handler',
      runtime: lambda.Runtime.NODEJS_12_X,
      timeout: cdk.Duration.seconds(10),
      memorySize: 128,
      vpc,
    });

    const albTargetGroup = new elbv2.ApplicationTargetGroup(
      this,
      'AlbTargetGroup',
      {
        vpc,
        targetGroupName: `${prefix}-tg`,
        targetType: elbv2.TargetType.LAMBDA,
        targets: [new LambdaTarget(helloWorldFunction)],
      }
    );

    alb.addListener('AlbListener', {
      protocol: elbv2.ApplicationProtocol.HTTP,
      port: 80,
      defaultTargetGroups: [albTargetGroup],
    });
  }
}

Lambda の実装は下記の通りです。Content-Typeを指定しないとapplication/octet-streamで返してしまうので、application/jsonを指定しています。

export async function handler(): Promise<any> {
  return {
    statusCode: 200,
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ hello: 'world' }),
  };
}

動作確認

curl で ALB にアクセスすると、期待通りのレスポンスが返ってきました。

$ curl xxxxx.ap-northeast-1.elb.amazonaws.com
{"hello":"world"}

複数の VPC Lambda でパスルーティングする構成

構成図

構成は下記の通りです。アクセスされた HTTP メソッドとパスによって、振り分け先を変えています。

実装

先ほどの実装をベースに変更点を記載します。

export class AlbVpcLambdaSampleCdkStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {

    ...

    // 新しい Lambda を作成。返す値以外は同じコード。
    const HogeFugaPiyoFunction = new lambda.Function(
      this,
      'HogeFugaPiyoFunction',
      {
        code: lambda.Code.fromAsset('dist/hoge'),
        functionName: `${prefix}-hoge-fuga-piyo`,
        handler: 'index.handler',
        runtime: lambda.Runtime.NODEJS_12_X,
        timeout: cdk.Duration.seconds(10),
        memorySize: 128,
        vpc,
      }
    );
    
    // これは不要なのでコメントアウト
    // const albTargetGroup = new elbv2.ApplicationTargetGroup(
    //   this,
    //   'AlbTargetGroup',
    //   {
    //     vpc,
    //     targetGroupName: `${prefix}-tg`,
    //     targetType: elbv2.TargetType.LAMBDA,
    //     targets: [new LambdaTarget(helloWorldFunction)],
    //   }
    // );

    // defaultTargetGroups ではなく、defaultAction として、404 NotFound を返す設定を入れる
    const listener = alb.addListener('AlbListener', {
      protocol: elbv2.ApplicationProtocol.HTTP,
      port: 80,
      // defaultTargetGroups: [albTargetGroup],
      defaultAction: elbv2.ListenerAction.fixedResponse(404, {
        contentType: elbv2.ContentType.TEXT_PLAIN,
        messageBody: 'NotFound',
      }),
    });

    // GET /hello であれば、helloWorldFunction に繋ぐ
    listener.addTargets('AlbListenerTargetHello', {
      priority: 1,
      conditions: [
        elbv2.ListenerCondition.httpRequestMethods(['GET']),
        elbv2.ListenerCondition.pathPatterns(['/hello']),
      ],
      targets: [new LambdaTarget(helloWorldFunction)],
    });
    
    // POST /hoge であれば、HogeFugaPiyoFunction に繋ぐ
    listener.addTargets('AlbListenerTargetHoge', {
      priority: 2,
      conditions: [
        elbv2.ListenerCondition.httpRequestMethods(['POST']),
        elbv2.ListenerCondition.pathPatterns(['/hoge']),
      ],
      targets: [new LambdaTarget(HogeFugaPiyoFunction)],
    });
  }
}

デプロイすると、ALB のリスナールールは下記の通りになります。

動作確認

指定する HTTP メソッドとパスによって、レスポンスが変わるようになりました。存在しないパスにアクセスした場合はNotFoundを返すようになっています。

$ curl xxxxx.ap-northeast-1.elb.amazonaws.com/hello
{"hello":"world"}

$ curl -X POST xxxxx.ap-northeast-1.elb.amazonaws.com/hoge
{"fuga":"piyo"}

$ curl xxxxx.ap-northeast-1.elb.amazonaws.com/foo
NotFound

おわりに

新年1本目の記事になりました。今月中に残り3本は書こうと思っています、がんばるぞお〜。

今回は以上になります。最後まで読んで頂きありがとうございました!