kakakakakku blog

Weekly Tech Blog: Keep on Learning!

AWS CLI で ALB 配下のインスタンスを deregister / register する

AWS CLI で ELB 配下のインスタンスをローリングデプロイする方法は去年にまとめた.aws elb deregister-instances-from-load-balanceraws elb register-instances-with-load-balancer を使うだけじゃなく,aws elb describe-instance-health を使って waiter を実装した点がポイントだった.ちなみに EC2 だと waiter を実装する必要はなく aws ec2 wait で完了を待つことができる.

kakakakakku.hatenablog.com

ALB の場合 aws elbv2 を使う

1年が経過し,今度は ALB 配下のインスタンスをローリングデプロイしようと考えていて,調べたところ ALB の場合は aws elbv2 を使う必要があった.さらに,ターゲットグループなど ELB とは違う概念があるため,サブコマンドもオプションも異なっていた.

ドキュメント

今回,以下の3コマンドを使った.

オプションは --target-group-arn にターゲットグループの ARN を指定し,--targets にインスタンス ID を指定する.ただし --targets に指定するときはフォーマットが決まっているため,以下のようにする.なお,ポートは省略することができる.

Id=string,Port=integer …

ALB のステータス

ELB は InServiceOutOfService のステータスしか存在しなかったが,ALB の場合はもっと多くある.

  • initial
  • healthy
  • unhealthy
  • unused
  • draining

ALB から deregister すると healthy → draining → unused と遷移する.register すると unused → initial → healthy と遷移する.unhealthy はヘルスチェックに失敗したときに出るため,通常 deregister / register をするときには出ない.

(正確に言うと他の遷移もあると思う)

f:id:kakku22:20170422233304j:plain

docs.aws.amazon.com

ALB ターゲットグループのドレインに注意する

ターゲットグループのデフォルトのドレインは 300 秒に設定されているため,deregister すると,毎回 300 秒 draining のままになる.アプリケーションの特性次第だけど,基本的には長いと思うので,30 秒などに短くした方が良いと思う.

docs.aws.amazon.com

ドレインに関しては前に検証した記事が参考になると思う.

kakakakakku.hatenablog.com

aws_utils.sh

共通機能は去年と同じで ALB を操作する部分を追加した.

#!/bin/sh

# ALB の状態を確認する間隔(秒)
SLEEP=3

#######################################
# AWS CLI をチェックする
#######################################
check_awscli() {
  if ! which aws >/dev/null; then
    echo 'awscli not found'
    exit 2
  fi
}

#######################################
# ホスト名からインスタンス ID を取得する
# Arguments:
#   $1 HOST NAME
# Returns:
#   INSTANCE ID
#######################################
get_instance_id() {
  instance_id=$(/usr/bin/aws ec2 describe-instances --filter "Name=tag:Name,Values=$1" --query Reservations[].Instances[].InstanceId --output text)
  if [ -z "$instance_id" ]; then
    echo 'host not found'
    exit 2
  fi
  echo ${instance_id}
}

#######################################
# インスタンスが ALB に反映されるまで待機する
# https://docs.aws.amazon.com/cli/latest/reference/elbv2/describe-target-health.html
# Arguments:
#   $1 TARGET GROUP ARN
#   $2 INSTANCE ID
#   $3 STATE (healthy or unused)
#######################################
alb_waiter() {
  while :
  do
    # 状態(healthy or unused)を取得する
    state=$(/usr/bin/aws elbv2 describe-target-health --target-group-arn $1 --targets Id=$2 --query TargetHealthDescriptions[].TargetHealth.State --output text)
    if [ "$3" = "${state}" ]; then
      echo "  TargetGroupARN: $1 InstanceId: $2 State:$3"
      break
    else
      echo "  TargetGroupARN: $1 InstanceId: $2 State:$state"
    fi
    sleep ${SLEEP}
  done
}

