レガシーなWebアプリにサービス間連携のためのAPIを追加する

運用するWebアプリの数が増えてくると、それらの間で何らかの連携を取らなければならないケースが増えてくる。

例えば、二つのWebアプリ間での連携を考えてみる。最も典型的なのは、下図のように同じデータベースを共有するやり方だろう。

web-api1

異なる立場の人が同じ情報にアクセスする必要がある場合に、このような形でアプリケーションを分ける事はよくある。例えば、エンドユーザーとシステム管理者が使うアプリケーションを分けておきたいケースは多いと思う。

二つのアプリケーションが、同じデータベースに対して等しく関心を持つのならこれで何の問題も無いように思えるが、同じデータベースを扱うので、少なからずデータアクセスの処理やビジネスロジックが重複する可能性が高い。ただ、この段階では、コミュニケーションコストを考えて若干の重複には目をつぶるか、共有ライブラリを作るぐらいの解決策が適当で、Web APIを作って共有するのは大げさに感じられるかもしれない。

Webアプリの横恋慕的依存関係

では、以下のケースだとどうだろうか?

web-api2

二つのWebアプリは、それぞれに関心を持つ別々のデータベースにアクセスしており、一見それぞれ独立しているように見えるが、例外的に「Web app 1」の方は「Web app 2」で管理している情報の一部にアクセスする必要が出てきた。果たして、「Web app 1」は直接「Database 2」にアクセスすべきだろうか?

というのが、たまチームが最近直面した問題であった。

サービス全体が小規模、あるいは二つのWebアプリが両方とも同じ人間によって開発されているのなら、手っ取り早く「Web app 1」から直接「Database 2」にアクセスしてしまうのもアリかもしれない。しかし、Webアプリごとに別々の担当者がいるようなケースでは、自分の担当するアプリケーションのデータベーススキーマを変更する必要が出てきた時に、そこに依存している他のアプリケーションの動作に影響を与えないよう、細心の注意を払わなければならなくなる。

あるいは、このような例外を際限なく認めると、どのWebアプリがどのデータベースにアクセスしているかを把握するのが難しくなってしまうという問題もある。

web-api-dep-hell

共有ライブラリ

例えば、最初のケースで挙げたように「Database 2」についての共有ライブラリを作って、それを「Web app 1」から利用するという手がある。これによってデータベースの変更をライブラリの背後に隠蔽出来るようになり、より安全に変更を行えるようになる。

web-api3

しかし、ライブラリ方式にも問題はある。まず一つは配布の問題である。一度作って配布したら終わりというわけにはいかないので、変更がある度に利用者に知らせて、利用者はライブラリを更新し、アプリケーションをビルドし直す必要がある。あるいは、ライブラリ方式は言語やフレームワークに依存するというのが一番大きな問題かもしれない。全てのWebアプリが同じ言語・フレームワークで開発されていれば問題ないが、このMicroservicesの時代ではそうでないことも多々あると思われる。実際に、たまチームでは「Web app 1」と「Web app 2」で異なるフレームワークを利用していたために、ライブラリ方式を選択肢に入れることは出来なかった。

Web API

そこで選択肢として残ったのが、「Database 2」を利用するための Web API を立ち上げて、「Web app 1」からはそのAPIにアクセスするという方法である。

もし仮に Web APIサーバーを立ち上げる場合、データアクセスやビジネスロジック部分は共有したいので、「Web app 2」からその部分を切り離して単独のAPIサーバーとし、残った「Web app 2」のフロントエンド部分と「Web app 1」でそのAPIを共有するというのが、一番美しい、いわゆるマイクロサービス的な解法のように思える。

web-api4

もし、今から「Web app 2」を開発するなら、今風に始めからAPIサーバーとフロントエンドを分けた形で実装するだろうと思う。しかし、「Web app 2」は数年前にフルスタックなフレームワークの上に構築された、ガッチガチな一枚岩のWebアプリであり、ロジック部分だけを切り出してAPI化するというのは結構な大手術になってしまう。「Web app 1」側から利用する機能もそれほど多くなく、独立したAPIサーバーを置くほどのメリットも感じられなかった。

Webアプリに追加したAPIへのアクセス制御

