迷ったらカレー

技術系の雑多なブログ

新卒入社したリクルートを退職しました

2020年2月末日付けで、新卒で入社したリクルートを退職しました。2016年の4月に入社したのでほぼ4年間いたことになります。遅ればせながら、退職エントリというやつを初めて書いております。在籍中は本当に多くの方々にお世話になりました。まずはじめに4年の間に関わり一緒に仕事をさせていただいた方々に改めてお礼を申し上げます。ありがとうございました。

さて、このエントリを書くにあたって、エモ100%のポエムにするというのも考えたのですが、そういうキャラでもないので真面目に書くことにします。 書いているうちに熱が入って結構エモくなってしまいました。

内容はオーソドックスに

  • なぜリクルートに入ったか
  • どんな仕事をしていたか
  • 4年間で変わったこと
  • これから何するの

という感じです。

なぜリクルートに入ったか

リクルートとの初めての接点は、大学院M1の冬にふらっと参加したインターンでした。 SFCという少し変わった環境で学部の頃から好きなことしかしてこなかったので、そろそろ外の世界を見ておかないとマズイのでは...という理由のない危機感に背中を押され、『インターン、エンジニア、報酬あり』というなんとも雑な条件でGoogle先生に尋ねて、一番上に出てきたのがリクルートインターン募集サイトでした。僕はTwitter耳年増だったので「学生エンジニアは搾取される立場にある」という認識が当時からあり、少なくとも報酬を出す会社というのがインターン先を探す一つの指標になっていました。いま思い返しても、正しい指標だったと思います。労働には対価を。当たり前のことです。学生の身であるからこそ、当たり前のことを当たり前にできる会社で働きたかったのです。

いざ参加してみると、思った以上にキャラが濃い人たちが同期に揃っていて、すごく楽しかったのを覚えています。いい意味で鼻っ柱をおられたというか、自分がいかにエンジニアとしてまだまだ学ぶことが多いかということを自覚させられました。その後、たまたまリクルートテクノロジーズでOJT的に学生バイトをやらせてもらうことになり、2ヶ月ほど機械学習とか統計とかを仕込んでもらったりしました。この時点でだいぶリクルートのことが好きになっていて、思い浮かぶ進路はD進かリクルートに就職か、という状態になっていました。

そうこうしているうちに縁あって新卒の選考も通り、最後まで迷ったものの、未来が予想できない方ということでリクルートへの就職を選びました。SFCでの教育の賜物なのですが、考え抜いた末に迷ったら、どうなるか予想できない方を選ぶと決めています。カオスに飛び込む、というやつです。たぶんその方が面白くなるので。

どんな仕事をしていたか

入社してすぐは、仕事がありませんでした。たしかにカオスを望んだのは僕ですが、これはさすがに予想を上回っていました。アサインされるはずだった開発プロジェクトが吹き飛んだ結果、入社早々社内ニート確定という珍事。新卒に向かって「仕事ないんだけど、何やりたい?」は相当ロックだったと思います。とりあえず大笑いして、入社してよかったと思いました。

もともとタスクを割り振られて何かするよりは、自分で課題を見つけて提案する方が好きだったので、これ幸いと自分でできそうなことを探し始めました。僕が配属されていたのは、アルバイトや中途などを対象とした求人サービスを扱う部門でした。サービスのシステムは、かなりレガシーかつ大規模だったので、おいそれと新卒の若造がコアな部分に触れるようなものではありませんでした。

それならば、比較的ライトに試せるWebのフロントエンドで、まだやっていないことをやれないかと模索しはじめ、ユーザの行動データを使ってUXを改善する試みなどを起案したりしました。起案するときにプロトタイプをさくっと作って見せていたのですが、その甲斐あってか当時の上司が面白がってくれて、実際のサービスで試してみることになりました。そのうちに一定の効果がありそうな施策があったりして、既存の開発チームとは別に遊撃部隊として座組みを作ってもらうことになりました。

また一方で、2年目の半ばくらいからは、社内の強いエンジニアの先輩のもとでバックエンドの開発案件にもアサインしてもらいちゃんとしたシステム の作り方をみっちり仕込んでもらいました。今思い返してみても、かなり恵まれた環境で仕事をしていたと思います。好き勝手させてもらう一方で、基礎となる考え方や経験を仕込んでもらっていたのですから、至れり尽くせりとはこのことです。

そのうちに「Web周りの改善やってる人」みたいな認識をもらいはじめ、レガシーなWebフロントエンドの表示速度改善をする案件もやりました。これはCODEZINEさんで記事にしていただきました。

codezine.jp

いろいろな開発案件に取り組んでいるうちに興味がうつっていき、組織としてパフォーマンスを出すために、どのような技術的アプローチができるかということを考えるようになっていました。制約条件が多い中での開発において、最もネックになっているのが組織構造や組織間の力学の違いであるように見えたのです。優秀な人が多いのに、それほどパフォーマンスがでないのはなぜか。その原因を取り除けば、もっとプロダクト開発を加速させられるのではないか。そんなことを思って、データサイエンティストとエンジニアが連携してプロダクト開発できるように、機械学習案件用のAPI基盤を作って検証させてもらったりもしました(障害起こしてすみませんでした)。

最後の一年弱は、運良く社内新規事業案件にアサインしてもらって、社内選りすぐりのプロフェッショナルたちと一緒に仕事していました。これほどに技術レベルが高い人たちばかりに囲まれて仕事するのは初めてだったので楽しくて仕方がありませんでした。また、それまで既存事業の改善一筋で、新規事業として新しい価値を模索する経験はしていなかったので目新しいというのもありました。ただ、一方で違和感も感じていました。まず新規事業とはいえ、社内のプロジェクトなので、大企業の力学に囚われる、ということについてです。既存事業に比べればしがらみは少ないものの、やはり意思決定の自由度とスピードがネックに見えました。また、当然のことながら事業としての新規性や価値が企業の既存アセットとシナジーを生まなければなりません。これは、既存アセットのリソースを優先的に利用できるという利点と背中合わせではありますが、そもそも既存アセットの有用性に強く依存するリスクがあること、また事業としてのポジショニングが難しいということは間違いありませんでした。そして、もう一つの違和感はおそらく温度感だったと思います。メンバーは間違いなく優秀でした。ビジネスサイドも、開発サイドも、いい人が揃っていたと思います。ただその一方で、熱量は足りていなかったように思います。それは僕もです。良くも悪くも社内新規事業なので、ポシャったとして、明日飯が食えないかもしれない、とはならないわけです。そのどこか他人事のような安心感が、少なからずあったように思います。僕のリクルートでの最後の仕事は、とある新規事業を看取るおくりびとでした。

4年間で変わったこと

この4年間で変わったのは、一言でいえば「良いシステム」というものの捉え方です。

学生時代はひたすら研究をしていたので、この頃はシステムは新たなアイデアを実装するための道具に過ぎませんでした。良いシステムとは、必要十分にアイデアを表現し動いているもののことでした。

社会人になって、システムの生む価値の方向性を意識しはじめ、誰のためのシステムか、ということを考えるようになりました。具体的には、技術的なアプローチで、実際のユーザにより良い体験を届けたいということ、またビジネスに貢献したいということを考えはじめました。良いシステムとは、期待した価値を誰かに届ける手段でした。

