【書籍レビュー】1冊ですべて身につくHTML&CSSとWebデザイン入門講座

フロントエンドエンジニアの関です。

vivit株式会社では書籍購入補助制度を導入しており、一定の条件はありますが申請すれば補助が出ます。

今回はその制度を活用して購入した書籍のレビューを公開したいと思います。

「1冊ですべて身につくHTML&CSSとWebデザイン入門講座」です。

www.webcreatorbox.com

こんな人におすすめ

著者のおすすめ

  • これからWebサイトを作り始める初心者
  • HTMLとCSSを基本から学びたい人
  • 美しいデザインのWebサイトを作りたい人
  • Webサイト制作の最新技術を学びたい人
  • 初心者向けにWebサイト制作を教えている人

読了後の体感

読了までにかかる目安時間

読み切る時間としては約2h、実際に手を動かして確認する箇所もあるので丁寧にやるともっと掛かりそう。

全く初めてという方は一日少しずつ数日かけて覚えていく感じになるかと思います。

読了後、身に付く知識

  • Webサイトが閲覧できるとはどういうことか、基本的な構造が理解できる
  • HTMLとCSSを使って一般的なレイアウトのWebサイトの作り方を理解できる

購入の経緯

私は実は新卒からバックエンドエンジニアとして3年程やっていて、転職を機にフロントエンドエンジニアに転向しフレームワークを使ったプログラミング的なサイトの構築はできるようになりました。 しかし、サイトを構築するにあたり基本的なマークアップやスタイルの作法をすっ飛ばしているので、流用や見様見真似のいわゆる「なんとなく」でやっていたのが恥ずかしながら実情でした。

個人的な信条として説明できない、根拠のない実装はなるべくしたくないのでこのタイミングで改めて入門しようと、調べ物でいつもお世話になっている Webクリエイターボックス の Mana さんが書いたこの本を購入しました。

www.webcreatorbox.com

内容について

この本の軸は、架空のカフェ「WBC CAFE」のWebサイトを実際に作る過程を一つひとつ要点を抑えながらHTML、及びCSSの基本的な使い方を紹介していくところにあります。 HTMLタグの説明を機械的に紹介していくリファレンスのような技術書ではなく、かなり実践的な手順で進行していくので手応えはあると思います。

また「こんな人におすすめ」として私が「Web開発者・Webデザイナーと一緒に仕事をする人」を挙げた理由にもなりますが、カフェのWebサイトを構築し始めるにあたり「『Webサイト』って何?『Webデザイン』って何?」という点について「Webサイトがどのように作られなぜ見られるのか」を平易な言葉選びで懇切丁寧に解説していたり、見やすい・使いやすいWebサイトを作るための「色使い」や「言葉選び」などのあまり技術的過ぎない部分も解説されているので、Web開発者・Webデザイナーと一緒に仕事をするにあたり高い解像度でコミュニケーションが取れるのではないかと思いました。

数ヶ月もすれば廃れていくくらい流れのある技術で、一過性な最新技術だけでない「考え方」まで学べるので入門から中級の振り返りまで役立つ一冊だと思いました。

最後に

vivit株式会社では各種ポジションでエンジニア採用を行っています。

アウトドア好きでも、もちろんそうでない方も是非一度覗いてみてください。

www.wantedly.com

いまさらだけど、GKEでElasticsearch 構成例

こんにちは、インフラエンジニアをやっている井島です。
hinataメディア では、右上の検索窓でElasticsearchが使われています。
hinataアプリ でも使われていて、結構クリティカルなところになってます。汗..

hinataメディアをAWSからGCPに移行する案件があり、そのなかでElasticsearchもGCPに移行することになりました。 GKE上で自前運用することにしたので、そんなElasticsearchの構成を紹介できればと思います。

AWSからGCPへの移行要件

  • Elasticsearch移行のために、プログラム改修は極力行わないようにする

    • 例えば、Elasticsearchバージョンアップを行い、そのための改修にはあまり工数はかけられない
  • Elasticsearchを使うアプリケーション機能は踏襲する

検討したこと

GCP Marketplaceから購入できるElastic CloudGCP Marketplace

結果:NG

  • メジャーバージョンアップが必要

  • 同義語、ユーザ辞書 まわりの適用方法を、改修する必要がある
    (現行のプログラムでは、ユーザ辞書などをテキストファイルで生成して、Elasticsearchがインデックス作成時にそれを読み込む方式)

GCP Marketplace でのElastic Cloud が使えない時点で、自前で構築するしか選択肢がなくなりましたね... あとは、GCEか、他のプロダクトでも使っているGKEかの2択です。

GCE (VMインスタンスに構築)

結果:NG

単純にVMインスタンスをAnsibleなどで構成管理コストが大きく、お手軽にスケールアウトさせにくいと考えたので、選択しませんでした。

hinataメディアRailsで、GKE上で動いています。
Railsが生成した同義語、ユーザ辞書のテキストファイルをElasticsearchサーバ内に送る必要があります。

  • 複数のElasticsearchサーバから同じファイルが参照される必要があり、 NFSサーバにファイルを置いてElasticsearchからマウントして参照させる方式で行く

  • AWSのEFSのようにマルチゾーンで安く初められるサービスはGCPには無く、NFSサーバも自前構築する
    Cloud Filestore があるのですが、単一ゾーンしか対応していなかったので、見送りました。

