PythonでGCPの認証を行う

GCPの認証ってたまにどうやるんだっけ?って忘れてしまうので備忘録として残しておこうと思います。 今回はCloud Pub/Subを例に認証を行います。

今回使用する言語

予備知識

サービスアカウントとユーザーアカウントの認証の仕組みに関しては下記の記事が参考になります。

medium.com

Cloud Pub/Subのlibraryの使い方に関しては下記になります。

googleapis.dev

サービスアカウントを用いて認証

環境変数を用いる

export GOOGLE_APPLICATION_CREDENTIALS="[PATH]"

上記のようにサービスアカウントが含まれるパスを指定してあげるとclient libraryが使えるようになります こちらの方法だとCloud Pub/SubにPublishする際は下記のように扱うことができます。

from google.cloud import pubsub_v1
import os

project_id = os.getenv("PROJECT_ID", "")
topic_id = os.getenv("TOPIC_ID", "")
publisher = pubsub_v1.PublisherClient()
topic_path = publisher.topic_path(project_id, topic_id)
publisher.publish(topic_path, b"Hello World")

ファイルパスを指定して認証

google.oauth2.service_account moduleを使って認証するやり方です。SERVICE_ACCOUNT_FILE_PATH環境変数にファイルのパスを指定している感じです。

from google.cloud import pubsub_v1
from google.oauth2 import service_account
import os
project_id = os.getenv("PROJECT_ID", "")
topic_id = os.getenv("TOPIC_ID", "")
service_account_file_path = os.getenv("SERVICE_ACCOUNT_FILE_PATH", "")
credentials = service_account.Credentials.from_service_account_file(service_account_file_path)
publisher = pubsub_v1.PublisherClient(credentials=credentials)
topic_path = publisher.topic_path(project_id, topic_id)

publisher.publish(topic_path, b"Hello World")

データを直接読み込んで認証

同じように先ほどのgoogle.oauth2.service_account moduleを使います。 Credentialsクラスで定義されているfrom_service_account_infoメソッドを使うことでファイルパスを指定することなくサービスアカウントの情報を直接渡して認証できます。 特にコンテナなど、jsonファイルを中に置きたくない場合に有効なのかなと思います。SERVICE_ACCOUNT_FILE環境変数にサービスアカウントの情報が入ってます。

from google.cloud import pubsub_v1
from google.oauth2 import service_account
import json
import os
project_id = os.getenv("PROJECT_ID", "")
topic_id = os.getenv("TOPIC_ID", "")
service_account_file = os.getenv("SERVICE_ACCOUNT_FILE", "")
credentials = service_account.Credentials.from_service_account_info(json.loads(service_account_file))
publisher = pubsub_v1.PublisherClient(credentials=credentials)
topic_path = publisher.topic_path(project_id, topic_id)

publisher.publish(topic_path, b"Hello World")

JWTを使って認証

google.auth.jwt.Credentialsを使って認証する方法です。
先ほどのgoogle.oauth2.service_account moduleの認証でもJWTを内部で扱っているのですが、アクセストークンの扱い方がそれぞれ違うみたいです。簡単にまとめると

使用ライブラリ 認証方法
google.oauth2.service_account module JWTを用いてoauth2.0のアクセストークンを獲得して、GCPの認証はoauth2.0のアクセストークンをBearerトークンとして行う
google.auth.jwt.Credentials JWTで生成したBearerトークンを使ってGCPの認証を行う

のような違いがあるみたいです。

from google.cloud import pubsub_v1
from google.auth import jwt
import json
import os
project_id = os.getenv("PROJECT_ID", "")
topic_id = os.getenv("TOPIC_ID", "")
service_account_file = os.getenv("GOOGLE_APPLICATION_CREDENTIALS", "")
publisher_audience = "https://pubsub.googleapis.com/google.pubsub.v1.Publisher"
credentials =  jwt.Credentials.from_service_account_info(json.loads(service_account_file), audience=publisher_audience)
publisher = pubsub_v1.PublisherClient(credentials=credentials)
topic_path = publisher.topic_path(project_id, topic_id)

publisher.publish(topic_path, b"Hello World")

ユーザーアカウントを用いて認証

環境変数を用いて認証

サービスアカウントを用いた方法と変わらないです。GOOGLE_APPLICATION_CREDENTIALSに使いたいユーザーアカウントのファイルパスを指定します。

