WebエンジニアのLoL日記

LoLをプレイしたりLJLの試合を見たりするのが好きなエンジニア。LoLのイベントやパッチノートなど気になった点を記事にしたり、LJLについの記事をかいたりしています。某社でWeb系のエンジニアとして働いているので、技術系の記事もたまに書きます。コンタクトを取りたい場合はtwitterまで。

PHPで新しくAPIサーバーを建てるまで - 開発のススメ方や利用している技術スタックなど

はじめに

この記事は ドワンゴ Advent Calendar 2018 8日目の記事です。

昨日の記事は @tasuwo さんの AWS AppSync + RDS を試してみた で、GraphQLのAPIサーバーを建てるにあたり、AWS AppSyncとRDSを使った場合の開発やデプロイについての記事でした。

新しくAPIサーバーを建てるまでの話

とあるAPIサーバー*1 を建てるにあたり言語だったりフレームワークだったり、開発からリリースまでに必要なものは何か。実際にサーバーを立ててみて何が重要だと感じたか、について書こうと思います。

これから新たなサービスやシステムを作ろうと思っている人の参考になればと思います。

新サービスを建てるにあたり考えなければいけないこと

ざっくり以下のことは考えなければいけないと思います。

  • 言語・フレームワークミドルウェアやサーバー構成などはどうするのか
  • ローカルの開発環境はどうやって作成するのか(個人が開発を始めるのに支障はないか)
  • コーディング規約や各種規約はどうするのか
  • テスト・CIはどうするのか
  • dev/staging/production の各環境へのデプロイはどうするのか
  • リリース前の負荷検証どうするのか
  • DDoSやSQLインジェクションなどの攻撃を受けないための対策をどうするのか
  • フェイルオーバーの仕組みなどどうするか
  • サーバー監視をどうするか

今回はAPIサーバーを立てます。過去APIサーバではドキュメント管理周りで問題を抱えていましたので、以下の問題も解決したいです。

  • ドキュメントをGit管理できないのか
  • ドキュメントと実装が乖離していく問題 *2 問題をどうするのか

開発を進めるにあたり

サービスの目的や「何を重視したいか」を考えることが重要です。何をしたくてそのサービスを作っているのか意識しないと軸がぶれていきます。僕が考えた点は以下の点です。

  • このAPIサーバーは非常に多くのチームから利用されることを想定されている。また、過去にAPIのドキュメントがメンテナンスされないことが多々あった。何らかの方法でドキュメントと実装に乖離がないことを確認したい。
  • 古いAPIでは、コードを見た時にこれがどんなレスポンスを返すのか、などが分かりづらいことがあったので、これがコードから明確になるようにしたい。
  • 古いAPIでは、コントローラー層が肥大化してメンテナンスし辛いコードも多くあったので、コントローラは何をどう書くかをはっきりと定め、保守性 を重視していきたい。

「◯◯を実装したい」となった時に、その実装方法は様々です。保守性を重視した書き方、実行速度を重視した書き方、など複数の書き方があります。その時に、書く人がどの書き方を採用すべきか、の指針になります。この指針が定まっていないと各々が好き勝手なコードを書き出して、ぐちゃぐちゃなコードが完成してしまいます。

言語・フレームワークミドルウェア選定

実はここに関して今回話すべき事はあまりありません。既存のライブラリや資産があったので、言語はPHPと決まっていましたし、ミドルウェアもほぼ決まっていました。

ただ、大きな議論になったのは以下の2点で

です

フレームワークどうするか

これに関しては、 Laravel, Lumen, Slimの3つが上がりましたが、最終的にSlimを採用しました。

www.slimframework.com

Slimの特徴としては、本当にマイクロなフレームワークという点で、ルーティングの機能とちょっとしたDIの機能くらいしか持っていません。ただ、既存のライブラリでDB接続やDIなどはすでに実装があり、LaravelやLumenを導入してもほとんどの機能は使わない事になってしまいます。そこで、シンプルで学習コストも低いSlimを採用しました。

本当に新規のサービスを作るのであればLaravelがおすすめです。

GraphQLを採用するか、JSONREST APIにするか

これに関してもだいぶ議論しました。我々のサービスとGraphQLの思想の相性は非常に良い感じたからです。しかし、我々のAPIは多岐に渡るクライアントから叩かれます。iOSAndroid、その他いろいろなデバイス...それを考えると、GraphQLを採用するのはサーバー側にとってもクライアント側にとってもコストが非常に高かったです。結局REST API形式を採用することにしました。

GraphQL について知りたい人は、公式ページや、

www.utakata.work

qiita.com

