astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

Sangriaで始めるGraphQL入門

f:id:astamuse:20191120111334p:plain
sangria
お久しぶりでございます。Scalaでバックエンドを開発しているaxtstar(@axtstart)です。

今回は、WebAPI開発時にSangria(GraphQL)を使ってみて便利だなと感じたので紹介します。

GraphQLとは?

GraphQL(グラフQL)は、APIのために作られた、データクエリとデータ操作のための言語と、保存されたデータに対してクエリを実行するランタイムである
~~
GraphQLは、2012年にFacebookの内部で開発され、2015年に公開された
(抜粋: Wikipedia)

GitHubやNetflix、SpotifyでGraphQLを利用しているというニュースも記憶に新しいかと思います。

REST vs GraphQL

上記、Wikipediaにもあるように、RESTと比較して、 ウェブAPIの開発に、その他のWebサービスと比較して、効率的、堅牢、フレキシブルなアプローチを提供することを目的としているようです。

また、機能としては、読み込み、書き込み(ミューテーション)、データのサブスクリプション(リアルタイム更新機能)を持っています。

RESTとGraphQLの比較表

-- REST GraphQL
主導 クライアントドリブン サーバドリブン
エンドポイント インターフェース数分 基本的には一つ
通信 JSON JSON

ただ、以下の点では注意が必要とのこと

  • 比較的新しい技術のため、サーバライブラリは、言語によって差がある
  • クライアントでどんなクエリでもかけるのでパフォーマンスに注意
  • キャッシュの機構はRESTより複雑になる
  • ファイルアップロードのような機能は無い

Sangriaとは?

Sangriaとは、Scala版GraphQLの実装です。

github.com

play-frameworkやakka-http上で動作するようです。

今回はakka-http上で動作する公式サンプルに基づいて説明していきます。

依存関係

akka-http、JSONの変換にcirceを用いたバージョンのbuild.sbtは以下な感じです。

libraryDependencies ++= Seq(
  "org.sangria-graphql" %% "sangria" % "1.4.2",
  "org.sangria-graphql" %% "sangria-slowlog" % "0.1.8",
  "org.sangria-graphql" %% "sangria-circe" % "1.2.1",

  "com.typesafe.akka" %% "akka-http" % "10.1.10",
  "de.heikoseeberger" %% "akka-http-circe" % "1.29.1",

  "io.circe" %% "circe-core" % "0.12.1",
  "io.circe" %% "circe-parser" % "0.12.1",
  "io.circe" %% "circe-optics" % "0.9.3"
)

公式のakka-httpサンプルとはほんの少し変えています。

起動

sbt run で起動すると、playgroundが下記でアクセスできます。

http://localhost:8080/graphql

こんな画面

f:id:astamuse:20191119231758p:plain
playground

こちらはある種のIDE*1になっていて、補完やサジェッスションを提供してくれるため、意外なほど簡単に目的のクエリを記述することができます。

クエリ例

query{
  humans{
    id
    name
    homePlanet
    current_time
  }
}

補完の例

f:id:astamuse:20191120095436p:plain
completion

また右側のDOCS、SCHEMAというところに存在するクエリの仕様が表示されます。

スキーマ表示

f:id:astamuse:20191120095549p:plain
schema

スキーマ定義

Scalaで記載するスキーマ定義はsangria.schema._ で提供される、DSL*2で記述します。

EnumType、InterfaceType、ObjectType、Argument、ListType、OptionTypeなどを定義でき、最終的にObjectTypeとしてクエリのスキーマとして公開します。 StringやIntなども組み込みのTypeが存在するようですが、LocalDateTimeのような、文字列化の法則が決まっていないようなTypeは自分でParserを記述する必要があるようです。

スキーマ定義の例

Enum

  val EpisodeEnum = EnumType(
    "Episode",
    Some("One of the films in the Star Wars Trilogy"),
    List(
      EnumValue("NEWHOPE",
        value = Episode.NEWHOPE,
        description = Some("Released in 1977.")),
      EnumValue("EMPIRE",
        value = Episode.EMPIRE,
        description = Some("Released in 1980.")),
      EnumValue("JEDI",
        value = Episode.JEDI,
        description = Some("Released in 1983."))))