上記方式で決めていて、永続ディスクはリージョナルにすることが出来ますが、片方のゾーンが止まった時、NFSサーバをフェールオーバーさせるよう構成する必要があり、運用難易度の高いシステムになってしまいます。

ここの切り替わりの仕組みはkubernetes のDeploymentのPod自動復旧に任せた方がシンプルなので、 これもVMインスタンスを選択しなかった理由です。

GKE上に構築

結果:採用
最終的にGKE上にすべて構築することにしました。

  • 現行と同じElasticsearchバージョン(5.x系)
  • Railsが生成した同義語、ユーザ辞書のテキストファイルをNFS経由でElasticsearchサーバから参照
  • プログラム改修なし

構成

お待ちかね、具体的な構成は以下です。

f:id:ijimakenta:20200823174519j:plain

Railsが検索リクエストを投げます。 インデックス作成にはSidekiq を使っており、Sidekiqがインデックス作成をリクエストします。

Elasticsearchは、役割ごとにコンテナを分けました。
コンテナ1つあたりで使用するリソースが増えて、GKEノードごとで使用されるリソースに偏りが大きくなることを緩和し、必要な役割のみスケールアウトできるように作業範囲を局所化したかったので、このようにバラバラで構成しています。

elasticsearch client はDeploymentで 永続ディスクなし、data, master の役割は Pod再起動時など、データが残っているとクラスタへの復帰が早くなるので、StatefulSetで永続ディスクありで構成しています。

さらにその永続ディスクは、リージョナルSSD永続ディスクを使用しています。
GKEクラスタのノードがリージョナル構成で、Elasticsearchで使う永続ディスクを単一ゾーンのものにすると、 せっかくGKEノードがリージョナルなのに、ElasticsearchのPodがデプロイされるゾーンが片方のゾーンに偏ってしまう恐れがあったので、 リージョナルSSD永続ディスクを使用しています。
実際に偏らずにPodデプロイが出来ています。

NFSサーバで使う永続ディスクも、DeploymentでのNFSサーバは1Podですが、そのPodがデプロイされるゾーンを偏らせないように、 ここもリージョナルSSD永続ディスクを使用しています。

基本的の1つのゾーンが停止してもElasticsearchを継続して使用できるよう考慮しています。

同義語、ユーザ辞書ファイルは、Deployment でNFSサーバ(elasticsearch-nfs-server)を1Pod作成し、それをPresistentVolume でマウントします。 必要なコンテナはPersistentVolumeClaimで、このPVを指定し接続しています。

Elasticsearchでは、インデックス作成する時、同義語、ユーザ辞書ファイルを読み込むように設定しています。この時、data と master 役割のElasticsearchが このファイルを参照する動きをしており、 この2つの役割のコンテナでNFSマウントをしています。

図にelasticsearch-discovery というService がありますが、これは TCP9300で各ノードがMaster とやり取りするときに使われるものになります。

さいごに

kubernetes マニフェストは、ここを参考にしました。
vivit では、Helmは使っておらず、Kustomize を使ってマニフェストを構成しているので、普通のマニフェストに読み直すのが少し大変だった感じですかね!
負荷的には、Data役割のPodが一番CPUを使っている感じです。

NFSサーバ、Docker for Mac 上ではうまく動かなかったので注意です。(はまりポイント)
今回の構成のNFSサーバは alpineイメージ で nfs-utils を使うよう作りましたが、Linux Kernel 付属のnfsdを使うもので、Mac上では動かなかったです.. コンテナ内ではLinux kernel を使う前提ものなのに、Mac のKernelを使おうとしているからかな...?

Mac 上で同じ構成を作りたかったら、PersistentVolume でhostPath で直接Macディレクトリを共有フォルダ(NFSのかわり)としてみてください。

参考

github.com

dzone.com

GraphQLについて

  • こんにちは、spotチームの名嘉眞です。spotチームはキャンプ場検索サービス(hinata spot)を開発しており、私はバックエンド担当として日々Goを書いてます。今回は業務でGraphQLを使っていることと、以下の書籍を読んだので、改めてGraphQLについて基本的なことをまとめてみました。

  • 初めてのGraphQL ――Webサービスを作って学ぶ新世代API

GraphQLとは

  • APIのための問合せ言語 クエリを実行してデータを呼び出すためのランタイムとも言われます。

  • 一般的にはHTTPプロトコルを使用

  • GraphQLは、クライアント/サーバー通信のための言語仕様。なので実際に実装するときは、GraphQLの言語仕様に則って、クライアント側とサーバー側の実装を行います。

グラフ理論について

GraphQLのメリット

書籍を読んでGraphQLのメリットについてまとめると以下のようになるかなと思いました。

  1. 1回のクエリで必要なフィールドだけ取得できる

  2. 1回のクエリで複数の異なる種類(定義された型)のデータを受け取ることができる

  3. GraphQLスキーマで定義された型でバリデーションされる

  4. GraphQLスキーマがフロントエンドとバックエンドの共通のスキーマ設計書とすることができる

  5. いろいろな言語に対応している。クライアント側とバックエンド側で言語が違っても大丈夫

