less is more

心のフルスタックエンジニア👨‍💻バイブスでコードを書いています🤘

ECSのサービスディスカバリーを試す

概要

サービスディスカバリーとは、マイクロサービスなんかを作るときには必須となるあれである。 AWSにおいては、各種リソース間で通信を行う際に、相手先のエンドポイントを解決する仕組みが用意されている。

外向き、つまり対インターネットに対しては、ロードバランサーを用意して、Route53でドメインをくっつけてというのが一般的だと思うが、内部でしか利用しないエンドポイントであれば、VPCのプライベートDNSクエリを用いればよい。

そしてECSサービスの場合は、タスクのオートスケーリングに対して、インスタンスの登録/解除を自動でマネージドしてくれる サービスディスカバリー という機能がある。それの仕組みと構築方法を紹介します。

結論から

  • ECSで特定のエンドポイントをもつサービスを作る際、ロードバランサーなしでも実現できる。
  • EIPとかで固定する必要もない。
  • サービス検出は完全にマネージド。ヘルスチェックはタスク定義のコンテナレベルのものでOK.

ユースケース

  • アプリ1をEC2にデプロイ。
  • アプリ2をECSにデプロイ。起動タイプはFargate.
  • アプリ1からアプリ2にHTTPリクエストを送る。
  • ロードバランサー (ALB) は利用しない。
  • アプリ1とアプリ2は同じVPCに配置する。
  • アプリ1はパブリックサブネットに配置する。
  • アプリ2はプライベートサブネットに配置する。

構成図はこんな感じ。 アプリ2は puma+nginxで、nginxのポート80番をエンドポイントとしている。 (ちなみにサンプルはSinatraで作ったが、Railsでも特に大差はない)

f:id:bluepixel:20200405232213p:plain

解決すべき課題

Fargateは起動するたびにIPアドレスが変わるため、アプリ1からはリクエストする先が分からなくなる。そのため、一般的にはロードバランサーを置いて名前解決と負荷分散を行うことになるのだが、今回取るのは別の方法。

ロードバランサーを作るのが面倒、コストを削減したい、L4やL7などロードバランサー特有の機能を使う予定がない、という場合は、ECSのサービスディスカバリーを使うことができる。

サービスディスカバリーを使った最終的な構成がこれ。

f:id:bluepixel:20200405232234p:plain

順に解説していく。

登場人物

  • AWS Cloud Map
  • Route53 Private DNS

AWS Cloud Map

AWS上で展開する各種サービスを検出するためのレジストリで、 誤解を恐れずにざっくり言ってしまえば、ただの名前空間

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

エンドポイントにカスタム名をつけて管理し、APIコールまたはVPC DNSクエリを通じて適切にサービスにルーティングを行う仕組み。

特にマイクロサービスを作る上では必須となるサービスディスカバリを支援してくれる。

一般にはIPアドレスをもった何かしらのサーバーリソースをイメージすることが多いと思うが、LambdaやSQSのエンドポイントも解決できる。

今回は ECS と VPC Private DNS と組み合わせて使用するので、特に意識する必要はなく、ただの名前空間くらいの認識でよい。

興味があれば以下の記事が詳しい。

詳細解説「AWS Cloud Map」とは #reinvent

Route53 Private DNS

Rout53 にパブリックではなくプライベートな Hosted Zone を作成して、VPC内のインターナルな名前解決を行う仕組み。

特定のVPCに紐づけて、当該VPC内からAWSのプライベートDNSに対してクエリを行う。

複数のVPCやリージョンをまたぐことも可能。

Amazon Route 53 Private DNSを複数VPCに適用する

今回は同一VPC内で起動したFargateタスクに対して、タスクに割り当てられたプライベートIPへの名前解決を行いたいので、要件に合致している。

構築

全てCloudFormationを用いて行う。 疎通確認用に、app1としてEC2インスタンスから作業を行うために、ssh接続のためのキーペアだけ事前に作成しておく。

話の本題の部分だけ解説するので、他のVPCやECSの定義はgithubを参照してほしい。

rezept/ecs-service-discovery

AWS Cloud Map

AWS Cloud Map に名前空間を作成する。

PrivateDNSNamespace:
  Type: AWS::ServiceDiscovery::PrivateDnsNamespace
  Properties:
    Name: osushi.service
    Vpc: !Ref VPC

VPCに紐づけた Private DNS クエリを利用するのでリソースタイプはAWS::ServiceDiscovery::PrivateDnsNamespace となる。 「Cloud Map」という言葉が出てこないので結構ややこしいのだが、コンソール上では [AWS Cloud Map] のページにリソースが作成される。