ObjectTypeの例

  val Human =
    ObjectType(
      "Human",
      "A humanoid creature in the Star Wars universe.",
      interfaces[CharacterRepo, Human](Character),
      fields[CharacterRepo, Human](
        Field("id", StringType,
          Some("The id of the human."),
          resolve = _.value.id),
        Field("name", OptionType(StringType),
          Some("The name of the human."),
          resolve = _.value.name),
        Field("friends", ListType(Character),
          Some("The friends of the human, or an empty list if they have none."),
          resolve = ctx ⇒ characters.deferSeqOpt(ctx.value.friends)),
        Field("appearsIn", OptionType(ListType(OptionType(EpisodeEnum))),
          Some("Which movies they appear in."),
          resolve = _.value.appearsIn map (e ⇒ Some(e))),
        Field("homePlanet", OptionType(StringType),
          Some("The home planet of the human, or null if unknown."),
          resolve = _.value.homePlanet),
        /* 試しに追加した型 */
        Field("current_time", Types.LocalDateTimeType,
          Some("current time."),
          resolve = _.value.current_time),
      ))

引数定義

  val LimitArg = Argument("limit", OptionInputType(IntType), defaultValue = 20)

LimitArg(最大取得件数)をクエリ定義の部分で利用する

      Field("humans", ListType(Human),
        arguments = LimitArg :: OffsetArg :: Nil,
        resolve = ctx ⇒ ctx.ctx.getHumans(ctx arg LimitArg, ctx arg OffsetArg))

上記、Types.LocalDateTimeType(LocalDateTime)のパーサー例*3

object Types {

  case object DateCoercionViolation extends ValueCoercionViolation("Date value expected")

  def parseDate(s: String) = Try(LocalDateTime.parse(s)) match {
    case Success(date) => Right(date)
    case Failure(_) => Left(DateCoercionViolation)
  }

  implicit val LocalDateTimeType = ScalarType[LocalDateTime](
    "DateTime",
    coerceOutput = (dt, _) => dt.toString,
    coerceUserInput = {
      case s: String => parseDate(s)
      case _ => Left(DateCoercionViolation)
    },
    coerceInput = {
      case StringValue(s, _, _, _, _) => parseDate(s)
      case _ => Left(DateCoercionViolation)
    })

}

パフォーマンス計測

GraphQLは複雑なクエリやネストの深いクエリをクライアントから自由に書けるようになっているためか、パフォーマンス測定を有効にできるようにサンプルではなっていました。

HTTP Headersに以下の設定を行うと有効になるよう*4です。

{
  "X-Apollo-Tracing":true
}

f:id:astamuse:20191119232453p:plain
tracing

クエリ(サーバサイド)

サーバサイド側でDSLライクにクエリを書くこともできます。 RESTのグルー(のり)として利用する*5のもありかなと思いました。

      path("query"){
        get {
          import sangria.ast._
          import sangria.macros._
          val queryAst: Document =
            graphql"""
            {
              humans {
                 name
              }
            }
          """
          executeGraphQL(queryAst, None, Json.obj(), false)
        }
      }

結果

$ curl http://localhost:8080/query
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   139  100   139    0     0    158      0 --:--:-- --:--:-- --:--:--   158
{"data":{"humans":[{"name":"Luke Skywalker"},{"name":"Darth Vader"},{"name":"Han Solo"},{"name":"Leia Organa"},{"name":"Wilhuff Tarkin"}]}}

クエリ

ここからはクライアント側でどのように呼び出すかを決めるクエリの記述法に関して見ていきます。

複数のスキーマを同時に呼び出す

query{
  humans{
    name
  }
  droids{
    name
  }
}

結果

f:id:astamuse:20191120101351p:plain
double schema

引数を取る場合

query{
  human(id:"1000"){
    name
  }
  droid(id:"2000"){
    id
    name
  }
}

結果

f:id:astamuse:20191120101835p:plain
argument

また、記述時に型エラーなどを教えてくれます(Validation)

以下は文字列型に数値を適用しようとしてエラー

f:id:astamuse:20191120102032p:plain
validation

クライアント側でクエリを自由に記述できるため、ある意味奇妙なクエリもかけるようです。

友達の友達の友達を検索

query  {
  humans{
    name
    friends{
      name
      friends{
        name
        friends{
          name
        }
      }
    }
  }
}

結果:

f:id:astamuse:20191120000357p:plain

まとめ