ファイルパスを指定して認証

google.oauth2.credentials moduleを使って認証します。

from google.cloud import pubsub_v1
from google.oauth2 import credentials 
import os
project_id = os.getenv("PROJECT_ID", "")
topic_id = os.getenv("TOPIC_ID", "")
user_account_path = os.getenv("GOOGLE_APPLICATION_CREDENTIALS", "")
credentials = credentials.Credentials.from_authorized_user_file(user_account_path)
publisher = pubsub_v1.PublisherClient(credentials=credentials)
topic_path = publisher.topic_path(project_id, topic_id)

publisher.publish(topic_path, b"Hello World")

データを直接読み込んで認証

こちらも同じようにgoogle.oauth2.credentials moduleを使います。

from google.cloud import pubsub_v1
from google.oauth2 import credentials 
import json
import os

project_id = os.getenv("PROJECT_ID", "")
topic_id = os.getenv("TOPIC_ID", "")
user_account_file = os.getenv("GOOGLE_APPLICATION_CREDENTIALS", "")
credentials = credentials.Credentials.from_authorized_user_info(json.loads(user_account_file))
publisher = pubsub_v1.PublisherClient(credentials=credentials)
topic_path = publisher.topic_path(project_id, topic_id)

publisher.publish(topic_path, b"Hello World")

まとめ

google-authを眺めていると、まだ挙げていない認証方法もあるのかなと思います。最近は色々なクラウド環境を使う機会が増えてきたので、どのような認証方法が提供されているのか把握できてるとスムーズに開発が行えると思います。
とりあえずGCPの認証に関してはある程度理解できたので今後困ることはなさそうです。

Buildkitを使ってDockerfile以外からビルドする

ZOZOテクノロジーズ #4 Advent Calendar 2019 15日目の記事です。 今回はBuildkitを使ってDockerfile以外のsyntaxからImageをBuildしてみたいと思います。

Buildkitとは

BuildkitはMobyが開発しているOSSで、docker buildをより高速にセキュアにbuildできるツールらしいです。Docker18.09に正式統合され、DOCKER_BUILDKIT=1を指定すればBuildkitが有効になります。

github.com

実装の方針

 Buildkitを使ってDockerfile以外からbuildするのですが、今回はyamlファイルからbuildできるようにしたいと思います。buildkitはbuildする際にLLBというDAG構造の中間言語に変換するのでdockerfile以外の記述方法に差し替えることが可能になっています。
 実装方法としてはbuildkitを使うことで有効になるsyntaxフォーマットを利用することで実装できます。syntaxフォーマットを利用するとdocker buildする際に対応したdocker imageをDocker Hubからもってきて、grpc経由でbuildkitdとやり取りしてBuildするらしいです。なのでDocker Hubのアカウントが必要です。
では、これからDockerfileの代替としてSamplefileというものを作っていきます。

実装

コードの実装

 Buildkitリポジトリに書かれているREADMEのExploring LLBの項目でBuildpacksGockerfileを実装している人が紹介されていたので先人の人たちのコードを参考にしてかなり雑に実装したコードが下記のリポジトリになります。cmd/main.goがgrpc関連でbuild.goYamlファイルをparseしてLLBに変換しているコードです。

github.com

buildしてDocker Hubにimageをpushする

 下記のようにDocker HubにpushするためのDockerfileを作成します。

FROM golang:1.12.0 AS builder

WORKDIR /build
COPY . ./
RUN CGO_ENABLED=0 go build -o /sample ./cmd/

FROM scratch
COPY --from=builder /sample /bin/sample
ENTRYPOINT ["/bin/sample"]

次にbuildなのですが、気をつけることとしては、syntaxフォーマットで指定した名前と同じになるようにbuild tagをつけます。 私のDocker Hubのアカウント名はjon20なので下記のコマンドでbuildします。

docker build . -t  jon20/samplefile

buildできたらDocker Hubにpushします。

docker push jon20/samplefile

確認

 下記のようにSamplefile.yamlを適当なディレクトリに作成します。

#syntax=jon20/samplefile
image: hello-world:latest

そしてdockerのbuildコマンドを実行します。

docker build -f Samplefile.yaml -t jon20/sample .

成功すれば下記のようにImageが作成されたことが確認できました。 f:id:jon20:20191214074017p:plain

まとめ