ひとつずつ詳しく書いていこうと思います。

1. 1回のクエリで必要なフィールドだけ取得できる

 GraphQLのスキーマ定義に方法についてはこの記事で説明しませんが、以下のようなスキーマ定義がされているとします。これはキャンプ場(spot)という型を定義しています。

# キャンプ場を定義した型、Spot型はid, name, catchPhrase, description, addressフィールドをもつ
type Spot {
  id: ID!
  name: String!
  catchPhrase: String!
  description: String!
  address: Address!
}

 このSpot型のオブジェクトを取得しようとした時、REST APIだと、http://example.com/api/spots/:id という感じになり、1つのオブジェクトの全てのフィールドが取得されると想像できます。

 ただ、本来はnameとcatchPhraseだけ取得できれば問題ないなど、全てのフィールドの取得は必要ない場面は多いかもしれません。

 GraphQLでは下記のように、取得したいフィールドを指定してリクエストを送ることができます。今回の例ではほとんど差はないですが、余分なデータを取得しないことでより高速にレスポンスを受け取ることができます。

# Spot型のnameとcatchPhraseフィールドのみを取得するクエリ
query {
  getSpot(spot: { id: "1" }) {
    name
    catchPhrase
  }
}

2. 1回のクエリで複数の異なる種類(定義された型)のデータを受け取ることができる

 こちらも例をあげて説明してみます。先ほどのキャンプ場(spot)のレビューという関連オブジェクトが定義されているとします。

type Review {
  id: ID!
  spotId: ID!
  userName: String!
  email: String!
  title: String!
  point: Int!
}

type Spot {
  id: ID!
  name: String!
  catchPhrase: String!
  description: String!
  address: Address!
  reviews: [Review!]!  # review型のリストをspot型に定義
}

 REST APIだと基本的には、1度対象となるオブジェクトのデータを取得し、そのIDなどをもとに、関連するオブジェクトを取得すると思います。

 GraphQLでは、スキーマ定義に関連するオブジェクトを定義することで1回のクエリで複数の型のデータを取得することができ、効率良くレスポンスを受け取ることができます。

# Spot型のnameとcatchPhraseフィールドと関連するレビューを取得するクエリ
# クエリの中で、spot型とreview型が入れ子になる
query {
  getSpot(spot: { id: "1" }) {
    name
    catchPhrase
    review {
      userName
      title
      point
    }
  }
}

3. GraphQLスキーマで定義された型でバリデーションされる

 GraphQLではスキーマ定義した型でバリデーションがされるため、例えば以下のように存在しないフィールドを指定してリクエストを送ろうとするとエラーとなります。

# 存在しないhogeというフィールドを指定してリクエスト
query {
  getSpot(spot: { id: "1" }) {
    name
    catchPhrase
    hoge
  }
}

# レスポンス
# {
#   "error": {
#     "errors": [
#       {
#         "message": "Cannot query field \"hoge\" on type \"Spot\".",
#         "locations": [
#           {
#             "line": 3,
#             "column": 5
#           }
#         ]
#       }
#     ],
#     "data": null
#   }
# }

スキーマ定義した内容でそのままバリデーションまで構築できるのはとても便利ですね。

4. GraphQLスキーマがフロントエンドとバックエンドの共通のスキーマ設計書とすることができる

 GraphQLを使うと、定義したGraphQLスキーマをもとに共通認識を持って、フロントエンドもバックエンドも開発することができます。(スキーマファースト)

  • スキーマとは... データの型の集合

  • スキーマファーストについて

    • 設計の方法論。スキーマファーストではチーム全員がデータ型について理解していて、バックエンドはデータの永続化とリクエストに応じて返すデータ型が何か、フロントエンドはユーザーインターフェースを組み立てるためのデータ型が何かを理解している必要がある。

 また、GraphQLには イントロスペクション というスキーマの詳細を取得できる機能があります。GraphQL PlayGround などで、Docsとschemaが確認できるのはこの仕組みがあるからです。

 実際に業務でも、GraphQL PlayGroundを使用していまして、この機能のおかげで常に最新のAPIドキュメントを共有して開発ができるのと、APIドキュメントの更新忘れなども発生しないというメリットもあります。(スキーマ定義にコメントも記載でき、コメントも反映されます)

5. いろいろな言語に対応している。クライアント側とバックエンド側で言語が違っても大丈夫

 GraphQLは、クライアント/サーバー通信のための言語仕様と記事の冒頭の方に書きました。あくまで言語仕様です。クライアント側はクライアント側の言語でGraphQLのAPIエンドポイントを叩くように実装する、バックエンドはバックエンド側の言語で、GraphQLのAPIが返すと定義したデータを生成できるように実装するということが可能になります。

 また、GraphQLをさらに便利にするライブラリも充実しています。例えば、GraphQLスキーマからGoのバックエンドのテンプレートを生成してくれるライブラリ、gqlgen などがあります。

