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

qiita.com