#######################################
# ALB からインスタンスを外す
# https://docs.aws.amazon.com/cli/latest/reference/elbv2/deregister-targets.html
# Arguments:
#   $1 TARGET GROUP ARN
#   $2 INSTANCE ID
#######################################
alb_deregister() {
  /usr/bin/aws elbv2 deregister-targets --target-group-arn $1 --targets Id=$2 > /dev/null
}

#######################################
# ALB にインスタンスを付ける
# https://docs.aws.amazon.com/cli/latest/reference/elbv2/register-targets.html
# Arguments:
#   $1 TARGET GROUP ARN
#   $2 INSTANCE ID
#######################################
alb_register() {
  /usr/bin/aws elbv2 register-targets --target-group-arn $1 --targets Id=$2 > /dev/null
}

start.sh

汎用的に書くとこのようになる.

#!/bin/sh

# スクリプトをインポートする
. ./aws_utils.sh

# コマンドチェックと定数の設定
check_awscli
ALB_TARGET_GROUP_ARN ='arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222'

HOSTS=(web01 web02)

for host in ${HOSTS[@]}
do
  # インスタンス ID を取得する
  INSTANCE_ID=$(get_instance_id ${host})

  # unused になるまで待機
  echo " deregister : ${INSTANCE_ID}"
  alb_deregister ${ALB_TARGET_GROUP_ARN} ${INSTANCE_ID}
  alb_waiter ${ALB_TARGET_GROUP_ARN} ${INSTANCE_ID} 'unused'

  #
  # ホストごとにデプロイなどを実行する
  #

  # healthy になるまで待機
  echo " register : ${INSTANCE_ID}"
  alb_register ${ALB_TARGET_GROUP_ARN} ${INSTANCE_ID}
  alb_waiter ${ALB_TARGET_GROUP_ARN} ${INSTANCE_ID} 'healthy'
done

実行すると,以下のように waiter をして,期待通りに動く.

 deregister : i-xxxxxxxx
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:draining
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:draining
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:draining
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:draining
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:draining
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:draining
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:draining
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:draining
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:draining
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:unused
 register : i-xxxxxxxx
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:initial
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:initial
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:initial
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:initial
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:healthy

 deregister : i-xxxxxxxx
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:draining
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:draining
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:draining
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:draining
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:draining
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:draining
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:draining
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:draining
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:draining
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:unused
 register : i-xxxxxxxx
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:initial
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:initial
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:initial
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:initial
  TargetGroupARN: arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:targetgroup/xxxxx/2222222222222222 InstanceId: i-xxxxxxxx State:healthy

まとめ

  • ELB (elb) と ALB (elbv2) では AWS CLI のサブコマンドが異なる
  • オプションも異なる
  • ALB ターゲットグループのドレインに注意する
  • aws elbv2 describe-target-health を使って waiter を実装すると正確に状態を把握しながら deregister / register ができる

CircleCI で ECS にデプロイをするサンプルプロジェクト circleci/go-ecs-ecr を試した

CircleCI を使って,ECS に Docker コンテナをデプロイする方法を調べていたら,CircleCI が公開しているサンプルプロジェクト circleci/go-ecs-ecr の存在を知ったので,さっそく試してみた.

circleci/go-ecs-ecr を動かすと,Golang の API が動く Docker イメージを作成して,ECR にプッシュして,ECS にデプロイする,という流れを簡単に試すことができる.僕自身が ECS に入門したばかりなので,勉強のキッカケとしてもお手軽でとても良かった.

github.com

ザックリと構成図を書くと,こんな感じになる.

f:id:kakku22:20170419014555j:plain

circleci/go-ecs-ecr を Fork する

まず最初に circleci/go-ecs-ecr を Fork する.このままだと circle.ymldeploy.sh に定義されたリージョンが全てバージニア (us-east-1) になっているため,東京 (ap-northeast-1) に変更する必要がある.例えば以下のように.

github.com

ECR にリポジトリを作成する

ECR に go-sample-webapp という名前のリポジトリを作成しておく.また aws ecr get-login コマンドで,ECR にログインできることを確認しておく.

$ $(aws ecr get-login --region ap-northeast-1)

