雑食日誌

Vue.jsとServerless。ときどきチーム開発

Apollo ServerからFireStoreのデータを取得する

概要

この記事は前回の記事の続きになります。

keinumata.hatenablog.com

前回はCloud Functions for Firebaseを使ってApollo Serverを構築しました。今回は、FireStoreのデータをGraphQLから取得するところまでやります。
サンプルは以下のリポジトリにまとめてます。

github.com

データの準備

サンプルデータはインターネットムービーデータベースを使用します。 GraphQL Schemaは以下のように定義しました。

type Movie {
  title: String
  year: Int
  info: MovieInfo
}

type MovieInfo {
  directors: [String]
  release_date: String
  rating: Float
  genres: [String]
  image_url: String
  plot: String
  rank: Int
  running_time_secs: Int
  actors: [String]
}

Movie型がタイトルと公開年と MovieInfo型を持っています。MovieInfoには詳細な映画情報が入力されています。 上記サンプルデータをFireStoreに追加します。とりあえず追加したかったので愚直にFor文を回す実装をしました。

import * as admin from "firebase-admin";
import MovieData from "./moviedata.json";

admin.initializeApp();

const db = admin.firestore();
MovieData.forEach(async movie => {
  await db.collection("movies").add(movie);
});

Firebaseのコンソールからデータを確認できれば完了です。

f:id:keinumata:20200104175849p:plain
FireStoreコンソール

クエリスキーマ

データが準備できたので、SchemaのクエリとFireStreを接続してみます。今回、Schemaのクエリは以下の二つを実装します。

type MovieListPayload {
  movies: [Movie]
  lastKey: String
}

type Query {
  popularMovies: [Movie]
  movieList(lastKey: String): MovieListPayload
}

popularMovies クエリは映画の順位から上位10個のデータを返すクエリです。movieList は映画一覧を10件ずつ返し、次の10件を取得するためのキー( lastKey )を一緒に返します。
それぞれリゾルバーを実装してみます。

映画上位ランキング一覧