そして、組織のパフォーマンスなどに目が向きはじめたあたりからは、人間の組織とそれを活かすための仕組みという意味で、システムのことを考えるようになりました。結局のところ、システムは人間と共存できなければいけません。良いシステムとは、という問いに対する今の僕の答えは「人間の営みに即した仕組みのこと」です。人の行動が変われば、システムもまた変わる必要があります。それはビジネスフェーズであったり、プロダクトの性質であったり、中の人のスキルレベルであったり、はたまた政治的な理由によるかもしれません。とはいえ、人の行動には目的があります。正しい目的を設定することができれば、自ずとそれを達成するための行動は決まってきます。良いシステムは、その行動を促し、助け、ときに戒める仕組みです。また、時間軸上のライフサイクルを意識するならば、徐々に移り変わる目的とそれに付随する行動の変化への対応も必要です。

この4年間で「良いシステム」を作りたい、という気持ちは変わっていません。ただ、いまは、人間の営みに寄り添い続ける仕組みを作っていきたいと考えています。

これから何するの

3月からは、リクルートの同期が立ち上げた Boulderというプロダクト検証フェーズのベンチャーでVPoEとしてエンジニアリングのリードと開発体制づくりをやっています。動きが激しいフェーズにおいて、急速なプロダクトの進化を支えるシステムと強い開発チームを作るべく日々楽しく働いています。詳細についてはまだ1文字も書いてない入社エントリで書く予定です。許して。

結びに

僕個人として、新卒でリクルートに入ったことはとても良かったと思っています。

就職先を考えている前途有望な学生や、転職を考えている優秀な方に出会った時は、必ず勧めてみるようにしています。いろいろ思い通りにならなかったり大変なことはありましたが、一度入ってみるのは間違いなく面白いと自信を持って言えます。それほどに優秀な人材が揃っていますし、様々なチャレンジや学びの機会がゴロゴロ転がっています。

『お前はどうしたいの』という有名な語録ネタがありますが、この問いは自分にむけてこそ価値があります(決してマウンティング用の便利フレーズではありません)。なんでもできる会社だからこそ、なぜ、何をしたいのか、自分がどうなりたいのかは常に考えていました。正直これから先のことは今まで以上にわかりませんが、この習慣はきっとこれからも僕を助けてくれるはずです。

別れの言葉を考えていましたが、きっとさようならではないですね。出戻り大歓迎の文化だから、何かの拍子に気付いたらまた中の人になっているかもしれません。その未来がくるかはわかりませんが、社会人としての原点が、自然とそう思える場所だということは、かけがえのない財産だと思っています。

ありがとうリクルート
またいつか、まだここにない出会いがあれば。

年の瀬だし今年1年どんなスタンプを使ったのか見てみる

これは、Recruit Engineers Advent Calendar 2019 22日目の記事です。

こんにちは。リクルートテクノロジーズの加藤です。 リクルートテクノロジーズのslackはtimesと呼ばれる分報文化が盛んで、ユーザそれぞれが自分のtimesを作って好き勝手呟いたり誰かと雑談したり、真面目に議論したりと、おもいおもいの使い方をしています。

そろそろ年の瀬なので、今年1年timesで使われたスタンプを集計したりしてみようかなと思い立ち、やってみましたという記事です。ソースは全て公開しているので、よかったら遊んでみてください。

github.com

出力イメージ

bot宛にメンションを飛ばすと、集計をして返してくれるというシンプルな作りになっています。 下画像は僕のtimesで実行してみた結果です。が多いことからどんなtimesなのかがよくわかります。

f:id:yoshiyuki-kato:20191221074808p:plain

遊び方

1. slackアプリケーションを作る

まず、ワークスペースに新しくアプリケーションを追加します。 アプリケーションが追加できたら、必要な権限などの設定をしていきます。

api.slack.com

botユーザを追加

f:id:yoshiyuki-kato:20191221075054p:plain

oauth権限を設定

データを取得するために必要な history 系の権限と、メッセージ投稿に必要な権限を設定します。bot権限は、botユーザを追加した時に自動で設定されます。 f:id:yoshiyuki-kato:20191221075118p:plain

eventサブスクリプションを設定

botあてのメンションに反応するため、app_mention イベントを設定します。 f:id:yoshiyuki-kato:20191221075429p:plain

botユーザをチャンネルに招待する

メンションを飛ばすなりすれば招待できます。app_mention イベントに反応するために必要です。 f:id:yoshiyuki-kato:20191221080608p:plain

2. ローカルで動かしてみる

$ git clone https://github.com/YoshiyukiKato/slack-reaction-counter
$ cd slack-reaction-counter
$ npm install

設定ファイルを作成

サンプルをコピーして、ローカル用の設定ファイルを作成します。 1で作成したslackアプリケーション設定画面から値を取得して、環境変数に設定します。

$ cp local.example.env local.env
export SLACK_SIGNING_SECRET=
export SLACK_ACCESS_TOKEN=

エンドポイントを公開

slackのeventを受け取るには、slackからのwebhook用のエンドポイントが必要です。 エンドポイントはグローバルに公開されていないといけないので、ローカルで動かすならngrokを使います。

$ npm run local
$ ngrok http 3000

ngrokで割り当てられたURLをslackアプリケーションのevent subscription URLとして設定します。

f:id:yoshiyuki-kato:20191221075237p:plain

3. リモートにデプロイする

僕はappengine上で動かしていますが、herokuでもなんでも動かしたい環境にデプロイしてください。 やることは一つだけで、デプロイが終わったらslackアプリケーションのevent subscription URLを更新するだけです。

さいごに

楽しんでいただければ幸いです。もしバグとか見つけたらIssueなど上げてくださると助かります。

Node.jsでサクッとGRPCを使うためのライブラリgrpc-kitをつくりました

この記事は、Recruit Engineers Advent Calendar 2018 4日目の代打記事です。

こんにちは。リクルートでエンジニアをやっている加藤です。 アドベントカレンダーの穴が寂しかったので埋め記事投稿です。つい先日、dev.toに投稿した元記事の和訳版記事になります。村上春樹にでもなった気分です。

タイトルの通りこの記事では、Node.jsでGRPCを簡単に使うためのライブラリを作ったので、そのご紹介をしたいと思います。

github.com

GRPCのアプリケーションをNode.jsで作るときには、grpc@grpc/proto-loaderというライブラリを使います。 この子達を使うとサクッとGRPCのサーバ/クライアントを作ることができてとても有難いのですが、いかんせんAPIがプリミティブなので個人的に使いづらさを感じていました。もっとこう、createClientとかcreateServerとかでインスタンス作って、useみたいのでミドルウェアっぽく設定追加して、という感じで使いたい欲求がありました。

そんなわけで、grpc@grpc/proto-loaderをラップしたgrpc-kitというライブラリをつくりました。jsペライチ100行未満の超お手軽ライブラリです。以下のような感じで使います。

greeter.proto

syntax="proto3";

package greeter;

service Greeter {
  rpc Hello (RequestGreet) returns (ResponseGreet) {}
  rpc Goodbye (RequestGreet) returns (ResponseGreet) {}
}

message RequestGreet {
  string name = 1;
}

message ResponseGreet {
  string message = 1;
}

server.js

const {createServer} = require("grpc-kit");
const server = createServer();

server.use({
  protoPath: "/path/to/greeter.proto",
  packageName: "greeter",
  serviceName: "Greeter",
  routes: {
    hello: (call, callback) => {
      callback(null, { message: `Hello, ${call.request.name}` });
    },
    goodbye: async (call) => {
      return { message: `Goodbye, ${call.request.name}` };
    }
  }
});

server.listen("0.0.0.0:50051");

