mazyu36の日記

某SIer所属のクラウドエンジニアのブログ

VPC LatticeをAWS CDK(L1 Construct)/ CloudFormationで実装する

re:Invent 2022でも話題になっていたVPC Latticeが2023/3/31にGAしました。

aws.amazon.com

キャッチアップついでにAWS CDK(L1 Construct)で実装したので、その内容をまとめます。なお L1 ConstructはCloudFormationとほぼイコールなので、CloudFormationで作成する際も同様になります。

※記事作成時点ではCDKのL2 Constructは存在しません。

以下今回構築したアーキテクチャです。

以下のように3つのVPC Lattice Serviceの構成で作成します。

  • EC2(Client):サービス間通信の送信元です。ここから他サービスにリクエストを投げて通信を確認します。
  • Lambda:サービス間通信の送信先その1です。HTTPSの通信とし、パスベースのルーティングや、CW Logsへのログ出力を検証します。
  • EC2(Server):サービス間通信の送信先その2です。HTTPの通信とし、VPCを跨いだ通信や、S3へのログ出力を検証します。

目次

1.そもそもVPC Latticeとは

これは他に素晴らしい解説記事があるのでそちらをご参照ください。例えば以下など。

qiita.com

少し触ってみた上での私の理解を一言でいうと「AWSのサービス間/VPC間等の通信をALBのように実現できるサービスメッシュ」です。

2.AWS CDK(L1 Construct), CloudFormationの実装方法

GA時点でCloudFormationが提供されており、対応する形でAWS CDKのL1 Constructも存在しています。

docs.aws.amazon.com

docs.aws.amazon.com

実装が必要なものは以下です。順番に簡単に説明します。

①Service Network関連

一番上位の概念がService Networkになります(ハブ的なもの。後続のServiceを束ねるようなイメージ)。

Service Networkを作成し、Service Network VPC AssociationでVPCと紐づける形になります。

②Service関連

ALBでいうロードバランサー的なものです。 Serviceにドメインネームを割り当てて、それを元に通信する形となります。

Serviceを作成し、Service Network Service AssociationでService Networkと紐づける形になります。

③Listener/Target関連

ここはALBのそれとほぼ一緒です。 なおService1つに対して、Listener 2つまで、Target Groupは5つまでが制限になります(2023/4/2現在)。

docs.aws.amazon.com

Access Log関連

アクセスログを出力する設定になります。S3, CW Logs, Kinesis Firehoseに出力します。

Access Log SubscriptionでSeviceNetwork or Serviceを出力先と紐づける形になります。

docs.aws.amazon.com

3.AWS CDK(L1 Construct) の実装内容

では実装内容に移ります。なおコードは以下に格納しています。

github.com

全体像

冒頭に示した通り、以下のようにEC2(Client)からEC2(Server)/Lambdaに対して通信を行うような、Servie Networkを構築します。

なお厳密にはEC2(Server)-LambdaのService間も通信できますが、今回は通信しないため矢印を省略しています。

プロジェクト構成は以下です。例のごとくConstructで構造化を行なっています。

.
├── cdk_vpc_lattice-stack.ts # Stackを定義
└── construct # Constructで構造化
    ├── lambda # Lambdaのコードを格納
    │   ├── default.py
    │   ├── first.py
    │   └── second.py
    ├── script # EC2 Server構成用のUser Dataを格納
    │   └── user-data.sh
    ├── ec2Construct.ts # EC2を定義(Client, Server)
    ├── lambdaConstruct.ts # Lambdaを定義
    ├── vpcConstruct.ts # VPCを定義
    └── vpcLatticeConstruct.ts # VPC Latticeを定義

実装方法詳細

vpcLatticConstruct.tsに実装している、VPC Latticeの実装内容の詳細を先述の①〜④に沿って記載していきます。

①Service Network関連

まずはServiceNetworkを作成し、VPCと紐付けていきます。

    //----------------VPC Lattice Service(Client EC2)-------------
    // Service Networkを作成
    const vpcLatticeServiceNetwork = new vpclattice.CfnServiceNetwork(this, 'VpcLatticeServiceNetwork', {
      authType: 'NONE', // NONE or IAM
      name: 'test-vpclattice-servicenetwork'
    })

    // VPC(EC2 Client) SGを作成して443と80からの接続を許可
    const vpcLatticeServiceNetworkSg = new ec2.SecurityGroup(this, 'VpcLatticeServiceNetworkSecurityGroup', {
      vpc: props.vpcConstruct.vpc,
      allowAllOutbound: true
    })
    vpcLatticeServiceNetworkSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443));
    vpcLatticeServiceNetworkSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80));

    // Service NetworktとVPC(EC2 Client)を紐付け。
    new vpclattice.CfnServiceNetworkVpcAssociation(this, 'VpcLatticeServiceNetworkVpcAssociation', {
      vpcIdentifier: props.vpcConstruct.vpc.vpcId,
      serviceNetworkIdentifier: vpcLatticeServiceNetwork.attrId,
      securityGroupIds: [vpcLatticeServiceNetworkSg.securityGroupId]
    })

