mazyu36の日記

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

Global AcceleratorのWorkshopをCDKで実装する

業務でGlobal Acceleratorを検討する機会があったのですが、恥ずかしながら使用したことがないため勉強がてらCDKで実装してみました。

題材としては以下のWorkshopのGlobal Acceleratorパートを使用しています。

catalog.workshops.aws

実装は以下にあります。

github.com

目次

Global Acceleratorとは

以下の動画が非常にわかりやすかったです(12:25~18:00あたり)。 www.youtube.com

以下上記動画から一部引用します。

まずGlobal Acceleratorとしてできることは主に以下3つです。

  • 複数のエンドポイント(オリジン)に対して単一のエンドポイント(2つの静的IPアドレス)を提供。エンドポイントはリージョン跨ぎも可能。
  • Global Accelerator -> エンドポイント はAWSのネットワークを通るため通信が高速になる。
  • エンドポイント障害を検知してフェイルオーバーも可能

単一のエンドポイントによるグローバル展開や、エンドポイント障害時のフェイルオーバーが可能。

よく比較されるCloudFrontとの違いについて。以下がGlobal Acceleratorの大きなメリットかと思います。

  • HTTP/HTTPS 以外でも利用可能
  • オリジンの完全な隠匿が可能。

CloudFrontとの違いという意味では以下の記事も参考になります。

tech.nri-net.com

アーキテクチャ

以下のようなシンプルな構成です。

  • バックエンドはALB+Lambdaで構成。Lambdaは動作しているリージョンと自身の関数名を返すのみ。
  • その前段にGlobal Acceleratorを配置。

なお今回はあえてALBにも直接アクセス可能な構成(ALBをパブリックサブネットに配置)としている点にご注意ください(Global Accelerator経由のアクセスと、ALB経由のアクセスでパフォーマンスの計測・比較を行うため)。

オリジン(ALB)を隠匿したい場合はプライベートサブネットに配置することが可能です。なおその際もVPCにインターネットゲートウェイをアタッチしておく必要はあるので、その点も注意が必要です(以下のドキュメントに記載あり)。

docs.aws.amazon.com

CDKプロジェクトの構成

├── README.md
├── architecture.drawio.svg
├── bin
│   └── workshop_globalaccelerator.ts
├── cdk.json
├── cdk.out
│   ├── cdk.out
│   ├── manifest.json
│   └── tree.json
├── curl-format.txt  # パフォーマンステストで利用
├── jest.config.js
├── lib
│   ├── construct
│   │   ├── backend.ts # ALB+Lambdaを実装
│   │   └── edge.ts # Global Acceleratorを実装
│   ├── lambda
│   │   └── index.py
│   └── workshop_globalaccelerator-stack.ts
├── package-lock.json
├── package.json
├── test
│   └── workshop_globalaccelerator.test.ts
└── tsconfig.json

CDKの実装

backendの実装

VPC, ALB, Lambdaを実装していきます。シンプルなのでサクッと実装してきます。

まずVPCはパブリックサブネットを2つ作成します。

    // ------ VPC -------
    const vpc = new ec2.Vpc(this, 'Vpc', {
      natGateways: 0,  // デフォルトだとNAT Gatewayが作成されてしまうため0を指定
      maxAzs: 2,
      ipAddresses: ec2.IpAddresses.cidr('10.135.0.0/16'),  // CIDRはWorkshopで使っていたものを指定(深い意味はない)
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'PublicSubnet',
          subnetType: ec2.SubnetType.PUBLIC,
        }
      ],
      enableDnsHostnames: true,
      enableDnsSupport: true
    }
    )

次にLambdaとALBを実装してきます。ここもあまり難しいところはありません。

ターゲットグループのデフォルトアクションとしてLambdaに転送します。

    // ------ Lambda -------
    const lambdaFunction = new lambda.Function(this, 'LambdaFunction', {
      runtime: lambda.Runtime.PYTHON_3_12,
      description: 'This Lambda function simply returns the AWS region and its name.',
      handler: 'index.lambda_handler',
      code: lambda.Code.fromAsset('./lib/lambda/')
    })


    // ------ ALB -------
    const alb = new elbv2.ApplicationLoadBalancer(this, 'ApplicationLoadBalancer', {
      vpc: vpc,
      vpcSubnets: vpc.selectSubnets({ subnetType: ec2.SubnetType.PUBLIC }),
      internetFacing: true,

    })
    this.alb = alb

    // connectionsを使用して、HTTP/Sを許可
    alb.connections.allowFromAnyIpv4(ec2.Port.tcp(443), 'Enable HTTPS access on the Application Load Balancer')
    alb.connections.allowFromAnyIpv4(ec2.Port.tcp(80), 'Enable HTTP access on the Application Load Balancer')

    // ターゲットグループを作成してLambdaを登録
    const targetGroup = new elbv2.ApplicationTargetGroup(this, 'LambdaTargetGroup', {
      healthCheck: {
        interval: cdk.Duration.seconds(35),
        timeout: cdk.Duration.seconds(30),
        healthyThresholdCount: 3,
        unhealthyThresholdCount: 5
      },
      targetType: elbv2.TargetType.LAMBDA,
      targets: [new targets.LambdaTarget(lambdaFunction)]
    })

    // リスナーを作成
    new elbv2.ApplicationListener(this, 'Listener', {
      loadBalancer: alb,
      port: 80,
      protocol: elbv2.ApplicationProtocol.HTTP,
      defaultAction: elbv2.ListenerAction.forward([targetGroup])
    })