Dockerfile以外から無事buildすることができました。 正直な所、部分部分しか理解できておらず、説明不足な所が多いですがBuildkitに興味を持っていただければ嬉しいです。

Vue.jsでgoogle mapを利用する

ゆるっと Advent Calendar 2019 2日目です。
今回はVue.jsでgoogle mapを使う際によく使いそうなところを紹介できたらと思います。

下準備

今回使用するライブラリは以下になります。

www.npmjs.com

yarn add vue2-google-maps

で追加できます。

index.jsを以下のように書き換えます。google mapのapiキーの取得に関しては割愛させていただきます。

import Vue from 'vue'
import App from './App.vue'
import * as VueGoogleMaps from 'vue2-google-maps'

Vue.use(VueGoogleMaps, {
  load: {
    key: process.env.VUE_APP_GOOGLE_MAP_API,
    libraries: 'places',
    region: 'JP',
    language: 'ja'
  }
})
Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

google mapの表示

Quickstartに書いてるまんまのコードです。GmapMapコンポーネントを利用します。

<template>
  <div>
    <GmapMap
      :center="{lat:10, lng:10}"
      :zoom="7"
      map-type-id="terrain"
      style="width: 500px; height: 300px"
    >
      <GmapMarker
        :key="index"
        v-for="(m, index) in markers"
        :position="m.position"
        :clickable="true"
        :draggable="true"
        @click="center=m.position"
      />
    </GmapMap>
  </div>
</template>

f:id:jon20:20191201222304p:plain

google mapの設定

optionsプロパティにMapOptionsを渡せます。 例としてStreetViewの表示を消します。

<GmapMap
      :center="{lat:10, lng:10}"
      :zoom="7"
      :options="{streetViewControl: false}"
      map-type-id="terrain"
      style="width: 500px; height: 300px"
    >
      <GmapMarker
        :key="index"
        v-for="(m, index) in markers"
        :position="m.position"
        :clickable="true"
        :draggable="true"
        @click="center=m.position"
      />
    </GmapMap>

StreetViewのアイコンが消えているのが確認できました。 f:id:jon20:20191201222344p:plain

イベントの発火

全部試していませんが、公式に記載されているイベントトリガを使えます。 試しにマップのドラッグが終了した際にメソッドを実行してみます。

<template>
  <div>
    <GmapMap
      :center="{lat:10, lng:10}"
      :zoom="7"
      :options="{streetViewControl: false}"
      map-type-id="terrain"
      style="width: 500px; height: 300px"
      @dragend="onDragEnd"
    >
      <GmapMarker
        :key="index"
        v-for="(m, index) in markers"
        :position="m.position"
        :clickable="true"
        :draggable="true"
        @click="center=m.position"
      />
    </GmapMap>
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  methods: {
    onDragEnd() {
      console.log("hoge");
    }
  }
};
</script>

表示している領域の座標取得

getBoundsを使います。LatLngBoundsクラスが返されるのでgetCentergetSouthWestで中心座標や南西の座標が取得できます。

マーカの設置

すでにQuickstartでGmapMakerが定義されていますのでこれを流用してpositionを設定すればマーカーが設置できます。dataプロパティにmarkersを定義します。

export default {
  name: "HelloWorld",
  data() {
    return {
      markers: [{ position: { lat: 10, lng: 10 } }]
    };
  },
  methods: {
    onDragEnd() {
      console.log("hoge");
    }
  }
};

マーカーを設置できました。 f:id:jon20:20191202015342p:plain

マーカの上にWindowを表示

GmapInfoWindowコンポーネントを利用します。マーカーをクリックしたらWindowを表示します。 optionsプロパティでInfoWindowOptionsを設定します。今回はpixelOffsetだけ設定します。