まとめ

 GraphQLは、使い方によってはレスポンスをより高速に受け取ることができることや、フロントエンドとバックエンドで共通のスキーマ定義をもとに開発することができるメリットがあります。

 私はvivitに入社してからGraphQLを使い始めましたが、フロントエンドエンジニアとGraphQLスキーマをもとに統一した言葉で会話が出来ることや、入社したばかりでもスキーマ定義を読むことで、なんとなく処理の内容が理解できたりしてGraphQLは素晴らしいと思いました。

 今回の記事では細かいスキーマ定義の方法やクエリの組み立て方は記載しませんでしたが、他にも便利な仕組みやライブラリもあるので、引き続き学んでいこうと思います。

vivitにjoinして1ヶ月でやったこと

  • こんにちは、今年の6月に入社したspotチームの名嘉眞です。spotチームはキャンプ場検索サービス(hinata spot)を開発しております。私はspotチームのバックエンド担当として日々Goを書いてます。

  • hinata spot

  • vivit入社前は主にRailsを書いていましたので、Goを業務で書き始めたのは入社してからになります。またGoだけでなく、gRPC、GraphQLも業務で使うのは初めてでした。

この記事はどんな記事?

  • vivitに入社してから1ヶ月の振り返りです。vivitでどんな技術を使っているのか、どんな開発をしているのか知ってもらえると嬉しいです。

joinして1ヶ月で自分がやったことを

入社して2日目ぐらいから、ペアプロしながら処理の流れやサービスの役割を教えてもらいました。

ペアプロVSCodeLive Share で先輩エンジニアが実際にコードを書くところを見ながら説明を受けたり、私が書くのを見てもらいながら行いました。

Live Share だと、どちらかがリモートでもペアプロやろうと思ってすぐにできるのが良いですね。

余談ですが、vivitではVSCodeを使う方が多くて、リポジトリVSCodeのsettings.json を管理しています。もちろん、使うエディタは自由です。(ちなみに私のエディタはvimです)

spotはいくつかのマイクロサービスで構成されています。バックエンドのサービス間はgRPCで通信しており、フロントエンドとバックエンドはGraphQLを使って通信しています。 ちなみにフロントエンドはReact+TypeScriptです。

f:id:shin018:20200720113557j:plain

gRPCの通信方式のうち、いまのところspotでは UnaryServerStreaming を使っています。

基本的には Unary で通信しますが、通信の容量が大きくなるレスポンスを返す場合は、 ServerStreaming を使います。

※gRPCの送受信データサイズの上限が4MBのため

※Unaryは単一のリクエストとレスポンスを返す通信

※ServerStreamingは単一のリクエストに対してstreamレスポンスを返す通信

# example protoファイル
# スポット一覧を取得する、returnsで stream と指定するとServerStreaming方式を定義します
rpc GetSpots(SearchRequest) returns (stream Spot);
# example Goファイル
func (s *spotServer)GetSpots(req *spot.SearchRequest) error {
  for _, spot := range res.Spots {
   // stream.Send でstream方式で送信
    err = stream.Send(spot)
    if err != nil {
      return err
    }
  }

  return nil
}

Goに関しては入社前からプライベートで書いていましたが、業務で通用するレベルかどうか少し不安はありました。 実際にはほぼ問題なかったですが、その理由の1つに、業務で使うGoのライブラリを自分がプライベートでも使ったことがあったということがあるかなと思います。

その1つがgorp です。gorpの特徴として、

  • 構造体へのマッピングが楽ちん
  • SELECT系は SQL を書く
  • EXEC系は SQL を書かなくても良い(書くこともできる)

ということがあります。 このライブラリ自体、難易度が高いということはないですが使ったことがあるということで業務にすんなり入れました。

gorpを導入した方に聞くと、構造体へのマッピングは必要だが、JOIN句やORDER BYなどSELECT文はSQLを書きたいということがあってgorpに決めたとのことでした。

また、vivitの別のチームでは他のライブラリを使っていたりしています。

# example Goファイル
dbmap = &gorp.DbMap{Db: _db, Dialect: gorp.MySQLDialect{Engine: "InnoDB", Encoding: "utf8mb4"}}
var spot *models.Spot
# selectはsqlで自由に書く
err := dbmap.SelectOne(&spot, "SELECT * FROM spots WHERE id = ?", id)

# insertはDBの該当tableと紐づいた構造体であれば下記のように書ける
err = dbmap.Insert(newSpot)

自分の最初のタスクがキャンプ場のレビュー機能の開発でした。

レビューのCRUD機能と投稿されたレビューをもとにキャンプ場の評価を設定するというものです。 このタスクで、Go、GraphQL、gRPCを使った処理の流れを一貫して実装しました。

記事の冒頭にも書きましたが、自分がGo、GraphQL、gRPCを業務で使ったことが無かったこともあり、新しい技術を使えてめちゃくちゃ楽しくできました。

また、実装に迷うところがあってもペアプロしたり、相談しやすい雰囲気、チームで取り組む感じがあったことも楽しくできた理由になります。

f:id:shin018:20200720113625j:plain

まとめ

この記事を書いている今も新機能の開発をしています。