そこで、「Web app 2」に Web API を追加することにしたが、この場合に問題になるのは API へのアクセスコントロールである。「Web app 2」は、エンドユーザーが利用するWebアプリなので、そこにそのまま API を追加すると、エンドユーザーもそのAPIにアクセス出来てしまう。

web-api5

APIに認証を追加するという方法も考えられるが、セキュアな認証の仕組みを導入すること自体が大仕事である上に、本来の目的がアプリ間連携であることを考えると、もっとより簡単で安全な方法があるような気もする。

そこで思い付いたのが、元から設定されているエンドユーザー向けのTCPポート(80番)の他に、新たにAPI専用のポート(8091番)でもリクエストを受け付けるようにして、そのポートに対してファイヤーウォール(たまチームではAWSのセキュリティグループ)でアクセス制御するという方法である。

web-api6

たまチームの「Web app 2」はTomcat上で動くWebアプリだったので、API用のHTTPポートを追加するためには、設定ファイル conf/server.xml に以下の一行を追加するだけでよかった。

<Connector port="8091" protocol="HTTP/1.1" connectionTimeout="20000" />

ポートを追加するだけだと、依然として既存のポートでもAPIにアクセス出来てしまうので、API内部ではアクセス元のポートを調べて、専用ポート(8091)経由の場合にのみアクセスを許可するようにする。

たまチームのケースでは、Webアプリはクラスタリング(冗長化)構成になっていてロードバランサーが間に入るため、実際の構成は以下のようになった。

web-api-in-aws

運用中のサービスに与える影響を最小限に抑えつつ、EC2-Classic上のRDSをVPC上に移行する

たまチームで開発しているWebサービスの大半は AWS EC2-Classic 上で動いているのだが、去年から標準でサポートされるようになった VPC (Virtual Private Cloud) に移行しないといろいろと不便なことが多くなってきた。というわけで、移行にあたって一番厄介だと思われる RDS の移行手順を以下にまとめた。

ちなみにこの手順は、EC2-Classic から VPC への移行だけでなく、VPC から VPC の移行にも適用出来るはず。

rds-migration1

大まかな流れとしては、

  1. リードレプリカ経由でClassic側のdumpを取り
  2. それをVPC側で復元
  3. 復元したデータベースをClassic側のslaveとして動かしてデータを同期
  4. 読み込み専用のアプリはここでVPC側に移行して動作確認
  5. VPC側をmasterに昇格した後(ここで一瞬だけ Read Only になる)
  6. 書き込みのあるアプリケーションもVPC側に切り替えて
  7. Classic側を潰せば

めでたく移行は完了する。

1. リードレプリカ経由でClassic側のdumpを取る

rds-migration2

まずはClassic側で移行用のdumpを取得する。リードレプリカを作ってすぐにリプリケーションを停止し、そのときのdumpとバイナリログの位置を取得しておく。リードレプリカ経由なので、この間、サービスの運用には一切影響を与えない。

バイナリログの保持期間を変更する

デフォルトのバイナリログの保持期間が短いので、VPC-slaveを作る間にログが消失してしまわないよう、Classic-masterで以下のように変更しておく。

CALL mysql.rds_set_configuration('binlog retention hours', 24);

現在の設定を確認する:

CALL mysql.rds_show_configuration;

レプリケーション用のユーザーを作る

Classic-masterでレプリケーション用のユーザーを作っておく。

GRANT REPLICATION CLIENT, REPLICATION SLAVE ON *.* TO 'repl_user' IDENTIFIED BY 'some-password';

Classic-masterのリードレプリカを作る

AWS管理コンソールで Classic-master となるRDSインスタンスを選び、”Instance Actions” から “Create Read Replica” を選択する。

レプリケーションをストップする

Classic-slave(今作ったレプリカ)のレプリケーションをストップする。

CALL mysql.rds_stop_replication;

バイナリログの位置を記録する

Classic-slaveで、レプリケーションの状態をメモる(どの位置からレプリケーションを再開すれば良いか)。

show slave statusG
...
    Master_Log_File: mysql-bin-changelog.050354
Read_Master_Log_Pos: 422
...
   Slave_IO_Running: No
  Slave_SQL_Running: No
...

Slave_IO_RunningSlave_SQL_RunningNoになっているので、ちゃんとレプリケーションが止まっていることが分かる。

Classic-slaveのdumpを取る

  • Classic-slaveのエンドポイントを classic-slave.rds.amazonaws.com、管理ユーザーを dbuser とする。