createServergrpc.Server をラップしたクラスのインスタンスを返します。このインスタンスuse メソッドを提供していて、greeter.protoへのパス、パッケージ名、サービス名、それからgreeter.protoで定義された各メソッド(hello, goodbye)のハンドラを渡すことができます。ハンドラには、asyncな子も、asyncじゃない子も渡せるようにしてあります。これで設定は完了なので、あとはlisten というお里が知れそうな名前のメソッドにアドレスを渡してサーバを起動します。

client.js

//client.js
const {createClient} = require("grpc-kit");
const client = createClient({
  protoPath: "/path/to/greeter.proto",
  packageName: "greeter",
  serviceName: "Greeter"
}, "0.0.0.0:50051");

client.hello({ name: "Jack" }, (err, response) => {
  if(err) throw err;
  console.log(response.message);
});

client.goodbye({ name: "John" }, (err, response) => {
  if(err) throw err;
  console.log(response.message);
});

createClientgrpc.Clientインスタンスをそのまま返します。これはもう処理をまとめて僕が好きな形にしただけです。

さらに詳しい使い方については、 リポジトリ のREADMEと、あとexampleにサンプルコードを入れてあるのでご参照ください。 この記事が何かのお役に立てば幸いです。

Google Kubernetes EngineにNAT付きでConcourse CIをたてる

(この記事は、Recruit Engineers Advent Calendar 2018 16日目の記事です)

こんにちは。リクルートでエンジニアをしている加藤です。 フロントエンドをやったり、バックエンドをやったり、プロダクティビティ的なことをやったりと、いろんなことをやっています。最近は、KubernetesGCPを触る機会が増えてきたのと、あとJenkinsがもういい加減辛くなってきてConcourse CIを触り始めました。つまりこの記事は、 最近やったこと全部盛り な内容になっております。すこし長いですがお付き合いください。

この記事でやること

この記事では、GKE(Google Kubernetes Engine)に、NAT付きでConcourseを立てる方法をご紹介します。今回想定している状況は、

  • ファイヤウォールの内側にいる、プライベートのサービス(GitlabやGithubEnterperiseなど)につなぎに行きたい
  • Concourseには特定のIPからのみ接続を許可したい
  • Concourseへの接続はHTTPSにしたい

というものです。エンタープライズの開発環境では、よくある状況かと思います。 全体構成は以下のようになります。

全体構成
全体構成

この構成を作るため、以下の順で進めていきます。

  1. NAT Gateway付きのGKEクラスタを作る
  2. Let's EncryptのTLS証明書をGKEクラスタで管理する
  3. ConcourseをGKEクラスタ上で動かす
  4. Cloud Armorで接続元IPを制限する

この記事で例示されるコードは、後ほどリポジトリにアップして、記事に追記します。

Concourse CI?

Concourse CIは、pivotalによって開発されている、オープンソースのCIツールです。ググるとJenkins vs Concourseみたいな記事が出るくらいには人気があります。特徴的な点としては、パイプラインの状態可視化と、設定ファイルによる明示的なタスク定義があります。ConcourseCIについては、数ある紹介記事・比較記事にお任せします。個人的にはMediumにある公式の紹介記事が丁寧に書いてあっておすすめです。

参考 - JenkinsとConcourse CIを比較 - Concourse official articles on Medium

0. 前提

⚠️ これ以降の手順は、以下の項目を前提として進みます。ご注意ください ⚠️

  • GCPプロジェクトを持っていること
  • ローカルマシンにGoogle Cloud SDKがインストールされていて、gcloud コマンドが利用可能であること
  • gcloud コマンドでログインしていて、GCPプロジェクトを設定していること
  • ローカルマシンにkubectlがインストールされていて、kubectl コマンドが利用可能であること

1. NAT Gateway付きのGKEクラスタを作る

GKEクラスタからのリクエストの送信元IPを固定するには、NAT Gatewayが必要です。今回、NAT Gatewayの役割を果たしてくれるのは、GCE(Google Compute Engine)のVMインスタンスです。このVMインスタンスNATインスタンスと呼びます。GKEクラスタのアウトバウンドのトラフィックは、全てNATインスタンスを経由するようにします。これによって、GKEクラスタからのリクエストの送信元IPは、NATインスタンスのものになります。プライベートサービスのファイヤウォールには、NATインスタンスのIPを許可する設定を追加してあげれば、GKEクラスタからの接続を許可することができます。

本章では、リソースのデプロイにCloud Deployment Managerを利用します。各リソース定義のフォーマットは、yamlをベースとしてjinja2で書かれています。サポートされているリソース定義については、Cloud Deployment Managerドキュメントのサポートされるリソースタイプから全て辿ることができます。

1.1. ネットワークリソースの定義

ネットワークリソースを定義します。必要なのは、以下の3つです。

構成のベースとなるVPCネットワークは、構成全体を囲むいわゆるグループのようなものです。networkというリソースで表現されます。GKEクラスタのサブネットと、NATインスタンスのサブネットは、ネットワークに属するグループです。subnetworkというリソースで表現されます。

resources:
######## Network ############
- name: devops-nat-network
  type: compute.v1.network
  properties: 
    autoCreateSubnetworks: false
######### Subnets ##########
######### For Cluster #########
- name: devops-subnet 
  type: compute.v1.subnetwork
  properties:
    network: $(ref.devops-nat-network.selfLink)
    ipCidrRange: 172.16.0.0/12
    region: asia-northeast1
########## For NAT Instance ##########
- name: devops-nat-subnet
  type: compute.v1.subnetwork
  properties: 
    network: $(ref.devops-nat-network.selfLink)
    ipCidrRange: 10.1.1.0/24
    region: asia-northeast1

GCPでは、ネットワークはCIDRとリージョンを持たず、サブネットがCIDRとリージョンを持っています。同じネットワークに属するサブネット同士は、CIDRの範囲が被ってはいけないので注意してください。この辺りは少し混乱するかもしれませんが、一旦はグルーピングがしたいんだな、くらいの理解で大丈夫です。さらに詳しくはVirtual Private Cloud(VPC)ネットワークの概要を読んでください。AWSと比較して違いが知りたいという方は【AWSしかやったことない人向け】AWSとGCPのネットワークの違いを理解してみようが非常に丁寧でおすすめです。

この設定で、以下のように枠だけの状態ができました。

枠だけの状態
枠だけの状態

各リソースのnameは適当につけました。今回はConcourseのための構成なので、全てにdevopsというprefixをつけています。後からどんな用途のリソースかわかればいいと思いますので、状況に応じて好きなものをつけてください。

1.2. NATインスタンスとGKEクラスタの定義

########## NAT Instance ##########
- name: devops-nat-vm
  type: compute.v1.instance 
  properties:
    zone: asia-northeast1-a
    canIpForward: true
    tags:
      items:
      - nat-to-internet
    machineType: https://www.googleapis.com/compute/v1/projects/{{ env["project"] }}/zones/asia-northeast1-a/machineTypes/f1-micro
    disks:
      - deviceName: boot
        type: PERSISTENT
        boot: true
        autoDelete: true
        initializeParams:
          sourceImage: https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-9-stretch-v20181113
    networkInterfaces:
    - network: projects/{{ env["project"] }}/global/networks/devops-nat-network
      subnetwork: $(ref.devops-nat-subnet.selfLink)
      accessConfigs:
      - name: External NAT
        type: ONE_TO_ONE_NAT
        ##asia-northeast1リージョンに外部IPアドレスを予約してから入れる
        natIP: 
    metadata:
      items:
      - key: startup-script
        value: |
          #!/bin/sh
          # --
          # ---------------------------
          # Install TCP DUMP
          # Start nat; start dump
          # ---------------------------
          apt-get update
          apt-get install -y tcpdump
          apt-get install -y tcpick 
          iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
          nohup tcpdump -e -l -i eth0 -w /tmp/nat.pcap &
          nohup tcpdump -e -l -i eth0 > /tmp/nat.txt &
          echo 1 | tee /proc/sys/net/ipv4/ip_forward
