MySQLコンテナでリストア済みイメージ作成

ひょんなことからコンテナを使ったシステムを構築していて、その部品として MySQL サーバーが必要になりました。

ど~やって用意するのが最も楽で速くて適切かな~って、黒魔術バランス派として考えた結果、こんな感じになりましたよ、という一例でございます。



目的

大雑把に目的をまとめると

  • MySQLサーバーをコンテナで稼働させる
  • ECS Task 内の複数コンテナのうちの1コンテナとする
  • 既存MySQLサーバーのデータをリストアする

  • プラス、構築としても運用としても、できるだけ無駄なく速くしたいという感じです。内容的には後半が本番って感じになりそうです。


    設計

    図を書く気分じゃないので、ザックリと処理順に説明すると

  • 既存MySQLサーバーで、バックアップとS3アップロードを定期実行する
  • S3オブジェクトの更新を発火元としてLambdaを実行する
  • LambdaがCodeBuildを実行する
  • CodeBuildがリストア済みMySQLイメージを作成して、ECRにアップロードする
  • ECS TaskでECRイメージからコンテナを起動し、並列する別コンテナから接続する

  • たいしたことはせずに済みましたが、要件的に少しずつ工夫しつつシンプルに仕上げてみた次第です。全く同じことをしようとする人はいないでしょうが、部分的に参考になれば嬉しい系。


    バックアップとアップロード

    まずは既存のMySQLサーバーから mysqldump を採って S3 に保存します。
    このスクリプトを cron で必要な更新頻度で定期実行します。

    今回は鍵を書いてますが、EC2 なら profile にすればいいし、
    もし鍵を書く場合は root しか読めないようにはしておきましょう。

    cron でも手動でも結果は同じなので、S3オブジェクト発火をしたい時は、お手々で実行します。


    S3オブジェクトからLambda発火

    mysqldump ファイルが更新されたら、CodeBuild 実行用 Lambda を実行するように Terraform で設定します。

    直接CodeBuildを実行できたらいいんですけど、それ言ってたら色んな追加要望がきてゴッチャになるので、まぁやらないでしょうね。


    LambdaでCodeBuildを実行

    Pythonコード

    ここでは codebuild-fire って名前の Lambda にしましたが、こいつは発火元から Build名 を受けて、その CodeBuild を実行するだけの処理です。手元のコードはフレームワークちっくになってるので、部分的に大事なとこだけ記載しておきます。

    まずCodeBuildのプロジェクト名をどう渡すかですが、S3からならば上記の aws_s3_bucket_notification -> lambda_function -> id で伝えることができます。もし CloudWatchEvent からなら、任意の──ここでは project というキーで渡せるようにしてあります。

    んで、project_name を取得したら、ガツンと実行するだけ

    start_build してしまえば、Build自体は非同期に動くので、Lambda もすぐ終了します。

    Terraformコード

    この記事の本質じゃないので、縦長になるのを避けるためにスルーしようとも思いましたが、根が親切なので記載しておきます。

    関数とログ、

    S3 からの実行するために、


    Build用リソース群

    これも本質じゃないですが(ry

    ECR

    イメージ保存用レポジトリを作ります。

    tag がなくなったイメージは不要なので、ルールはこんな感じ。


    CodeCommit

    内容はあとでまとめます。


    CodeBuild

    ほぼテンプレですが、環境変数 MYSQL_BACKUP_URL を設定して、buildspec で使うようにしています。


    DockerHub mysql の仕組み

    CodeCommitのソース内容に入る前に、公式mysqlコンテナ の説明を軽くしておきます。

    公式説明では色々機能を確認できますが、今回私がやりたいことは、起動時間短縮のために mysqldump を丸っとリストアしておき、コンテナとして起動した時にそれをデータとして普通に動かしたい。ということなので、

    「Initializing a fresh instance」機能にある、/docker-entrypoint-initdb.d/ の下に .sql.gz とか置いといたら、自動的にリストアするよってのを使います。

    なので、バックアップ採取でも、好きな bzip2 じゃなく gzip にしたわけです。

    ただ、この機能を素直に使うと、どういう挙動になるかというと、起動時にバックアップファイルがあるようにすれば、起動前にリストア処理を行い、その後に mysqld を起動する、という流れになります。

    データ容量が小さい場合はこれでよいのですが、今回は結構大きかったので、毎回コンテナ起動のたびにリストア処理が走るのは待機時間が無駄になるので、リストア処理だけ済ませたイメージを作れるように工夫しました。


    Build用CodeCommitソース

    この辺からがやっと本編です。

    buildspec.yml

    テンプレと違うところは、S3からバックアップファイルを取得しているのと、BUILDKITを有効にしているところです。

    なので、CodeBuild の実行Roll に、S3からバックアップファイルを取得できる権限を与えておく必要があります。


    Dockerfile

    ここはたいしたことない内容にみえて、結構な肝になっています。

    リストア専用スクリプト
    公式の ENTRYPOINT スクリプトは、リストアもデーモン起動も含んだ内容になっているのですが、リストアだけをしてお終いにする処理が必要になりました。

    そこで、公式 entrypoint スクリプト を手元に docker-entrypoint.sh として落としてきて、init.sh として編集しました。

    処理最後での mysqld の実行をやめているだけです。

    リストア処理
    この init.sh を使って、リストアだけを実行するのがここです。init.sh を実行すれば、mysql.sql.gz が解凍&リストアされるのですが、2つほど工夫を入れました。

    1つは、buildspecでS3から落としたバックアップファイルを含む、Current のソースコード一式を、ビルド中イメージの docker-entrypoint-initdb.d にマウントしたことです。これにより、サイズが大きいほど COPY での無駄な処理を省けます。この –mount は、BUILDKIT を有効にしないと使えないので、このために有効にしました。

    もう1つは、–datadir を指定したことです。本来は /var/lib/mysql なのですが、元イメージで VOLUME として指定されており、VOLUME 内のデータはビルド後に残らないと決まっていて、しかもその設定は上書きも削除もできないので、別ディレクトリにデータを残すことにしました。

    そのため、Dockerfileの最後で、CMD で引数を上書きして、データディレクトリを指定しています。

    公式のENTRYPOINTを上書き
    元の ENTRYPOINT 実体は /usr/local/bin/docker-entrypoint.sh なのですが、一部 Fargate 1.4.0 において不都合があったので修正して上書きしています。

    さきほどと同様、entyrpoint.sh としてコピーし編集しました。内容の差異は

    理由は繊細というかどうでもいいようなものですが、
    ECS Task 1つに複数コンテナを起動して、仮に app , mysql の2つを作成した時、app から mysql への接続方法は特に問題なく

    でできるのですが、アプリコードに極力手を加えないというインフラエンジニア視点において、socket 接続ができるようにしたかったのです。

    そうするには、/var/run/mysqld を app , mysql コンテナで共有する必要があり、Fargate 1.3.0 では特に考えずに設定したらできたものが、1.4.0 になると共有設定したディレクトリが、mysqld 起動時に root:root 0755 になって、mysql ユーザーが書き込めない、という Fargate Platform Version の違いがネックになってしまいました。バインドマウントのタイミングが異なる、ということなのでしょう。

    そのため、起動直前にディレクトリの所有権を書き換える必要があり、それができるのはまさにこのタイミングしかない!ということになり、微々たる内容ながらも上書きする決断をしました。

    ただ、Fargate 1.4.0 の仕様が最近また変わりつつあるので、あとで不要になる可能性もあります。



    あとは、ECS Task に app, mysql と並列して起動し、/var/run/mysqld をバインドマウントしておけば接続できるようになります。が、その辺は省略。

    んで接続の確認とか、リストアデータの確認を考えると、appコンテナにsshd・mysqlクライアントなどを入れておいたほうが早い。とかそーゆーのが逆にコンテナっぽいっていうか。

    小技の応酬を楽しむシステムって感じは、嫌いじゃないんですけど、要件によってこれが解!ってのが変わるので、小技=部品 的な記事もチマチマ上げていこうって気分になった次第であります:-)