mysqldump -u dbuser -h classic-slave.rds.amazonaws.com --single-transaction --compress --order-by-primary --max_allowed_packet=1G -p --databases database1 databases2  > classic-slave.dump

2. VPCにClassic-masterのレプリカを作る

rds-migration3

VPC内に立てたRDSインスタンスに、Classic-slaveのdumpをインポートしてClassic-masterへのレプリケーションを再開させる(VPC-slave)。

VPC-slaveとなるRDSインスタンスを立ち上げる

AWS管理コンソールでサクッと立ち上げる。

VPC-slaveにClassic-slaveのdumpをインポート

  • VPC-slaveのエンドポイントを vpc.rds.amazonaws.com、管理ユーザーを dbuser とする。
mysql -u dbuser -h vpc.rds.amazonaws.com -p --default-character-set=utf8 < classic-slave.dump

Classic-masterのセキュリティグループ設定を変更する

レプリケーションを可能にするために、VPC-slave から Classic-master にアクセス出来るようにセキュリティグループ(DB Security Groups)を設定する。

以下のような感じで VPC-slave のIPを調べて、そのIPからのアクセスを許可する:

$ ping vpc.rds.amazonaws.com

以下を Classic-master のセキュリティグループに追加。

CIDR/IP to Authorize:  xxx.xxx.xxx.xxx/32

レプリケーションをスタートさせる

VPC-slave にログインして、レプリケーションをスタートさせる(さっき作った専用ユーザーがここで活躍)。
パラメータは先ほどメモしたレプリケーションの状態を参考に。

CALL mysql.rds_set_external_master('classic-master.rds.amazonaws.com',3306,'repl_user','some-password','mysql-bin-changelog.050354',422,0);
CALL mysql.rds_start_replication;

レプリケーションの状態を確認:

show slave statusG

以下のような項目があれば、正しくレプリケーションが動作している。

...
 Slave_IO_Running: Yes
Slave_SQL_Running: Yes
...

レプリケーションがうまく行ったら、Classic-slaveは用無しなので削除する。
この段階で読み込み専用のアプリはVPC内に移行出来る。

3. VPC-slaveをmasterに昇格する

rds-migration4

Classic-masterをReadOnlyにする

FLUSH TABLES WITH READ LOCK;

Closes all open tables and locks all tables for all databases with a global read lock. This is a very convenient way to get backups if you have a file system such as Veritas or ZFS that can take snapshots in time. Use UNLOCK TABLES to release the lock.

として、全てのテーブルをロックしてから切り替えたいところなのだが、RDSではこの命令を実行するための権限がないことが判明。

簡単な代替策もないようなので、とりあえず編集を行う可能性があるサービスを全て「メンテナンス中」として操作出来ないようにした。

VPC-slaveのレプリケーションを停止する

CALL mysql.rds_stop_replication;
CALL mysql.rds_reset_external_master;

以後、こちらを master として扱う。

アプリケーションをVPC内に移動する

VPC内にアプリケーションサーバーを立てて、そちらを指すようにDNSの設定を書き換える。

後は古いClassic環境を潰して移行は完了。

Immutable Infrastructureを導入する

現在たまチームでは、以下のような自動化を実現している。

gyron-infra-old

自動化の柱は大きく分けて二つあって、まずは Deployment Pipeline。アプリケーションコードを変更して git push すると、Jenkins上でビルドされて、Amazon S3上へのパッケージのリリースから、アプリケーションサーバー上へのデプロイまで自動で行われる。

もう一つはサーバー群の構成管理 (Provisioning)で、Chef による Codification(コード化) が実現されている。

この環境でかれこれ二年ぐらい運用してきたが、自動デプロイは便利だと思いつつ、いくつかの問題点が明らかになってきた。

  • デプロイの際に更新されるのはアプリケーションだけなので、サーバー上のミドルウェアの更新をいつやるか、どうやって安全にやるかを考えるのはなかなか悩ましかった。
  • Chefを経由するより、まずはサーバーを直接直してしまおうという誘惑が常にある(突然のセキュリティアップデートが必要になった場合など)。
  • EC2のインスタンスがリタイアするときにあたふたしてしまう。
  • Chefのコードを書く人がインフラ担当者に限られてしまい、アプリケーション開発者側から手出し出来なくなる。
  • 簡単とは言えないChefにも問題があると思われ。
  • ProvisioningコードがCI上に乗っかってない。