<template>
  <div>
    <GmapMap
      :center="{lat:10, lng:10}"
      :zoom="7"
      :options="{streetViewControl: false}"
      map-type-id="terrain"
      style="width: 500px; height: 300px"
      @dragend="onDragEnd"
    >
      <GmapInfoWindow
        :options="infoOptions"
        :position="infoWindowPos"
        :opened="infoWinOpen"
        @closeclick="infoWinOpen=false"
      >
        hoge</GmapInfoWindow>
      <GmapMarker
        :key="index"
        v-for="(m, index) in markers"
        :position="m.position"
        :clickable="true"
        :draggable="true"
        @click="toggleInfoWindow(m)"
      />
    </GmapMap>
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  data() {
    return {
      markers: [
        { position: { lat: 10, lng: 10 } },
        { position: { lat: 11, lng: 11 } }
      ],
      infoOptions: {
        pixelOffset: {
          width: 0,
          height: -35
        }
      },
      infoWindowPos: null,
      infoWinOpen: false
    };
  },
  methods: {
    onDragEnd() {
      console.log("hoge");
    },
    toggleInfoWindow(marker) {
      this.infoWindowPos = marker.position;
      this.infoWinOpen = true;
    }
  }
};
</script>

hogeと書かれたWindowが表示されました。 f:id:jon20:20191202030713p:plain

まとめ

google mapを使用する際に使いそうな所を紹介しました。
結構細かく設定できるのでドキュメントを色々覗いてみるのも面白いと思います。

メルカリさんのスカラシップを利用してGo Conference 2019 Springに参加してきました

今回、運良くメルカリさんのスカラシップを利用してGo Conference 2019 Springに参加することができました!!

Go Conference 2019 Springでおもしろかったセッションを中心にブログに書き残せたらなと思います。
また、Go Conference 2019 Springの前日にも社内見学やランチなどがあり、その辺りも簡単に触れられればと思います。

参加の経緯

自分は普段、Ruby(Rails)を使うことが多いのですが、個人開発ではGoをちょくちょく使っていて、その開発の際にDBのテストはどう書くの?だったりエラー処理ってどうするのが良いの?かなどの疑問がありました。
たまたまGo Conference 2019 Springのタイムテーブルを見てたら先程の疑問が解決しそうなセッションがあり、行きたいな〜と思ってましたが交通費ツライと思ってて参加を見送ろうと思ってました。しかし、運良くTwitterでメルカリさんのスカラシップの情報が流れてきたのでダメ元で応募してみようと思い応募して参加できました。

前日

ランチ

メルカリさんとのランチ会がありました。が、当日にいきなりGoogleのGoチームも参加することに決まりマジか!ってなりました。GoチームにはKeynoteの方や明日発表する方がいて何を話そうか必死に考えてましたが、ランチ会で飛び交う会話がほとんど英語だったので正直理解しようとするのに必死でした(自分が質問して他の人に訳してもらったところくらいしかちゃんと理解できてない)
GoogleOSSの開発の話だったりGoのGenericsの話だったりエラー処理の話だったりを話してた気がします。
日本での発表が初めてだからとても緊張しているよって言っていたのはちゃんと聞き取れました!

社内勉強会

 ありがたいことにメルカリさんの社内勉強会に参加させていただきました。ガチガチの勉強会ではなくGoについての意見交換の場みたいな雰囲気で、気軽に質問ができました。こういった勉強会は気軽に開催できそうですし、他の社員さんと打ち解ける場にもできそうなのでいいなと思いました。

当日のセッション

Case studies of designing developer friendly libraries

speakerdeck.com

より良いライブラリの開発の仕方についてです。
Goの慣習に従うことや、拡張性を保つこと、コード生成を利用するなどが紹介されています。
特に拡張性を保つために、差し替え可能にする手段としてのFunctional Option Patternは使ったことがないので自分の開発に導入したいです。

エラー設計について/Designing Errors

docs.google.com

エラーの基本的なところから、そのエラーをGoでどう処理するのかを聞くことができました。
エラーは関係者によって求められるエラーが違い

  • (クライアント)アプリケーション -> エラーを識別できる
  • エンドユーザー -> 問題を解決するヒントになるようなメッセージが含まれている
  • 運用者 -> 問題の根本的な原因を調査、解決する情報が含まれている

と異なっている部分が紹介されてました。適切な情報を含んだエラーを返すことは運用する上で大事なことだし、Goに限らずアプリケーションの設計として大事だなと思いました。
また、Goでのエラーの処理としてアプリケーション固有のエラーコードを定義する方法があり、コードの見通しがよくなるしエラーの管理が楽になりそうだなと思いました。

Design considerations for container-based Go applications

speakerdeck.com

Goの実装事例を踏まえたコンテナアプリケーションの設計についてです。

  • Configration
  • Logging
  • Monitoring

の3つの設計方針を聞くことができました。
特にloggingの話で、STDOUT/STDERRに書き出し、JSON形式で構造化することでログの抽出が楽になりそうだなと思いました。

