LoginSignup
8
0

More than 3 years have passed since last update.

ScalatraのAPIをDockerで動かす

Last updated at Posted at 2019-12-18

ぷりぷりあぷりけーしょんずインフラ担当による ぷりぷりあぷりけーしょんず Advent Calendar 2019 の19日目

概要

普段クラウドエンジニアとして、パブリッククラウドを利用してアプリケーションサーバの構築といったインフラ寄りの仕事しています。

ただ個人学習としてミドルウェアをインストールしたり、NWの設定をしたりしてサーバを構築することは楽しいですが、サーバに乗っかっているアプリケーションが出来合いのものだと愛着が湧かないというのが不満でした。

ぷりぷりあぷりけーしょんずは名前の通りアプリケーションエンジニアが多数在籍(9割くらい?)しているサークルなので、サークル活動ではアプリケーションの話を聞くこともあったため、せっかくだし興味のあったScalaで業務でアプリ開発をしたことないけどなんとなくで自作してみたというお話です。

作ったもの

リクエストで投げるとJsonを返してくるという単純なAPIです。

e.g. ユーザ登録
curl http://localhost:8080/user -X POST -H "Content-Type: application/json" -d '{"name": "MSHR-Dec", "pswd": "mshr-dec"}'

e.g サイト登録
curl http://localhost:8080/site/MSHR-Dec -X POST -H "Content-Type: application/json" -d '{"name": "puri", "pswd": "puripuri"}'

e.g. 参照
curl http://localhost:8080/user
curl http://localhost:8080/user/MSHR-Dec
curl http://localhost:8080/site/MSHR-Dec

テーマはなんでも良いのですが、わかりやすいようにユーザの名前とパスワード、サイトの名前とパスワードを管理するという設定のAPIを、それぞれ別のプロジェクトで作成して、それらを連携させようというものになっています。
ユーザ側のプロジェクトを作成してしまえば、サイト側の実装はユーザの実装とほとんど同様なので省きます。

Scalatra

http://scalatra.org/
Sinatraを意識したフレームワークらしいです。

PythonだとDjangoのような規約の多いフレームワークと逆のFlaskのような存在です。

ディレクトリレイアウト

ディレクトリで終わっているものは基本デフォルトです。
ファイルについては今回編集または新規作成しています。

.
├── Dockerfile
├── README.md
├── build.sbt
├── project/
│   ├── assembly.sbt
│   ├── build.properties // デフォルト
│   ├── plugins.sbt // デフォルト
│   ├── project/
│   └── target/
├── src/
│   └── main/
│       ├── resources/
│       │   ├── application.conf
│       │   └── logback.xml // デフォルト
│       ├── scala/
│       │   ├── ScalatraBootstrap.scala
│       │   └── com/
│       │       └── hoge/
│       │           ├── app/
│       │           │   └── Launcher.scala
│       │           ├── controller/
│       │           │   ├── HealthCheck.scala
│       │           │   ├── SiteController.scala
│       │           │   └── UserController.scala
│       │           ├── mysql/
│       │           │   ├── Initialize.scala
│       │           │   ├── Mysql.scala
│       │           │   └── MysqlBase.scala
│       │           ├── model/
│       │           │   └── User.scala
│       │           └── utils/
│       │               └── HttpClient.scala
│       ├── twirl/
│       └── webapp/
└── target/
    ├── scala-2.12/
    │   ├── classes/
    │   ├── resolution-cache/
    │   ├── test-classes/
    │   ├── ppap.jar
    │   └── twirl/
    ├── streams/
    └── test-reports/

インストール

http://scalatra.org/getting-started/first-project.html
SBTを利用できる環境さえ用意してあれば、上記サイトを参考にしてScalatraの環境構築を行うことができます。

scala_version [2.12.3]: 2.12.6
sbt_version [1.0.2]: 1.2.1
scalatra_version [2.5.3]: 2.6.5

バージョンは上記の設定です。

ライブラリの追加

build.sbtのlibraryDependenciesを書き換えます。
MySQLへの接続やHTTPクライアントを利用する際に必要となるものを定義します。

build.sbt
libraryDependencies ++= Seq(
  "org.scalatra" %% "scalatra" % ScalatraVersion,
  "org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test",
  "ch.qos.logback" % "logback-classic" % "1.2.3" % "runtime",
  "org.eclipse.jetty" % "jetty-webapp" % "9.4.19.v20190610" % "container",
  "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
  "org.scalatra" %% "scalatra-json" % ScalatraVersion,
  "org.json4s"   %% "json4s-jackson" % "3.5.2",
  "org.scalikejdbc" %% "scalikejdbc" % "3.3.5",
  "org.scalikejdbc" %% "scalikejdbc-config" % "3.3.5",
  "mysql" % "mysql-connector-java" % "8.0.11",
  "org.eclipse.jetty" % "jetty-webapp" % "9.4.20.v20190813",
  "org.scalaj" %% "scalaj-http" % "2.4.2"
)