というわけで、たまチームへの新たな要求や DevOps の新たなトレンドなどを鑑みて、インフラの大幅刷新を行うことにした。

以下が、目下移行中、新インフラの概要図である。

gyron-infra-new

大きく変わるのはアプリケーション開発者が担当するビルドの部分。以下、重要なポイントごとに検討してみる。

DeploymentとProvisioningの統合

今までは別々に管理されていた Deployment と Provisioning のプロセスを統合して、アプリケーション側で Server Image の生成まで面倒を見るようにする。

現状のビルドプロセス:

deploy-and-provision-old

新ビルドプロセス:

deploy-and-provision

この統合によって、Provisioningの大部分がアプリケーション側に移動することになる。今まではインフラ担当者の責任だった Provisioning を各アプリケーションの開発者が担当することによって、誰もがアプリケーションコードを扱う感覚でProvisioningコードを扱えるようになることを狙いたい。

また、今回は Chef から Ansible への移行も行う。プロジェクト全体で Provisioning を共有する場合、あるいはインフラが大規模になるようなケースであれば Chef のメリットもあると思われるが、アプリケーションに Provisioning を含めてしまうという今回のケースでは、より習得が容易でコンパクトに書ける Ansible が向いているのではないかという判断である。

アプリだけでなくサーバー全体を含めたContinuous Integration

今までCI上で実施されていたのはアプリのビルドまでだった。新しい仕組みでは、サーバーのビルド(AMIの作成)までをCI上で回す。これによって、以下のようなメリットが得られる。

  • Provisioningも自動テストの対象にできる(serverspec
  • アプリケーションの変更を行う感覚でミドルウェアの変更を行える
  • サーバーのPortabilityが改善し、テスト用の環境などが構築しやすくなる

Immutable Infrastructure

新インフラのアプリケーションサーバーは、原則として、一度起動した後にsshなどでログインしてその状態を変更しないようにする。アプリケーションやミドルウェアを更新する場合は、コードを変更してServer Imageを作り直し、古いサーバーを破棄して新しいサーバーに入れ替える。

このような方針で管理されたインフラを「Immutable Infrastructure」と呼ぶ。インフラがコードに表現された以外の状態を取り得ないということは、コードさえあれば環境を容易に再現できるということであり、テストが簡単になる、理解しやすくなる、不測の事態が起こりづらくなる、Blue-Green Deployment がやりやすくなるなど、数多くのメリットがある。

DevOps Split – BuildとOrchestration

新しいインフラでは、Server Image (AMI) という Artifact を生成するプロセスと、そのArtifactを利用してOrchestration(サーバーの構成管理)を行うプロセスを分離する。前者は個々のアプリケーション開発者が担当し、後者はチーム全体で共同管理することになる。

以下は HashiCorp で紹介されているワークフロー:

Atlas-DevOps-Split

このワークフローを採用した場合、Artifactの作成で流れが一旦切れるため、git pushしたらアプリケーションの更新まで行われるという、現状実現できている自動化を再現するのが難しくなる。新インフラで得られるメリットに比べたら些細なことだと言えるかもしれないが、今後の課題ということでここに記録しておく。

CIのCodification – JenkinsからCircleCIへ

現状のインフラで自動化の要を担っているJenkinsであるが、ジョブの数が多くなってくると管理が大変になってくる上に、設定が Codification されていないという問題があるため、CircleCIに移行することにした。

Blue-Green Deployment

現状のインフラで Blue-Green Deployment を実現しているのは、状態(セッション)を持たないアプリケーションに限られ、かつデプロイ作業の多くは手動で行っている(ELBの操作など)。新インフラでは、以下のような手法を採用して、全てのアプリでより安全な Blue-Green Deployment の導入を目指す。

  • JEEアプリについては、 tomcat-redis-session-manager を利用してセッションをRedis (Elasticache) に保存するようにし、アプリケーションサーバーをステートレスにする。
  • Terraform を利用して新しい Server Image を立ち上げた後、Smoke Testで動作確認し、ELB配下で新旧の切り替えを行う。
  • Terraformによって、サーバーの設定・構成がコード化されると共に、管理コンソールで操作する必要がなくなるため、ヒューマンエラーの予防になる。