読者です 読者をやめる 読者になる 読者になる

オルトプラスエンジニアの日常をお伝えします!

ECSでコンテナと、コンテナが所属しているEC2インスタンスを紐付ける

どうもこんにちは id:kotamat です。 いつもお世話になっております。

背景

ECSは、コンテナを作成、実行する際、 すでに割り当てられているEC2のAutoScalingGroupに対して、 コンテナを作成できるEC2インスタンスを自動的に判別して、 コンテナを作成、起動してくれます。

これ自体は非常に嬉しいのですが、 ECSの設定だけだと上記の自動実行のせいで 下記のような使い方は難しいです。

  • コンテナへのAPIの実行(とくにコンテナ間での通信)
  • サービスのヘルスチェック(起動準備完了がコンテナのSTARTステータスではなく、ある特定のエンドポイントとする場合等)
  • EC2インスタンスの環境変数の取得
  • コンテナの更新タイミングの取得

そこで色々調べたところ、下記のブログにてその対策を見つけたので、こちらを多少改変させて使用してみました。 https://aws.amazon.com/jp/blogs/compute/service-discovery-for-amazon-ecs-using-dns/

こちらではSPVレコードを使ってRoute53にサービスのIPとDNSを通知していますが、 今回はCNAMEを使ったアーキテクチャーを紹介しようと思います。

アーキテクチャー

図は上記のブログを参考にしてもらいたいのですが、 簡単に言うと、EC2インスタンスの起動設定にてDockerAPIを監視するデーモンプロセスを設置、 そのデーモンプロセスが、コンテナ起動を検知したとき、EC2インスタンスの情報やコンテナの情報を整形して、Route53にレコードを追加します。 コンテナが終了した場合は、同様にレコードを削除するようにします。

Route53はこのECSクラスタが所属しているVPCにおけるプライベートDNSを所持しており、こちらのDNSレコードがコンテナ↔EC2インスタンスの関係を保持している事になります。

DockerAPIからはコンテナ起動時の環境変数が指定できるので、特定の環境変数が割り当てられていれば、上記処理を行うという制御にすることによって、TaskDefinition上でハンドリングができるようになります。

ただし、上記デーモンプロセスはEC2の終了時を検出できないため、CroudWatch + Lambdaで定期的に状態を監視。不要となったプライベートDNSのレコードを削除します。

実装

使用したもののレポジトリはこちらにおいてあります。 https://github.com/kotamat/service-discovery-ecs-dns

必要なAWSのサービス

  • ECS
  • EC2
  • AutoScaling
    • EC2起動設定はこちらで行う
  • CroudFormation
    • Webコンソール上で起動設定を設定出来ない場合はこちらを使用
  • Route53
    • プライベートDNSのハンドリング
  • Lambda
    • EC2インスタンス終了時のお掃除
  • CroudWatch
    • Lambda定期実行のトリガー
  • S3
    • デーモンプロセスをおいておくストレージ

デーモンプロセス

ecssd_agent.goをビルドしたものを使っています。 上記ブログにて使用されているのはGolangでしたので、一部改変して使用させてもらいました。 やっていることは簡単で

  1. EC2起動時に実行しDockerのイベントを監視
  2. ECSがコンテナを作成すると、startFnが呼ばれSERVICE_XXX_NAMEという環境変数が存在したら、Route53にその値をサブドメインとするCNAMEレコードを追加
  3. コンテナが終了するとstopFnが呼ばれ、該当のCNAMEを削除

XXXに当たるところは、そのサービスが割り当てたいポート番号を指定します。このポート番号はLambdaでの死活監視に使用します。 EC2の情報やECSのクラスタ情報、Route53への一覧書き込み、削除権限が必要なので、EC2インスタンスにはこのroleを事前に割り当てておく必要があります。

起動設定

ECSクラスタは作成時に自動的にAutoScalingを作成するため、こちらに起動設定を追加する場合は、CroudFormationの設定を変更する必要があります。 yamlでの設定はこんな感じになります。

Resources:
...
  EcsInstanceLc:
    Type: AWS::AutoScaling::LaunchConfiguration
    Metadata:
      AWS::CloudFormation::Authentication:
        S3AccessCreds:
          type: s3
          roleName: ecs-ec2-role
          buckets:
            - <bucket_name>
      AWS::CloudFormation::Init:
        config:
          files:
            /etc/init/ecssd_agent.conf:
              group: root
              mode: '000644'
              owner: root
              source: http://<bucket_name>.s3.amazonaws.com/ecssd_agent.conf
              authentication: S3AccessCreds
            /usr/local/bin/ecssd_agent:
              group: root
              mode: '000755'
              owner: root
              source: http://<bucket_name>.s3.amazonaws.com/ecssd_agent
              authentication: S3AccessCreds
      Comment: Install a simple application

今回実行ファイルはS3に置いているので、上記のように指定します。 ecs-ec2-roleはEC2のロールなので、必要に応じて変更してください。

Lambda

EC2が終了したときは上記デーモンプロセスが動作しないので、定期的にLambdaからCNAMEを監視、削除を行います。 CNAME一覧とクラスタのTaskDefinition一覧を取得し、CNAMEに指定されたDNSとTaskDefinitionで指定されたポート番号を使ってヘルスチェックを行い、2 ブログでは5分毎と設定がありましたが、サービスに応じて任意に設定してください。

まとめ

今回紹介した方法を使えば、コンテナとEC2インスタンスの紐付けをRoute53を使って、ほぼ同期的に取得できるようになるため、サービス単位ではなくコンテナ単位でのハンドリングができるようになります。