サービス自体にまだまだ足りない機能が多いこともあり、追加する機能の要件定義からエンジニアも携わり、実装・リリースするので、サービスが成長していくことを感じています。 また改善点もあり、その解決に取り組むのも楽しみです。

この1ヶ月では、Go、GraphQL、gRPCを使うことができましたが、フロントエンドや、インフラまわりにも興味があるのでいろいろチャレンジしていきたいと思います。

GraphQL の Fragment でコンポーネントの見通しがよくなった話

vivit株式会社でフロントエンドエンジニアをしている関です。

新型コロナウイルス感染症(COVID-19)への対策として発出された緊急事態宣言の影響で、弊社も2ヶ月ほどリモートワークとなり、私も駆け足で自宅に作業環境を構築しました。買っちゃいましたL字デスク。

さて、弊社ではいくつかのプロダクトのフロントエンドに React(Next.js) + TypeScript を採用しており、バックエンドとの通信には GraphQL(Apollo) を採用しています。

今回はその中で、コンポーネント分割をする際の GraphQL Query の定義と取得したデータの受け渡しについて話をします。

ここからは GitHub GraphQL API v4 を使って説明していきます。

developer.github.com

アジェンダ

  • pages から components へのデータの受け渡し方法について
  • Fragment で分割する問題点
  • graphql-anywhere が凄い

前提

この記事で利用している各技術の詳細な説明は省きます。

pages から components へのデータの受け渡し方法について

Next.js でページを作成する際、コンポーネントcomponents ディレクトリに分割して置くのが一般的かと思いますが、 GraphQL API で取得したデータを TypeScript の型定義に則って子コンポーネントで利用する際にどうやろうというのが問題になり、やり方の候補がいくつか挙がりました。

1. コンポーネントが必要な項目の取得を全てページで行う

メリット

  • データの受け渡しがいたってシンプル

デメリット

// pages/index.tsx
const query = gql`
  query Index {
    search(query: "github", first: 10, type: REPOSITORY) {
      nodes {
        ... on Repository {
          id
          nameWithOwner
          updatedAt
        }
      }
    }
  }
`;

export default () => {
  const { data, loading } = useQuery(query);
  const nodes = data?.search.nodes || [];

  if (loading) return <>Loading...</>;

  return (
    <>
      <Head>
        <title>Segmented fragment</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Repos repos={nodes} />
    </>
  );
};

ページ内で使う項目をすべてページ側で定義、取得しコンポーネントに渡しています。

これでは子コンポーネント内で使う詳細な項目の知識までページが持ってしまっているので、子コンポーネントの変更にページが引っ張られる形になってしまいます。密です。

2. コンポーネントが必要なデータの取得を自身で行う

メリット

デメリット

// components/Repos/index.tsx
const query = gql`
  query Index {
    search(query: "github", first: 10, type: REPOSITORY) {
      nodes {
        ... on Repository {
          id
          nameWithOwner
          updatedAt
        }
      }
    }
  }
`;

export const Repos = () => {
  const { data, loading } = useQuery(query);
  const repos = data?.search.nodes || [];

  const list = repos.map((repo) => (
    <tr key={repo.id}>
      <td>{repo.nameWithOwner}</td>
      <td>{repo.updatedAt}</td>
    </tr>
  ));

  return (
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Updated at</th>
        </tr>
      </thead>
      <tbody>{list}</tbody>
    </table>
  );
};

コンポーネント内に必要な項目を取得するクエリを書き、またそれを実行しています。

GraphQL の利点を正面からぶっ潰すパワーのある案です。

3. コンポーネントに必要なクエリを Fragment で記述、取得はページで行う

メリット

  • ページがコンポーネントに項目まで依存しない
  • データの取得回数も一度で済む

デメリット

  • そのままだとページが取得したデータをまるごとコンポーネントに渡すことになる
// components/Repos/index.graphql
fragment Repos on Query {
  search(query: "github", first: 10, type: REPOSITORY) {
    nodes {
      ...Nodes
    }
  }
}

fragment Nodes on Repository {
  id
  nameWithOwner
  updatedAt
}
// components/Repos/index.tsx
type Props = {
  result: ReposFragment;
};

export const Repos = ({ result }: Props) => {
  const repos = result.search.nodes as NodesFragment[];
  const list = repos.map((repo) => (
    <tr key={repo.id}>
      <td>{repo.nameWithOwner}</td>
      <td>{repo.updatedAt}</td>
    </tr>
  ));

  return (
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Updated at</th>
        </tr>
      </thead>
      <tbody>{list}</tbody>
    </table>
  );
};
// pages/index.graphql
query Index {
  ...Repos
}
// pages/index.tsx
export default () => {
  const { data, loading } = useIndexQuery();

  if (loading) return <>Loading...</>;

  return (
    <>
      <Head>
        <title>Segmented fragment</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Repos result={data} />
    </>
  );
};

この例ではコンポーネントで使う項目を Fragment で定義し、そこから GraphQL Code Generator で生成された **Fragment型を props の型に定義しています。

※ Fragment について Queries and Mutations | GraphQL

話は逸れますが、コンポーネント名のディレクトリを切った中に index.tsxindex.graphql を分けて定義し、クエリの実行には自動生成された Hooks を使うことによりクエリとコードの切り分けを行っています。詳しい実装は最後に記載するサンプルプロジェクトで参照できます。

