見出し画像

ECS と API Gateway との接続に Cloud Map を使う

Solution Architect の t_maru です。

今回は ECS Fargate 上で稼働するサービスを Cloud Map を使って API Gateway と接続する方法についてご紹介します。

※ 例として使用する API Gateway は HTTP API タイプを使用しています。REST API タイプの API を構成している場合は Cloud Map を使ったプライベート統合はできませんのでご注意ください。


CloudMap とは?

まず AWS のドキュメントで Cloud Map の説明を見てみましょう。

AWS Cloud Map は、クラウドリソース検出サービスです。Cloud Map ではアプリケーションリソースにカスタム名を付けることができ、これらの動的に変化するリソースの場所を自動的に更新します。アプリケーションがそのリソースの最新の場所を常に検出するため、アプリケーションの可用性が向上します。

https://aws.amazon.com/jp/cloud-map/faqs/

最近ではコンテナベースのシステムも当たり前になり、アプリケーション実行環境の水平スケール (インスタンスやコンテナを増やして処理能力を高くすること) が容易になっていますが、負荷状況によってコンテナの増減が起こるような環境の場合、通信先の接続情報 (IP アドレスやポート) は常に変更が繰り返されることになり最新の情報を管理することが重要であり、これを自前で実現するのは一苦労です。

そんな状況で有効なのが今回取り上げている Cloud Map です。Cloud Map は IP ベースのコンポーネントの状態 (コンテナの追加/削除なども含む) を継続的にモニタリングして、アプリケーションの接続先の情報を常に最新の状態に保ってくれますので、利用する側としては Cloud Map で予め定義した接続に使う名前空間を使って各種サービスが稼働している接続先情報を得ることができるようになります。

Cloud Map は IP ベースのサービス以外にも URL や ARN ベースのリソースに対しても名前空間を作成することができるため汎用性の高いサービスで用途は様々ですが、今回は Fargate 上で稼働している service に独自の名前空間を設定し API Gateway とプライベート統合する際に Cloud Map を使う例を紹介します。

API Gateway と Fargate service 接続する

下記に Cloud Map と Fargate, API Gateway HTTP API を使った構成例を示します。
ここで 1 点注意ですが、冒頭の注意書きにも記載した通り Cloud Map によるプライベート統合を使用できるのは HTTP API のみです。REST API の場合は Cloud Map や Application Load Balancer (ALB) を使った接続はできませんのでご注意ください。

API Gateway の REST API と HTTP API の違いについては下記の公式ドキュメントを参照してください。
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/http-api-vs-rest.html

Cloud Map を使った構成

API Gateway で設定した path にリクエストがあると API Gateway は Cloud Map に接続先を問い合わせを行い、それに従って Fargate 上で起動している service にリクエストが送信される形となります。
後ほど CDK を使ったリソース定義方法を掲載しますが、今回は ECS の Service discovery を設定することで間接的に Cloud Map を使用しています。

また、上記の図に Route 53 も表記しましたが、Cloud Map に設定した内容に従って自動的に Route 53 にレコード追加される仕組みとなっておりますので合わせて表記しました。

上記以外の構成でよく使われるものとしては、以下に示すように ALB を使った構成もあります。

ALB を使った構成

この構成の場合、API Gateway は設定した path にリクエストがあると ALB にリクエストを送信し、ALB が Target Group に登録された Fargate service に対してリクエストを転送する形となりますので、先程の例と API Gateway の API を叩いた結果は変わりませんが、内部的に行われているリクエストの送信される経路が少し異なることになります。

これらの構成のうち、ALB と Cloud Map を費用で比較してみると以下のようになります。

# ALB
  - 1 時間あたり 0.0243 USD -> 1 ヶ月あたり 約 17.5 USD


# Cloud Map
  - 0.1 USD / 登録リソース (1 ヶ月あたり)
  - ECS Service Discovery を使って登録されたリソースは無料

※ Tokyo Region の価格

※ 2024/1 時点での料金です。上記以外にもリクエスト単位で課金が発生しますので、詳細は公式の料金表をご確認ください。

このあと紹介する例のように ECS の Service discovery として使用する場合は Cloud Map に費用は発生しませんので、費用面で見ると CloudMap が有利となります。

※ 上記にはリクエスト単位で発生する料金は記載しておりません。ECS Service discovery を使う場合でもリクエスト数に応じた課金は発生しますので、完全無料で使えるわけではない点にご注意ください。