ECS で仮のタスク定義を作成する

CircleCI で実行するデプロイで自動的に ECS のタスク定義を作成するため,ここでは ECS クラスタを起動するためだけに仮のタスク定義を作成する.名前は sample-webapp-task にしておく.

ECS クラスタと ECS サービスを作成する

次に ECS クラスタと ECS サービスを作成する.名前は以下とする.

  • クラスタ名 : sample-webapp-cluster
  • サービス名 : sample-webapp-service
  • タスク定義名 : sample-webapp-task-family(自動的にデプロイされる)
    • タスク定義名のことをファミリーと呼ぶ

今回は検証用途なので,コンテナインスタンスは t2.micro を1台にした.ELB / ALB も使わないため,パブリックサブネットに配置して,限られた IP アドレスから 80 ポートを許可するようにした.

また Desired Count も 1 にし,Minimum Healthy PercentMaximum Percent はデフォルトのままにした.日本語コンソールの「最小ヘルス率」と「最大率」という訳が微妙すぎるでしょ…!

CircleCI の設定をする

Fork した go-ecs-ecr リポジトリで CircleCI を動くように設定したら,まずは CircleCI 専用の IAM User を作成する.名前は circleci-ecs とした.ポリシーは以下をアタッチする.

  • AmazonEC2ContainerRegistryFullAccess
  • AmazonEC2ContainerServiceFullAccess

次に CircleCI の Environment Variables で,以下の環境変数を設定しておく.

  • AWS_ACCOUNT_ID
  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY

ビルドを実行する

あとは CircleCI でビルドを実行すると ECS に Docker コンテナがデプロイできる.デプロイ後にコンテナインスタンスにアクセスすると,ちゃんとレスポンスが返ってきた.

$ curl http://xxx.xxx.xxx.xxx/
Hello World!%

仕組みを学ぶ

circle.yml

  • docker build を実行して $CIRCLE_SHA1 をタグとして Docker イメージを作成する
  • テスト時には docker run でコンテナを実行して,接続を確認する
  • master ブランチにプッシュされた場合は deploy.sh を実行する

deploy.sh

  • AWS CLI の設定をする
  • ECR に Docker イメージをプッシュする
  • タスク定義のリビジョンを更新して,最新の Docker イメージを反映する
  • サービスに紐付くタスク定義を更新して ECS にデプロイをする

詳しくは CircleCI のドキュメントにも載っているので,合わせて見ると良いと思う.

circleci.com

学んだこと

ECS は設定項目が多く,ポチポチとデフォルト設定のまま進めていたら,少し気になる点を発見した.

まず,クラスタを新規に作成するときに,ネットワーキングの部分で,デフォルトでは VPC を新規に作成する設定になっていた.何となく CloudFormation のスタック変更が長いなぁと思っていたら VPC まで作られていて,既存の VPC で動かすことが多いだろうし,ここは注意しておきたいところだなと思った.あと,自動的に作られるセキュリティグループは,デフォルトで 80 ポートを 0.0.0.0 で開放していた.ここも注意しておきたいと思った.

まとめ

ECS を学ぶのに最高な Black Belt 資料

非常によくまとまっていて素晴らしい資料だと思う.今のところ毎日1回は見ている気がする.また今月に資料の更新もされていて,情報の鮮度が保たれているという点も助かる!

www.slideshare.net

計画する技術

今日は社内勉強会で「計画する技術」というタイトルで発表をした.

前から少し「計画」のところに課題感があって,そのあたりの知識を組織に広めて欲しいというオーダーもあったため,僕が日々考えていることを言語化して,発表することにした.僕は今までに様々な「計画」の経験があり,実際に今の組織でも難易度の高いプロジェクトを何度も計画し,遂行してきたため,「計画」に対する信頼残高は増えているのではないかと思う.

発表資料

意識したこと

今回,発表資料を作るときに意識したことが2個あった.他にも話したいことはたくさんあったけど,組織マネジメントの話はまた別でしようと思って,あくまで「計画」に特化した話にした.

  • 明日からすぐに使える話をする
  • 開発プロセスに依存しない話をする