ランキング順に並べ替えたいので FireStore の orderByinfo.rank フィールドに対して実行します。その後 limit をかければ上位10位以内の映画一覧を取得できます。
Apollo ServerのリゾルバーはSchemaのフィールド名とTypeScript内のオブジェクトキー名が一致している必要があります。(今回は popularMovies
ゾルバーを実装すると以下になります。

const resolvers = {
  Query: {
    popularMovies: async () => {
      const snapshot = await db
        .collection("movies")
        .orderBy("info.rank")
        .limit(10)
        .get();
      return snapshot.docs.map(doc => {
        return doc.data();
      });
    }
  }
}

デプロイしてクエリを実行すると以下のようになりました。

f:id:keinumata:20200104181712p:plain
映画ランキング一覧クエリ結果

ランキング順に取得できています。(1位が欠損しているのが気になりますが。)

映画一覧

映画一覧はソートするキーを映画公開日にします。一回のクエリでは10件固定で取得でき、簡易的なページネーションを実装して、次の10件を取得できるようにします。 (ここでは任意のページや任意の映画数をクライアントから指定できる実装はしません。)

クエリ引数として lastKey を受け取れますが、nullableのためFireStoreへのクエリに条件分岐を追加します。
オブジェクトへの変換処理は映画上位ランキング一覧と同じです。

movieList: async (_: any, args: MovieListInput) => {
      let snapshot;
      const query = db
        .collection("movies")
        .orderBy("info.release_date", "desc");
      if (args.lastKey) {
        snapshot = await query
          .startAfter(args.lastKey)
          .limit(10)
          .get();
      } else {
        snapshot = await query.limit(10).get();
      }
      const movies = snapshot.docs.map(doc => {
        return doc.data();
      });
}

レスポンスにも lastKey を含める必要があります。レスポンスの場合は取得した映画一覧が空かどうかで lastKey をnullにするかを決めます。

      let lastKey: string | null = null;
      if (movies.length) {
        lastKey = movies[movies.length - 1].info.release_date;
      }
      return {
        movies,
        lastKey
      };

デプロイして動作確認してみます。クエリ引数を空にして実行すると以下のようになりました。

f:id:keinumata:20200106224457p:plain
映画一覧クエリ実行結果(引数なし)

映画一覧を公開順に取得できています。取得できた lastKey を基に再実行してみます。

f:id:keinumata:20200106224635p:plain
映画一覧クエリ実行結果(引数あり)

1回目とは異なるデータを取得できました。このままだと映画の公開日が一緒の場合、データが欠損してしまうので今後修正していきたいと思います。

まとめ

  • Apollo Serverを使ってFireStoreのデータを取得した
  • ゾルバーはバックエンドとの接続、データ取得を実装できる

Cloud Functions for Firebaseを使ってApollo Serverを構築する

はじめに

GraphQLのバックエンド実装は以下の方法があります。

  • 各言語に用意してあるライブラリ
  • Apollo Server
  • AppSync

Apollo ServerはNode.jsによるGraphQLサーバーを構築するOSSです。 クライアントだけではなくバックエンドも複数のデータソースから選択して実装することができます。 今回はCloud Functons for Firebaseを使ってApollo Serverを実装してみました。

Firebase初期設定

FirebaseのプロジェクトにCloud Functionsを追加します。言語はTypeScriptを選択しました。

$ firebase init functionsd
=== Functions Setup

A functions directory will be created in your project with a Node.js
package pre-configured. Functions can be deployed with firebase deploy.

? What language would you like to use to write Cloud Functions? TypeScript
? Do you want to use TSLint to catch probable bugs and enforce style? No
✔  Wrote functions/package.json
✔  Wrote functions/tsconfig.json
✔  Wrote functions/src/index.ts
✔  Wrote functions/.gitignore
? Do you want to install dependencies with npm now? Yes

TypeScript初期設定

TypeScriptの設定を行います。Cloud Functions for Firebaseを使用するため、 functions/ ディレクトリ内で作業します。

$ cd functins/
$ npm i -D @types/graphql @types/node typescript

tsconfig.json は以下のように編集しました。

  • JavaScriptファイルを build ディレクトリに出力
  • Node.js のCommonJS方式を使用するため、 "module": "commonjs" に変更
{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "strict": true,
    "importHelpers": true,
    "moduleResolution": "node",
    "sourceMap": true,
    "resolveJsonModule": true,
    "baseUrl": ".",
    "types": [
      "node"
    ],
    "paths": {
      "@/*": [
        "src/*"
      ]
    },
    "outDir": "build"
  },
  "include": [
    "src"
  ],
  "exclude": [
    "node_modules"
  ]
}

Apollo Server初期設定

Cloud Functions向けのプラグインである apollo-server-cloud-functions をインストールします。

$ npm i apollo-server-cloud-functions graphql
$ npm i -D @types/graphql

サンプルとして src/index.tsApollo Serverのサンプルと同じスキーマを実装します。プラグインGCPのCloud Functionsのため、インスタンス化した ApolloServerfunctions.https.onRequest 関数の引数に代入しています。

import { ApolloServer, gql } from "apollo-server-cloud-functions";
import * as functions from 'firebase-functions';

const typeDefs = gql`
  # Comments in GraphQL strings (such as this one) start with the hash (#) symbol.

  type Book {
    title: String
    author: String
  }

  type Query {
    books: [Book]
  }
`;

const books = [
    {
        title: 'Harry Potter and the Chamber of Secrets',
        author: 'J.K. Rowling',
    },
    {
        title: 'Jurassic Park',
        author: 'Michael Crichton',
    },
];

const resolvers = {
    Query: {
        books: () => books,
    },
};

const server = new ApolloServer({
    typeDefs,
    resolvers,
    context: ({ req, res }) => ({
        headers: req.headers,
        req,
        res
    })
});

exports.graphql = functions.https.onRequest(server.createHandler());

ローカルで動かす準備ができたので、Firebase emulatorsを起動してみます。TypeScriptがトランスパイルされていないので、 functions/ 配下でNPMコマンドを実行してます。