Expand observability in Go

docs.google.com

net/http/pprofruntime/traceの使い方について紹介されていました。
すごい丁寧に解説してくれたのでProfilingやTracingをあまり行ったことがない自分でも理解することができました。
実演でどう使うかを見ることができたのでスライド見ながら後で使ってみようと思います。

おまけ

自作キーボードのお店に寄って来ました。
分離式キーボードいいなってなりました。

まとめ

今回Googleの社員さんと話したり、今後の開発に活かせそうなセッションをたくさん聞くことができたので参加できてよかったです。セッションの内容は全部は理解できてないので後でスライドを見直そうと思います。
次にGo Conferenceのスカラシップがある時に応募に悩んでいる方の参考になればと思います。
最後にGo Conference 2019 Springに参加する機会を設けてくれたメルカリさんありがとうございました!

elmで作った静的サイトをgithub pagesで公開してみた

最近ブログ書くのをサボっていたのでなにか書こうと思い、そうだ最近触っているElmについて書こう!と思い書きました。時間がかなり遡るのですが年末年始は実家に帰省していて、その際にElmを試しに触っていて簡単なサイトを作ってGithub Pagesで公開したので、その手順を書き残しておきます。

Elmとは

 ちょっと触っただけなので詳しくは説明できないのですが公式から抜粋すると以下の特徴があります。
- 実行時の例外が一切起きない
- virtual DOMを使用するがすべての値はイミュータブルなのでベンチマークがすごくいい
- セマンティックバージョンを強制できる
などの特徴があります。

完成品

Elmで書いて動いたものは下のサイトになります

https://jon20.github.io/mypage/

Githubは下になります

github.com

最終的にディレクトリは以下になりました。

.
├── README.md
├── elm-stuff
├── elm.json
├── package.json
├── src
│   ├── Main.elm
│   ├── assets
│   │   └── flog.jpg
│   ├── index.html
│   ├── index.js
│   └── main.scss
├── webpack.config.js
└── yarn.lock

Elmの準備

Elmのインストールに関しては割愛させていただきます。 Elmのインストールができたら対象のディレクトリでelm initを行ってください。
src配下にElmのファイルを書いていく形になります。 特にElmで書いたものが手元になければElm-lang.orgのQuick Sampleにあるカウンターアプリがあります。コードは下のIntroductionにかかれています。

guide.elm-lang.org

Htmlファイル

Javascript Interopに書かれているようなやり方になります。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="icon" href="./assets/flog.jpg">
    <title>jon</title>
</head>
<body>
    <div id="elm"></div>
</body>
</html>

Javascriptファイル

汚いですが、idがelmの要素にelmを紐付ける形になります。

'use strict';
require('./main.scss');
import Image from './assets/flog.jpg'
const {
    Elm
} = require("./Main.elm")

const app = Elm.Main.init({
    node: document.getElementById('elm')
});

Webpackの使用

あんまりwebpack詳しくないのですが、色々参考にしながら作成しました。 必須になるプラグイン

だと思います... 画像とか扱う場合はfile-loaderが必要になると思います。
必要ない場合はfile-loaderらへんはコメントアウトしてください

const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
  entry: './src/index.js',

  output: {
    path: __dirname + '/dist',
    filename: 'index.js'
  },

  resolve: {
    extensions: ['.js', '.elm', '.scss']
  },

  module: {
    rules: [{
        test: /\.html$/,
        exclude: /node_modules/,
        use: [{
          loader: 'html-loader',
        }, ]
      },
      {
        test: /\.(jpg|png)$/,
        use: {
          loader: 'file-loader',
          options: {
            name: '[name].[ext]',
            outputPath: 'assets/',
          }
        }
      },
      {
        test: /\.elm$/,
        exclude: [/elm-stuff/, /node_modules/],
        use: {
          loader: 'elm-webpack-loader',
          options: {
            verbose: true,
          }
        }
      },
      {
        test: /\.scss$/,
        use: [{
            loader: 'style-loader',
          },
          {
            loader: 'css-loader',
            options: {
              url: true
            }
          },
          {
            loader: 'sass-loader',
          }

        ],

      }
    ],

    noParse: /\.elm$/
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html"
    })
  ],

  devServer: {
    inline: true,
    stats: 'errors-only'
  }
};