まずはService Networkの作成と、VPC(EC2 Client)の紐付けになります。

  • CfnServiceNetworkでServiceNetworkを作成。認証タイプは今回はNONEIAMによる制御も可能)
  • セキュリティグループとして443と80の接続を許可。
  • CfnServiceNetworkVpcAssociationでService NetworkとVPCを紐付ける。
    // VPC (EC2 Server)用のSGを作成
    const vpcLatticeServiceNetworkSecondSg = new ec2.SecurityGroup(this, 'VpcLatticeServiceNetworkSecondSecurityGroup', {
      vpc: props.vpcConstruct.secondVpc,
      allowAllOutbound: true
    })
    vpcLatticeServiceNetworkSecondSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80));

    // Service NetworktとVPC(EC2 Server)を紐付け。
    new vpclattice.CfnServiceNetworkVpcAssociation(this, 'VpcLatticeServiceNetworkSecondVpcAssociation', {
      vpcIdentifier: props.vpcConstruct.secondVpc.vpcId,
      serviceNetworkIdentifier: vpcLatticeServiceNetwork.attrId,
      securityGroupIds: [vpcLatticeServiceNetworkSecondSg.securityGroupId]
    })

続いてVPC(EC2 Server)を追加する箇所です。

  • セキュリティグループとして80の接続を許可。
  • CfnServiceNetworkVpcAssociationで作成済みのService NetworkにVPCを追加する。

※LambdaはVPCなので追加なし。

②Service関連

以降②〜④はLambdaのVPC Lattice Serviceを中心に記載します(他のServiceの実装も基本同じなので。違うところのみ言及)

ここではServiceを作成し、Service Networkと紐付けます。

    // Serviceを作成
    const vpcLatticeServiceLambda = new vpclattice.CfnService(this, 'VpcLatticeServiceLambda', {
      name: 'test-vpclattice-service-lambda',
      authType: 'NONE'
    })

    // ServiceNetworkとServiceを紐付け
    new vpclattice.CfnServiceNetworkServiceAssociation(this, 'VpcLatticeServiceNetworkServiceAssociationLambda', {
      serviceNetworkIdentifier: vpcLatticeServiceNetwork.attrId,
      serviceIdentifier: vpcLatticeServiceLambda.attrId
    })

流れとしては以下です。

  • CfnServiceでServiceを作成。Service Networkと同様に認証タイプはNONE or IAMを選択可能。
  • CfnServiceNetworkServiceAssociationでService Networkと作成したServiceを紐づける。

③Listener/Target関連

まずはデフォルトのTarget GroupとListenerを作成します。ALBにかなり近いです。

    // デフォルトのターゲットグループを作成
    const vpcLatticeTargetGroupDefaultLambda = new vpclattice.CfnTargetGroup(this, 'VpcLatticeTargetGroupDefaultLambda', {
      type: 'LAMBDA',
      targets: [{
        id: props.lambdaConstruct.defaultFunction.functionArn,
        port: 443
      }],
      name: 'vpclattice-tg-default-lambda'
    })

    // Listenerを作成
    const vpcLatticeListenerLambda = new vpclattice.CfnListener(this, 'VpcLatticeListenerLambda', {
      port: 443,
      protocol: 'HTTPS',
      serviceIdentifier: vpcLatticeServiceLambda.attrId,
      defaultAction: {
        forward: {
          targetGroups: [{
            targetGroupIdentifier: vpcLatticeTargetGroupDefaultLambda.attrId, //デフォルトのTarget Groupを作成。
            weight: 100,
          }],
        },
      },
      name: 'vpclattice-listener-lambda'
    })

流れは以下です。

  • CfnTargetGroupでデフォルトのTarget Groupを作成。ここではDefault用のLambda関数を指定。
  • CfnListenerでListenerを作成。またここでデフォルtのTarget Groupを指定 ※固定レスポンス404もCloudFormationではできるはずだが、うまくできず、、、調査中。

次にLambdaのServiceではパスベースのルーティング(/first/second)のための実装を行います。RuleとTargetGroupを実装します。

以下は/firstのパスの実装内容です(/secondの方もほぼ同じなので省略)。

    // TargetGroupを作成
    const vpcLatticeTargetGroupFirstLambda = new vpclattice.CfnTargetGroup(this, 'VpcLatticeTargetGroupFirstLambda', {
      type: 'LAMBDA',
      targets: [{
        id: props.lambdaConstruct.firstFunction.functionArn,
        port: 443
      }],
      name: 'vpclattice-tg-first-lambda'
    })

    // /firstに振り分けるためのRuleを作成。
    new vpclattice.CfnRule(this, 'VpcLatticeRuleFirstLambda', {
      action: {
        forward: {
          targetGroups: [{
            targetGroupIdentifier: vpcLatticeTargetGroupFirstLambda.attrId,
            weight: 100,
          }],
        },
      },
      match: {
        httpMatch: {
          method: 'GET',
          pathMatch: {
            match: {
              exact: '/first'
            },
            caseSensitive: true,
          },
        },
      },
      priority: 10,
      listenerIdentifier: vpcLatticeListenerLambda.attrId,
      serviceIdentifier: vpcLatticeServiceLambda.attrId,
      name: 'vpclattice-rule-first-lambda'
    })