########## FIREWALL RULES FOR NAT Instance ##########
## アウトバウンドトラフィック
- name: devops-nat-vm-firewall 
  type: compute.v1.firewall
  properties:
    allowed:
    - IPProtocol : tcp
    - IPProtocol : udp
    - IPProtocol : icmp
    - IPProtocol : esp
    - IPProtocol : ah
    - IPProtocol : sctp
    sourceTags: 
    - route-through-nat
    network: $(ref.devops-nat-network.selfLink)
## インバウンドトラフィック
- name: devops-nat-vm-ssh
  type: compute.v1.firewall
  properties: 
    allowed:
    - IPProtocol : tcp
      ports: [22]
    sourceRanges: 
    ## デバッグのため、特定のネットワーク内からのみNATへのSSHを許可
    - YYY.YYY.YYY.YYY/ZZ
    network: $(ref.devops-nat-network.selfLink)
########## GKE CLUSTER CREATION ##########
- name: devops
  type: container.v1.cluster
  metadata: 
   dependsOn:
   - devops-nat-network 
   - devops-subnet
  properties: 
    cluster: 
      name: devops 
      network: devops-nat-network
      subnetwork: devops-subnet
      nodePools:
      - name: default-pool
        initialNodeCount: 1
        config:
          tags:
          - route-through-nat
          # マシンタイプについてはhttps://cloud.google.com/compute/docs/machine-types?hl=ja
          machineType: n1-highmem-2 
          imageType: ubuntu
        autoscaling:
          enabled: true
          minNodeCount: 1
          maxNodeCount: 3
    zone: asia-northeast1-a

この設定で、以下のように枠に箱が入った状態ができました。

枠に箱が入った状態
枠に箱が入った状態

1.3. ネットワークルーティングの定義

########## GKE MASTER ROUTE ##########
- name: devops-master-route
  type: compute.v1.route
  properties:
    destRange: $(ref.devops.endpoint)
    network: $(ref.devops-nat-network.selfLink)
    nextHopGateway: projects/{{ env["project"] }}/global/gateways/default-internet-gateway
    priority: 100
    tags:
    - route-through-nat

########## NAT ROUTE ##########
- name: devops-route-through-nat
  metadata: 
    dependsOn:
    - devops  
    - devops-nat-network
  type: compute.v1.route
  properties: 
    network: $(ref.devops-nat-network.selfLink)
    destRange: 0.0.0.0/0
    description: "route all other traffic through nat"
    nextHopInstance: $(ref.devops-nat-vm.selfLink)
    tags:
    - route-through-nat
    priority: 800

この設定で、以下のように箱の間に道を通すことができました。

箱の間に道が通った状態
箱の間に道が通った状態

1.4. 静的IPアドレスを取得する

静的なIPアドレスを取得します。GCEのVMにデフォルトで割り振られるIPアドレスは動的なもので、インスタンスを再起動したりすると切り替わってしまいます。NATインスタンスの場合、これではあまり嬉しくありません。

静的なIPアドレスを取得する詳しい方法は公式のドキュメントを参照してください。アドレスを取得する際には、先ほど定義したGKEクラスタと同じリージョンを選択します。静的なIPアドレスが取得できたら、1.2 で空欄にしていた、NATインスタンス設定のnatIPに追記します。

取得したIPを許可するよう、プライベートのサービスのファイヤウォールに設定します。これで、クラスタからプライベートのサービスに繋がるようになりました。

プライベートサービスに繋がる状態
プライベートサービスに繋がる状態

1.5. 設定をデプロイする

ここまでで、基本的な構成の設定が完了しました。あとは、これをデプロイするだけです。デプロイには、gcloud コマンドを使います。cluster-with-nat-route.yamlというファイルに上記の設定がひとまとめにされていて、このデプロイにdevops-with-natという名前をつける場合、以下のようなコマンドになります。

$ gcloud deployment-manager deployments create devops-with-nat --config cluster-with-nat-route.yaml

処理には少し時間がかかります。処理が完了したらgcloudコマンドで、ローカルマシンのkubectlをGKEクラスタに繋げます。

$ gcloud container clusters devops get-credentials

1.6. Helmをインストールする

この章の仕上げとして、GKEクラスタHelmが使えるようにセットアップします。 Helmは、Kubernetes用のパッケージマネージャです。これ以降の2章、3章では、GKEクラスタへのデプロイに、Helmを利用します。

まず、ローカルマシンにHelmクライアントをインストールします。手順はリンク先を参照してください。完了すると、helmコマンドが使えるようになります。

GKEクラスタでHelmを利用するには、適切な権限が付与されたサービスアカウントが必要です。Helmのデフォルト設定に従い、tillerという名前でサービスアカウントを作成します。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: tiller
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: tiller
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
  - kind: ServiceAccount
    name: tiller
    namespace: kube-system

上記設定がhelm.yamlという名前の場合、GKEクラスタへの適用は次のように行います。

$ kubectl apply -f helm.yaml

サービスアカウントが作成できたら、ローカルマシンのhelmコマンドを初期化します。

$ helm init --upgrade --service-account tiller

2. Let's encryptのTLS証明書をGKEクラスタで管理する

GKEクラスタができたので、TLS証明書の準備に移ります。TLS証明書は、Let's Encryptで取得します。GKEクラスタで証明書管理を行う場合、cert-manager という超便利ツールを使います。

2.1. cert-managerをインストールする

cert-managerは、すでにhelmのパッケージが公開されており、コマンド一つでデプロイすることができます。今回は、cert-manager用に新しくnamespaceを切ります。

$ kubectl apply -f k8s/namespace.yaml
$ helm install --name cert-manager --namespace cert-manager stable/cert-manager

2.2. Let's EncryptへTLS証明書作成依頼を発行する

今回、ドメインAWSのRoute53で取得しているという想定です。その他のドメインプロバイダの場合、Issuerspec.acme.dns01の書き方が変わります。dns01でのチャレンジに使えるドメインプロバイダと設定の書き方は、cert-managerのドキュメントに書いてあります。

apiVersion: v1
kind: Secret
metadata:
  name: prod-route53-credentials-secret
  namespace: cert-manager
type: Opaque
data:
  secret-access-key: YOUR_AWS_SECRET_ACCESS_KEY(base64 encoded) 
---
apiVersion: certmanager.k8s.io/v1alpha1
kind: Issuer
metadata:
  name: letsencrypt-prod
  namespace: cert-manager
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: your_email_here@example.com
    privateKeySecretRef:
      name: letsencrypt-prod
    dns01:
      providers:
        - name: prod-dns
          route53:
            region: ap-northeast-1
            accessKeyID: YOUR_AWS_ACCESS_KEY_ID
            secretAccessKeySecretRef:
              name: prod-route53-credentials-secret
              key: secret-access-key
---
apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
  name: mycert
  namespace: cert-manager
spec:
  secretName: mydomain-net-tls
  issuerRef:
    name: letsencrypt-prod
  commonName: '*.mydomain.net'
  acme:
    config:
      - dns01:
          provider: prod-dns
        domains:
          - '*.mydomain.net'

上記設定がcert.yamlという名前の場合、GKEクラスタへの適用は次のように行います。

$ kubectl apply -f cert.yaml