例示用に使っている GitHub GraphQL API v4スキーマを TypeScript の型定義に落とし込む都合上コンポーネント側のクエリがゴチャっていますが、こういう場合にゴチャッとした部分をコンポーネント側に閉じ込められるのもメリットと言えますね。

さて一見良さそうですが一つ難点があります。

Fragment で分割する問題点

滅茶苦茶 度々メルカリの記事やコードを参考にしているのですが、見かけた記事内でこのようなやりとりを見つけました。 (何ならこの記事のほとんどは以下の記事に書かれていました)

engineering.mercari.com

フロントエンドからのGraphQLクエリを最適化した by vvakame · Pull Request #134 · mercari/mtc2018-web

取得したデータをそれぞれのコンポーネントに丸々渡しているのはやはり気持ち悪いと思うようで、 graphql-anywhere という名前を出していました。

graphql-anywhere が凄い

www.npmjs.com

実際には filter という graphql-anywhere 内のユーティリティメソッドなのですが、 GraphQL Code Generator で同じく生成された gql タグ(下記の例では ReposFragmentDoc)を元に取得したデータをフィルタリングすることができます。

import { filter } from "graphql-anywhere";
import Head from "next/head";
import * as React from "react";
import { Repos } from "../components";
import { ReposFragment, ReposFragmentDoc, useIndexQuery } from "../graphql";

export default () => {
  const { data, loading } = useIndexQuery();

  if (loading) return <>Loading...</>;

  return (
    <>
      <Head>
        <title>Segmented fragment</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Repos result={filter<ReposFragment>(ReposFragmentDoc, data)} />
    </>
  );
};

実は Apollo の公式ドキュメントでも graphql-anywhere を使ったフィルタリング方法が書かれています。 本稿では TypeScript のジェネリクスで動的に型を指定しています。

www.apollographql.com

実際に動作するプロジェクトを作成したので公開します。

github.com

動作には GITHUB_TOKEN という名前で OAuth トークンを環境変数に設定する必要があります。

ドキュメントに従って取得、設定をしてください。

developer.github.com

参考

FirebaseからGKE Ingressに Aレコードを切り替えた話

こんにちは、vivit株式会社でインフラまわりを担当している井島です。
セルフマネージドSSLの具体的な更新手順とか、ググっても全然出てこなかったので、記事にしてみました。
なにかの参考になればと思います!

背景

あるFQDNexample.com とします。)をGKEのIngressに移行してくるというものなのですが、 移行というものは、Firebase上で動いているサイトを、GKE上に持ってきて、 DNSを変更して、切り替え、というよくある話です。
単純にAレコード切り替えれば済むんじゃね?と思っていたら、意外とマネージドSSLに癖がありました。

GCPマネージドSSLの仕様

ところが、GCPの「マネージドSSL」というものは以下制限がありました。

  • ドメインDNS レコードがロードバランサのターゲット プロキシの IP アドレスを参照している。」

  • 「ターゲット プロキシが Google マネージド証明書リソースを参照している。」

    • →作成したマネージドSSLIngress に適用されていること
  • 「ロードバランサの構成(転送ルールの作成など)が完了している。」

    • Ingress 構築が完了していること

参考:SSL 証明書の概要  |  負荷分散  |  Google Cloud

上記すべてを満たす必要があります。

移行方法

一時的にLet's Encrypt からSSL証明書を取得し、それを一時的にingressで利用することで、FirebaseからGKEに無事、無停止でDNS切替できました。

実際に行った手順を紹介します。

手順

1. https://sslnow.ml/example.comSSL証明書を取得

sslnow.ml 簡単にLet's Encrypt SSL証明書を取得できます。感謝です!

2. 取得したSSL証明書GCPにセルフマネージドSSLとしてインポート(gcloudコマンド例)

ローカルPCでコマンドを実行します。

gcloud compute ssl-certificates create example-com-ssl-temp --certificate example.com_fullchain.crt --private-key example.com.key
ポイント
  • --certificate オプションでSSL証明書を指定するのですが、1つのファイル内単純にSSL証明書、中間SSL証明書の順序で2つの証明書を入れたファイルを指定します。
    ※中間証明書指定のオプションが無いみたいです。
  • ローカルディレクトリにある証明書・鍵ファイルを指定するので、Cloud Shellではできません。
3.IngressSSL証明書を適用
kind: Ingress
metadata:
  annotations:
    ingress.gcp.kubernetes.io/pre-shared-cert: >-
      example-com-ssl-temp
4. hostsファイル にIngress のIPを指定して、移行先のIngressSSL証明書が適用されているか確認
12.34.56.78 example.com
5. example.com のAレコードをIngressのIPに変更
6. GCPマネージドSSLを作成
gcloud beta compute ssl-certificates create example-com-ssl --domains exapmle.com
7. 作成したマネージドSSLIngressに適用

このとき、3で追加したセルフマネージドSSL証明書指定場所より前に設定する。

kind: Ingress
metadata:
  annotations:
    ingress.gcp.kubernetes.io/pre-shared-cert: >-
      example-com-ssl,example-com-ssl-temp

