asobannのAWS ECSデプロイでやったことと引っかかったこと その3

yattom.hatenablog.com

その1でasobannをAWS ECS上に構築その2でその構成をCloudFormation化した。したのだけどもう一息、Service Discoveryの対応と、MongoDBのデータ永続化が残っている。

SRVレコード対応にはPythonコードの実装が必要だった

その1にも書いたが、起動タイプがEC2でネットワークモードをbridgeにすると、個々のタスクはIPアドレスとポート番号の組み合わせでアクセスすることになる。1個のEC2インスタンスが10.0.0.82にあって、その上にアプリのタスクが3つ、それぞれポート32801, 32802, 32803で動いていたりする。公開ポートはDockerのポートフォワーディングによって、基本ランダムに決まる。ロードバランサーからアプリを指定するのはALBのTarget Groupを使うと、自動でマッピングしてくれる。

いっぽうアプリからMongoDBやRedisを指定するのは、Service Discoveryを使う。Service DiscoveryはプライベートのDNS名前解決なのだけど、DNSでIPとポート番号を両方解決するためにSRVレコードを使う。

このSRVレコードというのは初耳だったが、MongoDBは接続文字列でサポートしていた。mongodb+srv://... と書けばよいのだけど、気をつける点(引っかかった点)がいくつか。

  • 今回のアプリはPythonで、FlaskからPyMongoを使っている。SRVを使う接続文字列をPyMongoで扱うには、DnsPythonが必要で、パッケージをインストールしないといけない
  • サービスのURIのホスト名がhost.domain.tldという具合に3つ分ないと、エラーになる。たとえばmongodb.asobann-dev ではダメで、mongodb.asobann-dev.local ならいける。PyMongoがmongo+srv://に対応するときに利用するDnsPythonの制約。Private Namespaceを作るときに気をつける。
  • mongo+srv://を指定すると自動でtls=trueになるので、明示的にtls=falseを接続URIに付加する必要がある。とドキュメントに書いてあるが嘘で、ssl=falseじゃないとエラーになる。これはPyMongoの実装の問題な気がする。
  • SRVレコードは_Service._Proto.Nameという名前でレコードを作ることになっている。接続文字列が mongodb+srv://server.example.com/ だったら、AWS::ServiceDiscovery::ServiceのNameで指定するのは_mongodb._tcp.server.example.com.になる、という感じ。

Redisはそういう自動でSRVを解決するみたいな機能がまだないみたいなので、自前で何とかしないといけない。さっきDnsPythonを入れたので、以下のようなコードを書いて自分で解決してRedisに接続すればいい。(PyMongo内部の実装 を参考にした。なお本来のSRVの仕様では複数のAレコードに対応したり、TXTレコードから追加情報を引けたりするのだが、今回は端折っている。あとRedisはSSLもサポートしているが、それも省略。)

 def resolve_redis_srv(uri: str):
     '''
     Resolve redis+srv:// and return regular redis:// uri.
     Redis itself does not support connecting with SRV record but current AWS ECS
     configuration requires to use SRV record.
     Does not support TXT record.
     :param uri: connection uri starts with redis+srv://
     :return: redis://host:port/ uri resolved with SRV record
     '''
     assert uri.startswith('redis+srv://')
     import re
     from dns import resolver
     auth, host, path_and_rest = re.match(r'redis\+srv://([^@]*@)?([^/?]*)([/?].*)?', uri).groups()
     results = resolver.resolve('_redis._tcp.' + host, 'SRV')
     node_host = results[0].target.to_text(omit_final_dot=True)
     node_port = results[0].port
     node_uri = f'redis://{auth or ""}{node_host}:{node_port}{path_and_rest or ""}'
     return node_uri

EBSをDockerボリュームマウントするためREX-Rayドライバを使う

MongoDBはAWS DocumentDBを使えばマネージドサービスで楽ちんと思っていたら、課金がインスタンス時間単位で最小構成のdb.t3.mediumでも月額57USDとかになる。これにIO数やデータ量への課金もあるので、今のasobannで使うには贅沢すぎる。

というわけで、MongoDBも普通にServiceとしてECSで動かすことにした。なにもしないと、タスク再起動やEC2インスタンスの作り直しの時点でデータが消えてしまう。そこで、MongoDBのデータのみを別に保存するようにしたい。Dockerボリュームのマウントは、比較的最近(といっても2018年)にサポートされたらしい。EC2起動タイプではREX-Rayドライバを使えばEBSに保存すればいいようだ。

基本的な考え方から手順の解説まで見つけ、ほぼ同内容だけどYAMLで書いてあるものもあった。これに従ってTask Definitionを書き、スタックを更新したらすんなり成功して、よしこれでEC2インスタンスを停止してもデータが残るぞ!と思ったら残らなかった。コンソールから見てもEBSを使ってる様子がない。

結論としては、Task DefinitionのUserDataにあるrexrayドライバー(プラグイン)インストールのスクリプトが動いていなかった。この部分なんだけど、最終的にはdocker plugin ... の行だけにしたら動くようになった。

 #open file descriptor for stderr
 exec 2>>/var/log/ecs/ecs-agent-install.log
 set -x
 #verify that the agent is running
 until curl -s http://localhost:51678/v1/metadata
 do
    sleep 1
 done
 #install the Docker volume plugin
 docker plugin install rexray/ebs REXRAY_PREEMPT=true EBS_REGION=<AWS_REGION> --grant-all-permissions
 #restart the ECS agent
 stop ecs 
 start ecs

このスクリプトの結果は、/var/log/ecs/ecs-agent-install.logでわかる。curlでEC2インスタンス上のECSエージェントの起動を確認しているようなのだが、そんなものは起動していない。stop ecsだのstart ecsだのあるけど、そんなコマンドはない(systemctlのことだろうか?)。Metadataのcommandの02_start_ecs_agentという記述も、同じく動いていない。

   # 関係ない箇所を削除している
   ContainerInstances:
     Type: AWS::AutoScaling::LaunchConfiguration
     Metadata:
       AWS::CloudFormation::Init:
           commands:
             02_start_ecs_agent:
               command: start ecs

最終的にTask Definitionはこう、LaunchConfigurationはこんな設定となって、無事にデータを永続化できるようになった。関係ないけど、execとsetでこんなふうにログが出せるというのは知らなかったので勉強になりました。

ひとまずできた

以上で、asobann開発環境としての扱いで、AWS ECS上に構築できるようになった。この時点のCloudFormationテンプレートはこちらです。実行方法もREADMEに書いてあります。

github.com

yattom.hatenablog.com

yattom.hatenablog.com