証明書の発行には少し時間がかかります。申請状況の経過は次のコマンドで確認できます。

$ kubectl describe cert mycert -n cert-manager

Failed等の表示が途中でみられるかもしれませんが焦らず待ってみてください。完了するとmydomain-net-tlsというSecretが作成されます。作成されたかどうかは、以下のコマンドで確認できます。

$ kubectl get secret -n cert-manager

2.3. namespaceを跨いでワイルドカード証明書を使い回す

ワイルドカード証明書を、クラスタ内のnamespaceを跨いで使いまわしたい、というのは非常にわかりやすい要求です。Let's EncryptのAPIコールには件数制限があるということと、namespaceごとに証明書を取る手間を考えると、誰もがそうしたいと思うでしょう。しかし、cert-managerで取得した証明書をnamespaceを跨いで使うには、少し工夫が必要です。

cert-managerにはClusterIssuerというのがあり、これを使うとクラスタ内の異なるnamespaceで証明書を使いまわせそうに見えるのですが、悲しいことにそうはいきません。このIssueにもある通り、ClusterIssuer自体は使いまわせても、取得した証明書は使いまわせないという悲しい仕様になっています。

そこで、kubedを使います。一度Issuerで取得した証明書の変更を監視しておいて、対象となるnamespaceに都度連携する方式です。

kubedは、helmでインストールすることができます。

$ helm repo add appscode https://charts.appscode.com/stable/
$ helm install appscode/kubed --name kubed --namespace kube-system \
  --set apiserver.enabled=false \
  --set config.clusterName=devops

kubedのインストールが完了したら、先ほど作成されたmydomain-net-tlsに、kubedが監視するためのアノテーションを貼ります。

$ kubectl annotate secret mydomain-net-tls -n cert-manager kubed.appscode.com/sync="app=kubed"

この章の仕上げとして、Concourseをデプロイするnamespace を作り、そこにkubedで証明書を連携します。

apiVersion: v1
kind: Namespace
metadata:
  name: concourse
  labels:
    app: kubed

kubedが連携先として認識するため、labelにapp=kubedを設定しています。kubectlで設定を反映します。

$ kubectl apply -f concourse-namespace.yaml

証明書が連携されているかどうか確認して完了です。

$ kubectl get secret mydomain-net-tls -n concourse

ここまでで、構成は以下のようになっています。だいぶできてきました。

cert-managerで証明書を管理
cert-managerで証明書を管理

3. ConcourseをGKEクラスタにデプロイする

3.1. 静的なIPアドレスを取得してドメインに紐づける

Concourse用に静的なIPアドレスを取得します。1.4.の手順と同じ方法で取得し、concourseという名前をつけます。この名前はConcourseの設定で使います。取得したIPアドレスを、AWS Route53でドメインに紐づけます。今回は、concourse.mydomain.netという名前を紐づけた想定で進めます。

3.2. ソースをfetchして設定を書く

Helmで提供されているConcourseのパッケージに、設定を追加してデプロイします。まずソースをfetchしてきます。

$ helm fetch --untar stable/concourse

新しくconcourseというディレクトリが作られます。この中にある、values.yamlがデフォルトの設定ファイルになります。コピーして持ってきましょう。

$ cp ./concourse/values.yaml ./myvalues.yaml

この中に、変更する箇所がいくつかあります。設定ファイル全体をここで見るのは長すぎるので、ピックアップして見ていきます。設定ファイルには大きく分けてconcourseという項目と、webという項目があります。concourseにはアプリケーション内の設定を書き、webにはkubernetesリソース周りの設定を書くようになっています。

concourse:
  web:
    logLevel: error
    bindPort: 8080
    # IPを紐づけたドメイン
    externalUrl: https://concourse.mydomain.net 
    tsa:
      logLevel: info
      bindPort: 2222
      peerIp: #ここはあとで追加する
web:
  service:
    type: NodePort
  ingress:
    enabled: true
    annotations:
      #取得した静的IPの名前
      kubernetes.io/ingress.global-static-ip-name: concourse
    hosts:
      #IPを紐づけたドメイン
      - concourse.mydomain.net
    tls:
      #cert-managerで取得してkubedで連携した証明書
      - secretName: mydomain-net-tls
      #IPを紐づけたドメイン
        hosts:
          - concourse.mydomain.net

3.2. デプロイして設定をアップデートする

$ helm install --name concourse --namespace concourse -f concourse.yaml ./concourse

concourseのworkerはwebのpeerIpがないとエラーを吐き続けるのですが、これはデプロイ後でないとわかりません。

$ kubectl get svc concourse-web -n concourse
NAME                   TYPE        CLUSTER-IP        EXTERNAL-IP   PORT(S)                         AGE
concourse-web          NodePort    XXX.XXX.XXX.XXX   <none>        8080:30979/TCP,2222:31674/TCP   18s

XXX.XXX.XXX.XXXを、myvalues.yamlpeerIpに設定して再度デプロイします

$ helm upgrade -f myvalues.yaml concourse ./concourse

ここまでで、以下のような状態まできました。あと一息です。

LB経由でConcourseにアクセスできる
LB経由でConcourseにアクセスできる

3.3. 動作確認

Concourse自体はすぐに立ち上がるのですが、LBが立ち上がるまで少し時間がかかります。GCPのコンソール上で状態を確認しつつ、全てのリソースが立ち上がったら、接続できるかどうか確認します。

今回、ユーザ名とパスワードは変更していないので、デフォルト設定のままです。ユーザ名:test、パスワード:testでログインしてみましょう。

webから

https://concourse.mydomain.netにアクセスして、右上にあるloginボタンからフォームを開きます。

Concourse Web ログイン
Concourse Web ログイン

flyから

Concourseでは、flyというCLIを使って設定の操作を行います。ローカルマシンにflyをインストールして、以下のコマンドでログインします。

$ fly -t myci login -c https://concourse.mydomain.net -u test -p test

4. Cloud Armorで接続元IPを制限する

最後に、Cloud Armorを使って、接続元のIPを制限します。Cloud Armorは現在Beta版として提供されているセキュリティサービスです。LBに組み込むことで、GCP上のリソースをDDoS攻撃から守ってくれる他、ファイヤウォールの設定を書くこともできます。本来、kubernetesnetwork policyを書くのが王道だと思いますが、今後設定を使いまわしたいというのと、あと単純に使ってみたかったという理由なので許してください。

設定は、GCPのコンソール上から行うことができます。メニューから、ネットワークセキュリティ > Cloud Armorを選択します。

メニューからCloud Armorを選択
メニューからCloud Armorを選択

ポリシーの作成ボタンを押すと、設定編集画面が開きます。デフォルトアクションを拒否にして、許可ルールを追加していきます。ターゲットへのポリシーの適用で、先ほど作られたLBを選択したら完了です。

Cloud Armor ポリシーの作成
Cloud Armor ポリシーの作成

実際に、許可されたアドレス外からアクセスしようとすると、403 Forbiddenが表示されるので試してみてください。

さて、ここまでで全ての構成が完成しました! 長かった...。お付き合いいただきありがとうございました。

完成
完成

まとめ

この記事では、エンタープライズの開発環境によくある状況を想定して、GKEにConcourse CIを立てる方法をご紹介しました。GCPのネットワーク設定や、cert-managerとkubedの合わせ技による証明書の管理などは、かなりややこしい部分だと思います。かなり駆け足になってしまいましたので、随時記述を補足していきたいと思います。

この記事が何かのお役に立てば幸いです。

※残課題※