あたりを参考にしてください。

サーバー構成をどうするか

本番環境は、LBに数台のWebサーバーがぶら下がる構成になります。では、ステージングや開発環境はどうしましょう。我々は昔、本番環境はLB経由、ステージング環境や開発環境はLBを持たずに直接アクセスするという構成をになっていました。しかし、このように各環境の構成が異なると、「本番環境にデプロイして初めて問題が発覚する」ことありました。*3 今回のAPIサーバーはステージング環境でも開発環境でもLBを用意して各環境の構成を一致させました。

構成管理(Infrastructure as a Code)

Ansibleを利用して構成管理することにしました。既存の他のサービスにもAnsibleが使われており、導入コストが低かったためです。playbookもほぼ使いまわしで行けました。

ローカル開発環境はどうするのか

開発を進めるには当然ローカル環境が必要になります。最近ではVagrantやDockerを利用することが多いかと思います。構成管理にAnsibleを採用していれば、それをVagrantのboxに流すことで環境が完成する、でもよいでしょう。ローカル開発環境の作成は、開発者が最初にやることですので、簡単に作成できる、かつ、マニュアルがしっかりまとまっている 必要があります。

我々は、各個人ごとにクラウド上の検証環境があり、コードを書いてそこにデプロイすると動作確認ができるという仕組みになっています。クラウド上にサーバーを立ててAnsibleを流すだけなので、手順も非常にシンプルですぐに開発に着手できます。

CI環境を用意しておくことは重要

テスト・CIは非常に重要です。我々はJenkinsを使い、ブランチがpushされたりプルリクエストが投げられたりするたびにCIが回るようになっています。「CIは...とりあえず後回しにしよう」と考える人もいるかもしれません。しかし、CIこそ一番始めに用意すべきものなのです。なぜなら、

  • 最初に用意しておかないと用意するタイミングが無い
  • 後でCIを用意しようとしたら、すでにコードが汚すぎてCIを回せるような状況になかった

ということが起こるからです。我々がCIで主に行っているのは

の2つです。特に前者 コードの規約違反の検出 はCIで回しておかないとどんどんコードが汚くなっていくので、早めに導入しましょう。

コーディング規約や各種規約はどうするのか

コーディング規約に関しては、PHPであればPSR、Pythonであればpep8のような、「これを採用すべき」という規約があることが多いので簡単です。

問題はその他の規約です。

  • Controllerにはパラメータのバリデーション、レスポンスの整形以外のコードを書いてはいけない
  • Controllerは1つのAPIで1つ。PATHやメソッドごとにControllerクラスは分ける
  • namespaceはcontrollerに関しては controller/<APIのPATHと一致するようなnamespace>
  • 〜といったコードは〜のnamespaceに置く

こういった、コーディング規約以外の規約も定めておく必要があります。これははじめからすべてかっちりと決められるわけではありません。実装を勧めていくうちに決まることもあるでしょう。重要なのは 決められたことを文書化 しておくことで、後に開発する人が同じ場面に直面した場合に迷わないようにしておくことです。このような規約を最初に入念に決めておくことで、後の実装は規約に従って行われるので、保守性が高くなりかつ開発コストも下がります。総合的には最初に時間をかけて規約を決めたほうがお得なことが多いです。

API仕様作成時の方針について

「すでにDELETEされているリソースに対してもう一回DELETEを発行したらどういったレスポンスを返すべきか」に関しては、

  • 消えているという状態には変わりないのだから 200 OK を返す
  • すでに消えていて存在しないのだから 404 Not Found を返す

と、人によって主張が異なる場合があります。

別の例として「int型のユーザーIDに対して文字列が指定されて送られてきた場合」

  • ユーザーIDとして不正なのだから 400 Bad Request を返す
  • ユーザーは存在しないのだから 404 Not Found を返す

両方間違いではないと思います。

重要なのは、「エンドポイントによって動作が違う」ということを避けることです。「〜の時はレスポンスとしては 404 を返すようにしましょう」といったAPI仕様作成時の方針についても文書化しておいたほうが良いでしょう。

各環境へのデプロイはどうするのか

どうやってデプロイするのかも考える必要があります。何かデプロイツールがあればそれで 開発/ステージング/本番 にデプロイできますので、それで良しかもしれませんし、開発環境だけは特殊な要件が必要になるかもしれません。*4

我々はデプロイに関しても既存のデプロイツールを流用したので、そこまでコストはかかりませんでしたが、考えておく必要があります。

DDoSやSQLインジェクション対策はどうするのか

といった問題があります。

DDoS対策