CIでビルド・公開周りを自動化

今回はTravis-CIを使いました。 masterにpushされたらyarn install走らせて、gh-pages branchにpushする形となっています。
yarn buildはpackage.jsonで定義されたスクリプトが走っています。

language: node_js
elm:
  - elm0.19.0
node_js:
  - "10"

cache:
  yarn: true
  directories:
    - "node_modules"

branches:
  only:
    - master

install:
  - yarn install
  - yarn build

script:
  - echo "Skipping tests"

deploy:
  provider: pages
  skip-cleanup: true
  github-token: $GITHUB_ACCESS_TOKEN
  target-branch: gh-pages
  local-dir: dist
  on:
    branch: master

まとめ

超ざっくりですがElmをgithub pagesで公開する手順を書きました。簡単にElmで作ったプロダクトがノーコストで公開できるので便利ですね。
WebpackらへんやCIらへんも一緒に試せるのでぜひ挑戦してみてください。
Quick Sampleをgithub pagesで公開するだけなら余分な所が多いので余力があったらサンプルを追加します...
あと、今回作ったものを読み込むときに一瞬ちらつくのですが読み込み順の問題なのかな...

gRPCで動画ファイルの送受信を行ってみる

Go4 Advent Calendar 2018 11日目です。
今回はgRPCを使ってmp4で作成した動画ファイルをstreamを使って送受信してみようと思います。
コードがあまりきれいでないかもしれませんがよろしくお願いします。

はじめに

今回作成したサンプルコードはgithubに置いておきます。

github.com

gRPCとは

Googleが開発しているOpen SourceのRPC frameworkになり、Protocol Buffersを使ってサービス間のインターフェースをprotoファイルに定義します。また、試してませんが他言語間の通信も可能みたいです。

grpc.io

Protocol Buffersでどう定義するのかは下記のドキュメントに詳しく書かれています。

developers.google.com

protoファイルの定義

upload.proto

syntax = "proto3";
package upload;

service UploadHandler {
  rpc Upload(stream UploadRequest) returns (UploadReply) {};
}

message UploadRequest { bytes VideoData = 1; }

message UploadReply { string UploadStatus = 1; }

syntaxはproto3を利用しています。
serviceブロックの中に利用するメソッドを定義します。今回はmp4ファイルを受け取り、成功したらメッセージを返すようなメソッドを定義しています。
ここでUploadRequestの前にstreamをつけているのですが、これを記述することで一つのリクエストを複数のリクエストに分割することができます。今回はClient Streaming RPC方式になると思います。
また、このprotoファイルで生成したgoのファイルはclientとserverの両方必要になります。

Serverの作成

handler.go

func NewUploadServer(gserver *grpc.Server) {
    uploadserver := &server{}
    upload.RegisterUploadHandlerServer(gserver, uploadserver)
    reflection.Register(gserver)
}

type server struct{}

func (s *server) Upload(stream upload.UploadHandler_UploadServer) error {
    err := os.MkdirAll("Sample", 0777)
    if err != nil {
        return err
    }
    file, err := os.Create(filepath.Join("Sample", "tmp.mp4"))
    defer file.Close()
    if err != nil {
        return err
    }

    for {
        resp, err := stream.Recv()
        if err == io.EOF {
            break
        }
        if err != nil {
            return err
        }
        file.Write(resp.VideoData)
    }
    err = stream.SendAndClose(&upload.UploadReply{UploadStatus: "OK"})
    if err != nil {

        return err
    }
    return nil

}

handler.goではclientから受け取った動画データをSampleディレクトリにtmp.mp4として保存するような処理を行っています。成功したらOKの文字をclientに送信します。
main.go

func main() {
    lis, err := net.Listen("tcp", "localhost:8080")
    if err != nil {
        panic(err)
    }
    server := grpc.NewServer()

    handler.NewUploadServer(server)
    if err := server.Serve(lis); err != nil {
        panic(err)
    }
}

main.goではNewServerで空のgrpcサーバーの作成を行い、NewUploadServerで作成したserviceを登録してlocalhost:8080でサーバーを立ち上げます。NewServerは色々オプションを渡せるみたいです。

Clientの作成

main.go