以下残課題は、対応完了次第追記します。

  • 公開リポジトリへのソースコードのアップ。この記事で例示したコードをアップします。
  • この問題の対処。今はクライアント証明書に頼りきりなので、ちゃんとIP制限の設定をします。

Lighthouseのバッチ実行と結果レポートをいい感じでやるためのパッケージを作りました

nightharborというパッケージを作りました。対象ページのリストを読み込み、ひたすらlighthouseを実行して、それらの結果をレポーティングためのものです。名前はlighthouse(灯台)が稼働してそうなとこ、という安直な感じでつけました。

github.com

lighthouseって???

本題の前に、lighthouseについて軽くお話しします。ご存知の方も多いかとおもいますが、Googleが提供しているサイトパフォーマンス計測ツールです。サイトパフォーマンスをスコア化して表示してくれるだけでなく、読み込むまでにどれくらい時間がかかったのか、ページ速度を遅くしている要因が何なのか、どうすれば改善できるのか、などいろいろ丁寧に教えてくれるステキツールです。Chromeの開発者ツールにある Audits から使うことができます。ブラウザからの使い方は こちらなどをご覧いただければと思います。

本題

このlighthouseというツールですが、node.js上でも動きます。 今回作ったパッケージは、node.js上でlighthouseをバッチ実行するためのものです。

それなりの規模のWebサイトになってくると、パフォーマンスを調べたいページについて、手動でぽちぽちlighthouseを実行するのは結構な手間です。 加えて、「定期的にモニタリングしたい」とか「競合の計測もしたい」なんて要望もでてきたりしたら大変です。手動実行なんてしたくないですし、結果のファイルをどこかに手動アップロードしまくるなんてのも絶対にやりたくありません。

なので、やりたいこととしては、ざっくり以下のような感じです。

  1. 好きな場所から対象ページリストを読み込む
  2. 順番にlighthouseを実行する
  3. 好きな場所に好きな形式で出力する

1, 3を自由にカスタマイズできて、2は設定(lighthouse, puppeteer)が渡せるように作りました。図にするとこんな感じです。

f:id:yoshiyuki-kato:20180917003247p:plain

基本的な使い方

基本的な使い方については、サンプルプロジェクトをcloneして実行していただくのが一番手っ取り早いかと思います。

$ git clone https://github.com/YoshiyukiKato/nightharbor-example.git
$ cd nightharbor-example
$ npm install
$ npm start
https://google.com
    first-contentful-paint: 0.98
    first-meaningful-paint: 0.98
    speed-index: 0.99
    screenshot-thumbnails: null
    final-screenshot: null
    estimated-input-latency: 0.82
    time-to-first-byte: 1
    first-cpu-idle: 0.87
    interactive: 0.88
    user-timings: null
    critical-request-chains: null
    redirects: 1
    mainthread-work-breakdown: 0.75
    bootup-time: 0.97
    uses-rel-preload: 1
    uses-rel-preconnect: 0.74
    font-display: 1
    network-requests: null
    metrics: null
    uses-long-cache-ttl: 1
    total-byte-weight: 1
    offscreen-images: 1
    render-blocking-resources: 1
    unminified-css: 1
    unminified-javascript: 1
    unused-css-rules: 1
    uses-webp-images: 1
    uses-optimized-images: 1
    uses-text-compression: 1
    uses-responsive-images: 1
    efficient-animated-content: 1
    dom-size: 1

こんな感じの実行結果がコンソールに出力されたでしょうか。 nhb.config.jsには、このプロジェクトの全ての設定が書いてあります。

const {SimpleLoader} = require("nightharbor/loader");
const {SimpleReporter} = require("nightharbor/reporter");

module.exports = {
  loaders: [
    new SimpleLoader([
      { url: "https://google.com" }
    ])
  ],
  reporters: [
    new SimpleReporter()
  ],
  
  //following params are optional
  //default params are shown as comment 
  /*
  chromeNum: 1,
  puppeteerConfig: {
    headless: true
  },
  lighthouseConfig: {
    extends: 'lighthouse:default',
    settings: {
      onlyCategories: ['performance'],
    }
  }
  */
}

nightharborでは、CLIからの実行とプログラム上での実行の二つを用意していますが、どちらもこのconfigを渡すだけでOKです。 CLIの場合はconfigファイルへのpathを

$ npm i -g nightharbor
$ nhb --config ./path/to/config.js

プログラムからの場合は直接configオブジェクトをexecメソッドに渡します。

import nhb from "nightharbor";
import config from "./path/to/config";

nhb.exec(config)
  .then(() => console.log("done"));
  .catch(console.error);

ここからは、nhb.config.jsの内容について説明をしていきます。

loaders

loadersは、lighthouse実行対象リストを読み込むLoaderの配列です。SimpleLoaderは、組み込みのもっとも簡単なLoaderで、生の配列でlighthouseの実行対象を指定するためのものです。Loaderが扱う実行対象の情報には、urlというパラメータが含まれている必要があります。

{ "url": "https://google.com" }

いまのところ、組み込みのSimpleLoaderに加えて、僕自身が使うために作ったLoaderは以下の2つです。

loadersには、Loaderのインタフェースを実装したクラスのインスタンスであれば、含めることができます。以下のような感じで独自のLoaderを作ることもできます。

class CustomLoader {
  /**
   * @return {Promise<{ url: string, [key: string]: any }[]>}
   */
  load(){
    //some asynchronous fetch tasks such as read file and api request.
    return Promise.resolve([
      { url: "https://google.com" }
    ]);
  }
}

load メソッドがあればいいのでサクッと作れます。 なお、ここで読み込む対象ページの情報は、最終的にlighthouseの実行結果と結合されReporterに渡されます。なので、url以外にもレポートに含めたい情報を含めておくと便利かもしれません。

reporters

reportersは、lighthouseの実行結果(audits)を任意の形で出力するReporterの配列です。SimpleReporterは、組み込みのもっとも簡単なReporterで、コンソールにlighthouseの結果を出力します。

いまのところ、組み込みのSimpleReporterに加えて、僕自身が使うために作ったReporterは以下の三つです。

reportersには、Reporterのインタフェースを実装したクラスのインスタンスであれば、含めることができます。独自のReporterを作ることもできます。

class CustomReporter{
  /**
   * will be called when a lighthouse execution completed
   * @param {any} result
   * @return {void}
   */
  write(result){
    //do something
  }

  /**
   * will be called after all executions
   * @return {Promise}
   */
  close(){
    //do something
  }
}

Reporterには、一回のlighthouse実行後に結果が渡されるwriteメソッドと、全ての対象ページへのlighthouse実行が終了した後に呼ばれるcloseメソッドを実装します。なお、現状writeメソッドに渡ってくる結果情報は、データサイズを抑えるため、各audit(例えば、speed-indexなど)ごとscoreのみにしています(auditのインタフェースはこちらを見てください)。detailsなど他の情報も取りたいという要望ありましたらIssueください

chromeNum

puppeteerで起動するchromeの数です。対象ページが多い場合、一つのchromeで処理するのでは流石に時間がかかるので、何個も立ち上げて同時に処理を走らせることができます。ただし、数字を大きく設定する場合、マシンスペックを十分に確保した上で行う必要があります。

puppeteerConfig

puppeteerの設定です。詳しくはこちらをご覧ください。

lighthouseConfig

lighthouseの設定です。詳しくはこちらをご覧ください。

実際の使い方の例

僕自身は、S3から対象リストのCSVファイルを読み込ませるようにして、CloudWatch + AWS Batchで定期実行し、結果をS3とBigQueryに出力しています。 nightharborのアプリケーションはdocker imageにして、AWS ECRに上げています。