Apacheやnginxで制御する方法、LBやWAF *5で対策する方法、コードで対策する方法などがあります。お金がかかるとか実装コストがかかるだとか、どれも一長一短なので、サービスと相談しながら選択しましょう。

CORS, CSRF

SQLインジェクションはみんな意識するかと思いますが、CORSはCSRFは以外と漏れたりする部分です。特にパブリックに公開するサービスは気をつける必要があります。

ミドルウェア脆弱性

  • なにのミドルウェアがインストールされているのかを構成管理で把握しておく
  • どのようなライブラリを利用しているのか、パッケージマネージャーで一覧できるようにしておく

ようにしておき、早期発見し早期にパッチを当てるのが基本です。これに関してはWAFを使っても防げない場合がありますので、すぐにアップデートできる体制づくりが重要です。

フェイルオーバーの仕組みをどうするか

  • APIサーバーが1台落ちたときに利用者に影響が出ないか
  • 接続先のDBが1台落ちた時にサービスに影響がないか

といった点が重要です。

APIサーバーが1台落ちたときに利用者に影響が出ないか

LBからのヘルスチェックにより、サーバーが死んだ際はLBから自動で外れるようになっていますので、数台程度であれば問題にようになっています。

接続先のDBが落ちた時に影響が出ないか

我々は構成管理にconsulとconsul-templateを利用しています。

Consulを頑張って理解する

consulが書くDBに対してヘルスチェックを行います。例えばDBのSlaveが1台死んだ場合は、consulがそれを検知し、consul-templateがconfigファイルの書き換えをし、死んだSlaveには接続されないようになっています。Masterが死んだ場合の自動復旧は結構高コストです、こちらはDBによっては手動復旧になる場合もあります。もちろん、可用性が重要なサービスでは自動復旧を入れる必要があります。ここは、コストと重要度で判断することになります。

サーバー(サービス)監視

フェイルオーバーが発動したことを検知したり、何らかの理由でフェイルオーバーが発動しかなったり、あるいは、オペミスやバグったコードへのデプロイでサービスが死んだりといったことがあるので、サーバー監視を入れておくことが重要です。

サーバー監視にはzabbixを使っています。実際にAPIにアクセスしてみての監視もあります。また、ステージングと本番で同じ監視を入れています。ステージングにデプロイした際にすぐにコードの不具合に気づけるようにです。特に、自分が修正しようとしたAPI以外のAPIに思わぬ不具合が出ているとこの監視が役に立ちます。

ドキュメントをSwaggerで管理

今回はAPIドキュメントをSwaggerで管理しました。Swaggerについての詳細はここでは書きませんが、API仕様記述言語 です。

Swaggerについてはこのブログでも何回書かいていますので、参考にしてください。

www.utakata.work

当初はSwagger 2.0 に dreddというツールを導入しようとしていました。dreddはSwaggerで書かれたAPIしようと、実際のAPIに齟齬がないかを確認してくれるツールです。しかし、Swagger の最新は3.0 ですが、dreddが2.0までしか対応していないという問題があり、dreddの導入は断念しました。

「ドキュメントをGit管理できないのか」こちらは達成できましたが「ドキュメントと実装が乖離していく問題」こちらはまだ達成できていないので解決する必要があります...

Swaggerドキュメントのレンダリングには ReDocというツールを利用しています。

github.com

Swagger-UIというSwagger公式のレンダリングツールもあるのですが、見た目は圧倒的にReDocのほうが見やすいです。ReDocの方には CURLコマンドを生成してくれるツールが無い のが残念なところですが、パラメータやレスポンスが見やすいので最終的にReDocを採用しました。

まとめ

今回思ったこと

  • サービスの実装方針をきっちり決めておかないと一貫性の無いコードになる
  • 規約を文書化しておかないと実装がバラバラになりがち
  • CIは一番最初に導入しないと、コードがどんどん汚くなっていき、途中から導入しようとすると導入コストがどんどん高くなる

だいぶ雑多な文章になってしまいましたが、何かのヒントや参考になれば嬉しいです。

*1:GraphQLの採用を検討したのですが、利用システムが多いAPIなので無難にJSONを返すAPIとなりました

*2:ドキュメントのメンテナンスがきっちりなされていない

*3:LBが挟まるかどうかによって、ヘッダをLBがちゃんと中継してくれるかどうか、やTSLの終端がLBなのかサーバーなのか、などの違いがある

*4:たとえば、頻繁にブランチを切り返るので手間がかからない方法がいい、開発中のブランチも気軽にデプロイできるようにしたい、など

*5:Web Application Firewall なんかいい感じに攻撃を検出してブロックしてくれるシステム