Scalatraサーバ起動の設定

Scalatraで作成したAPIのDockerイメージdocker runで起動した際に、コンテナに8080番ポートでアクセスできるようにします。

project-root/src/main/scala/com/ppap/app/Launcher.scala
package com.ppap.app

import org.eclipse.jetty.server.Server
import org.eclipse.jetty.servlet.{DefaultServlet, ServletContextHandler}
import org.eclipse.jetty.webapp.WebAppContext
import org.scalatra.servlet.ScalatraListener

object Launcher extends App {
  val port = if(System.getenv("PORT") != null) System.getenv("PORT").toInt else 8080

  val server = new Server(port)
  val context = new WebAppContext()

  context setContextPath "/"
  context.setResourceBase("src/main/webapp")
  context.addEventListener(new ScalatraListener)
  context.addServlet(classOf[DefaultServlet], "/")

  server.setHandler(context)

  server.start
  server.join
}

MySQLとの接続設定

  • DBのフィールドの型を定義
project-root/src/main/scala/com/ppap/User.scala
package com.ppap.model

case class User(id: Int, name: String, pswd: String)
  • テーブルの初期設定やCRUDの定義

project-root/src/main/scala/com/ppap/mysql以下に下記のファイルを追加していきます。

MysqlBase.scala
package com.ppap.mysql

import scalikejdbc.config.DBs
import com.ppap.model._
import scalikejdbc.WrappedResultSet

trait MysqlBase {

  DBs.setupAll()

  protected val allColumns = (rs: WrappedResultSet) => User(
    id = rs.int("id"),
    name = rs.string("name"),
    pswd = rs.string("pswd")
  )

}
Initialize.scala
package com.ppap.mysql

import scalikejdbc._
import scalikejdbc.config._

class Initialize {

  DBs.setupAll()

  def createTable(): Unit = {

    val value = "admin"

    DB autoCommit { implicit session =>
      SQL("""
        CREATE TABLE IF NOT EXISTS `users` (
          `id` INT NOT NULL AUTO_INCREMENT,
          `name` VARCHAR(32) BINARY UNIQUE,
          `pswd` VARCHAR(32),
          PRIMARY KEY (`id`)
        )
      """).execute.apply()
    }

    DB autoCommit { implicit session =>
      SQL("insert into users (name, pswd) values (?, ?) on duplicate key update name = ?").bind(value, value, value).update.apply()
    }
  }

}
Mysql.scala
package com.ppap.mysql

import scalikejdbc._

import com.ppap.model._

class Mysql extends MysqlBase {

  def insert(name: String, pswd: String): Unit = {
    DB localTx { implicit session =>
      SQL("insert into users (name, pswd) values (?, ?)").bind(name, pswd).update.apply()
    }
  }

  def update(name: String, pswd: String): Unit = {
    DB localTx { implicit session =>
      SQL("update users set pswd = ? where name = ?").bind(pswd, name).update.apply()
    }
  }

  def delete(name: String): Unit = {
    DB localTx { implicit session =>
      SQL("delete from users where name = ?").bind(name).update.apply()
    }
  }

  def select(name: String):Option[User] = {
    DB readOnly { implicit session =>
      SQL("select * from users where name = ?").bind(name).map(allColumns).single.apply()
    }
  }

  def getAll(): List[User] = {
    DB readOnly { implicit session =>
      SQL("select * from users").map(allColumns).list.apply()
    }
  }

}
  • DBの接続情報を定義
project-root/src/main/resources/application.conf
# JDBC settings
db.default.driver="com.mysql.cj.jdbc.Driver"

# jdbc:mysql://DBのIPorドメイン:ポート/Database名?characterEncoding=UTF-8
db.default.url="xxx.xxx.xxx.xxx:3306/ppap?characterEncoding=UTF-8"
db.default.user="MSHR-Dec"
db.default.password="mshr-dec"

サイト管理プロジェクトとHTTPで接続

HTTPクライアントを作成します。
curl http://localhost:8080/site/MSHR-Decのユーザ名をDBのidに変更して、URIに指定する形でリクエストを投げています。

project-root/src/main/scala/com/ppap/utils/HttpClient.scala
package com.ppap.utils

import scalaj.http.Http

class HttpClient {

  def getAllSite(userId: Int) = {
    // サイト管理プロジェクトのIPを指定
    // DockerやK8sであればサービス名を指定
    val getUrl = s"http://fuga:8000/$userId"
    Http(getUrl).
      asString
  }

  def postSite(name: String, pswd: String, userId: Int): Unit = {
    // サイト管理プロジェクトのIPを指定
    // DockerやK8sであればサービス名を指定
    val postUrl = s"http://fuga:8000/$userId"
    val data =s"""{"name": "$name", "pswd": "$pswd"}"""

    Http(postUrl).
      postData(data).
      header("content-type", "application/json").
      asString

  }

}