f:id:yoshiyuki-kato:20180917025457p:plain

アプリケーションのimageはビルド後のサイズが気になるのでnodeのalpineベースで作ったのですが、lighthouseの公式docに従って進めても動かないという罠があって結構大変でした。AWS上での設定や実際に定期計測してみた話はまた別途記事にまとめようかと思いますが、とりあえずalpineベースのサンプルプロジェクトを作ったので、よかったらご活用ください。

github.com

まとめ

lighthouseをバッチ実行していい感じでレポーティングするためのパッケージ、nightharborをご紹介しました。 何かのお役に立てば幸いです。IssueもPRもお待ちしております(日本語でも英語でも大丈夫です)。

ログ収集基盤sentryのオンプレ版をローカルに立てて遊ぶ

アプリケーションログ収集基盤のsentryを触ってみたらすごい便利そうだったので、仕事でも使いたいなーと思ったのですが、ホスティング版をEnterprise向けに大々的に使おうとすると結構お金がかかりそうなので、まずは小さく試せるオンプレ版で使用感をみてみようという試み。 dockerベースでサクッと入れられるのでとても便利です。

github.com

手順は↑のREADMEに書いてある通りですが、一応。

0. クローンしてくる

$ git clone https://github.com/getsentry/onpremise.git
$ cd onpremise

1. ディレクトリつくる

$ mkdir -p data/{sentry,postgres}

2. シークレットキーを生成してdocker-compose.ymlに貼り付ける

$ docker-compose run --rm web config generate-secret-key

一瞬どこにキーが出力されてるのかわかりづらいですが、一番最後、WARNINGの後に出力されてるのがキーです。

...
WARNING: Image for service web was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
[このへんにキーが出力されてる]

こいつをコピーして、docker-compose.ymlに貼り付けます

...
version: '2'
services:
  base:
    restart: unless-stopped
    build: .
    environment:
      # Run `docker-compose run web config generate-secret-key`
      # to get the SENTRY_SECRET_KEY value.
      SENTRY_SECRET_KEY: [ここにキーを貼り付ける]
      SENTRY_MEMCACHED_HOST: memcached
...

3. データベースを構築する

$ docker-compose run --rm web upgrade

このときに、初期アカウントも作ります。ダーっとログが出力されたあと、プロンプトが起動するので順番に答えていきます。

Would you like to create a user account now? [Y/n]: Y
Email: [自分のメアドを入力]
Password: [好きなパスワードを入力]
Repeat for confirmation: [パスワード確認]
Should this user be a superuser? [y/N]: Y

最後に「superuserにする?」って聞かれるので、Yにしときました。adminはいた方がいいよね。

4. アプリケーションを起動する

$ docker-compose up -d

5. アプリケーションにアクセスする

http://localhost:9000でアクセスできるようになってます。

f:id:yoshiyuki-kato:20180203154141p:plain

さっき設定したアカウントのメアドとパスワードでログインします。

f:id:yoshiyuki-kato:20180203155218p:plain

  • Root URL: sentryサーバにアクセスするためのURL。ローカルで試すだけなのでhttp://localhost:9000にしました。外向きに公開するならば、そのURLを入力します。
  • Admin Email: sentryサーバの管理者アドレス。自分のメールアドレスを入れました。
  • Allow Registration: 誰でもアカウント作れるようにするかどうか。今回は自分だけですしデフォルトのままオフで。
  • Anonymize Beacon: sentryの開発元に送られるデータを匿名化するかどうか。管理者アドレスとかその辺の情報が全部除外されるようです。ただし、セキュリティアップデートとかの連絡はできなくなっちゃうよ、とのこと。実サービス向けでの運用始めたらオフにした方がいいのかもしれないですが、今回はお試しなのでデフォのままオンで。

もろもろ設定してcontinueを押すと、ダッシュボードに遷移します。

f:id:yoshiyuki-kato:20180203160603p:plain

超簡単でした。ここまで10分かかってない... せっかくなのでこの土日で遊び倒してみようと思います。

Serverless FrameworkとExpressでお手軽にAPIサーバを作ろう

この記事は、Recruit Engineers Advent Calendar 20176日目の記事です。

こんにちは。最近マルチロールな働き方をしていて自分が何者なのかわからなくなってきた加藤です。現在リクルートジョブズというところでエンジニアをしています。

日頃、社内用のちょっとしたツールとか、試験的なAPIとかを作るのにAWS LambdaやDynamoDBを使ってサーバレスな開発をしているのですが、この記事ではその際によくやるServerless FrameworkとExpressを活用した開発手法の紹介をしたいと思います。


Expressといえば、Node.jsのWebアプリケーションフレームワークとして有名ですが、最近ではaws-serverless-expressというのを使うと、AWS LambdaでもExpressを使って開発ができます。サラッと書きましたが、正直こいつが出てきた時僕は100万starくらいしたい気持ちでした。そのくらいlambdaを便利にしてくれたと思っています。

github.com

一方、Serverless Frameworkというと「?」となる方も多いかと思います。ざっくり説明すると、API GatewayAWS Lambdaなどのフルマネージドなサービスを組み合わせる、 サーバレス・アーキテクチャ による開発を簡単にしてくれるツールです。AWSには標準でCloud Formationというオーケストレーション機能がありますが、イメージとしては、Cloud FormationにわかりやすいCUIと拡張性のある開発環境がくっついたような感じです。プラグインを使うとローカルにAPI GatewayやLambda、S3、DynamoDBなどの開発環境を立ててることができるようになっています。

serverless.com

これら二つを組み合わせて使うと、簡単にサーバレスなAPIの開発ができるというお話です。

つくるもの

要件

よくありそうなTODOアプリのAPIをつくります。

機能 メソッド パス
一覧 GET /items
追加 POST /item
取得 GET /item/:id
更新 PUT /item/:id
削除 DELETE /item/:id

構成

API Gateway + Lambda + DynamoDBというベーシックな構成です。

ソースコード

github.com

前提条件

  • AWS CLIの設定が完了していること
  • node.jsがインストールされていてnpmコマンドが使えること
  • https://serverless.com/に登録していること

手順

1. 準備

serverlessのセットアップ

まず、npmでserverlessをグローバルに入れます。

$ npm install -g serverless

これで、serverless,slss,slsコマンドが使えるようになります。

$ sls

サブコマンドを指定しないで呼ぶと、ヘルプメニューとして使えるコマンド一覧が表示されます。この記事でもこの先何回もslsコマンドを使っていくことになります。 このタイミングで、Serverless.comにログインしておきます。

$ sls login

ブラウザが開くはずなので、よしなにログイン処理をすすめてください。

プロジェクトの初期化

$ mkdir sls-todo && cd sls-todo
$ npm init

必要に応じて、.gitignoreもよしなに書いてください。 さて、この手順の仕上げとして、今回必要になるファイルとディレクトリをあらかじめ設置しておきます。以下のような構造にしました。

.
├── package.json
├── seeds
│   └── todo-items.json
├── serverless.yml
└── src
    ├── app.js
    └── routes
        └── index.js

ここで、serverless.ymlというファイルを作成しました。これは、Serverless Frameworkの設定ファイルです。Serverless Frameworkはこのファイルに書かれている内容を実行していきますので、アプリケーションの本体といっても過言ではないかもしれません。3. serverless.ymlを書くに、完成形のserverless.ymlを載せてあります。

2. 使うもののインストール

Lambdaで使うやつ

  • aws
  • aws-serverless-express
  • express
  • body-parser
  • cors
  • compression