こうすることで、マネージドSSLが使われていることがわかるので、この後、安心して一時的に使った証明書を削除できます。
 ⇢ 同じドメインSSL証明書Ingressに適用したとき、最初に設定されているSSL証明書が参照されるようです。

8. 3で適用した一時的なSSL証明書Ingressから外す
9. 3で適用した一時的なSSL証明書GCPから削除する

最後に

一応セルフマネージドSSLからマネージドSSLへの移行方法は、ちらっと書いてありました。 cloud.google.com

fly.ioでGraphQLのキャッシュサーバを立てて高速化した話

この記事は GraphQL Advent Calendar 2019 の22日目の記事です。 qiita.com

こんにちは。 vivit株式会社というアウトドア関係のサービスを提供している会社で主にフロントエンドを担当している中村です。

本記事では、fly.ioでGraphQLのキャッシュサーバを立てて高速化した話をします。

はじめに

弊社では、Go + React(TypeScript)で開発しておりAPIにはGraphQLを採用しています。
今回は下記の理由でGraphQLのQuery結果をキャッシュするサーバを実装してみました。

  • 社内向け管理画面で呼び出しているQueryの応答時間が5秒ほどかかっているものがあり、開発時、オペレーション時にストレスが発生している。
  • 技術的な興味

本記事では、

  • GraphQLをどのようにキャッシュするのか
  • fly.ioでGraphQLのキャッシュサーバをたてた結果どれだけ高速化できたか
  • サンプルアプリケーションを作りながら実装についての解説

の3点をお話しします。

GraphQLにおけるキャッシュ戦略

REST APIの場合、URLごとに取得するURLが一意であるためURLをベースにキャッシュキーを生成できます。
しかし、GraphQLでは単一のエンドポイントのみを利用し、Request Bodyの内容によってレスポンス結果が異なるため、REST APIのようにURLベースでキャッシュキーの生成ができないという課題があります。

この課題は、Request Bodyの内容をhash化した値をキャッシュキーとしてもつKey-Value Storeを実装することで解決可能です。

fly.ioとは

JavaScriptのアプリケーションをCDN Edge で実行できるPaaSです。

flyで提供されているコードを使うことでCDN Edgeで動くHTTP load balancers, caching services等を実装できます。

GitHub上のサンプルも充実しており、Node.jsでサーバーサイドのコードに書いたことがあればあまり不自由は感じないと思います。

fly.io

例えば特定のKeyに紐づくValueをキャッシュさせるコードは下記で実現できます。

import cache from "@fly/v8env/lib/fly/cache";

fly.http.respondWith(async request => {
  await cache.set("cacheKey", "cacheValue");
  return new Response("Succeeded to write cache", { status: 200 });
});

GraphQLのキャッシュは、上記のcacheKeyにRequest Bodyをhash化したものを使い、cacheValueにはQueryの結果を格納することで実現できます。

また、fly.ioにはキャッシュのpurge機能があり特定のURLにリクエストが来たときにキャッシュ削除をおこなうとこともできます。
キャッシュがpurgeされるまでの時間も1秒程度だったので十分実用的だなという印象です。(環境による差はあるかもしれません)

どれだけ高速化出来たか

上記の技術を使い、管理画面でのみ使用しているQueryをキャッシュさせてみたところ 約5000ms => 約50ms とQueryの高速化が実現できました。

ひとまず弊社では社内で利用する管理画面に導入する予定です。

before f:id:taroodr:20191221180131p:plain

after f:id:taroodr:20191221180140p:plain

詳しい実装方法についてはサンプルアプリケーションを実装しながら解説します。

サンプルアプリケーションの実装

ここからはサンプルアプリケーションを実装していきます。
今回GraphQLサーバにはswapi-graphqlを使います。

github.com

(ちょうど先日StarWarsの新作が公開されましたね。)

完成したコードはこちらです。

github.com

  • Queryのキャッシュ・削除機能
  • URLベースのルーティング

を機能としてもっています。

プロジェクトの作成

プロジェクトを作成しコードを書いていきます。

npm i -g @fly/fly
mkdir swapi-edge-worker
cd swapi-edge-worker
touch package.json

package.jsonは下記のようにしておいてください。

// package.json
{
  "name": "fly-swapi-edge-worker",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "yarn fly server",
    "deploy": "yarn fly deploy",
    "test": "yarn fly test"
  },
  "dependencies": {
    "@types/md5": "^2.1.33",
    "graphql": "^14.5.8",
    "graphql-tag": "^2.10.1",
    "lodash": "^4.17.15",
    "md5": "^2.2.1"
  },
  "devDependencies": {
    "ts-loader": "^3.5.0",
    "typescript": "^3.1.2"
  }
}

また、TypeScriptで書きたいのでTypeScript用の設定もいれます。

touch tsconfig.json
touch webpack.fly.config.js
// tsconfig.json
{
  "include": ["./index.ts"],
  "compilerOptions": {
    "outDir": "./build/",
    "baseUrl": ".",
    "paths": {
      "@fly/cdn": ["./src/"]
    }
  }
}
// webpack.fly.config.js
module.exports = {
  entry: "./index.ts",
  resolve: {
    extensions: [".js", ".ts", ".tsx"]
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        loader: "ts-loader"
      }
    ]
  }
};