1. 明日からすぐに使える話をする

原理原則すぎる話や,難しい法則の話は控えるようにした.そういう話をしてしまうと,その場では「おー,そういう法則があるのか」と,わかった気になるけど,いざ自分で咀嚼しようとすると「で,どうしたら良いんだろう?」となってしまう可能性が高いと思ったから.例えば「不確実性コーン」の話とか,「バッファ計算」の話とか,「PMBOK」の話とか.

2. 開発プロセスに依存しない話をする

今の組織の開発プロセスは,ウォーターフォールでもないし,スクラムでもないし,カンバンでもなく,また何かしらの「型を崩した」ものでもなく,独特なものになっていると思う(個人的には悪い面もあるとは思う).そのときに「スクラムではこれは必須だから」という話をしても,他人事のように聞こえてしまう可能性が高いと思って,特定の開発プロセスに依存しない話をした.あくまでこれは即効性を意識したためで,抜本的に開発プロセスを見直す機会があるのであれば,ゼロから立て直したいと思う.

よく読む本

前に「カンバン勉強会」を実施したときの資料

kakakakakku.hatenablog.com

参考になる記事

PHP の Elasticsearch クライアント Elastica で Amazon ES に接続する

PHP から Elasticsearch を操作するために Elastica というライブラリを使っている.今まで EC2 で運用していた Elasticsearch を Amazon ES に移行する話があり,Elastica から Amazon ES を操作するときに悩むことが多かったため,まとめておこうと思う.

github.com

ちなみに,僕は使ったことはないけれど,Elastic 社から公式に提供されている elasticsearch-php というライブラリもある.ただ,Elastic 社の公式ライブラリだと,積極的に Amazon ES 対応をすることはないだろうし,Amazon ES を使う場合は,サードパーティライブラリの Elastica の方が良い気がする.

github.com

Amazon ES に接続する方法

Amazon ES に接続する方法を復習しておくと,大きく3種類ある.

  • リソースベース
  • IP ベース
  • IAM ユーザーとロールベース

docs.aws.amazon.com

今回のように,アプリケーションから Amazon ES に接続する場合は IAM を使う.IAM Role を使うのがベスト.必要に応じて IP 設定を変更して運用することも考えられるが,Amazon ES で IP 設定を変更すると,ノードが全台作り直されてしまうため,クラスタのデータ規模によっては1時間程度待たされることもあるので,実質不可能だと思うし,そもそも,アーキテクチャ的に微妙すぎる.

また,IP ベースの場合は未署名の匿名リクエストも受け付けることができるが,IAM を使う場合,署名付きリクエスト(AWS 署名バージョン 4)を送る必要がある.よって,ライブラリが署名付きリクエストに対応している必要がある.

docs.aws.amazon.com

docs.aws.amazon.com

Elastica から Amazon ES に接続する

github.com

Issue を見ると,Elastica は既に署名付きリクエストの対応がされているが,実装例などのドキュメントが全くなくて,よくわからなかった.困ったので Gitter で聞いてみたら,すぐ教えてもらうことができて助かった.簡単過ぎるけど,今回の構成をまとめた.

f:id:kakku22:20170407002340j:plain

ライブラリをインストールする

署名付きリクエストを送るためには,Elastica 3.1.1 以上が必要になる.ただし,このままで実行すると,以下のエラーが出る.

Class 'GuzzleHttp\Psr7\Uri' not found

Elastica の composer.json を見ると,suggest プロパティに以下の設定がある.suggest とは,基本的には必要ないけど,入れておくと便利な依存パッケージのことを意味している.Ruby の Gemfile にはそういう概念がないため,今まで知らなかった.