edgeの実装

Global Acceleratorを実装していきます。

L2 Constructが存在するのでサクサク実装できます(aws_globalacceleratoraws_globalaccelerator_endpoints)。

まずアクセラレータを作成します。

    // アクセラレータを作成
    const accelerator = new globalaccelerator.Accelerator(this, 'Accelerator', {
      ipAddressType: globalaccelerator.IpAddressType.IPV4
    })

次にリスナーを作成します。

    // リスナーを作成
    const listener = new globalaccelerator.Listener(this, 'Listener', {
      accelerator: accelerator,  // 作成したアクセラレータを設定
      portRanges: [{ fromPort: 80 }],  // ポートを指定
      clientAffinity: globalaccelerator.ClientAffinity.NONE, // クライアントアフィニティの設定(今回は無し)
      protocol: globalaccelerator.ConnectionProtocol.TCP  // プロトコルとして TCP or UDPを指定
    })

最後にリスナーにエンドポイント(今回はALB)を登録します。

    // リスナーにALBのエンドポイントグループを追加
    listener.addEndpointGroup('AlbEndpointGroup', {
      endpoints: [
        new ga_endpoints.ApplicationLoadBalancerEndpoint(
        props.alb,  // ALBを指定(今回はbackend.tsのalbをpropsで受け渡して設定)
        {
          weight: 128,  // 重みを設定
          preserveClientIp: true // クライアントのIPを保持するかを設定
        }
        )
      ],
    })

パフォーマンス計測

WorkshopのGlobal Accelerator パフォーマンスを参考に、Global Accelerator経由でのアクセスと、直接ALBにアクセスした際のパフォーマンスの違いをテストします。

今回は curl で 100kbのデータを5回取得した際のパフォーマンスの違いを確認します。

ap-northeast-1の場合

最初のリクエストはGlobal Acceleratorの方がかなり早いものの、それ以降は多少早いぐらいの違いです。

# Global Accelerator
$ for i in {1..5}; do curl -w @curl-format.txt -o /dev/null -s http://YOUR_ENDPOINT.com/100KB; sleep 3; done | grep -v time_total:0
  Total Time:  0.156340s |  Time to Connect:  0.011343s |  Time To Transfer:  0.011371s |  Time To First Byte:  0.116007s
  Total Time:  0.141812s |  Time to Connect:  0.014920s |  Time To Transfer:  0.015003s |  Time To First Byte:  0.097861s
  Total Time:  0.135022s |  Time to Connect:  0.013116s |  Time To Transfer:  0.013166s |  Time To First Byte:  0.096225s
  Total Time:  0.132770s |  Time to Connect:  0.009638s |  Time To Transfer:  0.009662s |  Time To First Byte:  0.093471s
  Total Time:  0.151768s |  Time to Connect:  0.015104s |  Time To Transfer:  0.015224s |  Time To First Byte:  0.112916s

# ALB
$ for i in {1..5}; do curl -w @curl-format.txt -o /dev/null -s http://YOUR_ENDPOINT.us-east-1.elb.amazonaws.com/100KB; sleep 3; done | grep -v time_total:0

  Total Time:  0.335299s |  Time to Connect:  0.132908s |  Time To Transfer:  0.133032s |  Time To First Byte:  0.257954s
  Total Time:  0.168047s |  Time to Connect:  0.014835s |  Time To Transfer:  0.014932s |  Time To First Byte:  0.109962s
  Total Time:  0.167855s |  Time to Connect:  0.012157s |  Time To Transfer:  0.012183s |  Time To First Byte:  0.108340s
  Total Time:  0.203879s |  Time to Connect:  0.015783s |  Time To Transfer:  0.015824s |  Time To First Byte:  0.141916s
  Total Time:  0.180761s |  Time to Connect:  0.014315s |  Time To Transfer:  0.014356s |  Time To First Byte:  0.120533s

us-east-1の場合

オリジンが地理的に遠いケースとして、us-east-1でもやってみました。

この場合は安定してGlobal Acceleratorの方が早い