エンドポイントの定義

project-root/src/main/scala/com/controller以下にAPIのエンドポイントの定義をします。
K8sのReadinessProbeなどのためにHealthcheck用のエンドポイントも作成すると良いです。

getリクエストのパラメータをJSONに変換して、その値をMySQLの操作をするオブジェクトに引数で渡しています。

UserController.scala
package com.ppap.controller

import org.scalatra._
import org.json4s._
import org.scalatra.json.JacksonJsonSupport
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.{read, write}

import com.ppap.mysql._

case class User(name: String, pswd: String)

class UserController extends ScalatraServlet with JacksonJsonSupport with MethodOverride {

  protected implicit lazy val jsonFormats: Formats = DefaultFormats

  get("/:name") {
    implicit val formats = Serialization.formats(NoTypeHints)
    val mysql = new Mysql()
    write(mysql.select(params("name")))
  }

}

サイト管理プロジェクトへ接続する必要があるためHttpClientを呼び出しています。

SiteController.scala
package com.ppap.controller

import org.scalatra._
import org.json4s._
import org.scalatra.json.JacksonJsonSupport
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.{read, write}

import com.ppap.mysql._
import com.ppap.model._
import com.ppap.utils._

case class Site(name: String, pswd: String)

class SiteController extends ScalatraServlet with JacksonJsonSupport with MethodOverride {

  protected implicit lazy val jsonFormats: Formats = DefaultFormats

  get("/:user") {
    val mysql = new Mysql
    val id = mysql.select(params("user")).get.id

    val httpClient = new HttpClient
    httpClient.getAllSite(id)
  }

}
HealthCheck.scala
package com.ppap.controller

import org.scalatra.ScalatraServlet

class HealthCheck extends ScalatraServlet {

  get("/") {
    views.html.hello()
  }

}

Postリクエストなども以下のようにすることで定義できます。

post("/") {
  val user = parsedBody.extract[User]
  val mysql = new Mysql()
  mysql.insert(user.name, user.pswd)
}
post("/:user") {
  val site = parsedBody.extract[Site]
  val mysql = new Mysql
  val id = mysql.select(params("user")).get.id

  val httpClient = new HttpClient
  httpClient.postSite(site.name, site.pswd, id)
}

Scalatraの初期化をproject-root/src/main/scala/ScalatraBootstrap.scalaで行います。

import org.scalatra._
import javax.servlet.ServletContext

import com.ppap.controller._
import com.ppap.mysql._

class ScalatraBootstrap extends LifeCycle {
  val table = new Initialize
  table.createTable()

  override def init(context: ServletContext) {
    context.mount(new HealthCheck, "/healthcheck")
    context.mount(new UserController, "/user")
    context.mount(new SiteController, "/site")
  }
}

Dockerイメージの作成

jarファイルの作成

build.sbtに下記を追加します。

build.sbt
resolvers += Classpaths.typesafeReleases

mainClass in assembly := Some("com.ppap.app.Launcher")

assemblyJarName in assembly := "ppap.jar"

また下記ファイルを追加します。

project-root/project/assembly.sbt
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.9")

ここまででjarファイル作成の準備ができたので、下記コマンドを実行してjarファイルを作成します。
$ sbt assembly
これでtarget/scala-2.12/ppap.jarというjarファイルが作成されます。
このファイルは$ java -jar target/scala-2.12/ppap.jarで実行することが可能です。

Dockerfileの作成

シンプルにjarファイルを実行するだけです。

FROM openjdk:8

ADD /target/scala-2.12/ppap.jar /root/ppap.jar

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "/root/ppap.jar"]

あとはDockerfileを元にDockerイメージを作成し、コンテナを実行すればScalatraのAPIを利用することが可能になります。

まとめ

PythonについてはLambdaやFlaskでのAPI作成をしたことがありますが、Scalaでの開発は初めてでした。
ただScalatraは制約が少なく、割とそれっぽい書き方をしただけですが、APIの作成をすることができました(コードの質はさておき...)。

Scalaはオブジェクト指向と関数型のマルチパラダイムですが、どちらもしっかりと学習したことがないので今回はどちらも生かせていないコードではありますが、今後はもう少し勉強してより見栄えの良いものを投稿できるように精進したいところです!

おまけ

k8s環境でこのAPIを利用する手順

DockerHubにイメージをPush

$ docker build -t ユーザ名/ppap-scala:1.0 .
$ docker push ユーザ名/ppap-scala:1.0

Secretの作成

$ docker login
$ kubectl create secret docker-registry regcred --docker-server=DockerhubのURL --docker-username=ユーザ名 --docker-password=パスワード
--docker-serverの値は~/.docker/config.json内にあります。

ポッドでDockerイメージを利用

Deploymentのspec.template.specに下記を記入します。

imagePullSecrets:
  - name: regcred

これでk8sに作成したDockerイメージを利用したポッドをデプロイできます!

参考URL

8
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
0