{
    (中略)
    "require-dev": {
        "aws/aws-sdk-php": "~3.0",
        "guzzlehttp/guzzle": "~6.0"
    },
    "suggest": {
        "aws/aws-sdk-php": "Allow using IAM authentication with Amazon ElasticSearch Service",
        "egeloen/http-adapter": "Allow using httpadapter transport",
        "guzzlehttp/guzzle": "Allow using guzzle 6 as the http transport",
        "monolog/monolog": "Logging request"
    },
    (中略)    
}

よって,Amazon ES に IAM で接続する場合は,以下のような composer.json になる(バージョン設定はご自由に).

{
    (中略)
    "require": {
        "ruflin/Elastica": "~3.2",
        "aws/aws-sdk-php": "~3.25",
        "guzzlehttp/guzzle": "~6.2"
    },
    (中略)
}

まだ aws/aws-sdk-php v2 を使っていると動かないため,合わせて aws/aws-sdk-php をバージョンアップする必要がある.

Elastica のクライアント実装

以下のように transportAwsAuthV4 を設定して,aws_region を設定すると,Amazon ES に IAM で接続できるようになる.

$client = new Elastica\Client(
    array(
        'host'       => 'xxx.ap-northeast-1.es.amazonaws.com',
        'port'       => 80,
        'transport'  => ['type' => 'AwsAuthV4', 'postWithRequestBody' => true],
        'aws_region' => 'ap-northeast-1'
    )
);

もし IAM Role ではなく,クレデンシャルを埋め込む場合(非推奨な実装)は,以下のようになる.

$client = new Elastica\Client(
    array(
        'host'                  => 'xxx.ap-northeast-1.es.amazonaws.com',
        'port'                  => 80,
        'transport'             => ['type' => 'AwsAuthV4', 'postWithRequestBody' => true],
        'aws_access_key_id'     => 'yyy',
        'aws_secret_access_key' => 'xxx',
        'aws_region'            => 'ap-northeast-1'
    )
);

まとめ

  • Elastica は Amazon ES の接続をサポートしている
  • ドキュメントが詳しくないため,困ったら Gitter で聞いてみると良い
  • 必ず IAM Role を使おう

参考情報 : Amazon ES を運用するときに知っておくと便利なこと

kakakakakku.hatenablog.com

mysqldiff を使って継続的に MySQL のデータベーススキーマの差分をチェックする

最近,環境ごとのデータベーススキーマの差分をチェックする機会があった.プロダクション環境とステージング環境ならまだしも,開発環境だと検証のために追加したインデックスがそのままになっていたり,開発が途中で止まってしまって日の目を見ることがなかったテーブルが残っていたり,そういうことって比較的あるのではないかなと思う.特に今の環境だと,マイグレーションの仕組みが整っていないという課題もあり,より一層,データベーススキーマに差分が出やすくなってしまっている.

今回は MySQL から公式に提供されている mysqldiff というツールを使ってデータベーススキーマの差分をチェックした.

mysqldiff をインストールする

mysqldiff は MySQL Utilities という MySQL の管理ツールパッケージの中に同梱されている.現在だと v1.6 が最新になっている.

yum で簡単にインストールすることができる(リポジトリは適宜指定すること).

$ sudo yum install -y mysql-utilities

$ which mysqldiff
/usr/bin/mysqldiff

$ mysqldiff --version
MySQL Utilities mysqldiff version 1.6.5
License type: GPLv2

mysqldiff を使う

基本的には以下のように使う.データベースだけではなく,テーブルを指定して限定的な差分をチェックすることもできる.

  • --server1 … 比較するデータベースの接続設定
  • --server2 … 比較するデータベースの接続設定
  • --force … デフォルトだと,差分が1件出ると止まってしまうため,最後まで流すために指定する
  • --skip-table-options … テーブルのオプションに差分があっても無視するために指定する
  • --changes-for=server2 … デフォルトだと server1 を修正する形で認識されるため,server1 を起点として server2 を修正する形にする
$ mysqldiff --server1=root@localhost --server2=root@localhost database_a:database_b --force --skip-table-options
$ mysqldiff --server1=root@localhost --server2=root@localhost database_a.users:database_b.users --force --skip-table-options