$ npm install --save express aws-sdk aws-serverless-express body-parser cors compression

Serverlessプラグイン

  • serverless
  • serverless-offline
  • serverless-dynamodb-local
  • serverless-plugin-include-dependencies
$ npm install --save-dev serverless serverless-offline serverless-dynamodb-local serverless-plugin-include-dependencies

3. serverless.ymlを書く

serverless.ymlを書くのは、最初はちょっと骨が折れるかもしれません。ボトムアップに作っていくのは結構きついものがあるので、ほぼ全部入りのUser GuideのServerless.ymlから、自分が使いたい部分を抜き出してきて使うのがオススメです。各リソースのイベントについて細かい設定がしたい場合、Eventsの各ページが参考になります。

今回は以下のように設定を書きました。各項目の内容はコメントに書いてある通りです。

service: sls-todos

# 実行環境に関する設定
provider:
  name: aws
  runtime: nodejs6.10
  region: ap-northeast-1

# 利用するプラグインについて
plugins:
  - serverless-offline
  - serverless-dynamodb-local
  - serverless-plugin-include-dependencies

# デプロイ時のパッケージの内容について
package:
  include:
    - src/**

# lambdaを実行するトリガの設定。今回はAPI Gatewayのルーティング設定
functions:
  sls-todo:
    role: ${DYNAMO_ACCESS_ROLE} # dynamoにアクセスできるロール
    description: TODOリストAPIのルーティング
    handler: src/routes/index.handler
    events:
      - http:
          path: /todos
          method: get
          cors: true
      - http:
          path: /todo
          method: post
          cors: true
      - http:
          path: /todo/{id}
          method: get
          cors: true
          request:
            parameters:
              paths:
                id: true
      - http:
          path: /todo/{id}
          method: put
          cors: true
          request:
            parameters:
              paths:
                id: true
      - http:
          path: /todo/{id}
          method: delete
          cors: true
          request:
            parameters:
              paths:
                id: true

# ローカルのdynamodbの起動設定
custom:
  dynamodb:
    start:
      port: 8888
      inMemory: true
      migrate: true
      seed: true
    seed:
      test:
        sources:
          - table: sls-todos
            sources: [./seeds/todo-items.json]

# CloudFormationの形式でのリソース定義
resources:
  Resources:
    slsTodos:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: sls-todos
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1

4. DynamoDBのローカルへのインストールと起動

serverless.ymlpluginsを書いたことで、プラグインが提供するコマンドが使えるようになっています。

slsと打つと、以下のようなコマンドが一覧に追加されて表示されるはずです。

dynamodb ...................... undefined
dynamodb migrate .............. Creates local DynamoDB tables from the current Serverless configuration
dynamodb seed ................. Seeds local DynamoDB tables with data
dynamodb start ................ Starts local DynamoDB
dynamodb remove ............... Removes local DynamoDB
dynamodb install .............. Installs local DynamoDB
$ sls dynamodb install

これで、ローカルにDynamoDBがインストールされます。 DBの準備ができたので、今度はseedファイルの準備をします。

seeds/todo-items.json

[
  {
    "id" : "test",
    "todo" : "Hello World!"
  }
]

seedファイルの準備ができたら、dynamodbを起動します。

$ sls dynamodb start

先ほど、serverless.ymlのdynamodbパートでの起動時オプションにseed: truemigrate: trueをつけたので、起動と同時にseedファイルの内容がDBに反映されます。

5. ExpressでAPIを書く

使っているミドルウェアは少し違いますが、基本的には普通にExpressを利用するときと同じ書き方ができます。ここでは、ルーティングに関わらない共通の処理をapp.jsにまとめています。

src/app.js

const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const compression = require('compression');
const awsServerlessExpressMiddleware = require('aws-serverless-express/middleware');
const app = express();

app.set('view engine', 'pug');
app.use(compression());
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(awsServerlessExpressMiddleware.eventContext());

module.exports = app;

一つ完全に違うのはapp.jsroutes以下のルーターを読むのではなく、ルーターapp.jsを読み込んでいるという点です。これは、app.jsを核にして一つのサービスが立つのではなく、各ルーターが独立したサービスとして立ちapp.jsという共通設定を利用する形になっているためです。

src/routes/index.js

const express = require("express");
const awsServerlessExpress = require('aws-serverless-express');
const AWS = require("aws-sdk");
const app = require("../app");
const router = express.Router();

//ローカルかリモートかで、dynamodbのconfigを切り替える
const dynamoConfig = process.env.NODE_ENV === "development" ? {
  region: "localhost",
  endpoint: "http://localhost:8888", 
} : {};
const ddc = new AWS.DynamoDB.DocumentClient(dynamoConfig);

//一覧
router.get('/todos', (req, res, next) => {
  ddc.scan({
    TableName: "sls-todos"
  }).promise()
  .then((result) => res.send(result))
  .catch(next);
});

//追加
router.post('/todo', (req, res, next) => {
  ddc.put({
    TableName: "sls-todos", 
    Item: req.body.Item
  }).promise()
  .then((result) => res.send(result))
  .catch(next);
});

//取得
router.get('/todo/:id', (req, res, next) => {
  ddc.get({
    TableName: "sls-todos",
    Key:{
      id: req.params.id,
    }
  }).promise()
  .then((result) => res.send(result))
  .catch(next);
});

//更新
router.put('/todo/:id', (req, res, next) => {
  req.body.Item.id = req.params.id;
  ddc.get({
    TableName: "sls-todos",
    Item: req.body.Item
  }).promise()
  .then((result) => res.send({ message: "update success!" }))
  .catch(next);
});

//削除
router.delete('/todo/:id', (req, res, next) => {
  ddc.delete({
    TableName: "sls-todos",
    Key:{
      id: req.params.id,
    }
  }).promise()
  .then((result) => res.send({ message: "delete success!" }))
  .catch(next);
});

router.use((err, req, res, next) => {
  res.status(500).send(err);
});

app.use("/", router);

const server = awsServerlessExpress.createServer(app);
exports.handler = (event, context) => awsServerlessExpress.proxy(server, event, context);

6. ローカルで動かす

ここまでで、アプリケーションの設定とコードの準備が全て整いました。offlineコマンドで、ローカルにサーバを立ち上げてみましょう。

$ sls offline
Serverless: Starting Offline: dev/ap-northeast-1.

Serverless: Routes for sls-todo:
Serverless: GET /todos
Serverless: POST /todo
Serverless: GET /todo/{id}
Serverless: PUT /todo/{id}
Serverless: DELETE /todo/{id}

Serverless: Offline listening on http://localhost:3000

localhost:3000でサーバが立ち上がります。serverless.ymlに設定したルーティングに対して、リクエストを投げてみましょう。curlやブラウザでやるのもいいですが、個人的にはpostmanがオススメです。

7. デプロイする

$ sls deploy

これだけです。ローカルの時と同じく、デプロイされた先のAPIを叩いてみましょう。

$ sls remove

挙動の確認ができて、必要がなくなったら畳んでしまいましょう。リモートにデプロイしたまま放置していると、知らないうちにお金がかかっていることがあるので...。

まとめ

  • Serverless frameworkを使うと嬉しいことがたくさん
    • フルマネージドなサービスを使ってサクッとAPIを作れる
    • APIをつくるときにExpressを使うとコードも書きやすくてなおよい
    • プラグインを使えばローカルに検証環境を作るのも簡単
    • デプロイするのも、逆に落とすのもコマンド一つでOK
    • ただしserverless.ymlを書くのだけはがんばってください

それではみなさま、よきサーバレスライフを!!