$ npm run serve
✔  functions: Emulator started at http://localhost:5000
i  functions: Watching "/Users/numatakeisuke/working/poc/apollo-google-cloud-functions/functions" for Cloud Functions...
✔  functions[graphql]: http function initialized (http://localhost:5000/apollo-server-sample/us-central1/graphql).

表示されたURL(http://localhost:5000/apollo-server-sample/us-central1/graphql)をブラウザで開くとGraphQLクライアントが表示されます。 Booksからタイトルを取得すると画像のようになりました。

f:id:keinumata:20200101221227p:plain
GraphQL実行画面

まとめ

Cloud Functions for Firebaseをバックエンドに選択してApollo Serverを実装してみました。 Cloud Functions のデプロイが簡単なので、予想しいていたより早く実装できました。

今後はFireStoreのデータを返す仕組みを作ってみます。

参考

JavaScriptでURLエンコード

URLクエリ内に別URLをもたせたい時にURLをエンコードする方法についてまとめます。 今回はJavaScriptで実装しました。

encodeURI

はじめ、JavaScriptのencodeURIを試してみました。 本ブログのURLを引数に実行してみます。

> encodeURI('https://keinumata.hatenablog.com/')
'https://keinumata.hatenablog.com/'

結果は引数と何も変わらないです。これはencodeURI関数がURLを構成する予約語(;,/?:@&=+$)およびエンコードせず使用できる非予約語(-_.!~*'())はエンコードしないためです。 試しに空白を入れて実行してみます。

> encodeURI('https://keinumata.hatenablog.com/? a')
'https://keinumata.hatenablog.com/?%20a'

日本語もエンコードされます。

> encodeURI('https://keinumata.hatenablog.com/ジャヴァスクリプト')
'https://keinumata.hatenablog.com/%E3%82%B8%E3%83%A3%E3%83%B4%E3%82%A1%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88'

今回はURLのクエリに使用するため、予約語エンコードしたいので他の関数を探してみます。

encodeURIComponent

encodeURIComponentはencodeURIと比較してURLを構成する予約語(;,/?:@&=+$)もエンコードします。エンコードせず使用できる非予約語(-_.!~*'())はエンコードしません。 試しにencodeURIで試したURLを実行してみます。

> encodeURIComponent('https://keinumata.hatenablog.com/')
'https%3A%2F%2Fkeinumata.hatenablog.com%2F'

スラッシュやコロンもエンコードできました。

結論

  • URLクエリにURLを使用する場合はencodeURI ではなく encodeURIComponent を使う

参考

developer.mozilla.org

Vue Composition APIとTypeScriptの組み合わせ

この記事はギルドワークスAdvent Calendarの3日目の記事です。 Vue.jsのバージョン3にてリリース予定のComposition APIとTypeScriptを組み合わせについて紹介します。

adventar.org

Composition APIとは

Composition APIは関数ベースでコンポーネントを実装する機能です。関数ベースのため、機能の再利用性を高める役割を持っています。他のライブラリではReact hooksやSvelteに近い機能です。

公式のRFCに記載されているモチベーションの1つにTypeScriptとの相性を高めることが書かれています。本記事ではサンプルコードを踏まえてなぜTypeScriptと相性がよくなるのか考えてみます。

https://vue-composition-api-rfc.netlify.com/#better-type-inference

準備

Vue2系で試す場合はプラグインが提供されているので、インストールする。

https://github.com/vuejs/composition-api

$ npm install @vue/composition-api --save
or
$ yarn add @vue/composition-api

インストールが完了したらVue.use を追加します。

import Vue from 'vue';
import VueCompositionApi from '@vue/composition-api';

Vue.use(VueCompositionApi);

TypeScriptと併せて使用する、かつVS CodeでVeturを使用している場合、設定を以下のように変更する必要があります。

{
  "vetur.useWorkspaceDependencies": true
}

Options APIとComposition APIの比較

TypeScriptとの併用がやりやすくなるかを見るために、基本的なVue.jsのAPIであるOptions APIと比較してみたいと思います。 今回は両方のAPIを使って同じ機能を持ったモーダルコンポーネントを実装してみます。 機能としては以下になります。

  • モーダルの表示・非表示ができる
  • モーダル内に日付を表示し、ボタンによってカウントアップできる
  • 日付が条件に達したらメッセージを表示

f:id:keinumata:20191202225611p:plain
メッセージ無

f:id:keinumata:20191202225508p:plain
メッセージ有

以下のGitHubにても公開しています。

https://github.com/keinuma/compare-options-and-composition

テンプレート

テンプレートおよびスタイルは共通のコードを使用します。(スタイルのコードは今回の記事では紹介しません。)

<template>
  <div>
    <div :class="{ 'modal-overlay': showModal }">
      <div
        v-if="showModal"
        class="modal-isshow modal-open"
      >
        <div class="modal-container">
          <button class="close-btn btn btn-outline-info" @click="closeModal">
            閉じる
          </button>
          <slot></slot>
          <div>
            <div v-show="isLastDate" class="alert alert-danger" role="alert">
              <p class="alert-last-date">
                クリスマスになりました。<br />
                アドベントカレンダーの最終日です。
              </p>
            </div>
            <p>この記事はギルドワークスアドベントカレンダーの{{ date }}日目の記事です。</p>
            <button
              type="button"
              class="btn btn-info"
              @click="countUpDate" 
              :disabled="isLastDate">
              次の日
            </button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

Options API

Options APIの場合は以下になります。 TypeScript対応は Vue.extend を使用しています。

import Vue from 'vue'

export default Vue.extend({
  name: 'ModalOptions',
  props: {
    showModal: {
      type: Boolean,
      required: true,
      default: false
    }
  },
  data() {
    return {
      date: 1
    }
  },
  computed: {
    isLastDate() {
      return this.date === 25
    }
  },
  methods: {
    countUpDate() {
      if (this.isLastDate) {
        return
      }
      this.date++
    },
    closeModal(e: Event) {
      e.stopPropagation()
      this.date = 1
      this.$emit("onCloseModal")
    }
  }
})

Composition API

Composition APIの場合は以下になります。 基本的に setup 関数の中でデータやメソッドの定義を行います。 リアクティブなデータはComposition APIで定義されている refreactive 関数から定義できます。 モーダルを閉じるメソッドで使用しているように、Vue インスタンスへは context を使用してアクセスできます。

import {
  createComponent,
  ref,
  computed
} from '@vue/composition-api'

type Props = {
  showModal: boolean
}

export default createComponent({
  props: {
    showModal: {
      type: Boolean,
      required: true,
      default: false
    }
  },
  setup(props: Props, context) {
    const date = ref(1)
    const isLastDate = computed(() => {
      return date.value === 25
    })
    const countUpDate = () => {
      if (isLastDate.value) {
        return
      }
      date.value++
    }
    const closeModal = (e: Event) => {
      e.stopPropagation()
      date.value = 1
      context.emit("onCloseModal")
    }
    return {
      showModal: props.showModal,
      date,
      isLastDate,
      countUpDate,
      closeModal
    }
  }
})

考察

2つのコードを比較すると、Composition APIはOptions APIに比べて this がなくなっています。 これまで Vuethis にはデータやメソッドのような開発者が定義したアプリケーション的意味合いと、filter やVue RouterなどのVueインスタンス自身の2つの意味を持っていました。Composition APIの場合、前者を setup 関数内で定義、後者を context に持たせることで this の責務を分離しています。 責務を分離することで型定義を推論を効かせしやすくしていると考えられます。

また、 Options APImethods でも同じことはできるのですが、相互参照が見えやすい setup 関数だからこそ機能としての再利用性を意識しやすくなります。そのため、外部モジュールへの抽出や構造化が促進されると考えられます。

まとめ

  • Composition APIによってTypeScriptが使いやすくなる
  • Vue 3待ち遠しい

DevLOVE X参加記録

先日DevLOVE Xというイベントに参加してきた。とても豪華なスピーカー陣が5トラック並行にセッションするため、何を聞きにいくか一番悩んだイベントだったと思う。
色々な分野の有力者の方々から深い話をたくさん聞けて面白く、多くの学びがあった。

devlove.wixsite.com

特に印象に残った講演をふりかえっていく。(以下敬称略

エンジニア、エンジニアリングマネージャーとして成長するために必要なこととは? by 安西 剛

speakerdeck.com

今回のイベントで一番考えさせられたセンションだった。 最近の開発ではチームに着眼したふりかえりを実施するものの、個人に焦点を当てたふりかえりはなかなか広まっていない。 しかし、エンジニア個人の成長を考えると一人のふりかえりがより重要ではないかというお話だった。 私自身一人のふりかえりを実施していたが、以下の新しい観点を得られた。

  • KPT/YWTに加えて感情を言語化する
  • ふりかえりした結果を共有する

私は自分の嫌な一面やみたくないところと向き合いたくないから感情を言語化することを避けてきていたように感じる。 セッションを聞いて、感情と向き合うことで自分のモチベーションを知ることができ、成長につながることがわかった。 自分のことは十分知っているだろうという認知バイアスにかかっていたことを自覚させられた瞬間だった。

個人のふりかえりを共有することは気恥ずかしさがあり一人ではやるという発想にすらいたっていなかった。 しかし、自分が見えない観点をフィードバックしてもらえることは重要だと気付かされた。

ふりかえりの結果、学んだことは習慣化しないと意味がなくなる。 無意識におこなわれる習慣にうまく組み込めるようにルーチンを作っていきたい。

それはYAGNIか? それとも思考停止か? by 川島 義隆

www.slideshare.net

最近私はエンジニアとしての働き方、役割が変わり、言われた物を作るよりも価値のあるものを作ることにコミットするようになってきている。
そんな中で YAGNI を間違って認識し、作り始めるスピードを正としてきはじめた自分にはいい教訓になった。
考えることを安易に止めず、本当はもっといい設計があるのではないか、考えることに時間を使う方が投資対効果が高いこともある。 なので、どこまでが本当にユーザーに問わないとわからない領域でどこまでは思考することで最善を尽くせるのかバランスを意識しないといけないと解釈した。

具体的な設計では状態や区分を実装するときにインターフェースを切る考えを学んだ。 ホットスポットを見つけるツールも使っていきたい。

自分に向いている楽しめる仕事をするための目標設定 -失敗のストーリーに分析と対策を添えて- by 川鯉 光起

speakerdeck.com

過去の経験から自分を知り、学びを抽象化する具体的な方法を知ることができた。 エンジニアの自分にとっては「エンジニアリングが好き」で考えを止めず、さらに目的を深掘りしていくことが学びになった。 自分なりに解釈すると、以下のようなる。

  • 経験してきた中で印象に残るストーリーを切り出す
  • ストーリーから事実(仕事)と感情を洗い出す
  • 並べた事実と感情がなぜ起きた/感じたかを問い直す
  • 問いに答えてさらに問い直すことを重ねて学びに抽象化する

これらから自分は作ることよりも価値を届けること、使ってもらえることにモチベーションを感じることが改めてわかった。 他にも紹介されていたキャリアアンカーストレングスファインダーをやってみたいと思う。

アウトカムを出す! 楽しむ! 両方やるために経験を味方にする! by 高橋 陽太郎

poohsunny.hatenablog.com

アウトカムは重要であると同時にエンジニアの楽しさにも経験を通して繋がっていることを学んだ。

セッションを聞くまでプロダクト作りは成果にコミットできいないと意味がないけれども、全員のエンジニアの楽しさにつながるわけではないのではないかと思っていた。 エンジニアは技術領域と何を作るかを決める領域のそれぞれ比重を持っていたからだ。 センションを聞いたおかげで、足りないのは視座を高めたり詳細まで降りたりすることを共有することだと学んだ。

コーディングという観点だけを見ると限りなく綺麗にしていきたいし、最適化したくなる。 ただ、プロダクトとして何を目指していて、現在どういう状況なのかを共有し、理解することでチーム全員が同じ方向を向けるのではないかと思った。 なので、エンジニアに限らずチームメンバーがどの分野に興味があり、どこにギャップがあるか観察して思いを共有していきたい。

まとめ

今回のイベントからは一貫して学びを得ることの重要性を知れた。 学びは自分だけでなく、チームやプロダクトにも当てはめることができる。 これからは積極的に学びを得るためのフィードバックサイクルを早くしていきたい。

最後に、ここまで濃ゆい時間はもう過ごせないと感じるほど素敵な二日間を提供してくださった運営の方々に圧倒的感謝…

使いやすいプロダクトを目指して

使いにくいプロダクト

ある案件で開発したプロダクトがユーザーにとって使いにくいと評価されることがあった。
プロダクトの受け入れ条件を満たし、チームの中で認められたプロダクトであってもユーザーにとって価値が高いわけではない。
頭でわかっていたつもりであってもはじめて経験したときに少なからず戸惑いがあった。
どうすればユーザーにとって使いやすいプロダクトを開発できるのか試行錯誤している考えをまとめてみる。

エンジニアとしての開発

多くの開発では素早く正確に実装することが求められている。 また、何を作れば正解かわからないプロダクト開発では変更容易性も兼ね備えなければならない。
そのため私は以下のことを考えながらプロダクトを開発していた。

  • 設計指針から外れていないか
  • 責任を明確に分離できているか
  • ミスをしていないか
  • 可読性の高いコードになっているか

この時点で意識しないといけないことは多い…
ふりかえるとエンジニアとして開発するときはミクロな目線で糸を縫うような作業をしているようだなと感じた。
このうえでユーザビリティを高めるためにはユーザー目線を得る必要があると思った。

ユーザー目線の獲得

ユーザーの視点を借りるためにはどうすれば良いか。一番はユーザーにプロダクトを体験してもらうことだと思う。
体験してもらっている様子を観察して、問いかけたり、プロダクトの感想を伝えてもらって方針を変えたり機能を追加していく。

しかし、ターゲットユーザーに実際にプロダクトを提供するためには、多くの人を巻き込みプロダクトを一定レベルまで完成しないといけないためコストが大きい。 プロダクトの方針一つ一つをユーザーに問いかける訳にはいかないので、一定レベルまでエンジニアがプロダクトを形作っていかなければならない。
一定レベルまで仕上げるためにはエンジニアがユーザー目線を持たないといけない。 エンジニアがユーザー目線を得るためにはユーザーに共感する必要がでてくる。 共感するためには、バックグラウンドや感じる課題、プロダクトを使用するときに気にかけることなどを知らないといけない。

これらからユーザー目線の獲得は一朝一夕でなくユーザーと対話を重ねることで作り手側にユーザー目線を宿していく深い経験ではないだろうか。
理想の姿とは感じつつも常にエンジニアがユーザーと対話できる状況ではない。対話なしでもエンジニア個人の基準でプロダクトを作り続けられるようになりたい。

違和感を感じる

私が考えたユーザー目線に近づくためのアプローチはプロダクトの違和感を拾うことだった。
違和感はプロダクトを初めて見たときに感じやすい。そのため開発を続けているとプロダクトの当たり前が自分の中に浸透してしまう。 今まで開発してきたプロダクトではリストの見せ方やボタン配置・配色、ページ遷移時のインタラクションなどを当たり前にしてしまっていたように思う。

この当たり前がチームの中に浸透してしまうと、ユーザーに見せるまで誰も疑問に思うことなくプロダクトを改善する機会を失ってしまうことになる。 なのでプロダクトを改善するためには意識的にユーザーになりきって違和感を感じることが大事なのではないかと考えた。

目線を意識的に切り替える

違和感を感じるためにユーザーになりきったまま細やかな開発を遂行するのは難易度が高い。私自身ミクロな目線とマクロな目線を同時に持ち合わせられるほど器用になれない。
なので、まずは実装に集中するとき(開発モード)とユーザーになりきるとき(ユーザーモード)の二つを意識している。

ユーザーとしてプロダクトを触って違和感を感じたらプロダクトバックログに追加する。追加したプロダクトバックログアイテムを元に使いやすさを求めて開発を進めていく。 目線切り替えを繰り返していくと個人の中でプロダクトの改善サイクルが回り始める。

改善を続けていても一人では限界がある。この限界は開発チーム全員が互いに目線を切り替えて、議論を重ねることで超えられるのではないだろうか。 壁を超え続けたプロダクトはきっとユーザーにとっての価値に近づくと信じている。

AppSyncのデプロイについてまとめてみた

はじめに

ある案件でAppSyncを使うことになったが、商用利用の事例が少なくデプロイ方法がわからなかった。
自動でデプロイできる仕組みを探したところ、Serverless FrameworkのAppSyncプラグインがあったので使ってみた。

AppSyncとは

AppSyncはAWSで利用できるGraphQLベースのマネージドサービスである。

特徴として

  • バックエンドサービスを自由に切り替えることができる
  • GraphQLベースのため、フロントエンドに必要なデータを定義できる

などがある。
特にバックエンドにはLambdaやDynamoDB、HTTPなど多様なリソースを選択できる。

前提

今回の案件では以下の条件があった。

  • AppSyncをLambdaバックエンドとHTTPバックエンドの二種類を実装
  • フロントエンドはアプリ(iOS, Android) とWebアプリ(JavaScript)
  • 環境は開発環境・検証環境・本番環境の3つ
  • CloudFormation経験は薄く、Sceptreを使ってECS(Fargate)を作成した経験のみ

デプロイ方法の選定

現状AppSyncを商用利用して、デプロイするためには以下の手段が考えられた。

  • CloudFormationを書く
  • Amplify CLIを使う

CloudFormationを使ってデプロイするためにはYaml地獄と戦い、トライアンドエラーを繰り返す必要がある。 時間的猶予がなかったため却下した。

Amplify CLIはコマンド一つでAWSリソースを作ることができる強者である。 しかし、以下の懸念点があった。

  • 開発当初は env 機能がなかったため、環境を複数設定できない
  • 複数のフロントエンドで共有することが難しい

そのため、リソースの作成にAmplify CLIを使用しなかった。

他に良いデプロイ方法はないか探していたら、以下の記事に遭遇し、ServerlessFrameworkのAppSyncプラグインを知った。
read.acloud.guru

Serverless Frameworkを使う予定があったので、試してみたら思いのほか手軽だった。
ここからは実際にデプロイした手順を記録していく。

Serverless FrameworkでAppSyncデプロイ

今回はAppSyncのバックエンドにLambdaを使用する。 GraphQL SchemaにはブログWebアプリケーションを想定に実装する。

サンプルはGitHubにもあげているので併せてみていただきたい。

github.com

CLIインストール

NPMでインストールする。

$ npm install -g serverless

プロジェクト初期化

プロジェクトを初期化する。
このタイミングでは package.json は作成されていない。

$ serverless create --template aws-nodejs --path appsync-deploy
$ cd appsync-deploy
$ ls -a
.gitignore     handler.js     serverless.yml

create コマンドを実行するには template オプションが必要である。
template の種類は以下に詳細が記載されている。

serverless.com

成果物のうち serverless.yml はCloudFormationのもとになるYaml定義。
handler.jsAWS Lambdaを使用する場合の実行ファイルのため、AppSyncのデプロイだけでは不要である。

プラグインのインストール

ServerlessFrameworkのプラグインはNPMパッケージでまとめられているため、 package.jsonプラグインを管理していく。
Node環境がまだないので、改めてNPM初期化を実施する

$ git init
$ npm init 
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (appsync-deploy)
version: (1.0.0)
description:
entry point: (handler.js)
test command:
git repository:
keywords:
author:
license: (ISC)
About to write to appsync-deploy/package.json:

{
  "name": "appsync-deploy",
  "version": "1.0.0",
  "description": "",
  "main": "handler.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

AppSyncプラグインをインストールする。

$ npm i serverless serverless-appsync-plugin

デプロイコマンドをNPMの scripts に定義する。

"scripts": {
  "deploy": "serverless deploy -v"
},

これでデプロイを実行する準備が整った。 serverless.yml を整えながら、AppSyncの設定を加えていく。

Serverless Framework共通設定

はじめに基本設定を記述していく。
serverless.yml を開くと例が記載されているが、今回必要な部分は少ないので消す。

service: appsync-deploy

provider:
  name: aws
  region: ap-northeast-1

plugins:
  - serverless-appsync-plugin

service はCloudFormationのStack Nameに使用される部分であり、 デプロイ環境の総称となる。今回はプロジェクトのディレクトリ名を使用する。

provider はCloud Providers(AWS)およびリージョンの設定をする。
plugins にはインストールしたAppSyncのプラグインを記述する。

AppSyncの基本設定

このサンプルではAppSyncの認証に Amazon Cognitoを選択する。
Schemaは簡易的にするために QueryMutation を一つずつにした。
Schemaの詳細を以下に記載する。

schema {
    query: Query
    mutation: Mutation
}

type Query {
    listArticle: ArticlesResponse
}

type Mutation {
    createArticle(title: String, content: String): ArticlesResponse
}

type Article {
    id: Int
    title: String
    content: String
}

type ArticlesResponse {
    errorCode: String
    articles: [Article]
}

Schemaと認証設定を serverless.yml に追記する。

custom:
  accountId: ${env:AWS_ACCOUNT_ID}
  appSync:
    name: BlogApp # AppSyncのAPI名
    authenticationType: AMAZON_COGNITO_USER_POOLS # API Keyなど選択
    userPoolConfig:
      # Cognitoの設定
      awsRegion: ap-northeast-1
      defaultAction: ALLOW
      userPoolId: ap-northeast-1_XXXXX
    schema: schema.graphql

Data Sources

バックエンドにはLambdaを使用するため、二つのARNが必要になる。

  • LambdaそのもののARN
  • AppSyncがLambdaを実行するRoleのARN

Roleはデフォルトの設定で作成されないため、事前にコンソールから作るか、 Serverless Frameworkの resources 機能で作る。

DataSourcesを serverless.yml に反映すると以下になる。

custom:
  accountId: ${env:AWS_ACCOUNT_ID}
  appSync:
    name: BlogApp # AppSyncのAPI名
    authenticationType: AMAZON_COGNITO_USER_POOLS # API Keyなど選択
    userPoolConfig:
      # Cognitoの設定
      awsRegion: ap-northeast-1
      defaultAction: ALLOW
      userPoolId: ap-northeast-1_XXXXX
    schema: schema.graphql
    dataSources:
      - type: AWS_LAMBDA
        name: BlogAppResolver
        config:
          functionName: blog-app-resolver
          lambdaFunctionArn: "arn:aws:lambda:ap-northeast-1:${env:AWS_ACCOUNT_ID}:function:blog-app-resolver"
          serviceRoleArn: "arn:aws:iam::${env:AWS_ACCOUNT_ID}:role/service-role/appsync-ds-lam-xxxxxxxxxxxxx

Mapping Templates

最後にMapping Templatesを整える。
Mapping Templatesのリクエストとレスポンスそれぞれをファイル化し、 mapping-templates ディレクトリに格納することでAppSyncに反映する仕組みになっている。

レスポンスのMapping Templatesは共通化させて設定した。

レスポンス
common-response.vtl

$util.toJson($context.result)

Queryのリクエス
query-list-blog-request.vtl

{
  "version" : "2017-02-28",
  "operation": "Invoke",
  "payload": {
    "path": "list_blog",
    "data": $util.toJson($context.args)
  }
}

Mutationのリクエス
mutation-create-blog-request.vtl

{
  "version" : "2017-02-28",
  "operation": "Invoke",
  "payload": {
    "path": "create_blog",
    "data": $util.toJson($context.args)
  }
}

これらを mapping-templates ディレクトリ以下に格納する。ディレクトリ名は変更することもできる。

Mapping Templatesの設定を serverless.yml に反映すると以下のようになる。

custom:
  accountId: ${env:AWS_ACCOUNT_ID}
  appSync:
    name: BlogApp # AppSyncのAPI名
    authenticationType: AMAZON_COGNITO_USER_POOLS # API Keyなど選択
    userPoolConfig:
      # Cognitoの設定
      awsRegion: ap-northeast-1
      defaultAction: ALLOW
      userPoolId: ap-northeast-1_H8OHxpYAa
    schema: schema.graphql
    dataSources:
      - type: AWS_LAMBDA
        name: BlogAppResolber
        config:
          functionName: blog-app-resolver
          lambdaFunctionArn: "arn:aws:lambda:ap-northeast-1:${env:AWS_ACCOUNT_ID}:function:blog-app-resolver"
          serviceRoleArn: "arn:aws:iam::${env:AWS_ACCOUNT_ID}:role/service-role/appsync-ds-lam-ln4jdz-blog-app-resolver"
    mappingTemplates:
      - dataSources: BlogAppResolber
        type: Query # Query, Mutation, Subscription
        field: listArticle # Schema内のフィールド名
        request: "query-list-blog-request.vtl"
        response: "common-response.vtl"
      - dataSources: BlogAppResolber
        type: Mutation
        field: createArticle
        request: "mutation-create-blog-request.vtl"
        response: "common-response.vtl"

デプロイ実行

package.json に登録したコマンドでデプロイを実行する。 サンプルはCognitoやRole ARNを仮で記載しているので、編集してから実行する。

$ npm run deploy

エラーが出る場合は環境変数SLS_DEBUG を追加することで、実行の詳細を確認することができる。

$ export SLS_DEBUG=* 

Serverless Frameworkは最初に、 serverless.yml ファイルをコンパイルして、CloudFormationに変換し、S3バケットに格納する。
成果物は .serverless ディレクトリにも保存される。

S3に格納されたら、CloudFormationが実行され、定義したAWSリソースが生成される。

まとめ

  • AppSyncの情報は少ない
  • デプロイはServerless Frameworkが便利

参考

github.com