RESTでAPIを作ろうとした場合、丁寧につくれば作るほど、Endpointが細分化していき、またそれらを繋げるためにエッジ-サーバ間のラウンドトリップが増加する、もしくは統合的な項目を持った、メンテナンス性の低いEndpointが増加する問題があると感じていますが、GraphQLの場合はクエリに複数のスキーマ呼び出しを行ったりといったことが柔軟なので、エッジ-サーバ間の通信量をおさえつつもtidyな開発ができるのでは無いかと期待しています。

最後になりましたが、アスタミューゼでは現在、エンジニア・デザイナーを絶賛大大大募集中です! 興味のある方はぜひ下記バナーからご応募ください!!

*1:graphql-playground-reactというものを利用している

*2:ここに記載

*3:https://github.com/axtstar/sangria-akka-http-example/blob/forBlog/src/main/scala/Types.scala

*4:ここで指定しているので変更は可能

*5:ここに

GitLab Pagesで複数SwaggerUIを自動で公開する

こんにちは、開発部のomiです。
前回の投稿から7ヶ月が経っていてびっくりしました。絶対背が伸びたと思います。

今回は、現在携わっている新しいプロジェクトで導入した GitLab Pages について書きたいと思います。

経緯

今のプロジェクトではAPI設計にSwaggerを使用しました。

Swagger Specで書いた仕様は、最初はSwagger Codegenでhtml化し、毎回サーバにアップして公開していました。

修正の都度ローカルでhtml化し、サーバにアップする作業は面倒ですし、アップを忘れて仕様書と実装のずれが出てくる恐れもあります。

そこで、我がチームの頼れるリーダーイケマスこと池田さん lab.astamuse.co.jp に提案していただいた、GitLab Pagesでのhtml公開を行ってみることにしました。

↓GitLab Pages 公式 Document
GitLab Pages

手順

今回、この方の方法を真似してみることにしました。

gitlab.com

私たちのプロジェクトのSwagger Specは用途ごとに分かれており、それぞれを別のURLで同時に公開したかったので、少し工夫する必要がありました。
Swagger Specが一つだよ、という方は↑の方の設定のままで大丈夫です。

Projectの任意のフォルダにSwagger Specを配置する

project
.
├── README.md
├── api-docs
│   ├── api01
│   │   └── api01.yaml
│   ├── api02
│   │   └── api02.yaml
│   ├── api03
│   │   └── api03.yaml
│   ├── api04
│   │   └── api04.yaml
│   └── api05
│       └── api05.yaml

今回は api-docs というフォルダの下にyamlを配置しました。
先述の通りこのプロジェクトのSwagger Specは用途ごとに分かれていたのでそれぞれサブディレクトリに入れてあげる必要があります。

.gitlab-ci.yml に設定を記述する

.gitlab-ci.yml に以下の通り設定を記述します。

image: hseeberger/scala-sbt:8u181_2.12.6_1.2.3

variables:
    DOCS_FOLDER: "api-docs"

cache:
    paths:
        - ./node_modules

stages:
    - deploy