entryPointとなるindex.ts も作成します。

touch index.ts

ルーティング設計

ルーティングはindex.ts に実装していきます。 今回は下記のように定義しました。

  • / キャッシュのsetとget、GraphQL serverへのリクエストをおこなう
  • /purge キャッシュのpurgeをおこなう

これでそれぞれのURLに一致したときに指定した関数を呼び出せるようになります。

import { purgeCache } from "./src/purgeCache";
import { cacheQuery } from "./src/cacheQuery";

const mounts = {
  "/purge": purgeCache,
  "/": cacheQuery
};

// routing
const routeMounts = async (req: Request) => {
  const url = new URL(req.url);
  for (const path of Object.getOwnPropertyNames(mounts)) {
    const backend: (req: Request) => Promise<Response> = mounts[path];

    // handle routing
    if (url.pathname === path || url.pathname.startsWith(path + "/")) {
      return await backend(req);
    }
  }

  return new Response("not found", { status: 404 });
};

// @ts-ignore
fly.http.respondWith(routeMounts);

キャッシュのsetとget

/ にリクエストが来た場合の関数を実装します。
やっていることは単純です。

  1. Request Bodyをhash化したものをKey, Queryの結果をValueとしたものをKey-Value Storeに格納
  2. 2回目以降のリクエスト時にKeyが同じだった場合はキャッシュした値を返す、違う場合は1をおこなう
// /src/cacheQuery
import cache from "@fly/v8env/lib/fly/cache";
import { calcCacheKey } from "./calcCacheKey";
import { calcTagName } from "./calcTagName";
import * as _ from "lodash";

const graphqlEndpointURL =
  "https://swapi-graphql.netlify.com/.netlify/functions/index";
const cacheExpireSeconds = 60;

export const cacheQuery = async function(req, init) {
  if (req.method === "POST") {
    const body = await req.json();
    const cacheKey = calcCacheKey(body);
    const cachedValue = await cache.getString(cacheKey);

    // cacheがあれば返す
    if (cachedValue) {
      return new Response(cachedValue, {
        headers: { "content-type": "application/json" }
      });
    }

    const response = await fetch(graphqlEndpointURL, {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify(body)
    });

    // リクエストが成功時にcacheに保存する
    if (response.status < 400) {
      const resBody = await response.json();
      const tagNames = calcTagName(body);
      await cache.set(cacheKey, JSON.stringify(resBody), {
        ttl: cacheExpireSeconds,
        // tags を指定してキャッシュのpurgeをするのでここの命名は重要
        tags: ["swapi", ...tagNames]
      });
      return new Response(JSON.stringify(resBody), response);
    }
  }
  return new Response("not found", { status: 404 });
};

キャッシュのpurge

fly.ioではKey-Value の組み合わせ1つ1つに紐づけてタグの名前を指定可能です。 タグの名前を指定することで、同名のタグがついたすべてのキャッシュのpurgeが可能です。

// purgeCache.ts
// cacheをpurgeする
export const purgeCache = async function(req, init) {
  if (req.method === "POST") {
    const body = await req.json();
    const tagName = body.tagName;
    const isPurged: boolean = await cache.global.purgeTag(tagName);
    return new Response(JSON.stringify(isPurged));
  }
};

タグの名前はgraphql-tagで生成したastから任意の値を作ることで、柔軟に変更することもできます。
今回のはRequest Bodyに含まれるQuery名を指定しました。

※プロジェクトによって最適なタグ名は変わってきそうです。

import gql from "graphql-tag";
import * as _ from "lodash";

export const calcTagName = (parsedBody): string[] => {
  const ast = gql(parsedBody["query"]);
  // タグ生成の条件を変更することで柔軟なcache設計ができる
  const tags = ast.definitions.map(definition => {
    return definition.selectionSet.selections.map(selection => {
      return selection.name.value;
    });
  });

  // Query名の配列を返す
  // {
  //   allFilms {
  //     films {
  //       title
  //       episodeID
  //     }
  //   }
  // }
  // の場合 ["allFilms"]
  return _.flatten(tags);
};

デプロイ

デプロイに関してもcliからコマンド1つで可能なのでスムーズです。

  1. アカウントの作成
    まず fly.ioにアクセスしアカウントを作成する。

  2. ログイン
    fly login コマンドを使いログイン。

  3. アプリケーションの作成
    fly apps:create でアプリを作成します。アプリの名前を聞かれるので適当に命名する。

  4. デプロイ
    あとは fly deploy コマンドを叩くだけでデプロイ完了。

実際にデプロイされたサンプルアプリケーションは下記にホスティングしています。

https://swapi-edge-worker.edgeapp.net/

終わりに

fly.ioを使うことでEdgeで動くGraphQLのキャッシュサーバーの開発ができました。
Queryをどのようにキャッシュするかはサービスごとの特性によって最適解は変わりますが、一部のみに導入するだけでも十分価値はあると思います。

参考

github.com

https://qiita.com/wawoon/items/e597776a10c30e7088a1qiita.com