サンプルとして users テーブルの差分をチェックしてみた.以下の例では email_UNIQUE 制約が存在しないことをチェックできている.

$ mysqldiff --server1=root@localhost --server2=root@localhost database_a.users:database_b.users --force --skip-table-options --changes-for=server2
# server1 on localhost: ... connected.
# server2 on localhost: ... connected.
# Comparing database_a.users to database_b.users           [FAIL]
# Transformation for --changes-for=server2:
#

--- database_a.users
+++ database_b.users
(中略)
   `email` varchar(255) DEFAULT NULL,
(中略)
   `updated_at` int(10) unsigned DEFAULT NULL,
   `deleted_at` int(10) unsigned DEFAULT NULL,
   PRIMARY KEY (`id`),
-  UNIQUE KEY `email_UNIQUE` (`email`),
# Compare failed. One or more differences found.

完全に一致している場合は,以下のように [PASS] と表示される.

$ mysqldiff --server1=root@localhost --server2=root@localhost database_a.users:database_b.users --force --skip-table-options --changes-for=server2
# server1 on localhost: ... connected.
# server2 on localhost: ... connected.
# Comparing database_a.users to database_b.users    [PASS]
# Success. All objects are the same.

差分を SQL 形式で出力する

--difftype=sql を指定すると,データベーススキーマを一致させるために必要な SQL を出力することができる.

$ mysqldiff --server1=root@localhost --server2=root@localhost database_a.users:database_b.users --force --skip-table-options --changes-for=server2 --difftype=sql
# server1 on localhost: ... connected.
# server2 on localhost: ... connected.
# Comparing database_a.users to database_b.users           [FAIL]
# Transformation for --changes-for=server2:
#

ALTER TABLE `database_b`.`users`
  ADD UNIQUE INDEX email_UNIQUE (email);

# Compare failed. One or more differences found.

微妙なところ : AUTO_INCREMENT だけを無視することができない

比較対象のデータベースが TRUNCATE されてなく,AUTO_INCREMENT を保持している場合,デフォルトだと差分として検知されてしまう.そのために --skip-table-options を使っているが,これだと AUTO_INCREMENT だけでなく ENGINECHARSET の差分も無視してしまう.ここは少し微妙だなと思った.

--- database_a.users
+++ database_b.users
(中略)
-) ENGINE=InnoDB AUTO_INCREMENT=123 DEFAULT CHARSET=utf8
+) ENGINE=InnoDB DEFAULT CHARSET=utf8
# Compare failed. One or more differences found.

微妙なところ : --difftype=sql は完璧じゃない

今回試したケースだと,MySQL の PARTITION の定義が抜けている場合に,データベーススキーマの差分としては検知できるものの,--difftype=sql では検知されなかった.現状だと --difftype=sql は完璧じゃなく,参考程度に使うのが良いと思う.

CI として継続的に差分をチェックする

日々開発を進めている中で,データベーススキーマに差分が生まれてしまうことはあると思う.よって今回は mysqldiff を使ったデータベーススキーマの差分チェックを CI として継続的に実行できるように自動化した.よって,意図しない差分が出た場合は,容易に気付くことができるようになった.

補足 : mysqldump --no-data を使う

mysqldiff の紹介をメインに書いたため省略してしまったけど,実際に使う場合は環境ごとにネットワークが分離されていることも多いと思う.まさに今回もそのような環境下で試していたものの,mysqldump --no-data でデータベーススキーマを取得して,S3 にアップロードすることによって,CI を実行するサーバに各環境のデータベーススキーマを集約することができる.補足として書いておく.

まとめ

  • mysqldiff を使うと,MySQL のデータベーススキーマの差分をチェックすることができる
  • オプションが多くあるため,ドキュメントを確認すること
  • AUTO_INCREMENT の差分が出る場合は --skip-table-options オプションを使うが,CHARSET などの差分も無視されてしまう
  • --difftype=sql は完璧じゃないので,参考程度に使う
  • 一回実行して終えるのではなく,CI として継続的に差分をチェックするべき