pages:
    image: node:10-alpine
    stage: deploy
    before_script:
        - npm install swagger-ui-dist@3.22.1
    script:
        - mkdir -p public
        - cp -rp $DOCS_FOLDER public/
        - cd public/$DOCS_FOLDER/
        - find . -mindepth 1 -type d -exec cp -p ../../node_modules/swagger-ui-dist/* {} \;
        - ls -1 | while read FILE ; do sed -i "s#https://petstore\.swagger\.io/v2/swagger\.json#${FILE}.yaml#g" ${FILE}/index.html ; done
    artifacts:
        paths:
            - public
    only:
        - develop

variables
Swagger Specを入れたフォルダを指定します。
pages:stage
html公開を走らせたいタイミング
pages:script
GitLabではpublicという名前のフォルダが公開対象と決まっているため、public配下にhtmlを配置してあげる必要があります。
DOCS_FOLDERで指定したSwagger Specを入れたフォルダをpublicフォルダにコピーし、各サブディレクトリ配下にswagger-ui-distフォルダをコピーします。
デフォルトでは公開するhtmlのurlがhttps://petstore.swagger.io/v2/swagger.json になっているので、ここをまるっとそれぞれのyaml / jsonの名前に変換します。
pages:artifacts
path:publicで生成されたジョブの成果物をジョブが成功した後にGitLabに送信します。

これが、developブランチのdeploy時に毎回走ることになります。

Swagger Specから生成されたhtmlが公開される

上記設定をしておくと、developブランチがdeployされたタイミングで

http://[Project].[GitLabのホスト]/[api-docsまでのパス]/api-docs/api01/

というようなURLでAPI仕様書が公開されます。

※イメージ

f:id:astamuse:20191109183606p:plain
swagger-ui-image

最後に

今までの方法だとサーバ上のhtmlとプロジェクトにpushされているyamlの内容が食い違うというようなことが起こり得ていましたが、
今回の対応でyamlと公開されるhtmlが連動するようになり、yamlの管理も徹底されるようになったのでとても良い効果だと思いました。

弊社ではエンジニア・デザイナーを募集中です!興味を持っていただけましたらバナーからよろしくお願いします!
以上です。読んでいただきありがとうございました。

SSL証明書の有効期限一覧を自動生成した話

こんにちは。開発部のtorigakiです。 弊社ではSSL証明書を使ったサイトが300以上ありますが、このSSL証明書の期限をEXCELなどを使って手作業で管理しようとするとなかなか大変な作業となります。

そこで自動的にSSL証明書の有効期限の一覧を生成する仕組みを作ってみましたので、今回はそのお話をさせていただければと思います。

route53からドメインリストを取得する

弊社ではDNS管理にroute53を使っていますので、route53をコマンドラインで使えるツールを使って運用ドメインの一覧を取得します。

ドメイン一覧の取得にはcli53を使用しました。

SSL証明書を使っているFQDNの一覧は以下のように取得します。

  • cli53 list にて運用ドメインを取得。
  • cli53 export ${DOMAIN}にて各ドメインごとのゾーンを取得し、そのゾーン情報からAレコードを取得。
  • Aレコード+ドメイン名からFQDNを取得。

上記で取得したFQDN一覧の中にはSSL運用していないサイトも含まれますので、SSL運用しているサイトに絞り込みます。 絞り込む方法は、opensslコマンドでアクセスし応答の有無で判定します。 opensslコマンドは以下のように実行します。 応答が返ってこないときの対策として先頭にtimeoutをつけて実行します。

timeout 3 openssl s_client -connect ${FQDN}:443 -servername ${FQDN} < /dev/null 2> /dev/null

上記で応答が返ってきたFQDNに対して、以下コマンドで有効期限を取得します。

openssl s_client -connect ${FQDN}:443 -servername ${FQDN} < /dev/null 2> /dev/null | openssl x509 -noout -startdate -enddate | grep notAfter

上記を実行すると以下の結果が得られます。

notAfter=Feb 13 12:00:00 2021 GMT

SSL証明書の発行署名者も一覧に加える場合は、以下コマンドにて発行署名者を取得します。

openssl s_client -connect ${FQDN}:443 -servername ${FQDN} < /dev/null 2> /dev/null | openssl x509 -noout -issuer

上記を実行すると以下の結果が得られます。

issuer= /C=US/O=DigiCert Inc/OU=www.digicert.com/CN=RapidSSL RSA CA 2018

上記3つを取得できたら、FQDN、発行署名者、有効期限 のCSV形式でファイルに出力します。

CSVファイルをHTMLファイルに変換する

ブラウザで一覧を見れるようにするため、生成したCSVファイルをHTMLに変換します。

変換はシェルスクリプトで実行します。変換スクリプトはこちらを参考にさせていただきました。

HTML形式に変換した結果、ブラウザにて以下のように一覧表示できるうようになります。

f:id:astamuse:20191029144212p:plain
SSL

まとめ

以上のように、cli53とopensslコマンドから情報収集してCSV出力し、HTML変換する処理を毎週1回cronで実行するようにしておけば自動的にはSSL証明書の期限管理をできるようになります。

またこの一覧とは別に、証明書期限が30日前になったらSlack宛に通知する仕組みも入れたりしてSSL証明書の更新忘れ防止をしております。

今回はSSL証明書期限管理について工夫した点について紹介させていただきました。 少しでもSSL証明書の運用されている方のお役に立てれば幸いです。

弊社では引き続きエンジニア・デザイナーを募集中ですので、ご興味のある方は下からご応募いただければと思います。

Copyright © astamuse company, ltd. all rights reserved.