Amazon ECSでHAProxyのDockerコンテナを各Taskに配置するのがなかなか良い件

どうも、コンテナ芸人です。久しぶり?にAmazon ECSを使ったネタをお届けします。

今のプロジェクトではDBにMySQLを利用していますが、コスト低減のためにMaster(Multi-AZ)だけRDSでSlaveはEC2で構築しています。で、SlaveにはELB経由で参照していましたが、最近この仕組みをHAProxyコンテナに置き換えてみたらなかなか良かったぞという話です。

旧構成

これがつい先日までの構成。

  • ECS Task*1は**Nginx+Node(React)+API(Go)+td-agent**で1セット
  • NginxはAPIコンテナや、SSRでHTMLを返却するNodeコンテナへProxyする
  • td-agentはログ転送用
  • DBに更新・参照をするのはAPIコンテナのみ

この構成で着目して欲しいのはSlave参照にELBを利用しているところ。ELBはバックエンドがEC2であれば何でも使えるので、SlaveをEC2にしてしまえば付け外しもしやすい。

ELBのネック

便利なELBですが、厄介な特性もあります。

  • ELBのPre-warmingで申請が必要(消耗案件)
  • ELBはスケールすると、ENIを消費する。サブネット内のIPをそれなりに消費するため、いざインスタンスを増やそうというときの障壁になったりする
  • HTTPでない場合、ヘルスチェックはTCPレベルでしかできないので3306へはただのポートチェックしかできない。HAProxyではやろうと思えばわりかし色々できる
  • HAProxyにした方が色々と監視上も都合が良い

と、こんな事情からInternal ELBをやめてHAProxyにしてみることにしました。

新構成

新しい構成ではInternal ELBを利用を止め、各ECS TaskにHAProxyのコンテナを配置させるようにしました。

  • ECS Taskは**Nginx+Node(React)+API(Go)+td-agent+HAProxy**で1セット
  • Slave参照はHAProxyコンテナ経由で行う
  • APIからHAProxyはDockerのLinks*2によって通信する

APIからHAProxyを通したSlaveへの通信

例えば、ECS Taskにhaproxyというコンテナ名で登録し、APIのコンテナからlinkしていればAPIコンテナからはhaproxyというで名前解決ができる。つまり、APIから見たSlaveの接続先はhaproxy:3306となる。

HAProxyコンテナ内のhaproxy.cfgには利用するEC2 Slaveを全て設定しておき、DNSラウンドロビンする。事実上、各TaskそれぞれにMySQLへのロードバランサーを持つみたいなことが実現できるわけです。

HAProxyコンテナが死んだら?

当然HAProxyコンテナが死ぬ場合もあるでしょう。そのようなケースに備えて、Task内の全てのコンテナのessentialを全てtrueに設定します。

essentialを全てtrueに設定すればTask内のどれか一つのコンテナがダウンした場合、そのタスクは続行不可能と見なされます。essential設定によって、1つのコンテナが死んで他が生きているという異常な状態を回避することが出来ます。

ECSはTaskの数を指定した希望数(DesiredCount)に保つように動作します。何らかの異常によってTaskが終了した場合、もしTaskの再起動によって回復可能なケースであれば勝手に復旧します。

HAProxyを各Taskに配置するメリット

複数のSlaveに分散するためにHAProxyを配置するという手法は古くからあるものですが、HAProxyコンテナ形式の良いところは以下のようなことでしょうか。

  • コンテナで済むので、構築の手間が省ける
  • 集約的にHAProxyを立てる場合、全体でどれくらいのリクエストを受けるかを見積って十分なキャパシティのものを構築する必要がある
  • 各タスクに配置されるので基本的に冗長化とかを考えたりしなくて良い

HAProxyのDockerイメージ

若干課題があるといえば、HAProxyのイメージの管理です。Slaveの全ての接続先はhaproxy.cfgに定義するため、オーソドックスにやるならば変更がある度にdocker buildをせざるを得ないといったところでしょうか。 イメージにhaproxy.cfgを閉じ込めるよりはSlave群を環境変数で定義しておき、docker runするときにhaproxy.cfgを生成するようにした方が良い気がしてますが、この辺の管理の仕方はまだ模索中ですね。

イメージ自体はみんな大好きAlpine LinuxベースのHAProxyイメージを利用すると良いかと思います。APKで普通にインストールできて、10MBくらいのHAProxyイメージで済むのでエコです。Dockerfileはこんな感じでしょうか(haproxy.cfgは煮るなり焼くなり好きにしてください)。

FROM gliderlabs/alpine:3.3

RUN apk add --no-cache bash haproxy && \
    rm -rf /tmp/* && \
    rm -rf /var/cache/apk/*

# COPY haproxy.cfgのコピー等をご自由にどうぞ

EXPOSE 3000 3306

CMD ["haproxy", "-f", "/etc/haproxy/haproxy.cfg"]

所感

というわけでSlaveの管理にはまだ課題があるものの、なかなかよい感じです。

ちなみにHAProxyを各Taskに置く案を思いついたのは自分ではなくて、ウチのプロジェクトを担当するインフラエンジニアなのですが、個人的にはなかなか妙案だったと思うので是非拍手を送ってあげてほしいと思います(感謝)。

*1:Taskはコンテナの集合単位のこと *2:Dockerのコンテナ間通信の仕組み