ロコガイド テックブログ

「地域のくらしを、かしこく、たのしく」する、株式会社ロコガイドの技術部ブログです。主にトクバイ・ロコナビのサービス開発について発信しています。

「地域のくらしを、かしこく、たのしく」する、株式会社ロコガイドの技術部ブログです。
主にトクバイ・ロコナビのサービス開発について発信しています。

Schemaの適用作業を開発者に移譲した話

f:id:otany41:20201008191014p:plain

はじめに

ロコガイドでインフラ全般を担当している大谷(0tany)です。

普段は、多くの会社でインフラ/SRE/WEBオペレーション等を担当している方々と同様に

  • サービスのインフラ構築・運用
  • 監視/障害対応
  • キャパシティ/コストプランニング
  • セキュリティ
  • 各種インフラ作業のリクエスト

などに関する仕事をしています。

ロコガイドのインフラチームが抱えている問題として、特段インフラチームがやらなければいけない仕事でなくとも、業務上開発者に割り当てている各種権限やインフラレイヤのナレッジ共有の問題から、インフラチームが握り込んでしまっている作業がいくつかあり、それが開発チーム・インフラチーム双方への負担となっていました。

開発効率を上げていくため少しずつ作業の見直しと権限の移譲を進めているのですが、そのうちの1つ、トクバイサービスのSchema適用作業を開発チームに移譲した話を紹介します。

トクバイのSchema管理について

トクバイサービスではSchema管理ツールとしてRidgepoleというRubyのライブラリ(gem)を使っています。

また、ユーザー個人情報などの、セキュリティレベルを高く扱わなければいけない情報を含むテーブルは、テーブル名に任意のprefixを付与して運用しています。 本記事では便宜上 secret_ prefixとして説明します。この secret_ prefixの有無によりテーブル毎にセキュリティレベルを分け、 アプリケーションや、業務上アクセスが必要なインフラチームにのみアクセス権限を付与する運用としています。

問題分析

これまでSchema変更の作業は、以下のようなフローで運用されていました。

  1. 開発者がSchema定義をmaster merge
  2. インフラに本番適用依頼Issueをリクエスト
  3. インフラ作業としてSchemaを適用
  4. 開発者がSchema適用確認
  5. アプリケーションのデプロイ

この運用だと、Schema変更完了まで開発チームがデプロイを待機したり、インフラチームにSchema変更依頼が差し込まれたりするなど、いくつか問題が発生していました。

このような運用となっている原因は前述の secret_ テーブルにあります。 これらはその性質上、開発チームにすべての権限を開放することができません。とくに参照(SELECT)権限についてはより厳格な運用を求められています。 逆に開発チームがsecret_ テーブルの中身を見れない状態を維持しつつ、Schemaが変更可能な状態を作れば、インフラへの作業依頼はしなくてよくなるはずです。

改善トライ

トライ1. MySQLの権限付与(失敗)

権限付与を改善すれば良いとわかれば作業はシンプルです。手始めにRidgepoleでapplyする際のMySQLユーザーに必要な権限を付与すれば良いと考えました。
検証のため、以下の権限を付与してみます(ユーザー名、DB名、テーブル名は便宜上のものです)。CREATE権限はDB単位で開放し、既存の作成済みテーブルについてもsecret_以外のものについてはフル権限を与えるように設定しました。

mysql> show grants for developer@'%';
+------------------------------------------------------------------------------------------------------------------+
| Grants for developer@%                                                                                           |
+------------------------------------------------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO 'developer'@'%' IDENTIFIED BY PASSWORD '*FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'         |
| GRANT CREATE ON `tokubai`.* TO 'developer'@'%'                                                                   |
| GRANT ALL PRIVILEGES ON `tokubai`.`plans` TO 'developer'@'%'                                                     |
| GRANT ALL PRIVILEGES ON `tokubai`.`products` TO 'developer'@'%'                                                  |
| GRANT ALL PRIVILEGES ON `tokubai`.`shops` TO 'developer'@'%'                                                     |
| GRANT ALL PRIVILEGES ON `tokubai`.`leaflets` TO 'developer'@'%'                                                  |
| *                                                                                                                |
| *                                                                                                                |
| *                                                                                                                |
| GRANT ALL PRIVILEGES ON `tokubai`.`logs` TO 'developer'@'%'                                                      |
+------------------------------------------------------------------------------------------------------------------+