func main() {
    connect, _ := grpc.Dial("localhost:8080", grpc.WithInsecure())

    defer connect.Close()
    uploadhalder := upload.NewUploadHandlerClient(connect)
    stream, err := uploadhalder.Upload(context.Background())
    err = Upload(stream)
    if err != nil {
        fmt.Println(err)
    }
}

func Upload(stream upload.UploadHandler_UploadClient) error {
    file, _ := os.Open("./sample.mp4")
    defer file.Close()
    buf := make([]byte, 1024)

    for {
        _, err := file.Read(buf)
        if err == io.EOF {
            break
        }
        if err != nil {
            return err
        }
        stream.Send(&upload.UploadRequest{VideoData: buf})
    }
    resp, err := stream.CloseAndRecv()
    if err != nil {
        return err
    }
    fmt.Println(resp.UploadStatus)
    return nil
}

予めsample.mp4ファイルを対象のディレクトリに用意してmakeでどれくらい送るか定義しています。そしてfor内でひたすらserverにリクエストを投げまくってます。 送り終わったらCloseAndRecvでstreamをcloseしてサーバーからのメッセージを待ちます。うまく行けばOKと返ってきます。

完成!

実際に試してみるとclientから送られてきたSample.mp4ファイルがserverのSampleディレクトリ内にtmp.mp4として保存されたことが確認できました。

まとめ

  • streamを扱うことで動画データを分割して送ることができた。特に大容量の動画ファイルを送信したときに少しずつデータを送ることができるのでネットワークの負荷が減りそう
  • ライブストリーミングの変換処理とかstreamで実装すると良さそうな気がする

Go言語でgraphqlを触ってみた

ゆるっとアドベントカレンダー Advent Calendar 2018 2日目です。
最近ハッカソンでGraphqlを使って開発を行っているので、今回はGraphqlの復習で簡単なGraphqlサーバをGo言語を使って実装してみます。

Graphqlとは?

GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data

GraphqlはAPIへ問い合わせるためのクエリ言語になります。一つのエンドポイントを用意して、予め定義しておいたGraphqlのクエリを投げることで必要な情報を持ってくることができます。

graphql.org

今回使う外部ライブラリ

  • labstack/echo
  • graphql-go/graphql
  • graphql-go/handler

github.com

github.com

github.com

今回実装したもの

localhost:8080/graphqlにブラウザでアクセスするとGraphqlのIDEが起動して、Graphqlのqueryを投げると下の写真の右のようにdataが返ってくるようなものになります。 f:id:jon20:20181202050619p:plain

今回実装したものは↓のGithubに置いておきます。

github.com

解説

基本的にgraphql-go/graphqlのexampleを参考に実装しています。
まずREST APIのように何か投げたら何か返ってくるものをGraphqlで実装したいためSchemaを定義します。ここで気をつけることは投げられるqueryにはQueryとMutationがあります。SchemaConfigを見ているとSubscriptionがあるのですが調べたかんじだとリアルタイムのデータを取得するときに使うらしいです。まだ使ったことはないので今回は触れません。
QueryとMutationなのですが、QueryはCRUDでいうとCの部分で、単にデータを取得したいときに使います。MutationはR、U、Dの部分で、データに変更を加えたい際に使います。

graphql.org

今回はSampleとしてQueryとMutationを使ってschemaを定義します。
仕様は以下のとおりです。

  • Query
    • IDとPasswordを渡したらMessageが返ってくる
  • Mutation
    • UsernameとAgeを渡したらMessageが返ってくる(Mutationなので何かしらの更新処理を入れる必要があるが今回は省略します)

QueryのSchemaの実装

graphql-sample/query.go at master · jon20/graphql-sample · GitHub

func SetQuery() graphql.Fields {
    query := graphql.Fields{
        "SampleQuery": &graphql.Field{
            Type:    SampleQueryType(),
            Args:    SampleQueryArgs(),
            Resolve: SampleQueryResolve,
        },
    }
    return query
}

func SampleQueryType() *graphql.Object {
    sampleType := graphql.NewObject(graphql.ObjectConfig{
        Name: "SampleQuery",
        Fields: graphql.Fields{
            "Message": &graphql.Field{
                Type: graphql.String,
            },
        },
    })
    return sampleType
}

func SampleQueryArgs() map[string]*graphql.ArgumentConfig {
    sampleArgs := graphql.FieldConfigArgument{
        "ID": &graphql.ArgumentConfig{
            Type: graphql.NewNonNull(graphql.String),
        },
        "Password": &graphql.ArgumentConfig{
            Type: graphql.NewNonNull(graphql.String),
        },
    }
    return sampleArgs
}