# Global Accelerator
$ for i in {1..5}; do curl -w @curl-format.txt -o /dev/null -s http://YOUR_ENDPOINT.com/100KB; sleep 3; done | grep -v time_total:0
  Total Time:  0.928702s |  Time to Connect:  0.013130s |  Time To Transfer:  0.013305s |  Time To First Byte:  0.575303s
  Total Time:  0.796797s |  Time to Connect:  0.015324s |  Time To Transfer:  0.015440s |  Time To First Byte:  0.449703s
  Total Time:  0.777429s |  Time to Connect:  0.009782s |  Time To Transfer:  0.009876s |  Time To First Byte:  0.429472s
  Total Time:  0.791958s |  Time to Connect:  0.010153s |  Time To Transfer:  0.010175s |  Time To First Byte:  0.443317s
  Total Time:  0.783636s |  Time to Connect:  0.015055s |  Time To Transfer:  0.015138s |  Time To First Byte:  0.436786s

# ALB
$ for i in {1..5}; do curl -w @curl-format.txt -o /dev/null -s http://YOUR_ENDPOINT.ap-northeast-1.elb.amazonaws.com/100KB; sleep 3; done | grep -v time_total:0

  Total Time:  1.210771s |  Time to Connect:  0.287146s |  Time To Transfer:  0.287263s |  Time To First Byte:  0.549709s
  Total Time:  1.075512s |  Time to Connect:  0.168787s |  Time To Transfer:  0.168881s |  Time To First Byte:  0.414120s
  Total Time:  1.191307s |  Time to Connect:  0.185972s |  Time To Transfer:  0.186055s |  Time To First Byte:  0.463744s
  Total Time:  1.097473s |  Time to Connect:  0.169945s |  Time To Transfer:  0.170174s |  Time To First Byte:  0.436452s
  Total Time:  1.122181s |  Time to Connect:  0.174007s |  Time To Transfer:  0.174257s |  Time To First Byte:  0.430129s

AWS Global Accelerator Comparison

以下AWSがGlobal Acceleratorのパフォーマンステストをするサイトを公開しています。

speedtest.globalaccelerator.aws

やってみたところ、オリジンが地理的に遠い海外リージョンはGlobal Acceleratorの方が早いものの、東京リージョンはむしろ少し遅くなってしまいました(何回かやったら東京リージョンでも若干早くなったりしました。まあ誤差の範囲ですね)

高速化だけを目的にGlobal Acceleratorを導入する場合、オリジンの地理的な位置は考慮する必要がありそうです。

おまけ:ALBをプライベートサブネットに配置してオリジンを隠匿

最後にオリジンを隠匿したい場合のCDKの実装を紹介して終わりにします。

ALBをプライベートサブネットに配置します。

まずVPCの実装において以下2点の修正を行います。

  • サブネットをプライベートサブネットに変更
  • インターネットゲートウェイを作成してアタッチ
    // ------ VPC -------
    const vpc = new ec2.Vpc(this, 'Vpc', {
      natGateways: 0,
      maxAzs: 2,
      ipAddresses: ec2.IpAddresses.cidr('10.135.0.0/16'),
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'PrivateSubnet',
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED, // ★プライベートサブネットに変更
        }
      ],
      enableDnsHostnames: true,
      enableDnsSupport: true
    }
    )

    // ★インターネットゲートウェイを作成
    const cfnInternetGateway = new ec2.CfnInternetGateway(scope, 'InternetGateway', {
    })

    // ★IGWをVPCにアタッチ
    new ec2.CfnVPCGatewayAttachment(scope, 'IGW2VPC', {
      vpcId: vpc.vpcId,
      internetGatewayId: cfnInternetGateway.ref
    })

先述した通り、Global Acceleratorからプライベートサブネット内のオリジンにアクセスするために、VPCにインターネットゲートウェイをアタッチしておく必要があります(L2 ConstructだとサブネットにPublicを含まない場合は、明示的にインターネットゲートウェイを作成してアタッチする必要あり)。

docs.aws.amazon.com

なおVPCにインターネットゲートウェイをアタッチしていない状態だと、Global Acceleratorのデプロイ時にエラーになります。

17:10:34 | CREATE_FAILED        | AWS::GlobalAccelerator::EndpointGroup     | EdgeConstructListe...pointGroupCC007889
Resource handler returned message: "arn:aws:elasticloadbalancing:us-east-1:000000000000:loadbalancer/app/Worksh-Backe-1Va3Z1XjTWyk/58e70c28cb484862 does not have an internet gateway in its VPC vpc-0000000000000 (Service: AWSGlobalAccelerator; Status Code: 400; Error Code: InvalidArgumentException; Request ID: beca2206-bd21-49d3-97a2-7
b17deac6212; Proxy: null)" (RequestToken: 838c4d3f-5055-c750-5ed0-d090e5ea90fd, HandlerErrorCode: GeneralServiceException)

次にALBをプライベートサブネットに配置して、タイプをinternalにします。

    // ------ ALB -------
    const alb = new elbv2.ApplicationLoadBalancer(this, 'ApplicationLoadBalancer', {
      vpc: vpc,
      vpcSubnets: vpc.selectSubnets({ subnetType: ec2.SubnetType.PRIVATE_ISOLATED }), // ★プライベートサブネットに変更
      internetFacing: false,  // ★Internet-facingをオフ(internalに変更)
    })

これでオリジンを隠匿しつつ、Global Accelerator経由でアクセスは可能になります。