権限を最適化したこのdeveloperユーザーを使って試しにテーブルを作成してみましょう。 以下が実際に上記の権限でテーブルを作成しようとしたときに出力された結果です。

Enter developer@tokubai password: ****************
Table `shop_scores`: New table
Apply `Schemafile`
 
-- create_table("shop_scores", {:unsigned=>true, :options=>""})
  -> 0.0612s
-- change_table("shop_scores", {:bulk=>true})
[ERROR] Mysql2::Error: SELECT command denied to user developer@'10.xx.55.235' for table 'shop_scores': SHOW KEYS FROM `shop_scores`
  2:   t.column("shop_id", :"bigint", {:null=>false, :limit=>8})
  3:   t.column("score", :"float", {:unsigned=>true, :null=>false, :default=>0, :limit=>24})
  4:   t.column("created_at", :"datetime", {:null=>false})
  5:   t.column("updated_at", :"datetime", {:null=>false})
  6: end
*  7: change_table("shop_scores", {:bulk => true}) do |t|
  8:   t.index(["shop_id"], {:name=>"idx_shop_id", :unique=>true})
  9: end
       /Users/0tany/.rbenv/versions/2.5.8/lib/ruby/gems/2.5.0/gems/mysql2-0.4.5/lib/mysql2/client.rb:120:in `_query'
rake aborted!
Command failed with status (1): [ridgepole --apply --bulk-change --check-re...]
/Users/0tany/src/github.com/locoguide/tokubai/schema/rake_helper.rb:84:in `block (2 levels) in ridgepole'
...(スタックトレース略)...
Tasks: TOP => production:apply
(See full trace by running task with --trace)

メッセージを見れば分かる通り、何故かSELECT権限の不足で失敗してしまいました。

[ERROR] Mysql2::Error: SELECT command denied to user 'developer'@'10.xx.55.235' 

これはRidgepoleにある、apply時にschemaの変更差分を標準出力に表示する機能によるもので、今回のケースだと新規テーブル作成の権限は付与できるものの、作成したテーブルへの権限は何も付与されていません。 そのため新しく作成したテーブルの変更差分を生成するとき、テーブル情報を取得できずにエラーを出力し異常終了しています。

エラーを回避する手段を色々検討してみましたが、以下を満たすMySQL権限の振り方が存在しなかったため、権限設定ベースでのトライは断念しました。

  • これから作成される未知のテーブルの権限を事前に付与する(not DBレベル)
  • 機密情報を扱う secret_ prefixのテーブルには権限を付与しない

トライ2. バッチ化(成功)

次のトライとして、Schema適用専用のバッチアプリケーションを実装しました。変更が必要なときにoneshotバッチとしてECS上に実行する仕組みです。 トライ1では、MySQL上で表現が難しい権限付与がネックとなりましたが、これをECSタスクに付与するIAM権限とMySQL権限の組み合わせで解消することができました。

具体的には、権限周りの課題を以下の3ステップで対処しました。

  1. DBレベルでフル権限を持つMySQLユーザーを作成し、そのユーザー名/パスワード等をcredstash経由で保存
  2. credstash経由での取得権限を、Schema適用バッチのECSタスクIAMのみに付与
  3. バッチJOBをいつでも開発者から叩けるように整備
f:id:otany41:20201008150202p:plain
図1.全体構成図

この構成により、開発者はMySQLユーザーもパスワードもわからないため、機密情報を扱う secret_ prefixのテーブルへはアクセスできません。代わりにバッチアプリケーションを叩くことにより、好きなタイミングでSchema変更ができる仕組みとなっています。Schema適用作業を全面的に開発者に移譲できたため、開発効率の改善に貢献することができました。

今回のケースではCredstash(KMS&DynamoDB) × ECSという組み合わせでしたが、DBへのアクセス権限をIAMで管理し、AWSのCompute基盤から処理を実行させるという手法は他にも活用パターンがありそうです(例えばAWS Secrets ManagerとLambdaという組み合わせ等)。

最後に

今回、ロコガイドのインフラ作業の権限移譲の事例を紹介しました。他にも移譲できそうな作業は数多く抱えているので、これから引き続き権限移譲をすすめ、この場で改善事例を紹介していきたいと考えています。