App Engineを試す vol4 ~サービス同士を連携させる~
概要
AppEngine勉強日記、4日目。
この記事では、default
サービス上のNuxtからexpressを経てapis
サービスのAPIにアクセスするところを検証します。
すごい、4日連続で続いたよ。アドベントカレンダーいけんじゃね?
背景
AppEngineが便利っていう話を聞いたので、AppEngineでどこまでできるのかを試す連載。 連載のゴールとしては、NuxtをBFFとしておいてAPIで処理を行うというマルチサービスの構成。
作るサービスの仕様
- 題名と本文を投稿できる
- 投稿すると、題名と本文を閲覧できるページに遷移する
以上。いわゆるnopasteと呼ばれるサービスです。slackの時代、もう知らないひともいるんじゃなかろうか。
あ、コードやテストについては、日付更新まで2hしかなかったのでとりあえずなるはやの全力で書いたので結構雑です。 そのうちリファクタする。
defaultサービスの変更
通信用にaxiosをとりあえず入れる。
> cd root > npm i axios
ページの作成
ついでにlayoutも作るけど省略。
./root/pages/index.vue
投稿用の画面を作る。
title
とbody
を入力して、post
を押下すると/apis/posts
にPOST
リクエストを投げる。
<template> <form class="form"> <div class="form-group"> <label for="title" class="form-label">Title</label> <div> <input type="text" id="title" class="form-control" v-model="input.title"> </div> </div> <div class="form-group"> <label for="body" class="form-label">Body</label> <div> <textarea id="body" rows="10" v-model="input.body"></textarea> </div> </div> <div class="form-group"> <button type="button" class="btn-primary" :disabled="!is_valid" @click="submit">Post</button> </div> </form> </template> <script> import axios from 'axios' export default { data() { return { input: { title: null, body: null } } }, methods: { is_valid() { return this.body }, async submit() { const { data } = await axios.post('/apis/posts', { title: this.input.title, body: this.input.body }) this.$router.push(`/posts/${data.id}`) } } } </script>
pages/posts/_id.vue
閲覧画面の作成。
/apis/posts/:id
から文書情報を取得して表示する。
<template> <div v-if="content"> <h2>{{content.title || 'no title'}}</h2> <div> {{content.body}} </div> </div> </template> <script> import axios from 'axios' export default { data() { return { content: null } }, mounted() { axios.get(`/apis/posts/${this.$route.params.id}`) .then(({ data }) => { this.content = data }) } } </script>
expressサーバにAPIを追加
POSTリクエストを受け取れるようにbodyParserをセット。
GET /apis/posts/:id
と POST /apis/posts
のルーティングを作成。
apisサービスのAPIを立たけるようにする。
このとき、apisサービスのオリジンは環境変数から渡せるようにする。
diff --git a/root/app.js b/root/app.js index 35a950f..c9120af 100644 --- a/root/app.js +++ b/root/app.js @@ -5,17 +5,34 @@ const express = require('express'); const app = express(); const { Nuxt, Builder } = require('nuxt') +app.use(express.json()) +app.use(express.urlencoded({ extended: true })); + // Nuxt.js をオプションとともにインスタンス化する const config = require('./nuxt.config') config.dev = process.env.NODE_ENV !== 'production' const nuxt = new Nuxt(config) -app.get('/message', (req, res) => { - res - .status(200) - .send('Hello, world!') - .end(); -}); +const axios = require('axios') +app.post('/apis/posts', (req, res) => { + const data = req.body + axios.post(`${process.env.APIS_ORIGIN}/posts`, { + title: data.title, + body: data.body + }) + .then(({ data }) => { + res.json({ id: data.id }) + }) +}) +app.get('/apis/posts/:id', (req, res) => { + axios.get(`${process.env.APIS_ORIGIN}/posts/${req.params.id}`) + .then(({ data }) => { + res.json(data) + }) + .catch(({ response }) => { + res.status(response.status).json(response.body) + }) +}) app.use(nuxt.render)
app.yamlにapisサービスのオリジンを設定
diff --git a/root/app.yaml b/root/app.yaml index eed1e47..6c4cf07 100644 --- a/root/app.yaml +++ b/root/app.yaml @@ -1 +1,4 @@ runtime: nodejs10 + +env_variables: + APIS_ORIGIN: https://apis-dot-astral-web-260712.appspot.com
deploy
> npm run build > gcloud app deploy
これだけではまだ見れないので、続けてapisサービスを作っていく
apisサービスの変更
GET /posts/:id
にリクエストがきたらサービスから任意のIDのレコードを取得して返却。
POST /posts
にリクエストが来たらサービスにtitle
とbody
を渡してid
を返却。
import { Controller, Post, Body, Get, Param, NotFoundException } from '@nestjs/common'; import { PostsService } from './posts.service'; @Controller('posts') export class PostsController { constructor(private readonly postService: PostsService) { } @Get(':id') async fetchById(@Param('id') id: string): Promise<{id: number, title: null|string, body: string}> { const row = await this.postService.fetchById(Number(id)); if (!row) { throw new NotFoundException(); } return row; } @Post('') async save( @Body('title') title: null|string, @Body('body') body: string, ): Promise<{ id: number }> { return await this.postService.save({ title, body }); } }
サービスを定義する。面倒なのでメモリ上にレコードを保持する。
import { Injectable } from '@nestjs/common'; @Injectable() export class PostsService { private rows: Array<{ id: number, title: string|null, body: string }>; constructor() { this.rows = []; } async fetchById(id: number) { return this.rows.find((row) => row.id === id); } async save({ title, body }: { title: string|null, body: string }): Promise<{ id: number }> { const id = this.rows.length + 1; this.rows.push({ id, title, body }); return { id }; } }
PostsModule
を定義して、ControllerとServiceを関連付ける。
import { Module } from '@nestjs/common'; import { PostsController } from './posts.controller'; import { PostsService } from './posts.service'; @Module({ imports: [], controllers: [PostsController], providers: [PostsService], }) export class PostsModule {}
最後にAppModuleにPostsModuleを追加して完了
diff --git a/apis/src/app.module.ts b/apis/src/app.module.ts index 8662803..5c11c51 100644 --- a/apis/src/app.module.ts +++ b/apis/src/app.module.ts @@ -1,9 +1,10 @@ import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { PostsModule } from './posts/posts.module'; @Module({ - imports: [], + imports: [PostsModule], controllers: [AppController], providers: [AppService], })
deployする
> npm run build > gcloud app deploy
動作を確認する
gcloud app browse -s default
すると投稿画面が開く。
値を入力してPostすると表示ページに遷移する。
できたー!
まとめ
なんとか4日目の対応を乗り切ったぜ...!
ここまで勧めてみたけど、いくつか気になる点がある。
今後の課題にしていきます!
次回予告
というわけで、明日はデータストア周りさわってみたいなー!
App Engineを試す vol3 ~NestJSでサブシステムを動かす~
概要
AppEngine勉強日記、3日目。 この記事ではNestJSで立てたAPIサーバをAppEngineでサブシステムとして動かすとこまでを解説します。
背景
AppEngineが便利っていう話を聞いたので、AppEngineでどこまでできるのかを試す連載。 連載のゴールとしては、NuxtをBFFとしておいてAPIで処理を行うというマルチサービスの構成。
ディレクトリの整理
昨日作ったNuxtのサービスをディレクトリを掘って移動する。
defaultサービスになるのでわかり易い名前を...と思ったが、default
では流石にわかりにくい。
とりあえずroot
と命名して移動する
> git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) renamed: app.js -> root/app.js renamed: app.yaml -> root/app.yaml renamed: nuxt.config.js -> root/nuxt.config.js renamed: nuxt/pages/index.vue -> root/nuxt/pages/index.vue renamed: package-lock.json -> root/package-lock.json renamed: package.json -> root/package.json
NestJSのセットアップ
@nestjs/cli
を入れる。
> npm i -g @nestjs/cli > nodenv rehash
apis
ディレクトリを指定してプロジェクトをセットアップする。
> nest new apis ⚡ We will scaffold your app in a few seconds.. CREATE /apis/.prettierrc (51 bytes) CREATE /apis/README.md (3370 bytes) CREATE /apis/nest-cli.json (64 bytes) CREATE /apis/package.json (1687 bytes) CREATE /apis/tsconfig.build.json (97 bytes) CREATE /apis/tsconfig.json (336 bytes) CREATE /apis/tslint.json (426 bytes) CREATE /apis/src/app.controller.spec.ts (617 bytes) CREATE /apis/src/app.controller.ts (274 bytes) CREATE /apis/src/app.module.ts (249 bytes) CREATE /apis/src/app.service.ts (142 bytes) CREATE /apis/src/main.ts (208 bytes) CREATE /apis/test/app.e2e-spec.ts (630 bytes) CREATE /apis/test/jest-e2e.json (183 bytes) ? Which package manager would you ❤️ to use? npm ✔ Installation in progress... ☕ 🚀 Successfully created project apis 👉 Get started with the following commands: $ cd apis $ npm run start Thanks for installing Nest 🙏 Please consider donating to our open collective to help us maintain this package. 🍷 Donate: https://opencollective.com/nest
こんな感じのディレクトリが作成される。
> ls -lah apis/ total 848 drwxr-xr-x 15 mizuki staff 480B 12 3 23:12 . drwxr-xr-x 9 mizuki staff 288B 12 3 23:11 .. drwxr-xr-x 9 mizuki staff 288B 12 3 23:11 .git -rw-r--r-- 1 mizuki staff 375B 12 3 23:11 .gitignore -rw-r--r-- 1 mizuki staff 51B 12 3 23:11 .prettierrc -rw-r--r-- 1 mizuki staff 3.3K 12 3 23:11 README.md -rw-r--r-- 1 mizuki staff 64B 12 3 23:11 nest-cli.json drwxr-xr-x 668 mizuki staff 21K 12 3 23:12 node_modules -rw-r--r-- 1 mizuki staff 389K 12 3 23:12 package-lock.json -rw-r--r-- 1 mizuki staff 1.6K 12 3 23:11 package.json drwxr-xr-x 7 mizuki staff 224B 12 3 23:11 src drwxr-xr-x 4 mizuki staff 128B 12 3 23:11 test -rw-r--r-- 1 mizuki staff 97B 12 3 23:11 tsconfig.build.json -rw-r--r-- 1 mizuki staff 336B 12 3 23:11 tsconfig.json -rw-r--r-- 1 mizuki staff 426B 12 3 23:11 tslint.json
とりあえずローカル環境でサーバを起動する。
> cd apis > npm start > apis@0.0.1 start /Users/mizuki/workspace/20191203-example-appengine/apis > nest start [Nest] 47954 - 2019-12-03 11:15:03 PM [NestFactory] Starting Nest application... [Nest] 47954 - 2019-12-03 11:15:03 PM [InstanceLoader] AppModule dependencies initialized +18ms [Nest] 47954 - 2019-12-03 11:15:03 PM [RoutesResolver] AppController {/}: +5ms [Nest] 47954 - 2019-12-03 11:15:03 PM [RouterExplorer] Mapped {/, GET} route +2ms [Nest] 47954 - 2019-12-03 11:15:03 PM [NestApplication] Nest application successfully started +2ms
> curl -v localhost:3000 * Rebuilt URL to: localhost:3000/ * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 3000 (#0) > GET / HTTP/1.1 > Host: localhost:3000 > User-Agent: curl/7.54.0 > Accept: */* > < HTTP/1.1 200 OK < X-Powered-By: Express < Content-Type: text/html; charset=utf-8 < Content-Length: 12 < ETag: W/"c-Lve95gjOVATpfV8EL5X4nxwjKHE" < Date: Tue, 03 Dec 2019 14:15:52 GMT < Connection: keep-alive < * Connection #0 to host localhost left intact Hello World!%
プロジェクトをデプロイする
例によってapp.yamlを作成する。
今回はdefaultのサービスではなく、サブシステムとなるapis
をservice
に指定する。
> echo ' service: apis runtime: nodejs10 ' > app.yaml
apis
のディレクトリでデプロイコマンドを実行する。
以下のようにターゲットが表示されるので、問題なければy
で実行。
target url
がhttps://apis-dot-astral-web-260712.appspot.com
になっていることに注目。
> gcloud app deploy Services to deploy: descriptor: [/Users/mizuki/workspace/20191203-example-appengine/apis/app.yaml] source: [/Users/mizuki/workspace/20191203-example-appengine/apis] target project: [astral-web-260712] target service: [apis] target version: [20191203t232045] target url: [https://apis-dot-astral-web-260712.appspot.com] Do you want to continue (Y/n)? y Beginning deployment of service [apis]... Created .gcloudignore file. See `gcloud topic gcloudignore` for details. ╔════════════════════════════════════════════════════════════╗ ╠═ Uploading 29 files to Google Cloud Storage ═╣ ╚════════════════════════════════════════════════════════════╝ File upload done. Updating service [apis]...done. Setting traffic split for service [apis]...done. Deployed service [apis] to [https://apis-dot-astral-web-260712.appspot.com] You can stream logs from the command line by running: $ gcloud app logs tail -s apis To view your application in the web browser run: $ gcloud app browse -s apis
さー、デプロイできたぞー、と思ったがーー
> curl https://apis-dot-astral-web-260712.appspot.com <html><head> <meta http-equiv="content-type" content="text/html;charset=utf-8"> <title>500 Server Error</title> </head> <body text=#000000 bgcolor=#ffffff> <h1>Error: Server Error</h1> <h2>The server encountered an error and could not complete your request.<p>Please try again in 30 seconds.</h2> <h2></h2> </body></html>
ーーなんかエラーになるな...
困ったときはとりあえずログを確認。-s
でサービスを指定する。
> gcloud app logs tail -s apis Waiting for new log entries... 2019-12-03 14:22:46 apis[20191203t232045] "GET / HTTP/1.1" 500 2019-12-03 14:22:46 apis[20191203t232045] sh: 1: exec: nest: not found 2019-12-03 14:22:48 apis[20191203t232045] "GET /favicon.ico HTTP/1.1" 500 2019-12-03 14:22:49 apis[20191203t232045] sh: 1: exec: nest: not found 2019-12-03 14:22:52 apis[20191203t232045] "GET / HTTP/1.1" 500 2019-12-03 14:22:52 apis[20191203t232045] sh: 1: exec: nest: not found 2019-12-03 14:22:52 apis[20191203t232045] "GET /favicon.ico HTTP/1.1" 500 2019-12-03 14:22:53 apis[20191203t232045] sh: 1: exec: nest: not found 2019-12-03 14:23:22 apis[20191203t232045] "GET / HTTP/1.1" 500 2019-12-03 14:23:22 apis[20191203t232045] sh: 1: exec: nest: not found
nest not found
らしい...
@nestjs/cli
がdevDependencies
に含まれているため、インストールされないようだ。
npm start
のコマンドからnestの依存を外す。
diff --git a/package.json b/package.json index 6f344f7..01bd10c 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "prebuild": "rimraf dist", "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "start": "nest start", + "start": "npm run start:prod", "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main",
さあさ、デプロイデプロイっ
> gcloud app deploy Services to deploy: descriptor: [/Users/mizuki/workspace/20191203-example-appengine/apis/app.yaml] source: [/Users/mizuki/workspace/20191203-example-appengine/apis] target project: [astral-web-260712] target service: [apis] target version: [20191203t233112] target url: [https://apis-dot-astral-web-260712.appspot.com] Do you want to continue (Y/n)? y Beginning deployment of service [apis]... ╔════════════════════════════════════════════════════════════╗ ╠═ Uploading 1 file to Google Cloud Storage ═╣ ╚════════════════════════════════════════════════════════════╝ File upload done. Updating service [apis]...done. Setting traffic split for service [apis]...done. Deployed service [apis] to [https://apis-dot-astral-web-260712.appspot.com] You can stream logs from the command line by running: $ gcloud app logs tail -s apis To view your application in the web browser run: $ gcloud app browse -s apis
ログを起動しとく
> gcloud app logs tail -s apis 2019-12-03 14:33:24 apis[20191203t233112] 2019-12-03 14:33:24 apis[20191203t233112] > apis@0.0.1 start:prod /srv 2019-12-03 14:33:24 apis[20191203t233112] > node dist/main 2019-12-03 14:33:24 apis[20191203t233112] 2019-12-03 14:33:25 apis[20191203t233112] [Nest] 28 - 12/03/2019, 2:33:25 PM [NestFactory] Starting Nest application... 2019-12-03 14:33:25 apis[20191203t233112] [Nest] 28 - 12/03/2019, 2:33:25 PM [InstanceLoader] AppModule dependencies initialized +21ms 2019-12-03 14:33:25 apis[20191203t233112] [Nest] 28 - 12/03/2019, 2:33:25 PM [RoutesResolver] AppController {/}: +6ms 2019-12-03 14:33:25 apis[20191203t233112] [Nest] 28 - 12/03/2019, 2:33:25 PM [RouterExplorer] Mapped {/, GET} route +3ms 2019-12-03 14:33:25 apis[20191203t233112] [Nest] 28 - 12/03/2019, 2:33:25 PM [NestApplication] Nest application successfully started +3ms
満を持してcurlを叩くーー
> curl https://apis-dot-astral-web-260712.appspot.com
ーーが、レスポンスが帰ってこない!なんぞ!? 首をかしげながら考える私。 んー通信出来へんてこたぁサービス起動してへんのかな? やー、ゆうてもログでは起動しるやん?
... ... ...あっ 唐突に思いつく私。
diff --git a/src/main.ts b/src/main.ts index 13cad38..7357152 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,6 @@ import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); - await app.listen(3000); + await app.listen(Number(process.env.PORT) || 3000); } bootstrap();
あー
> npm run build > gcloud app deploy
ああー
> gcloud app logs tail -s apis 2019-12-03 14:40:25 apis[20191203t233845] > apis@0.0.1 start:prod /srv 2019-12-03 14:40:25 apis[20191203t233845] > node dist/main 2019-12-03 14:40:25 apis[20191203t233845] 2019-12-03 14:40:27 apis[20191203t233845] [Nest] 28 - 12/03/2019, 2:40:27 PM [NestFactory] Starting Nest application... 2019-12-03 14:40:27 apis[20191203t233845] [Nest] 28 - 12/03/2019, 2:40:27 PM [InstanceLoader] AppModule dependencies initialized +23ms 2019-12-03 14:40:27 apis[20191203t233845] [Nest] 28 - 12/03/2019, 2:40:27 PM [RoutesResolver] AppController {/}: +7ms 2019-12-03 14:40:27 apis[20191203t233845] [Nest] 28 - 12/03/2019, 2:40:27 PM [RouterExplorer] Mapped {/, GET} route +25ms 2019-12-03 14:40:27 apis[20191203t233845] [Nest] 28 - 12/03/2019, 2:40:27 PM [NestApplication] Nest application successfully started +3ms
あ〜〜〜〜〜〜〜
> curl https://apis-dot-astral-web-260712.appspot.comHello World!%
まとめ
AppEngineかんたんでとてもわかり易いですね!!!!!
次回予告
サービス2つ作っただけじゃね? 明日は連携できるようにコード書きます!がんばります!
App Engineを試す vol2 ~NuxtをAppEngineで動かす~
vol2 NuxtをAppEngineで動かす
概要
この記事ではAppEngineのデフォルトサービスでNuxtを動かすまでの手順を解説します。
背景
AppEngineが便利っていう話を聞いたので、AppEngineでどこまでできるのかを試す連載。 連載のゴールとしては、NuxtをBFFとしておいてAPIで処理を行うというマルチサービスの構成。
ローカルマシンにnode v10.16をインストール
nodenvを使ってるけどお好きなのでどうぞ。
> nodenv install 10.16.0 > nodenv local 10.16.0
プロジェクトの初期化
> npm init
nuxtのセットアップ
Expressで連携させたいので一旦手動でnuxtを入れますよ。
create-nuxt-app
を使いたい方は作成してからよしなに調整するのじゃ。
> npm install --save nuxt
ソースの作成
以下のファイルを用意する。
- nuxt.config.js
- nuxt/pages/index.vue
> cat nuxt.config.js module.exports = { srcDir: "./nuxt" }
> mkdir -p nuxt/pages > cat nuxt/pages/index.vue <template> <p>hello world!</p> </template>
起動コマンドを指定して、動作確認
取り替えず開発用にビルドできるようにする
diff --git a/package.json b/package.json index b493bb3..d3dc0a3 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "", "main": "index.js", "scripts": { + "start": "nuxt dev", "test": "echo \"Error: no test specified\" && exit 1" }, "author":
起動する。
> npm start
動作を確認する。
curl http://localhost:3000/
AppEngineへのdeploy
一旦この段階でAppEngineにdeployしてみる。
app.yaml
を最低限の構成で追加する。
> cat app.yaml runtime: nodejs10
dev
のままだと動かないので、start
に変更。
diff --git a/package.json b/package.json index d3dc0a3..0c2ac81 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "", "main": "index.js", "scripts": { - "start": "nuxt dev", + "start": "nuxt start", + "build": "nuxt build", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "",
ローカルでビルドしておく。
> npm run build
Let's deploy
> gcloud app deploy
deploy後、gcloud app browse
でページを開いてみると、
Nuxtで実装したページがホスティングされたのがわかります。
Expressでハンドルする。
Nuxtが動いたので記事タイトル的にはOKなんですけど、最終的にはバックエンドでAPIと通信したいのです。 そのためにはBFFと連携するバックエンド部分の実装が必要なのです。
そこをお手軽にExpressで作っちゃいます。
Expressのエントリポイントを作成し、Nuxtをバンドルするようにします。
/message
はExpressで処理されているのをわかりやすく確認するために追加したルートです。
> cat app.js 'use strict'; const express = require('express'); const app = express(); const { Nuxt, Builder } = require('nuxt') // Nuxt.js をオプションとともにインスタンス化する const config = require('./nuxt.config') config.dev = process.env.NODE_ENV !== 'production' const nuxt = new Nuxt(config) app.get('/message', (req, res) => { res .status(200) .send('Hello, world!') .end(); }); app.use(nuxt.render) // Start the server if (config.dev) { // hot-reload for development new Builder(nuxt).build().then(listen) } else { // for production listen() } function listen() { const PORT = process.env.PORT || 8080; app.listen(PORT, () => { console.log(`App listening on port ${PORT}`); console.log('Press Ctrl+C to quit.'); }); } module.exports = app;
npm start
でexpressのエントリポイントを実行するように変更します
diff --git a/package.json b/package.json index 0c2ac81..a616d33 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "start": "nuxt start", + "start": "node app.js", "build": "nuxt build", "test": "echo \"Error: no test specified\" && exit 1" },
さあ、デプロイしましょう。
> npm run build > gcloud app deploy
gcloud app browse
で変わらずにNuxtのページが表示されていることが確認できます。
Expressに追加したルートが動いているかも確認しましょう。
> curl https://astral-web-260712.appspot.com/message Hello, world!%
実行できていますね! これで今日の目標は達成ですっ
次回予告
なんとなくBFFとしてのサービスが動きそうなことが確認できたので、 明日はAPIサーバ用のサービスを作ってみます!
App Engineを試す vol1 - ExpressをAppEngineで動かす -
概要
この記事ではAppEngineのデフォルトサービスでサンプルのExpressを動作させることを目指します。
背景
AppEngineが便利っていう話を聞いたので、AppEngineでどこまでできるのかを試す連載。 連載のゴールとしては、NuxtをBFFとしておいてAPIで処理を行うというマルチサービスの構成。
AppEngineの準備
- GCPでプロジェクトの作成
gcloud
コマンドのインストールgcloud init
でプロジェクトの指定
gcloud init
でデプロイ対象のプロジェクトを選択できるので行っておく。
※これを行わない場合はproject
オプションで都度project id
を指定して実行することも可能。
> gcloud -v Google Cloud SDK 272.0.0 beta 2019.05.17 bq 2.0.50 core 2019.11.16 gsutil 4.46`
初期プロジェクトの準備
以下からサンプルコードを取得する https://cloud.google.com/appengine/docs/standard/nodejs/quickstart?hl=ja#download_the_sample_code
> git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples Cloning into 'nodejs-docs-samples'... remote: Enumerating objects: 17475, done. remote: Total 17475 (delta 0), reused 0 (delta 0), pack-reused 17475 Receiving objects: 100% (17475/17475), 15.33 MiB | 212.00 KiB/s, done. Resolving deltas: 100% (11409/11409), done.
AppEngine用のディレクトリに移動する。
> cd nodejs-docs-samples/appengine/hello-world/standard
ディレクトリの構成はこんな感じ。
> ls README.md app.js app.yaml package.json test
app.yaml
がAppEngineの挙動指定のファイル。
中身としては runtime: nodejs10
が指定されている。
スタンダード環境なので、動作がNodeJSのv10なんだよね。
> cat app.yaml # Copyright 2017, Google, Inc. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # [START gae_quickstart_yaml] runtime: nodejs10 # [END gae_quickstart_yaml]
次は実行する app.js
の中身。
至って普通のExpressのアプリだが、const PORT = process.env.PORT || 8080
でポートを環境変数から受け取れるようにしている。
AppEngineはPORT
を指定して起動するので、環境変数から受け取れるようにしないとサービスの疎通ができないのだ。
> cat app.js // Copyright 2017 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. 'use strict'; // [START gae_node_request_example] const express = require('express'); const app = express(); app.get('/', (req, res) => { res .status(200) .send('Hello, world!') .end(); }); // Start the server const PORT = process.env.PORT || 8080; app.listen(PORT, () => { console.log(`App listening on port ${PORT}`); console.log('Press Ctrl+C to quit.'); }); // [END gae_node_request_example] module.exports = app;
最後にpackage.json
.
scripts.start
にnode app.js
が指定されており、起動時にapp.js
を実行するようになっている。
> cat package.json { "name": "appengine-hello-world", "description": "Simple Hello World Node.js sample for Google App Engine Standard Environment.", "version": "0.0.1", "private": true, "license": "Apache-2.0", "author": "Google Inc.", "repository": { "type": "git", "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" }, "engines": { "node": ">=8.0.0" }, "scripts": { "start": "node app.js", "test": "mocha --exit test/*.test.js" }, "dependencies": { "express": "^4.16.3" }, "devDependencies": { "mocha": "^6.1.4", "supertest": "^4.0.2" } }
こいつをデプロイしてみる。 deployの内容がCLI上で確認できる。
> gcloud app deploy Services to deploy: descriptor: [/Users/mizuki/workspace/20191201-example-appengine/nodejs-docs-samples/appengine/hello-world/standard/app.yaml] source: [/Users/mizuki/workspace/20191201-example-appengine/nodejs-docs-samples/appengine/hello-world/standard] target project: [astral-web-260712] target service: [default] target version: [20191201t220456] target url: [https://astral-web-260712.appspot.com] Do you want to continue (Y/n)?
プロジェクトやソースが適切なのを確認する。
問題ないので、y
を押してEnter。
Do you want to continue (Y/n)? y Beginning deployment of service [default]... Created .gcloudignore file. See `gcloud topic gcloudignore` for details. ╔════════════════════════════════════════════════════════════╗ ╠═ Uploading 6 files to Google Cloud Storage ═╣ ╚════════════════════════════════════════════════════════════╝ File upload done. Updating service [default]...⠧
deploy中... 暫く待つ。 暫く待つと、deploy結果が出てくる。
File upload done. Updating service [default]...done. Setting traffic split for service [default]...done. Deployed service [default] to [https://astral-web-260712.appspot.com] You can stream logs from the command line by running: $ gcloud app logs tail -s default To view your application in the web browser run: $ gcloud app browse
gcloud app logs tail -s default
を実行すると default
サービスのログをtailしてくれる。
gcloud app browse
を実行すると、ブラウザでサービスを開いてくれる。
面倒な人はcurlで確認する
> curl https://astral-web-260712.appspot.com Hello, world!%
アプリが動作していることがわかった。
モバイルファクトリー在籍中に開発したOSSの振り返り
この記事はモバイルファクトリー Advent Calendar 2018の17日目の記事です。
どうも、@mizuki_rです。 2018年にモバイルファクトリーを退職して、今は弁護士ドットコムという企業でエンジニアをやっています。
モバイルファクトリーには2011年に新卒として入社して2018年まで8年近く務めました。 その中で、様々なプロジェクトや役回りを任されてきましたが、今日は在籍期間に制作したOSSを供養したいと思います。
代表作
Spica
Spica - the HTTP client for dealing with complex WEB API.
2013年に制作した、複雑なAPIクライアントを簡易化するOSSです。 なんでスピカって名前だったのかはもう覚えてない。
当時複数キャリアや様々な外部APIとの通信が必要なプロジェクトにおり、 APIごとに異なるフォーマットや命名規則をModel層に持ち込むことが嫌で実装しました。
APIのリクエストをORMのようにラップし、共通のI/Fでリクエスト、IteratorとRowクラスを返し、 APIの仕様で閉じた実装を隔離するためのモジュールです。
APIから受け取った情報をビジネスロジックで運用したいのに、 なぜ整形処理や変換処理をビジネスロジックでやらねばならんのかと。 外部APIが連携することでテスト不能になっていいのかという思いで作りました。
僕の中ではトップ3に入るくらい熱量のこもった作品です。もう作れない。
検索したら当時YAPCでLTしたときの動画が出てきて震えた。
hariko
GitHub - rymizuki/node-hariko: Mock Server that implements the API Blueprint specification.
Mock Server that implements the API Blueprint specification.
2015年に制作した、API-Blueprintの仕様に基づくモックサーバーです。 張り子。ハリボテを用意するツールなので。命名したのは@nekobatoくんです。
当時の開発フローがフロントエンドで情報設計とプロトタイピングの後にAPI実装という流れだったので、 APIをモックサーバで代替する必要がありました。 しばらくJSONベースの簡易モックサーバを運用して、しかしドキュメンテーション不足を感じたので、 ドキュメントとモック、あわよくばテストデータをかね揃えるツールが欲しいとなり作りました。
当時、API-BlueprintとSwaggerが勢力を二分していた(はず)時期で、Markdownでかけるってことでこちらを採用。 実際開発中もヘビーに利用していました。
syntagme
Syntagme is a flux's flamework.
2016年に制作した、fluxフレームワークです。 草案によると「記号化されたfluxを紬、拡張する」ことを目指していたようです。恥ずかしいなこれ。
もともとは、AngularJSにFluxを導入してビジネスロジックを抽象化し、ReactなりRiotなりに載せ替えようっていう媒のツールを目指していました。 いくつかこだわりがあって、「非同期処理を前提とする」「シンプルなReducer」「利用者はActionとReducerだけを考えればいい」「拡張可能」をいかに氏実現するかを考えました。
当時からすでにReduxは存在していましたが、Reduxの複雑性はサーバとフロントを兼任するエンジニアたちにはコストが高いと考え、 よりシンプルな頭で思考停止しても書ける学習コストの低いフレームワークを目指しました。
実際これにより、AngularJSとVueJSの共存が実現し、かつフロントエンドもだいぶ実装しやすくなったので、僕はこのフレームワークを生み出したことを誇りに思ってます。まあ、もう使うことはあるまいが...
Perl系
代表作って程いばれるわけではないけど一応書いたぞっていう奴ら。 当時はAmon2にハマっていて、いい感じに欲しいものをプラグイン化したくてかいたのが多いです。
Amon2-Web-Auth-Path
Set the trigger for each certification path.
2013年。URI単位で認証を切り替えられるヤツ。
Amon2-Plugin-Web-Validator
Request validator keep it simple controller.
2013年。コントローラーでの煩雑なリクエストバリデーションを簡略化するやつ。 2016年くらいに後輩がこれを更に強力にしたようなヤツ書いてて、あー...ってなった思いで。
Amon2-AccessControl
2013年。詳細が思い出せない。 多分Auth-Pathをより強力にしたようなやつ。
jQuery系
はい、jQuery系ですよ。form周りの処理をいい感じにしたかった。
jquery-fillInForm
2013年。$form.fillInForm(data)
ってしたらそれに紐づくinputを探して値を打ち込む。
jquery-formize
2013年。真面目に書いたやつ。 formのcontrol要素に対してゲッターとセッターをはやしてオブジェクト的に扱えるようにしたやつ。
jquery-pageNavigator
2013年。たぶんpagerをいい感じにするやつ。
Heimdall
Heimdall is a form validation module for jquery or zepto.
2013年。温泉発火村での作品。 zeptoのことみんな覚えてる?たしか 仮面の人にzeptoでも使えるようにしない?って提案されて対応した気がする。
フォームバリデーションツールとして、Backbone、jQuery, Zeptoでの利用を想定していた。
そもそもなんで「ヘイムダル」なのか。北欧神話の神が由来で、巨人の軍勢がビフレストを渡ってアースガルズへ侵攻するのを知らせる、という門番の役割を持ってる、ということから。
えもい(恥ずかしい
Backbone系
いぇーーーい、Backboneだぜーーーー!
backbone-pjax
2013年。BackboneでPjaxしたかった。 jquery.pjax.jsとbackboneを連携しようとして生み出されたヤツ。
backbone-validator
2013年。後のHeimdall
AngularJS系
AngularJSは1.x系だよ。
angular-statename
AngularJS Directive for insert the current state name in the DOM.
2015年。ui-routerのstate名を任意のタグに差し込んでくれる。 ページによって背景とかスタイルを大きく変えたいときとかに使う。
angular-scroller
AngularJS Directive for shorthand of IScroll 5.
AngularJSでIScrollを使いたいときに、いい感じに隠蔽してくれるやつ。
angular-period
AngularJS Directive for switching the DOM in the period.
2015年。期間の前・中・後に合わせてDOMを出し分けするヤツ。 Dateにハマったとの、setTimeoutに指定できる時間に上限があることを知った。
その他
とくに分類できないけど、せっかく書いたので供養。
action-tracker.js
The easy-to-use library for Google Analytics's tracking
2015年。GoogleAnalyticsをいい感じにwrapするヤツ。 いらなくね?って後で気づいた。むしろ書いたことを忘れててプロジェクトの中に入ってるのに気づいて「まじかよ...」ってなった。
grunt-xslate
Compile Text::Xslate template files from Grunt.
2015年。Perl製テンプレートエンジンをあろうことかGruntJSでコンパイルを試みたプロジェクト。 普通にプロジェクトで使ってた気がする。
hubot-scheduler
Extension of hubot to execute scheduled task.
2018年。指定時刻にhubotに発言させる拡張。 当時slackでpostとかsnippetとかを指定時刻に投稿できなくて必要だった。 確かcronで動いてたWebHookをhubotに移植するときに作ったんだっけな。
今は叛逆性MAのイベントリコメンドに使ってます。
electron-ffp
2015年。Electronのおためしで作ったプロジェクト。みてみてcoffee-scriptだよ。 ffpはFastFindPositionの略で、minifyされたJavaScriptから任意の位置をすばやく発見するためのツール。
errorログに流れてきたけどそれがどこのエラーなのかわからない!っていうときにつかった。
まとめ
8年に渡って実装してきた公開ライブラリを一気に紹介しました。 これ以外にも僕のリポジトリには作り途中で放棄したライブラリなどが残っており...
こうして振り返ってみると、思いの外向けにライブラリを発信していて(発信力はともかくとして)高いモチベを意地していたんだなと思います。 OSSで作る意義は、先日の吉祥寺pmで@fujiwaraさんが話していて、「これだ!!!」ってなったのでぜひ読んでみてください。
こういったOSS活動を促進してくれる文化があり、またともに意見交換できる環境があるモバイルファクトリーに感謝を。 願わくば、この文化を今後も発展させていってほしいですね!
10月に弁護士ドットコム株式会社に入社しました
10月1日から弁護士ドットコム株式会社の税理士ドットコム事業でエンジニアとして働き始めました。
え、今11月? いや、ごめん、普通に忘れてました。
忘れないうちに転職に至った経緯や、今後の展望を書きます。 とにかくこの1ヶ月いろいろあって割と記憶が飛び気味。それくらい濃密に過ごせている。 まあ、とりとめなくだらだら書くんであしからず。
転職ドラフトでの出会い
自分はこれ以上成長できない、と感じた
退職エントリでも書いたんですが、 当時の僕は自分のキャリアに見通しが立っていない状態でした。
仕事はこなしているし、生産性も決して低いわけではないと思ってはいる。 それでも、満足感や達成感がなく、自分のキャリアの限界を感じていました。これ以上自分は成長しないのではないか、楽しめる仕事なんてないのではないか、と諦めの気持ちもありました。
まあ、この1年くらいまではキャリアや今後の展望とかをほとんど意識せずに貢献だけを考えてやりたいことを提案、実装、改善とがむしゃらに走ってきたっていうのが一つの要因だとは思っています。自身のキャリアについて考えてこなかった。
しかし、メンバーの育成をサポートする上で僕のプレゼンスの強化を考えたときに、今のままじゃ先が無いなと感じました。
他の会社と繋がりたいけど、経路がない
そんなこんなでいろいろ悩みをもんもんと抱えていたんですが、まあとにかく他の会社ではこういう人をどう扱うのか?って言うのを切り口にしようと考えました。
この時点では転職のタイミングとか考えてなかったし、自分にマッチししそうな環境があるかもわからない状態だったんで、転職を前提とした相談みたいなのを知り合いにするのは少々抵抗がありました。まあ、僕が「辞める」って言い出したときの影響とかも考えつつ、ではありましたが。
なんとか内密に浅く始められないか、と考えました。
いろいろ転職系のサービスにも登録してたんですが、概要とか「とにかく一回あって話しましょ」っていうエージェントからのリクエストに答えるのは手間だと思っていました。登録しておしまい。メールは読まない。
そこで遥か昔に登録した転職ドラフトをお試し気分で使ってみようと思い立ったんです。
まずはレジュメを書くことで状況を整理する
転職ドラフトは最初のエントリ時にレジュメを登録します。 そこで今後自分がどういう方向性を出したいのか、を なんとなく 書き出します。 確定とかじゃなくていい。とりあえず思いついたことをだらっと書きました。
ついで、プロジェクトとかでこれまでやってきたことを書き出してみてみます。 幸い僕はこういう文章を長ったらしく書くのが得意だったのと、何よりそこそこ濃ゆい経験をしてきたので書くこと自体はこまらなかった。逆に主張したい点に絞って優先度の低い案件を削りました。
こうしてレジュメを書くことで「人の役に立ちたい」「求められたい」という要求と「面白いことがしたい」という漠然とした思いを抱えていました。
とはいえこれだけでは、転職すると表明するには物足りなさがあったので、この先は実際に話をして感じたものを土台に考えようと思いました。
実際何回かのドラフトを経て数社から指名をいただきました。
技術か、ビジネスか
幸いなことに指名金額も現状の自分の給与と比較してみると違和感なく、 会ってみたい、話してみたいと思う内容の指名だったのでカジュアル面談から各社と話を伺いました。
実際最初はほんと漠然としていた思いだったんですが、 前職で採用もやっていたため、どういうことを聞くか話すかを考えられたので事前に何通りかシミュレーションはしていました。
僕が目指す方向性として「フロントエンドとしてUI/UXにコミットできるポジション」あるいは「関心のあるビジネスの問題解決をサポートするポジション」そのどちらかで「今よりも楽しそう」という感覚を得られるかどうかを元に話を聞きに行きました。
お硬そう? いや、全然そんなことはない
名前からして硬そうなイメージありますよね? 僕もそう思いましたし、何人かからも言われました。関心を持ったのは事業領域が全く知らない領域だったことと、僕の経験が活かせる領域だったこと、そして過去何度か参加した勉強会のスポンサーをしていたことです。
スポンサーをやっていた、というのは結構大きくて、僕自身勉強会と組織の成長について考えていたりもしたのでそこに力を入れてることは僕の方向性との相性を感じました。
面白くないですか? 全く違うドメインで自分を活かせそうなことをやっていて、期待に合いそうな動きをしている。
最初こそ緊張したものの、カジュアル面談から各選考まで実に楽しく進めさせてもらいました。 いや、ほんと。何人かとお話させてもらいましたが、CTO以外は「これ面接ですか?」ってくらい楽しくおしゃべりしてました。CTOとの面接は「あ、これ、面接ですね」って感じだったw
縁、期待、そして未知の領域への挑戦
弁護士ドットコムの選考を進めていた頃は、転職ドラフト経由じゃないところも含めて数社の選考を進めていました。 実際転職活動してみると、各社興味が強くて、どこも行きたいって気持ちになってなかなか悩みました。いろんな選択肢を考慮してどのシナリオが一番納得感があるか。
今振り返ると、弁護士ドットコムを選んだ決めては大きく3つありました。
縁を感じた
選考の過程で現在の上司と話したんですけど、その人が前職で採用やってたときの他社の採用担当で結構印象深く残っている人だった。
期待を感じた
判断基準として、アサイン予定のチームや関わりそうなエンジニアとお話させてもらった際に、「この人達と働くと面白そう」という期待をいだきました。 実際これは間違ってなくて、僕の中ではめっちゃ刺激のある議論をたくさんさせてもらえてる。いやほんと、転職前にどういう人と働くのか確認する機会は大事です。あのときの感覚は間違ってなかったなって、入社して速攻思いました。
ビジネス領域に関心を引かれた
当時語られた過去の経緯、現状、今後の展望に対して僕が貢献できそうという期待。それに加えて、税理士や弁護士という領域に関しては全くの未知。しかし、それが社会貢献につながることや今後自分の人生にどういう影響を与えるか予測できないという点で興味を引かれました。
結構これは感覚の話で言語化が未だに難しいんですけど、他ではできない領域であり、ちょうど今から面白くなるフェーズで「え、面白そう」って思ってました。
もちろん、当時内定を頂いてた他の企業も面白そうって感覚があってすごい悩んだんですけど、この選択がきっと一番予測できない未来だったし、後悔しないと信じられた選択でした。
そして1ヶ月働いた現在、後悔してないしむしろ充実してる。
お仕事楽しいです
実際に一ヶ月働いてみて、いい意味でも悪い意味でも当初の想定を裏切りました。
任された裁量の大きさ
この1ヶ月のアクションを説明するとソレだけで連載記事がかけるくらいのボリュームがあるので省略しますが、とにかく提案が「いいじゃん」「そういうのを待っていた」「すごい、感動してる」みたいになんかみんながめっちゃ反応返してくれる。 期待を感じるし、期待に応えようと頑張れるし、フィードバックを感じ取れて貢献の実感がある。(慢心はしたくないので謙虚に振る舞うけどめっちゃ嬉しい)
プロ意識
いや、この言葉使うのどうなの?って思うけど、実際職務に真摯に向き合ってくれている。 ビジネスドメイン的にエンタメとは違う方向性、ニーズがあるからっていうのもあるとは思うけれど、関係している人たちみんな本当に真摯に向き合っていて、だからこそ話やすいし議論が楽しい。
対レガシーシステム
思っていたより技術的負債があるなぁという。 とはいえ、深刻さや絶望はそんなに感じていない。知識不足って気もするけど、でもメンバーがちゃんと向き合おうとしていて、信頼できるエンジニアが側にいて、経験的にもなんとかする方法は思いつく。できるかどうかは別だけど、でもやろうと思ってやれないことはないって感じている。
今後の展望
目標設定を通して、今後の方向性とかはアウトプットしてきた。 当面僕がやりたいことも出来たし、ただそれはやれば結果が出るものではないし、じんわりと影響を与えるものなので全然気は抜けない。 ただ、これまで僕がやりたがらなかった領域に僕が積極的にアクションを始めている。この変化が結構驚きで、いい意味で自分の変化が予測できなくなった。 いろんな人と意見交換しながら、改めて自分のキャリアを見つめ直すことができそう。
この先、どういう展開を踏むかはまだわからないけど、僕にとっても周囲にとっても良い状況を作れるよう頑張ろうと思う。
最後に
これからよろしくおねがいします。
なお社外発表とかそういう系の活動はこれまでどおり気が向いたりissueが来たり相談されたりしたら動くつもりなんで気軽にはなしてください。 あと、今回の記事は割と雑に書いたので詳しく聞きたいとかあればTwitterとかで声かけてくれれば話せることは話します。なんせ濃密すぎて1万文字とか余裕で超えそうなんで...
今後ともよろしくね☆
モバイルファクトリーを退職します
9月末付けで株式会社モバイルファクトリーを退職します。本日8/24が最終出社でした。
まだあまり実感はなくて、(弊社では伝統の)退職者発表や挨拶なんかをしながら「あー辞めるのかー」という気持ちを出しつつ、でもなんかやっぱ実感はわかないなぁって思ってるところです。
いろいろ思い出を振り返りながら、まあ当たり障りの無い範囲で何をやってきたかとかをつらつら書いてます。
なれそめ
もともとWeb系がやりたかったので、Web系ベンチャーを探していました。 ベンチャーって絞ってたのは、割と裁量もらえてガンガン勉強できそうって印象があったから(まあ実際そうだった)。実際当時の人事担当と会って「あ、ベンチャーっぽい」ってなったのが印象深いですね。
やってきたこと
基本スタンスとしてはフロントエンドエンジニアであることが長かったですが、 最初はサーバサイドでPerlを書いていたし、度々Perl書いたりExpress書いたりAWS触ったり、最近はNuxtさわったりと、Webに関しては全般的にさわっていました。
後期になるに連れて、チームや人にフォーカスが広がり、サービスに対するUXに限らずチーム内の体験をどう改善するかについて検討・行動を繰り返していました。 実際、チームの会議体を精査したり、社内勉強会を主催したり、1on1とかしてメンバーと接したりと結構いろいろやりました。
もともと対人戦は苦手意識が強く、避けてきては居たんですけど、怯えながらも挑戦させてもらえて、実際それでフィードバックもらえてと本当に学びの多い環境でした。
どんな立場だったか
- エンジニア
- フロントエンドエンジニア
- チームリーダー
- メンター
- 育成サポート
- 採用サポート
どんなことをしてきたか
- 新人研修(座学・マナー・サービス開発)
- 恋愛シミュレーション(ガラケ)
- 着メロ・占いサイト(スマホ)
- ライブ壁紙配信サービス
- 恋愛シミュレーション(アプリ)
- 位置情報ゲーム(TSUTAYAオンラインゲーム)
- 位置情報ゲーム(アプリ)
- ブロックチェイン
なんで転職するの?
結構いろいろがんばってきたつもりでは居たんですけど、まあ少し疲れたというか、自分の方向性がわからなくなって、かつ目指して登れるレイヤまではきちゃったかなというのが感触としてありました。
一歩引いて振り返ってみると、「あれ自分なにがしたいんだっけ?」「わからん」ってなることが多くて、でも今やってることが嫌だってわけではない。目的やチーム、人に貢献すること自体が目的ではあった。
30って年齢の節目を迎えるタイミングで、今一度自分のみのふりを考えてみようと思って、外に出始めたのがきっかけです。
次の会社について
ーーは、入社したら改めて。
今後どうしてきたいの
これから探していこうかなと。 まあまだ折り返し地点って感じでもないし。
謝辞
株式会社モバイルファクトリーをはじめ、関わってくれた方々に感謝を。 ほんと、8年前の自分では想像できないくらい成長していると思う。
恒例の
まあ恒例のって思って貼ったけど、技術書籍とかKindleで欲しいからあんまりリストに乗せるものがなくてな...
- 飲み会に誘う
- iTunesカードを叩きつける
- http://amzn.asia/6pGzFmF
おまけ
- 同期からキックボードをもらいました。ラーメンたべにいくの腰が軽くなりました
- 選別にiTunesカードをいただきました。回します。
引けたぜ pic.twitter.com/h4ABohqgR2
— 水月 涼 (@mizuki_r) 2018年8月24日
彼氏ちゃれーーーーんじ!爆死 pic.twitter.com/Qrowdj2TsP
— 水月 涼 (@mizuki_r) 2018年8月24日