f:id:bluepixel:20200405232312p:plain

名前はドメイン名、つまりはエンドポイントのサフィックスになるので、それっぽい名前をつけることをお勧めする。

ECS

マネジメントコンソールでサービスを作成する際に設定することができる [サービス検出(オプション)] は、CloudFormation上では、ServiceRegistries というプロパティで設定する。

f:id:bluepixel:20200405232334p:plain

ECSService:
  Type: AWS::ECS::Service
  Properties:
    Cluster: !Ref ECSCluster
    DesiredCount: !Ref ServiceDesiredCount
    LaunchType: FARGATE
    NetworkConfiguration:
      AwsvpcConfiguration:
        AssignPublicIp: DISABLED
        SecurityGroups:
          - !Ref SecurityGroup
        Subnets:
          - !Ref PrivateSubnet
      ServiceName: !Ref AppName
      TaskDefinition: !Ref ECSTaskDefinition
      ServiceRegistries:
        - ContainerName: nginx
          ContainerPort: !Ref Port
          RegistryArn: !GetAtt DiscoveryService.Arn

今回は、nginxがリクエストを捌くので、nginxコンテナの名前とポートを指定する。 ここで指定したコンテナのポートとプライベートIPアドレスが、Cloud Mapからターゲットとして検出され、自動でDNSに登録されることになる。

RegistryArn に指定するためのCloudMapのサービスを登録する。

DiscoveryService:
  Type: AWS::ServiceDiscovery::Service
  Properties: 
    Description: discovery service
    DnsConfig:
      RoutingPolicy: MULTIVALUE
      DnsRecords:
        - TTL: 60
          Type: A
        - TTL: 60
          Type: SRV
    NamespaceId: !Ref PrivateDNSNamespaceID
    Name: myapp

AレコードとSRVレコードの両方を登録する。 TTLはとりあえず60秒。

NamespaceId には VPC の項で作成した名前空間を指定。

Name は、エンドポイントのプレフィックスサブドメイン)となる部分。 これで前述の名前空間と合わせて、 myapp.osushi.service でクエリすると、Rouute53のプライベートDNSを経由して、nginxコンテナの80番ポートにリクエストが届くようになる。

CloudMapの階層を整理しないとちょっと混乱するかもしれないのでざっくりと図解する。 名前空間サービスサービスインスタンスの3つのグループがある。

f:id:bluepixel:20200405232352p:plain

サービスインスタンスの実体は、Fargateのタスクだったり、Lambdaだったりする。 オートスケールすれば、そのタスクの数だけサービスインスタンスとして登録される。 ALBと同じ感じで理解してもらえればいい。

f:id:bluepixel:20200405232420p:plain

f:id:bluepixel:20200405232433p:plain

ヘルスチェックはコンテナごとの設定に準拠する。 成功・失敗に応じて、サービスインスタンスの登録と解除をマネージドしてくれる。

HealthCheck:
  Command:
    - CMD-SHELL
    - curl -f http://localhost/
    - "|| exit 1"
  StartPeriod: 30

Route53 Private DNS

この時点で、もうできている。

f:id:bluepixel:20200405232453p:plain

CloudMapが自動でプライベートなホストゾーンを作成し、 ECSの設定に応じて、検出したレコードを登録している。

f:id:bluepixel:20200405232509p:plain

値は、ステータスが HEALTHY なタスクのプライベートIPになっている。

疎通確認。 アプリ1として立てているEC2インスタンスssh接続し、以下のコマンドを実行する。

dig myapp.osushi.service +short

f:id:bluepixel:20200405232529p:plain

5つタスクを起動しているので、5つのプライベートIPに解決されている。 次にHTTPリクエストを送る。

curl -I myapp.osushi.service

f:id:bluepixel:20200405232542p:plain

ちゃんと返ってきてる。 ちなみに80番ポートは、セキュリティグループでパブリックサブネットのCIDRのみを許可している。 外部からはもちろん接続できないし、VPC外になるのでプライベートDNSによる名前解決も不可能。

そういえばすでに存在するドメインを指定した場合、先にプライベートに問い合わせるからパブリックな方には疎通できないとかなんとか。

所感

ロードバランサーなしでさくっと作れるのは便利。

あまり構築も運用コストもかけたくない社内用のシステムをサクッと作る場合とか、ECSを選択しやすくなる。

ALBの機能を使い倒さない場合はこれで十分かな 🤔

大規模なマイクロサービスを運用する場合はどうなんだろう。