CDK によるリソースの定義例

今回も CDK を使って API Gateway, Fargate, CloudMap を構築してみようと思いますが、まず前提条件を整理しておきます。

  • VPC は構築済み

  • Fargate service は VPC 内で Private subnet に配置される

  • Private subnet には Fargate のデプロイや動作に必要な VPC Endpoint が設定済み

  • Security group なども作成済み

  • CDK

    • TypeScript を使用

    • version 2.80.0 を使用

上記の条件で記載した事項がすでに設定されている状態として CDK で記載したリソース定義をそれぞれのサービスの設定部分を抜き出して説明していきます。

まず、Cloud Map の設定です。
CDK で構築する場合 `service_discovery` モジュールを使い以下のように Private namespace と Discovery service を作成します。

import * as service_discovery from 'aws-cdk-lib/aws-servicediscovery';

// ...

const privateDnsNamespace = new service_discovery.PrivateDnsNamespace(this, 'FargateNamespace', {
  vpc,
  name: 'sample-name-space',
});

const ecsDiscoveryService = privateDnsNamespace.createService('EcsDiscoveryService', {
  name: 'EcsDiscoveryService',
  dnsRecordType: service_discovery.DnsRecordType.SRV,
  dnsTtl: cdk.Duration.seconds(60),
  customHealthCheck: {
    failureThreshold: 1,
  },
});

次に Fargate の service との紐づけですが、service を定義した上で `associateCloudMapService` メソッドを使って Cloud Map との紐づけを行います。

import * as ecs from 'aws-cdk-lib/aws-ecs';

// ...

const containerImageUri = this.node.tryGetContext('CONTAINER_IMAGE_URI');

// ...

const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', {
  cpu: 512,
  memoryLimitMiB: 1024,
});

taskDefinition.addContainer('TaskContainer', {
  image: ecs.ContainerImage.fromRegistry(containerImageUri),
  portMappings: [{ containerPort: 8080 }],
});

const backendFargateService = new ecs.FargateService(this, 'BackendService', {
  serviceName: 'BackendService',
  cluster: ecsCluster,
  taskDefinition,
  desiredCount: 1,
  vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
});

// FargateService に対して Cloud Map の紐づけを行う
backendFargateService.associateCloudMapService({
  service: ecsDiscoveryService,
  containerPort: 8080,
});

最後に API Gateway と Cloud Map の接続ですが、integration で Discovery service の ARN を指定する形での紐づけとなります。

import * as apigw from 'aws-cdk-lib/aws-apigatewayv2';

// ...

const vpcLink = new apigw.CfnVpcLink(this, 'ApiGwVpcLink', {
  name: 'ApiGwVpcLink',
  subnetIds: [privateSubnetId1, privateSubnetId2],
  securityGroupIds: [apiGwVpcLinkSecurityGroupId],
});

const api = new apigw.CfnApi(this, 'BackendApi', {
  name: 'BackendApi',
  protocolType: 'HTTP',
  corsConfiguration: {
    allowOrigins: ['*'],
    allowMethods: ['*'],
    allowHeaders: ['Content-Type', 'Authorization', 'X-Api-Key'],
    allowCredentials: false,
    maxAge: 600,
  },
});

const apiIntegration = new apigw.CfnIntegration(this, 'BackendApiIntegration', {
  apiId: api.attrApiId,
  integrationType: 'HTTP_PROXY',
  integrationMethod: 'ANY',
  connectionType: 'VPC_LINK',
  connectionId: vpcLink.attrVpcLinkId,
  payloadFormatVersion: '1.0',
  // ここで discovery service の ARN を指定することで紐づけを行う
  integrationUri: ecsDiscoveryService.serviceArn,
});

まとめ

今回は Cloud Map の紹介と、使用例として API Gateway (HTTP API) と Fargate 上にデプロイした Service を Cloud Map を使って統合する方法を説明しました。

Cloud Map は紐付けるサービスを名前空間で管理し正常なホストを接続先情報として提供するので、マイクロサービスとは非常に相性のよいサービスです。ECS を使ってシステムを構築している方は Cloud Map の利用も検討してみてはいかがでしょうか。


みんなにも読んでほしいですか?

オススメした記事はフォロワーのタイムラインに表示されます!