Help us understand the problem. What is going on with this article?

Akka HTTPでメンテナンス時のレスポンスを制御する

サブスクリプションの管理SaaSを提供しているアルプ株式会社でエンジニアをしています、@showmant です。
これはScala Advent Calendar 2019 14日目のエントリです。

はじめに

サービスを運用していると、場合によってはサービスを停止し、メンテナンス時間を入れることがあると思います。
今回はAkka HTTPで運用しているアプリケーションのメンテナンス時のハンドリングについて簡単にご紹介させてください。

ユースケース

メンテナンス時にメンテナンス中であることをクライアントに返却する

やりたいこと

  • confの切り替えによってメンテナンス中か否かを制御したい
    • これにより、ある程度柔軟にメンテナンスモードに入ることができる
  • RejectionHandlerを使ってレスポンス制御したい
    • Routingをシンプルにし、RejctionHandlerにロジックを集中することで、Routingが間違っていて事故が起こるようなことを避けたい

実装

Custom Rejection の実装

標準でもAuthのRejectionをはじめ、いくつものRejectionを搭載していますが、今回はカスタムのRejectionクラスを作ってみます。作り方はRejectionを継承すればいいだけですので、簡単です。

import akka.http.scaladsl.server.Rejection

case object MaintenanceRejection        extends Rejection
final case class MaintenanceRejection() extends Rejection

Rejection Handlerの実装

RejectionHandlerは ドキュメントのサンプルのように書けばいいのですね。 もちろん entity の部分をJSONレスポンスに変更したりもできます。

import akka.http.scaladsl.model.StatusCodes._
import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import sample.akkaHttp.MaintenanceRejection

RejectionHandler.newBuilder()
  .handle{
    case MaintenanceRejection =>
      complete(HttpResponse(ServiceUnavailable, entity = "メンテナンス中です"))
  }
  //他のRejectionの制御方法をここに追加していけばよい
  .handleNotFound { complete((NotFound, "Not here!")) }
  .result()

Routingの実装

Routingでどのような条件分岐でreject をするかはプロダクトによって大きく変わってくると思います。例えば、hcは通すけど、それ以外はメンテナンスモードにいれる、というようなこともあると思います。
今回は本題に集中するため、それらは省いてシンプルにメンテナンスモードのON/OFFで分岐する実装をサンプルとしました。

import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import sample.MaintenanceRejection

class SampleRouter(
  conf: SampleConf
) {
  override def routes: Route =
    extract(ctx => ctx.request) { request =>
      if (conf.isInMaitenance) reject(MaintenanceRejection)
      else ??? //メンテナンスモードでなければこちらでハンドリングする
    }
}

サーバーの実装

最後にサーバー立ち上げ時に、RejectionHandlerの読み込みと、Routing を差し込めばOKです。

object SampleApiServer extends App {
  implicit val system       = ActorSystem()
  implicit val materializer = ActorMaterializer()
  implicit def myRejectionHandler =
    RejectionHandler
      .newBuilder()
      .handle {
        case MaintenanceRejection =>
          complete(HttpResponse(ServiceUnavailable, entity = "メンテナンス中です"))
      }
      //他のRejectionの制御方法をここに追加していけばよい
      .handleNotFound { complete((NotFound, "Not here!")) }
      .result()
  val route = (new SampleRouter).routes

  Http().bindAndHandle(route, "localhost", 9000)

}

おわりに

実際のプロダクトでは、特にRoutingをどのようにわけるか、他のRejectionが一緒に積まれた場合はどうするかなど、考えるべきことがもう少しあるかのように思います。
記事を書き始めると意外とシンプルであまり中身がない感じになってしまいましたが、どなたかの参考になれば幸いです。

あしたは @grimrose@github さんです!

showmant
サーバーサイドエンジニアです。Scalaをよく書き、よく愛しています。 アルプ株式会社を創業しました。エンジニア大募集中です。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away