流れは以下です。

  • CfnTargetGroupでTarget Groupを作成。
  • CfnRuleで振り分けのためのRuleを作成し、Service/Listenerと紐づける

LambdaのServiceに関する実装は以上です。

あとはEC2の場合だと、ALBのようにヘルスチェックも設定できます。以下実装例です。

    const vpcLatticeTargetGroupServerEc2 = new vpclattice.CfnTargetGroup(this, 'VpcLatticeTargetGroupServerEc2', {
      type: 'INSTANCE',
      targets: [{
        id: props.ec2Construct.serverInstance.instanceId,
        port: 80
      }],
      config: {
        port: 80,
        protocol: 'HTTP',
        vpcIdentifier: props.vpcConstruct.secondVpc.vpcId,
        healthCheck: {  // 以下ヘルスチェックの設定
          enabled: true,
          healthCheckIntervalSeconds: 15,
          healthCheckTimeoutSeconds: 15,
          healthyThresholdCount: 3,
          matcher: {
            httpCode: '200',
          },
          path: '/', 
          port: 80,
          protocol: 'HTTP',
          unhealthyThresholdCount: 3,
        },
      },
      name: 'vpclattice-tg-server-ec2'
    })

Access Log関連

最後にアクセスログの設定をします。

    // CW Logsのロググループを作成。
    const logGroup = new logs.LogGroup(this, "VpcLatticeLogGroup", {
      logGroupName: "vpc-lattice-lambda",
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      retention: logs.RetentionDays.THREE_DAYS
    })

    // アクセスログの設定を実施。
    new vpclattice.CfnAccessLogSubscription(this, 'VpcLatticeServerLambdaAccessLogSubscription', {
      destinationArn: logGroup.logGroupArn,  // 出力先を指定
      resourceIdentifier: vpcLatticeServiceLambda.attrId,  // Service Network or Serviceを指定
    });

CfnAccessLogSubscriptionで出力元(Service Network or Service)と出力先(CW Logs or S3 or Firehose)を紐づけるのみです。上記はCW Logsの場合の例です。

4.実際にデプロイ

ではデプロイしてみます。代表的なところを抜粋しつつマネコンで見ていきます。

Service Network

想定通り、以下で作成されています。

  • Service associationsが3:EC2(Client), Lambda, EC2(Server)
  • VPC associationsが2:EC2(Client)のVPCとEC2(Server)のVPC

Service(Lambda)

Serviceも想定通り3つ作成されています。

以下、Lambda Serviceの詳細を見てみます。Domain nameが後ほど疎通時に使用するものになります。

ルーティングの設定を見てみると以下のようになっています。/first, /second, それ以外でパスベースでルーティングするようになっています。この辺りはALBに近いですね。

TargetGroup(EC2 Server)

EC2(Server)のTarget Groupを見てみます。ヘルスチェックが動作していることが確認できます。これもALBほぼそのままという感じです。

5.動作確認

では早速動作確認をしてみます。

(1). EC2(Client) -> Lambda(HTTPS

以下画像の赤のルートの通信が問題ないことを確認します。

確認内容は以下です。

  • HTTPS通信でパスルーティングが行えているか
  • CW Logsにログが出力されているか。

早速EC2(Client)にセッションマネージャーで接続し、LambdaのVPC Lattice ServiceのDomain nameを元にリクエストを投げてみます。

改行の関係で若干わかりづらいですが、以下4つのリクエストを投げ、想定通りの挙動であることが確認できました。

  • /firstでリクエスト → firstのLambdaが呼び出される
  • /secondでリクエスト → secondのLambdaが呼び出される
  • パスつけずにリクエスト → defaultのLambdaが呼び出される
  • 存在しないパス(/hogehoge)でリクエスト → defaultのLambdaが呼び出される

次にCW Logsのログを確認します。

上記のようにログが出力されていました。

(2). EC2(Client) -> EC2(Server)

次に以下のルートの通信を確認します。

EC2(Server)はhttpdでhtmlを公開しているのみです。

ここで確認したい内容は以下です。

  • HTTP通信でVPCを跨いだ通信が可能か。
  • S3にログが出力されているか。

先ほどと同様にEC2(Server) のVPC Lattice ServiceのDomain nameにリクエストを投げたところ、レスポンスが返ってきました。問題なさそうです。

次にS3のログ確認です。こちらも問題なさそうです。ただCW Logsと比較すると若干ラグがありそうな感じでした(1~2分程度)。

以上で今回確認したかった点については問題ないことを確認しました。

おわりに

使ってみた印象としては、ALB(L7)の感覚でAWSサービス間やVPC間通信を実現できるのは楽だなという感じです(その分ハマった時の解析がしんどそうな気もしますが)。

IAMによる制御も便利そうなので、今後検証してみたいと思います。

L1 Construct(CloudFormation)だとAssociationで紐付けが必要になるところが少し面倒ですが、ここは将来的にL2 Constructの登場すれば緩和されるかと思います。