func SampleQueryResolve(params graphql.ResolveParams) (interface{}, error) {
    resp := &models.SampleMutateResp{
        Message: "Hello, This is SampleQuery",
    }
    return resp, nil
}

汚い実装ですが、肝となるのはSetQuery()です。Fieldsに定義したいFieldを定義します。FieldはArgsがqueryを使うときに渡す値の定義、Resolveにはqueryを叩いたときの処理を渡してあげます。

MutationのSchemaの実装

graphql-sample/mutation.go at master · jon20/graphql-sample · GitHub

func SetMutation() graphql.Fields {
    mutateQuery := graphql.Fields{
        "SampleMutate": &graphql.Field{
            Type:    SampleMutateType(),
            Args:    SampleMutateArgs(),
            Resolve: SampleMutateResolve,
        },
    }
    return mutateQuery
}

func SampleMutateType() *graphql.Object {
    sampleType := graphql.NewObject(graphql.ObjectConfig{
        Name: "SampleMutate",
        Fields: graphql.Fields{
            "Message": &graphql.Field{
                Type: graphql.String,
            },
        },
    })
    return sampleType
}

func SampleMutateArgs() map[string]*graphql.ArgumentConfig {
    sampleArgs := graphql.FieldConfigArgument{
        "Username": &graphql.ArgumentConfig{
            Type: graphql.NewNonNull(graphql.String),
        },
        "Age": &graphql.ArgumentConfig{
            Type: graphql.NewNonNull(graphql.Int),
        },
    }
    return sampleArgs
}

func SampleMutateResolve(params graphql.ResolveParams) (interface{}, error) {
    resp := &models.SampleMutateResp{
        Message: "Hello, This is SampleMutate",
    }
    return resp, nil
}

実装手順としてはqueryと同じです。どちらも共通して気をつけるポイントとしては指定できる型です。Scalar型があるのですが公式のDocumentを見たかんじだとID、String、Int、Float、Booleanだけになります。もしそれ以外の型を使いたい場合は自分で定義しなければなりません。

graphql.org

SchemaのSetupとIDEの用意

graphql-sample/graphql.go at master · jon20/graphql-sample · GitHub

func GraphqlSetting() http.Handler {
    query := fields.SetQuery()
    mutateQuery := fields.SetMutation()
    rootMutation := graphql.NewObject(graphql.ObjectConfig{Name: "RootMutation", Fields: mutateQuery})
    rootQuery := graphql.NewObject(graphql.ObjectConfig{Name: "RootQuery", Fields: query})

    schema, err := graphql.NewSchema(graphql.SchemaConfig{
        Query:    rootQuery,
        Mutation: rootMutation,
    })
    if err != nil {
        panic(err)
    }
    h := handler.New(&handler.Config{
        Schema:     &schema,
        Pretty:     true,
        GraphiQL:   false,
        Playground: true,
    })
    return h
}

GraphqlのIDEはhandler.Config{}のところで設定できます。指定できるIDEの種類としてはgraphiqlとgraphql-playgroundがあります。今回はgraphql-playgroundを使います。

github.com

github.com

Graphqlサーバの立ち上げ

graphql-sample/main.go at master · jon20/graphql-sample · GitHub

func main() {
    route := echo.New()

    route.POST("/graphql", echo.WrapHandler(graphql.GraphqlSetting()))
    route.GET("/graphql", echo.WrapHandler(graphql.GraphqlSetting()))

    route.Logger.Fatal(route.Start("localhost:8080"))
}

あとは localhost:8080/graphqlのroutingを追加するためechoを使います。

完成!!

定義したSchemaは↓のように見れます。 f:id:jon20:20181202071549p:plain

Queryに定義したqueryの実行結果 f:id:jon20:20181202072117p:plain Mutationに定義したqueryの実行結果 f:id:jon20:20181202072121p:plain

まとめ

  • 単一エンドポイントにまとめられるのでREST APIで実装する場合と比べてエンドポイントの管理が楽になりそう
  • Documentを自動で生成してくれるのでSwaggerなどを用いてDocumentを実装する手間が省ける。
  • ファイルアップロードとかを実装する場合はどうじっそうすればいいのだろうか?