Upgrade to Pro — share decks privately, control downloads, hide ads and more …

mixi tech note #02

mixi tech note #02

#技術書典7 に出典されたミクシィグループエンジニア有志による技術書です。

<< 目次 >>
1章:GoとUnityで作るマルチプレイヤーゲーム
2章:「釣り×機械学習」に挑戦した話
3章:Elm + Haskell で作る Web アプリ
4章:メガベンチャーの新人賞を取るまでにやった7つのこと
5章:BitriseのiOS Auto Provisionを導入した話
6章:SSH経由なプロキシを作る
7章:Makefileマニアクス with Go

<< TECH NOTE 一覧 >>
mixi tech note #01
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-01

mixi tech note #02
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-02

mixi tech note #03
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-03

mixi tech note #04
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-04

mixi tech note #05
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-05

mixi tech note #06
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-06

mixi tech note #07
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-07

MIXI TECH NOTE #08
https://speakerdeck.com/mixi_engineers/mixi-tech-note-number-08

XFLAG Tech Note Vol.01
https://speakerdeck.com/mixi_engineers/xflag-tech-note-vol-dot-01

XFLAG Tech Note vol.02
https://speakerdeck.com/mixi_engineers/xflag-tech-note-vol-dot-02

MIXI ENGINEERS

September 22, 2019
Tweet

More Decks by MIXI ENGINEERS

Other Decks in Technology

Transcript

  1. まえがき 本書「mixi tech note #02」は、ミクシィグループに所属する有志達によって執筆・制作された 技術書です。実際の現場で使われた技術や考え⽅、また、個⼈的に興味・関⼼のある分野から、思い 思いに執筆いたしました。そのため、各章それぞれで完結している内容になっていますので、好きな 章から好きな順番でお楽しみください。 また、本書は、ミクシィグループにある技術的知⾒やアイデアを積極的に共有・公開していくこと で、世の中により良いサービスが溢れ出すことを願って刊⾏されています。掲載されている情報は、

    執筆者⾃⾝の環境で検証し執筆されたものですので、ご参考にされる際は、ご⾃⾝の責任で判断し ご活⽤ください。なお、⽂章表現につきましても、執筆者⾃⾝の⾔葉で伝えたく、フランクな表現と なっておりますことご理解いただければと思います。 ディベロッパーリレーションズチーム⼀同 ◆本書に関するお問い合わせ先   https://twitter.com/mixi_engineers ◆ミクシィグループについて   https://mixi.co.jp/ ※ʠミクシィʡ 、 ʠmixiʡ 、 ʠmixi ロゴʡ は、株式会社ミクシィの商標または登録商標です。また、各 社の会社名、サービス及び製品の名称は、それぞれの所有する商標または登録商標です。 iii
  2. ⽬次 まえがき iii 第 1 章 Go と Unity で作るマルチプレイヤーゲーム

    1 1.1 はじめに . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.2 技術構成 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 1.3 ゲームを作る . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.4 Protocol Buffers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 1.5 通信する . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 1.6 クライアントとサーバのやりとりの例 . . . . . . . . . . . . . . . . . . . . . . . . . 12 1.7 MVP の完成 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 1.8 まとめ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 1.9 今後の予定 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 1.10 雑記 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 1.11 参考 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 第 2 章 「釣り×機械学習」に挑戦した話 17 2.1 はじめに . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 2.2 Cloud AutoML について . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 2.3 AutoML Vision で⿂種判別機を作る . . . . . . . . . . . . . . . . . . . . . . . . . 20 2.4 おわりに . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 第 3 章 Elm + Haskell で作る Web アプリ (2019 年度編) 33 3.1 題材: AnaQRam . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 3.2 フロントエンドを作る . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 3.3 サーバサイドを作る . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 3.4 終わりに . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 第 4 章 メガベンチャーの新⼈賞を取るまでにやった 7 つのこと 49 4.1 はじめに . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 4.2 私がやった 7 つのこと . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50 v
  3. ⽬次 4.3 終わりに . . . . . . .

    . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 第 5 章 Bitrise の iOS Auto Provision を導⼊した話 55 5.1 本章でのゴール . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 5.2 Automatically manage signing とは . . . . . . . . . . . . . . . . . . . . . . . . . 55 5.3 導⼊⼿順 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 5.4 最後に . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 第 6 章 SSH 経由なプロキシを作る 61 6.1 OpenSSH におけるポート転送 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 6.2 プロキシサーバを作る . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 6.3 終わりに . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 第 7 章 Makefile マニアクス with Go 69 7.1 Makefile の誤解を解く . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 7.2 Makefile 実践テク . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 7.3 終わりに . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 著者紹介 95 vi
  4. 第 1 章 Go と Unity で作るマルチプレイヤー ゲーム 1.1 はじめに

    この本の記事のテーマは各⾃⾃由! です。せっかくの⾃由です。この機会に新しいことに挑戦し ます。今回の挑戦は、マルチプレイヤーゲーム開発です。サーバも Go で⾃作します。初挑戦という こともあり、 「簡単なゲームを簡単に作る」⽅法を模索します。 近年、サーバ側でゲームの処理を⾏っているゲームが増えています。STADIA などはサーバ側で 動作するゲーム画⾯をクライアントに送るだけです。ゲームにおいてサーバ側で処理を⾏うことは今 後ますます増えていくことでしょう。 私はサーバサイドのプログラム経験がほとんどありません。Go の経験もありません。⾒よう⾒ま ねで挑戦して書きます。また、最近社内で⾏われていた「クリーンアーキテクチャ」 *1読書会で得た 考え⽅も実験します。 対象読者 マルチプレイヤーゲームを作ってみたいけれど、何から⼿を付けて良いのかわからない⼈。 得られるもの ミニマムなマルチプレイヤゲームを作る広く浅い過程。 得られないもの 技術的な深い話。 *1 クリーンアーキテクチャ: https://www.amazon.co.jp/dp/4048930656 1
  5. 第 1 章 Go と Unity で作るマルチプレイヤーゲーム 1.2 技術構成 1.2

    技術構成 マルチプレイヤーゲームはプレイヤーが⼿にするクライアントと、ゲームをまとめるサーバにより 構成されます。クライアントとサーバは通信を⾏いますので通信プロトコルが必要です。通信するた めにはプログラムで使⽤しているオブジェクトを通信できる形に変換するシリアライザが必要です。 今回、サーバは Go で開発します。クライアントは Unity で作成します。通信は TCP ソケットを 使⽤します。シリアライザは、Protocol Buffers を使⽤します。 サーバは Go Go はサーバ⽤に開発された⾔語です。何やら、サーバが簡単に書けるという噂です。サンプル コードを読むと、たしかに無駄のないシンプルなコードです。こんなに簡単にサーバを作れるのかと 衝撃を受けました。サーバは⼤量のリクエストをさばく必要があります。CPU のコアを有効に使う にはマルチスレッドが必須です。Go はゴルーチンというスレッドみたいなものを、OS のスレッド を活⽤しながら⼤量に⽣成が可能です。書籍「Go ⾔語による並⾏処理」 *2の帯には「ゴルーチンを湯 ⽔のごとく使え!」と⼒強く書いてあります。並⾏処理をゴルーチンに分けたとしても、それらが協 調するしくみも必要ですが、それはチャネルとして提供されます。チャネルを使⽤することで並⾏処 理を安全に書けます。 クライアントは C#(Unity) すばらしいゲーム開発環境です。開発⾔語は C#です。 通信は TCP ソケット インターネットの接続はソケットというものを使⽤します。ソケットにはいろいろなプロトコルが ありますが、代表的なものは UDP と TCP です。TCP は細かいことを気にしなくてもちゃんと通 信ができます。UDP は⾃⼰責任でいろいろやれば最適なものを作れます。UDP と TCP 詳細は別途 調べていただくことにして、今回は簡単に作るために TCP を使⽤します。 シリアライザは Protocol Buffers シリアライザはたくさんありますが、今回は Protocol Buffers を選びました。速度で⽐較する ともっと速いシリアライザがありますが、これにはそれを上回るメリットがあります。Protocol Buffers の最⼤のメリットは、各種プログラム⾔語の橋渡しができることです。proto ファイルを定 義することで、そこから Go、C#などのさまざま⾔語のシリアライザが⽣成できます。今回のよう な、Go と C#の間を通信するときには、もってこいです。 *2 Go ⾔語による並⾏処理: https://www.amazon.co.jp/dp/4873118468 2
  6. 第 1 章 Go と Unity で作るマルチプレイヤーゲーム 1.3 ゲームを作る シリアライズとデシリアライズとシリアライザ

    シリアライザについて補⾜します。C#のオブジェクトは、そのままの形ではソケットで送受信が できません。ソケットでやりとりできるのはバイト配列ですので、変換が必要です。 そのような変換をシリアライズといいます。逆に、受け取った⽅はそのバイト配列をオブジェクト に変換します。これをデシリアライズといいます。 シリアライズとデシリアライズのコードを⼿書きすると量が膨⼤になりますし、ミスも含まれま す。その仕事を肩代わりしてくれるのがシリアライザです。 1.3 ゲームを作る さてゲームを作りましょう。今回は作り⽅を学ぶだけですので、ゲームというには物⾜りないゲー ムを作ります。 ゲームの概要 起動すると、プレイヤーは「その世界」に現れます。そこには、敵がいます。もしかしたら、ほか のプレイヤーもいます。プレイヤーは移動と攻撃ができます。敵を攻撃して倒すとゴールドがでま す。ゴールドを集めると⾃慢できます。敵も攻撃します。プレイヤーは、敵の攻撃を⾷らうとライフ が減ります。ライフがゼロになると死にます。 作り⽅を作る 作る流れを考えます。ミニマムなものから順番に育てます。できるだけ⽴派に仕上げたい! とい う気持ちはいつもありますが、限られた時間という現実に打ちのめされます。現実に負けて完成し ないことが⼀番悲しいので、段階的に作ります。まずは、動作する最⼩のものの完成を⽬指します。 MVP(Minimum Viable Product) と呼ばれています。動くものができてしまえば、たとえおもしろ くなくても、いや最初はおもしろくなくて当たり前ですが、誰かに⾒せたりアイデアを実験したり する環境が⼿に⼊ります。発展の可能性を感じればさらに進めて、だめならまた別のものを試しま しょう。 何から作る? 下記のような流れで作ります。 1. ゲームの⾒た⽬を Unity で作る。 2. エンティティを分析する。 3. ゲームのエンティティのテストを作る。 4. ゲームのエンティティを実装する。 3
  7. 第 1 章 Go と Unity で作るマルチプレイヤーゲーム 1.3 ゲームを作る 5.

    通信部分を作る。 6. クライアントのコードを実装する。 7. Minimum Viable Product 完成。 8. PDCA!PDCA! クライアントの⾒た⽬を作る クライアントから着⼿します。とはいえ、コードはまだ書きません。Unity でオブジェクトを並べ て、必要なものを可視化します。プログラムで作るクラスには「⾒えないもの」もありますが、 「⾒ えないもの」は「⾒えるもの」のためにあります。紙に鉛筆で書いても良いのですが、Unity でオブ ジェクトを並べるほうが簡単です。クライアントに必要なものを Unity でシーンに並べました。 図 1.1: スクリーンショット 絵が出るとやる気が出てきます、脳内で動く姿が浮かんできます。この⾒た⽬を作るためにアセッ トを購⼊しました。Cartoon Cubic Characters Pack*3です。楽しい雰囲気がよいです。 エンティティを分析する 「エンティティ」の意味が、つかみどころのない⾔葉でしたので調べました。⼀番ふさわしく感じ たのが「エンティティは要素の識別とそのモデルである」という説明です。もう少し具体的に⾔い換 えると、ゲームの要素を抽出して、Id とか Enum とか Class を作って、それらの関係をメソッドで 表現することです。 さて、 「ゲームの概要」からエンティティを抽出します。単純に単語を羅列します。 「プレイヤー、 世界、敵、ライフ、移動、攻撃、ゴールド」があります。さらに、Unity のヒエラルキーにあるオブ ジェクトを羅列します。 「Ground、Hero、Boss、Camera、Light」です。⽇本語にして合成すると *3 Cartoon Cubic Characters Pack by 4TUDIO https://assetstore.unity.com/packages/3d/characters/ cartoon-cubic-characters-pack-124156 4
  8. 第 1 章 Go と Unity で作るマルチプレイヤーゲーム 1.3 ゲームを作る 「プレイヤー、世界、敵、ライフ、移動、攻撃、ゴールド、地⾯、ヒーロー、ボス、カメラ、ライト」

    になりました。それらを、アプリの中にゲーム世界があると考えて、アプリのエンティティとゲーム のエンティティに分けます。 ゲームの要素 世界、ヒーロー、ボス、ライフ、攻撃、ゴールド、地⾯ ゲームのルール ゲームの要素使って、ゲームのルールを作ります。要素と要素の関係と発⽣する出来事を書きま す。これがコーディング時の ToDo にもなります。 • ヒーロー:地⾯の上に乗る。 • ヒーロー:攻撃ができる。 • ヒーロー:移動ができる。 • ヒーロー:攻撃を⾷らうとライフが減る。 • ヒーロー:ライフがゼロになると死ぬ。 • ヒーロー:ゴールドを取得できる。 • ボス:地⾯の上に乗る。 • ボス:攻撃ができる。 • ボス:移動ができる。 • ボス:攻撃を⾷らうとライフが減る。 • ボス:ライフがゼロになると死ぬ。 • ボス:死ぬとゴールドを出す。 • 世界:ボスがいなくなるとボスを⽣成する。 • 世界:要求を受けるとヒーローを⽣成する。 アプリケーションのエンティティ アプリの役割は、⼈間とゲームを双⽅向につなぐことです。双⽅向とは 2 つの流れです。ひとつは ゲームを可視化してプレイヤーに⾒せること、もうひとつはプレイヤーの⼊⼒操作をゲームに伝える ことです。 • アプリ:プレイヤーの⼊⼒を受け付けて、ゲームに伝える。 • アプリ:プレイヤーの⽣成要求をゲームに伝える。 • アプリ:プレイヤーのヒーローの位置にカメラを動かす。 • アプリ:カメラから⾒える世界を画像にする。 • アプリ:ネットワークに接続する。 5
  9. 第 1 章 Go と Unity で作るマルチプレイヤーゲーム 1.3 ゲームを作る •

    アプリ:ネットワークから世界の情報を取得する。 ここまで分析すると作るべきものが⾒えてきます。コードが書けそうです。 ゲームエンティティの実装 ゲームのエンティティをインタフェースで表現して、最終的に struct に合成しました。 リスト 1.1: chara.go type IChara interface { ILife IPosition IAttacker ICharaStatus ID() uuid.UUID } type IHero interface { IChara IGold } ゲームエンティティのテストを書く ゲームのルールとエンティティはサーバで実⾏します。そのため Go で作成します。ヒーローのテ スト部分を抜粋しました。下記のようなコードになります。 リスト 1.2: game_test.go package main import ( "github.com/stretchr/testify/assert" "testing" "fmt" . "github.com/g3n/engine/math32" ) func testHero(t *testing.T) fmt.Println("ヒーロー⽣成") hero := NewHero() fmt.Println("ヒーローのライフは 100 である") assert.Equal(t, hero.Life(), HeroLife) fmt.Println("ヒーロー:移動ができる。地⾯に潜らないので、y はマイナスにならない") hero.Move(&Vector3{10, -10, 0}) 6
  10. 第 1 章 Go と Unity で作るマルチプレイヤーゲーム 1.4 Protocol Buffers

    hero.Move(&Vector3{0, 0, 10}) assert.Equal(t, *hero.Position(), Vector3{10, 0, 10}) fmt.Println("ヒーロー:ダメージを⾷らうとライフが減る。") hero.Damage(10) assert.Equal(t, hero.Life(), 90) fmt.Println("ヒーロー:ライフがゼロになると死ぬ。") hero.Damage(1000) assert.Equal(t, hero.Life(), 0) assert.Equal(t, hero.Status(), Dead) fmt.Println("ヒーロー:ゴールドを取得できる。") hero.AddGold(10) assert.Equal(t, hero.Gold(), 10) } 1.4 Protocol Buffers 先述のとおり、Protocol Buffers を使⽤します。 共通で使⽤する protoc のインストール protoc を イ ン ス ト ー ル し ま す 。https://github.com/protocolbuffers/protobuf/ releases/protoc-3.9.0-win64.zip を 展 開 し て 、protoc が 存 在 す る デ ィ レ ク ト リ を 環 境 変数の PATH に追加します。 サーバ (Go) 側のインストール Go のコードを protoc が出⼒するには protoc のプラグインが必要です。下記のコマンドでインス トールできます。 $ go get -u github.com/golang/protobuf/protoc-gen-go/ クライアント (Unity, C#) 側のインストール VisualStudio の NuGet を使⽤して必要な dll を⼊⼿します。 1. VisualStudio で新規にプロジェクトを作成します。 2. プロジェクトの種類は何でも良いと思うのですが、コンソールアプリ (.NET Core) を選びま 7
  11. 第 1 章 Go と Unity で作るマルチプレイヤーゲーム 1.4 Protocol Buffers

    した。 3. そして、 「ツール->NuGet パッケージマネージャー>ソリューションの NuGet パッケージの 管理」を開きます。 4. Google.Protobuf と Google.Protobuf.Tools をインストールします。 *4 5. Google.Protobuf.dll を Unity のプロジェクトにコピーします。 *5 Protocol Buffers でシリアライザを⽣成 proto ファイル作成して protoc を実⾏すると、Go と C#のコードを⽣成されます。 proto を書く メッセージは Protocol Buffers で定義した BattleMessage 型でやりとりします。BattleMessage は details を持っていて、これには Any 型であり任意の型を⼊れられます。ボスを作る、ヒーローが 移動するといった機能ごとにメッセージを定義し、details に⼊れて送信します。受信側は detail の 型から呼び出すべき関数を決定します。 ヒーローの部分だけ抜粋します。 リスト 1.3: dragon.proto syntax == "proto3"; import "google/protobuf/any.proto"; package main; message BattleMessage { google.protobuf.Any details == 1; } message Position { float x = 1; float y = 2; float z = 3; } message GuidBytes { bytes data = 1; } message CreateHero { GuidBytes id = 1; *4 c:/users/**/.nuget/packages/ に⼊ります。(**はユーザー名です。) *5 c:/users/**/.nuget/packages/ges/google.protobuf/3.9.1/lib/net45/Google.Protobuf.dll にあります。 8
  12. 第 1 章 Go と Unity で作るマルチプレイヤーゲーム 1.5 通信する Position

    position = 2; } message HeroMove { GuidBytes id == 1; Position position = 2; } message HeroAttack { GuidBytes id = 1; } protoc を実⾏する protoc に、⼊⼒と出⼒の場所と proto ファイルを指定して実⾏します。 protoc -I. --csharp_out=./DragonClient/Assets/Dragon/Scripts/ \ --go_out=./DragonServer dragon.proto 成功すると、 dragon.pb.go と Dragon.cs が⽣成されます。 1.5 通信する 切れ⽬のないデータから切り出す TCP はバイト配列でやりとりしますが、そのデータはストリームとよび、切れ⽬がわかりません。 ⾃分で切り出す必要があります。デシリアライズするときは、シリアライズの結果のバイト配列を過 不⾜なく渡します。切れ⽬のないストリームから、デシリアライズに必要な塊を切り出します。 具体的には |サイズ (4byte)|データ (任意 byte)| という構造を作ります。この構造により、データ の塊のサイズを受信し、その後そのサイズ分のデータを受信して、デシリアライズができます。 サーバ側の通信 Go の⽅は「プログラミング⾔語 Go」 *6のチャットアプリのサンプルをベースに作成しました。 チャネルをうまく使って並⾏処理がスッキリ書いてあります。https://github.com/adonovan/ gopl.io/blob/master/ch8/chat/chat.go この URL のサンプルにコードを追加します。ゴルー *6 プログラミング⾔語 Go: https://www.amazon.co.jp/dp/4621300253 9
  13. 第 1 章 Go と Unity で作るマルチプレイヤーゲーム 1.5 通信する チンの中で指定バイト数を受信し、Protocol

    Buffers のデシリアライズ (Unmarshal) をします。 BattleMessage のデシリアライズに成功したら、コマンドを実⾏するゴルーチンにチャネルで BattleMessage を送ります。下記に、その部分のコードを抜粋しました。 リスト 1.4: main.go sizeBuf := make([]byte, 4) for { var bodySize int32 // size { _, err := io.ReadFull(conn, sizeBuf) if err != nil { break } bodySize = byteToInt(sizeBuf) } // body { bodyBuf := make([]byte, bodySize) _, err := io.ReadFull(conn, bodyBuf) if err != nil { break } r := new(BattleMessage) proto.Unmarshal(bodyBuf, r) messages <- r.Details.String() commands <- *r } } それぞれの解析に必要なサイズが貯まるまでブロックして貯まると実⾏されます。BattleMessage のデシリアイズが成功すれば commands チャネルで全体の処理へ回します。ゴルーチンはブロック しますが、その間に Go は別のゴルーチンの処理を進めます。プログラマーが yield return や、await などを書く必要がないところが Go の魅⼒です。 C#の場合 画⾯を更新するという定期処理があるので Go のときほど簡単にはいきません。ブロックしないよ うに受信して、BattleMessage に必要なデータを受信するまでバッファリングします。 リスト 1.5: Network.cs void Recv() { // 受信処理 var s = tcp.GetStream(); 10
  14. 第 1 章 Go と Unity で作るマルチプレイヤーゲーム 1.5 通信する if

    (s.CanRead) { if (s.DataAvailable) { var count = s.Read(buffer, 0, buffer.Length); for (var n = 0; n < count; ++n) { buffer2.Add(buffer[n]); } } } // サイズチェック int size = 0; if (buffer2.Count < 4) { return; } var sizeBuf = new byte[4]; for (var n = 0; n < 4; ++n) { sizeBuf[n] = buffer2[n]; } if (BitConverter.IsLittleEndian) { Array.Reverse(sizeBuf); } size = BitConverter.ToInt32(sizeBuf, 0); // コマンド成⽴? if (buffer2.Count < 4 + size) { return; } var buffer3 = buffer2.ToArray(); { Debug.Log($"recv command {size}"); var battleRequest = BattleMessage.Parser.ParseFrom(buffer3, 4, size); Debug.Log($"enqueue {battleRequest.Details.TypeUrl}"); this.recieved.Enqueue(battleRequest); } // 残りの部分を詰める buffer2.Clear(); for (var n = size + 4; n < buffer3.Length; n++) { buffer2.Add(buffer3[n]); } } 必要なデータがそろったらデシリアライズして、BattleMessage のキューにためます。別の場所で そのキューから取り出して実⾏します。 11
  15. 第 1 章 Go と Unity で作るマルチプレイヤーゲーム 1.6 クライアントとサーバのやりとりの例 1.6

    クライアントとサーバのやりとりの例 やりとりの⼿順を決めて実装します。 ゲーム開始時 1. サーバが起動すると、内部でワールドとボスを⽣成します。そして、クライアントを待ちます。 2. クライアントが起動すると、Join をサーバに送ります。 3. サーバは、Join を受け取ると、Hero を⽣成します。そして、その時点に存在するボスとヒー ローの数だけ CreateBoss、CreateHero をクライアントに送信します。 4. クライアントは、CreateBoss, CreateHero を受信すると、それぞれのオブジェクトを⽣成し ます。 ヒーローの移動のやりとり 1. クライアントで⼊⼒があると HeroMove をサーバに送信します。 2. サーバは、HeroMove を受け取ると、ヒーロの位置情報を更新し、接続済みの全クライアント に HeroMove を送ります。 ボスの移動 ボスはサーバで動作させます。しかしサーバの CPU リソースは節約したいので、クライアントか らアクセスがないときは⽌まるようにします。そのため、クライアントから何かアクセスがあると前 回の更新時間からの経過時間をチェックして、ボスを更新します。クライアントは、接続を確認する ためのハートビートとして⼀定間隔で必ず何かを送信します。 1. クライアントから何かアクセスがある。 2. 前回の更新時間から⼀定以上経過していれば、ボスを移動させる。 3. ボスの移動情報を全クライアントに送信する。 1.7 MVP の完成 現時点では、ボスが現れ、ヒーローが複数現れ、ヒーローが移動するだけです。ほかのダメージな どのゲームエンティティは実装済みですが、クライアントの実装と通信コマンドの追加がまだです。 そして、世の中に公開するためにはまだまだいろいろな作業が残っています。でも、動いています。 最低限の動くものができました。⽴派な MVP です。 12
  16. 第 1 章 Go と Unity で作るマルチプレイヤーゲーム 1.8 まとめ 図

    1.2: スクリーンショット 1.8 まとめ 今後のプロジェクトで使⽤するかもしれないので、Go を学びつつ、Protocol Buffers を学びまし た。また、プログラマーとしての基礎体⼒をつけるため、ゲームの分析から、シリアライズ⽅法の検 討、MVP の作成までを⾏いました。 「マルチプレイヤーゲームを作ってみたいが、何から⼿を付けて 良いのかわからない⼈。 」の取っ掛かりになれば幸いです。 1.9 今後の予定 公開⽤サーバの検討 AWS vs Heroku AWS でサーバを⽴てようとしましたが、どうにもオーバースペックです。選択肢が多すぎてセッ トアップをするだけで消耗してしまいます。Heroku はそのあたりをかなり簡単にしてくれそうで す。もちろん Go にも対応しています。どうやら Heroku は AWS で動作しているのですが、東京 リージョンには未対応(enterprise は対応)ですのでレスポンスは不利かもしれません。また、仕事 で使うかもしれないのは、Heroku より AWS です。学習的な側⾯からも AWS の⽅が良いかもしれ ません。 通信の暗号化 意外とチートする⼈は多いみたいです。パケットを暗号なしで流すと簡単に解析できてしまいま す。いたちごっこにはなるのですが、少なくとも「簡単」にはできないくらいの対策は⼊れます。 13
  17. 第 1 章 Go と Unity で作るマルチプレイヤーゲーム 1.10 雑記 認証

    せっかくのネットワークゲームですので、経験値とかはどんどんためていきたいですよね。そのた めには、プレイヤーが⾃分のデータにアクセスできるようにする必要があります。パスワードとかパ スワードを忘れたらメールでリカバリさせるとか作るとなるとそれなりに⼯数がかかります。このあ たりも Firebase や AWS にも便利なサービスがあるようです。 1.10 雑記 Protocol Buffers との距離感 Protocol Buffers に半信半疑でしたので、⽣成される型がゲームのコードに⼊らないように作りま した。当然、変換コードを⼿書きしているので、冗⻑さにしんどくなります。特に、Go の知識が浅 いせいなのですが、無駄が多くなります。Protocol Buffers と結婚、あるいは⼼中する覚悟ができれ ばコードがスッキリします。 ゲームルールとエンティティ 最近、クリーンアーキテクチャという本を読みました。それによるとアーキテクチャの設計におい て重要なのはビジネスルールであり、ユースケースです。フレームワーク、⾔語、プラットフォーム、 ハードウェアなどの技術要素は「詳細」であり「⼿段」です。もちろん、⼿段の重要性は⼀段下がり ます。 エンジニア職である⾃分は、技術の⽅が重要だと思っていました。特段意識していたわけではあり ませんが、それが当たり前だと思っていました。技術書に技術の優先度はビジネスルールより低いと 指摘されました。⾔われてみれば仕事として当然です。この本はビジネスルールの変更に合わせて⼿ 段を変更すべきであり、⼿段は交換可能にすべきだと主張しています。今回、Unity と Go を使うと いう⼿段の選定を先にやって、ゲームのルールとユースケースの分析に移りました。この本に従うな ら逆であるべきでしょう。 テストを先に書く 最近はテストを先に書くようにしています。テストを書くと、そのコードがどう使われるのかが はっきりします。テストは使⽤例にもなるのでコードの理解の助けになります。Unity にもテストラ ンナーがありますし、Go も go test と書くとテストが⾛る機能が標準で⼊っています。昔はテス トのためのライブラリを⽤意したり組み込む⼿間がありましたが、今はテストが当たり前になりま した。 14
  18. 第 1 章 Go と Unity で作るマルチプレイヤーゲーム 1.11 参考 gRPC

    を検討したが⾒送った 当初は gRPC も候補でしたがやめました。 通信の先は通信相⼿のある関数を呼び出すので、シリアライズのほかに、関数を呼び出すコードが 必要です。gRPC はその呼び出すまでの⼀連のコードを Protocol Buffers と同様に、proto ファイル から多数の⾔語⽤に⽣成してくれます。とても魅⼒的です。 しかし、⾒送ったのは gRPC はリクエストとレスポンスが対になる設計だからです。gRPC で相 ⼿の関数を呼ぶと、必ず相⼿の返答があります。しかし、ゲーム中に送受信するデータは対にはなり ません。たとえばキャラが移動すると、座標をどんどん相⼿に送ります。また、ほかのプレイヤーの ヒーローの座標もリクエストをなしでどんどん受信したいのです。 Go の感想 Go は並⾏処理の機能が⾔語の設計段階から⼊っているので、とても素直なコードになっています。 並⾏に関する機能を後付けした⾔語には真似できないレベルです。たとえば、C#の、async, await, yield のようなキーワードがありません。 Go は変数の宣⾔の順番が C, C++, C#, Java... と逆順なのでかなり⼾惑いました。int[] foo; で はなく var foo []int と書きます。その⽅が読みやすい! という理屈に納得してはいるのですが、慣 れるには時間がかかりそうです。 並⾏処理間での情報共有にはロックで調停するのですが、気を付けないとデッドロックの原因にな ります。Go はチャネルでゴルーチンをつなぐことで、このロックを避けています。トラブルが防⽌ されます。 1.11 参考 参考⽂献 • プログラミング⾔語 Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES) Alan A.A. Donovan (著), Brian W. Kernighan (著), 柴⽥ 芳樹 (翻訳) https://www. amazon.co.jp/dp/4621300253 • Go ⾔語による並⾏処理 Katherine Cox-Buday (著), ⼭⼝ 能迪 (翻訳) https://www.amazon. co.jp/dp/4873118468 • クリーンアーキテクチャ Robert C.Martin (著), ⾓ 征典 (翻訳), ⾼⽊ 正弘 (翻訳) https: //www.amazon.co.jp/dp/4048930656 参考サイト • Protocol Buffers https://developers.google.com/protocol-buffers/ 15
  19. 第 1 章 Go と Unity で作るマルチプレイヤーゲーム 1.11 参考 •

    プログラミング⾔語 Go のサンプル https://github.com/adonovan/gopl.io 16
  20. 第 2 章 「釣り×機械学習」に挑戦した話 Google が公開している機械学習のプロダクトを使って、趣味の釣りと機械学習をかけ合わせた個 ⼈プロジェクトを紹介します。 2.1 はじめに モバイルアプリや

    Web サービスには機械学習が使⽤されています。⼀般的には AI という⾔葉で ⾝近となりました。Google は Google Cloud Next 2017 の Keynote で AI の⺠主化について発表し ました。Google は多くの⼈に AI の恩恵を受けられるよう、画像認識 API や翻訳 API を始め多くの プロダクトをリリースしてきました。また Google I/O 2019 では、モデルの収縮化に成功したこと や AutoML Vision Edge*1を発表しました。この発表により、インターネットを介さないオフライン の画像認識を取り⼊れやすくなりました。こちらの技術は近年課題となっている、プライバシーの保 護にもつながると注⽬を集めています。 機械学習のレイヤと学ぶハードル 機械学習をプロダクトに取り⼊れたいと思ったとき、またはゼロから機械学習を学びたいと思った とき何から始めれば良いのか分からないことが多いです。筆者も機械学習を触ることは数学などの基 礎知識がないと何もできない印象がありました。しかし Google が AI の⺠主化を⽬指しているため、 機械学習の深い知識がなくとも機械学習の恩恵を受けられるよう多くの機械学習プロダクトを発表し ています。各プロダクトには特徴があり、実装難易度が⾼いものほど⾃由度が⾼く、⾃由度は低いが 容易に実装できるものがありますので、プロダクトやご⾃⾝の状況に合わせて使い分けていただけれ ばと思います(図 6.1) 。これから紹介する技術は 2019 年 8 ⽉執筆時点の情報です。GCP のプロダ クトは頻繁にアップデートがあるため、公式サイトの情報も合わせて参照することを推奨します。 *1 AutoML Vision Edge: https://firebase.google.com/docs/ml-kit/automl-image-labeling 17
  21. 第 2 章 「釣り×機械学習」に挑戦した話 2.1 はじめに 図 2.1: 機械学習の実装難易度と⾃由度 ML

    APIs 画像認識や機械翻訳などの機械学習プロダクトを REST API で公開しているものです。画像認識 API であれば画像を送信するだけで認識結果を JSON 形式で返してくれます。こちらは学習済のモ デルを使⽤します。 Cloud AutoML 教師データをアップロードし、オリジナルモデルを作成します。モデルの学習や推論のコーディン グは不要で、教師データの準備とラベル付などの簡単な作業だけでモデルを実装できます。作成した モデルは TensorFlow Lite 向けのモデルに書き出せたり、ML APIs のように API リクエストで推 論できます。 BigQuery ML BigQuery に保存されているデータからモデルを作成します*2。学習、評価、推論を SQL Query で実装するのが特徴です。線形回帰(予測) 、2 項ロジスティック回帰(分類) 、多項ロジスティック 回帰(分類) 、K 平均法クラスタリングのモデルをサポートしています。 カスタムモデル TensorFlow, XGBoost などを使いモデルの学習から推論まですべてを実装します。より深い機械 学習の知識やアルゴリズムの知識が必要となりますが、⼀番⾃由度が⾼いです。 *2 BigQuery ML: https://cloud.google.com/bigquery-ml/docs 18
  22. 第 2 章 「釣り×機械学習」に挑戦した話 2.2 Cloud AutoML について 2.2 Cloud

    AutoML について Cloud AutoML*3は機械学習に詳しくない場合でも、教師データの準備と GUI の操作で簡単にカ スタムモデルを作成できるプロダクトです。Cloud AutoML は下記のような機械学習をサポートし ています。⾃分たちのユースケースに合わせた⽅法で分析したいときに便利なプロダクトです。今回 は Cloud AutoML Vision を使った画像認識の例を紹介します。 AutoML Natural Language AutoML Natural Language*4は⽂章コンテンツを独⾃のカテゴリに分類したり感情分析をするモ デルを作成します。たとえばニュース記事を独⾃のカテゴリで振り分けたい時に便利です。 AutoML Tables AutoML Tables*5は表形式のデータを分析するモデルを作成します。購買履歴から収益を最⼤化 したり、不正⾏為をしているユーザーを特定したい時に便利です。 AutoML Translation AutoML Translation*6は翻訳のモデルを作成します。通常の機械翻訳では認識できない専⾨的で ニッチな語彙を翻訳したい時に便利です。 AutoML Video Intelligence AutoML Video Intelligence*7は動画の分析やオブジェクトトラッキングをするモデルを作成しま す。動画に含まれる不適切なコンテンツを⾒つけたり、動画の内容に応じた広告を作成したい時に便 利です。 AutoML Vision AutoML Vision*8は画像認識のモデルを作成します。どんな画像か認識したい時に便利です。 *3 Cloud AutoML: https://cloud.google.com/automl *4 AutoML Natural Language: https://cloud.google.com/natural-language/automl *5 AutoML Tables:https://cloud.google.com/automl-tables *6 AutoML Translation:https://cloud.google.com/translate/automl *7 AutoML Video Intelligence:https://cloud.google.com/video-intelligence/automl *8 AutoML Vision:https://cloud.google.com/vision/automl 19
  23. 第 2 章 「釣り×機械学習」に挑戦した話 2.3 AutoML Vision で⿂種判別機を作る 2.3 AutoML

    Vision で⿂種判別機を作る AutoML Vision は独⾃に定義したラベルにしたがって画像を分類するような機械学習モデルを作 成できます。たとえば、あなたは料理店を営んでいるとします。お店のメニューを識別したいと思っ た時、お店のメニューの写真とメニュー名のリストさえ⽤意すれば、AutoML Vision が⾃動的に解 析しメニュー識別のモデルを作成します。Cloud Vision API では「ラーメン」や「ハンバーグ」と いった抽象的な画像認識はできても、お店独⾃のメニューを判別することはできません。今回のプロ ジェクトでは過去に釣った⿂の画像と AutoML Vision で⿂種判別機の作成に挑戦しました。 AutoML Vision を理解し画像データを準備する AutoML Vision を使う前にユースケースと必要な画像データを理解しておくことが重要です。ど のような結果がほしいか、その結果を得るにはどのような画像データとカテゴリが必要か、そして⼈ 間がそのカテゴリを認識できるかが重要です。⼈間が認識できないものは Cloud AutoML でも認識 が困難ということを念頭に置きましょう。また教師データとして扱う画像データは、1 ラベル毎に最 低 100 枚以上のサンプルの画像が必要であり、1000 枚以上が望ましいとされています。そして、各 ラベルのサンプル数はそれぞれ均等になるよう推奨されているため、最もサンプル数の少ないラベル が、最もサンプル数の多いラベルの 10% 以上となるようにします。たとえば最もサンプル数の多い ラベルが 5000 枚だった場合は、どのラベルも最低 500 枚のサンプルを⽤意するのが望ましいです。 以上のことを踏まえてた、事前チェックの例は次のとおりです。 どんな結果がほしいかゴールを決める 釣った⿂の画像で⿂種判別機を作りたい。釣り場で撮影された画像であり、1 ファイルにつき 1 匹 の⿂の画像が写っているものとする。 その画像認識は Cloud Vision API では実現不可能か Cloud Vision API ではロックフィッシュ*9やフラットフィッシュ*10といった抽象的な⿂種の判別 となるため、より詳しい⿂種を判別するために Cloud AutoML が必要。 *9 ロックフィッシュ: アイナメやソイなど岩場に⽣息する⿂の総称 *10 フラットフィッシュ: カレイやヒラメなど砂地に⽣息する⿂の総称 20
  24. 第 2 章 「釣り×機械学習」に挑戦した話 2.3 AutoML Vision で⿂種判別機を作る 図 2.2:

    Cloud Vision API の結果 ⼈間でも識別することが可能か 各ラベル(⿂種)の特徴がハッキリしているため、⿂に少し詳しいひとなら⼗分識別できる。⼤き さによって名前が変わる出世⿂は対象外とする。 各ラベルのサンプル枚数が 100 枚以上かつ均等に⽤意しているか 1種類に付き 100 枚から 150 枚ほどのサンプル画像を⽤意している。 教師データの準備 1. 教師データを釣る 2. 画像データの準備と整理 3. Google Cloud Storage に画像をアップロード 4. ラベル付き CSV を準備 教師データを釣る テーマは「釣り×機械学習」ですので、教師データの元となる⿂はすべて⾃分の⼿で釣りました。 最終的に集めた画像枚数は 10000 枚ほどで、そこから使えそうな画像を 1862 枚選びました。いつも は外道*11で残念と思う⿂も今回のプロジェクトでは貴重な教師データとなりました。 *11 外道: 狙っている本命以外の⿂ 21
  25. 第 2 章 「釣り×機械学習」に挑戦した話 2.3 AutoML Vision で⿂種判別機を作る ⿂種 学習に使⽤した画像枚数

    アナゴ 142 枚 アナハゼ 134 枚 ギスカジカ 192 枚 ヒガンフグ 135 枚 イシガレイ 151 枚 クジメ 149 枚 クサフグ 166 枚 マコガレイ 163 枚 メバル 145 枚 メゴチ 163 枚 ムラソイ 163 枚 リュウグウハゼ 159 枚 画像データの準備と整理 Cloud AutoML Vision に使⽤するサンプルを各ラベル毎に 100 枚以上、各ラベル均等な枚数で準 備しましょう。画像を撮影する際はいろいろな⾓度で撮影するのがコツです。画像を 1 枚 1 枚撮影、 管理するのはたいへんですので、物体を動画撮影し FFmpeg や OpenCV で静⽌画を取り出しましょ う。ファイル形式は JPEG、PNG、WebP、GIF、BMP、TIFF に対応しており、最⼤ファイルサ イズ 30MB となります。また誤った画像が混ざっていると、正しく学習されないので誤った画像が ⼊らないよう意識しましょう。画像データは各ラベル毎にディレクトリを⽤意すると便利です。 // ラベル毎にディレクトリを分ける images/ainame/ainame001.jpg images/mebaru/mebaru001.jpg // あるいはラベルとセット毎に分ける images/ainame/train/ainame001.jpg images/ainame/validation/ainame002.jpg images/ainame/test/ainame003.jpg images/mebaru/train/mebaru001.jpg images/mebaru/validation/mebaru002.jpg images/mebaru/test/mebaru003.jpg Google Cloud Storage に画像をアップロード 画像データの準備ができたら GCS にアップロードします。アップロード先は{GCP のプロジェク ト ID-vcm}の配下にします。 $ gsutil -m cp -r ainame/ gs://projectid-vcm/images/ 22
  26. 第 2 章 「釣り×機械学習」に挑戦した話 2.3 AutoML Vision で⿂種判別機を作る ラベル付き CSV

    を作成 画像のアップロードが完了したら、画像とデータセットの種類、カテゴリラベルを紐付ける CSV データを準備します。 ▪データセットの種類 画像に対してデータセットの種類を指定します。データセットの種類は以下 の 3 種類があります。 • TRAIN – この画像を使⽤して、モデルをトレーニングします。 • VALIDATION – この画像を使⽤して、トレーニング中にモデルが返す結果を検証します。トレーニングで はより正解に近いパラメータを探すため、実利⽤に近いデータを使うのが望ましいです。 • TEST – この画像を使⽤して、モデルのトレーニング後にモデルの結果を検証します。 これらの値をセットしなかった場合、Cloud AutoML Vision は 3 つのセットをランダムに 80% を トレーニングに、10% を検証に、10% をテスト⽤に配分します。 ▪ラベル情報 画像に対してラベル情報を付与します。独⾃の単語でラベルを作成できますがラベル は、⽂字で始まり、⽂字、数字、およびアンダースコア以外を含まないようにする必要があります。 1 ファイル最⼤ 20 個のラベルを含めることができます。今回は 1 ファイルに付き 1 匹の⿂が写って いる場合の画像認識をしたいため 1 ファイルにつき 1 ラベルとします。複数のラベルを付けたい場 合は続けてカンマ区切りで増やします。 作成した CSV の例 TRAIN,gs://projectid-vcm/images/anago/train/anago001.jpg,anago VALIDATION,gs://projectid-vcm/images/anago/validation/anago002.jpg,anago TEST,gs://projectid-vcm/images/anago/test/anago003.jpg,anago TRAIN,gs://projectid-vcm/images/aji/train/aji001.jpg,aji VALIDATION,gs://projectid-vcm/images/aji/validation/aji002.jpg,aji TEST,gs://projectid-vcm/images/aji/test/aji003.jpg,aji 複数のラベルを付けた CSV の例 TRAIN,gs://projectid-vcm/images/anago/train/anago001.jpg,anago,congridae,maanago 作成した CSV は画像ファイルと同じように、{GCP のプロジェクト ID-vcm}の配下にアップロー ドしましょう。 23
  27. 第 2 章 「釣り×機械学習」に挑戦した話 2.3 AutoML Vision で⿂種判別機を作る データセットの作成 ラベル付き

    CSV を使いデータセットを作成します。Cloud AutoML Vision のコンソールにアク セスし NEW DATASET をクリックします。 図 2.3: データセットの作成 任意の名前を付けます。 図 2.4: データセット名を⼊⼒ 先ほどアップロードした CSV ファイルを指定し、CREATE DATASET を選択すると画像のイン ポート作業が始まります。 24
  28. 第 2 章 「釣り×機械学習」に挑戦した話 2.3 AutoML Vision で⿂種判別機を作る 図 2.5:

    インポート開始 画像のインポートは画像の容量や枚数によって時間がかかるので、コーヒーでも飲みながら気⻑に 待ちましょう。インポートは⾮同期で実⾏されるためコンソールを閉じても問題ございません。また 完了時と失敗時にはお知らせメールが届き、エラーログは GCS に保存されます。 図 2.6: インポート 画像のインポートとラベル付が完了すると、各ラベルの枚数や画像を確認できます。不要な画像を ⾒つけたら学習する前に削除しましょう。 図 2.7: 各ラベルとその枚数 25
  29. 第 2 章 「釣り×機械学習」に挑戦した話 2.3 AutoML Vision で⿂種判別機を作る トレーニング いよいよ学習します。TRAIN

    タブを選択し、各ラベルの枚数を確認し問題なければ START TRAINING をクリックします。 図 2.8: START TRAINING モデル選択の画⾯がでてきます。クラウドベース向けやオンデバイス向けなど、⽤途に合わせたモ デルを選択できます。今回は Edge TPU でリアルタイム推論をしたいため Edge を選択しました。 オンデバイス向けのモデルでは、Lowest, Best, Higher とモデルの品質を選択できます。認識精度が ⾼いものほど認識に時間がかかり、認識精度が低いものほど速く認識します。使⽤⽤途に合わせて使 い分けるとよいでしょう。 26
  30. 第 2 章 「釣り×機械学習」に挑戦した話 2.3 AutoML Vision で⿂種判別機を作る 図 2.9:

    モデル選択 学習もインポートと同じく時間がかかる作業ですので、コーヒーでも飲みながら気⻑に待ちましょ う。今回の実験では学習に1時間ほどかかりました。こちらの作業も⾮同期で実⾏されるため、コン ソール画⾯を閉じて問題ありません。 評価 EVALUATE タブから評価結果を確認できます。 27
  31. 第 2 章 「釣り×機械学習」に挑戦した話 2.3 AutoML Vision で⿂種判別機を作る 図 2.10:

    EVALUATE 推論 オンデバイス向けに作成したモデルですが、PREDICT タブから画像をアップロードし画像認識 の実際の精度を確認できます。この時推論に使⽤する画像は、なるべく学習データにはない未知の データをいれてみましょう。 教師データに⼀切使っていないイシガレイの画像で検証したところ、90% の確率でイシガレイと いう結果が出ました。 図 2.11: 90% の確率でイシガレイとでる 28
  32. 第 2 章 「釣り×機械学習」に挑戦した話 2.3 AutoML Vision で⿂種判別機を作る Edge TPU

    でリアルタイム画像認識 Edge TPU とは IoT (Internet of Things) デバイスなどの低電⼒デバイス向けに作成された ASIC (特定⽤途向け集積回路)で⾼性能な推論ができます*12。Edge TPU を搭載したデバイスが Coral と いうブランドで Google から販売されており、Edge TPU が直接シングルボードコンピュータに搭載 されたデバイス Coral Dev Board と USB で接続する USB アクセサリ版の Coral USB Accelerator があります*13。今回は Coral USB Accelerator と、ラズベリーパイ、カメラモジュールを使いオフ ライン推論を試してみました。上記のトレーニングで Edge 向けでモデルを作成しているので、簡単 にオフライン推論を実⾏できます。AutoML Vision の PREDICT タブからモデルとラベルが⼊っ た zip を GCS にエクスポートし、ラズベリーパイにモデルとラベルを保存します。 図 2.12: GCS にモデルとラベルをエクスポート USB Accelerator と、ラズベリーパイ、カメラモジュールを接続し、Coral の公式サイト*14を参考 に Edge TPU を実⾏するためのライブラリとサンプルをラズベリーパイにインストールします。 *12 Edge TPU: https://coral.withgoogle.com/docs/edgetpu/faq *13 Coral Edge TPU Devices: https://aiyprojects.withgoogle.com/edge-tpu *14 Get started with the USB Accelerator: https://coral.withgoogle.com/docs/accelerator/get-started 29
  33. 第 2 章 「釣り×機械学習」に挑戦した話 2.3 AutoML Vision で⿂種判別機を作る 図 2.13:

    EdgeTPU とラズベリーパイ ライブラリ、サンプル、モデル、ラベル情報がラズベリーパイ上にそろったらサンプルを実⾏し ます。 $ python3 Python ライブラリのパス/edgetpu/demo/classify_capture.py \ --model モデルのパス \ --label ラベルのパス 実⾏するとカメラが起動し画像認識が始まります。画像認識されたラベル名が画⾯上部に表⽰され ます。精度の⾼い低いにこだわらず、⼀番精度が⾼かったラベル名を表⽰するシンプルなサンプルで す。こちらは Lowest のモデルを使⽤したのですが、なんと 100ms 以下で判別できました。 図 2.14: 画像認識されたラベル名 mebaru が表⽰されている 30
  34. 第 2 章 「釣り×機械学習」に挑戦した話 2.4 おわりに 2.4 おわりに 機械学習初⼼者の筆者にとって機械学習は未知の世界のものでしたが、Cloud AutoML

    Vision を 使⽤することによってオリジナルの画像認識を作成でき、冒頭で紹介した AI の⺠主化を肌で感じる ことができました。また最も関⼼がある釣りをテーマにすることで、楽しく機械学習に触れることが できました。今後はさらに機械学習の知識を深めるために TensorFlow などの機械学習ライブラリを 使った開発にも挑戦したいと考えています。 31
  35. 第 3 章 Elm + Haskell で作る Web アプリ (2019

    年度編) 表題の通り、Elm と Haskell で Web アプリケーション作成します。⾔わずもがな、フロントエ ンドが Elm でサーバサイドが Haskell です。全体の流れとしては、まず Elm で簡単なアプリケー ションを作成し、それにバックエンドを Haskell で追加してみます。 本稿では下記のことを記述しています。 • Elm でステップバイステップにアプリケーションを作成する • Elm でポートを利⽤し端末のカメラデバイスにアクセスする • Haskell コードから Elm コードを⽣成しサーバとクライアントの重複を減らす 3.1 題材: AnaQRam まずは本稿で作成する Web アプリケーションについて説明します。作るのは「AnaQRam」とい う簡易的なパズルゲームです。 • アナグラムパズルと QR コードを組み合わせたもの • ⽂字列の各⽂字が伏字に (クエスチョンマーク) なっている • QR コードをスキャンすると該当の⼀⽂字だけ伏字が解ける • ⽂字列はシャッフルされているので並び替えて正解の⽂字列にする これは筆者が学⽣のころ (2016 年) に学祭の幼児向けゲームアプリ (Android) として作成したもの で、QR コードを部屋中に隠して宝探しする感じです。また過去にバージョン 2 として、正解までの タイムを記録してランキングするだけのバックエンドを Haskell で付けてみました (2017 年)*1。今 回はバージョン 3 として、これをブラウザアプリに書き直します。 *1 https://github.com/matsubara0507/anaqram-server 33
  36. 第 3 章 Elm + Haskell で作る Web アプリ (2019

    年度編) 3.2 フロントエンドを作る 図 3.1: AnaQRam の完成画像 3.2 フロントエンドを作る 前述した通り、ステップバイステップにいきましょう! なお、フロントだけの部分は GitHub の matsubara0507/anaqram-web リポジトリ*2にあります。また、各ステップでの成果物は matsubara0507/anaqram-web-samples リポジトリ*3にあります。 Step1. Elm からカメラを使う AnaQRam は QR コードを読み取れる必要があります。ですので、まずは Elm を使ってブラ ウザから端末のカメラデバイスにアクセスしてみましょう。ブラウザでカメラにアクセスするには MediaDevices.getUserMedia() という JavaScript のメソッド*4を呼び出す必要があります。現 在の最新バージョンである Elm-0.19 では Elm としてこのメソッドを呼び出す⽅法はないはずです ので、JavaScript を Elm から呼び出してあげる必要があります。とりあえずまずは JavaScript で getUserMedia() メソッドを呼び出してみましょう。そのために、今回は WebRTC のサンプルコー ドを参考にします*5。 サンプルコードからロギングとエラーハンドリングを省いたコード const constraints = { audio: false, video: true }; *2 https://github.com/matsubara0507/anaqram-web *3 https://github.com/matsubara0507/anaqram-web-samples *4 https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/getUserMedia *5 https://webrtc.github.io/samples/src/content/getusermedia/gum 34
  37. 第 3 章 Elm + Haskell で作る Web アプリ (2019

    年度編) 3.2 フロントエンドを作る async function initCamera(videoId) { try { const stream = await navigator.mediaDevices.getUserMedia(constraints); document.getElementById(videoId).srcObject = stream; } catch (e) { handleError(e); // ここの実装は割愛 } } HTML 側は id=videoId を設定した video タグを⽤意するだけで良いです。さて、これを Elm 側で利⽤するにはどうすれば良いでしょうか? ポート機能 Elm から JavaScript のコードを直接呼び出すにはポート機能を使います。ポート機能には Elm 側から呼び出す外向きのメッセージと JavaScript 側から呼び出す内向きのメッセージがあります*6。 今回はカメラを起動するだけなので外向きのメッセージを使います。 ポート機能の利⽤例 port module AnaQRam.QRCode exposing (..) port startCamera : () -> Cmd msg Elm は純粋関数型プログラミング⾔語です。そのため、副作⽤も型として表現する必要があり、 Cmd a 型はそのような型の⼀つです。つまり、Elm にとって JavaScript のコードを呼び出すことは 副作⽤となります。上記のコードは Elm 側のインタフェースで、実装は JavaScript 側で与えます。 ポート関数に JavaScript から実装を与える // flags は Elm コードの JavaScript 側から与える初期値 const flags = { ids: { video: ’video_area’ }, size: { width: 300, height: 300 } }; // true だけではなくカメラのサイズとリアカメラ優先フラグを与える const constraints = { audio: false, video: {...flags.size, facingMode: "environment" } }; const app = Elm.Main.init( { node: document.getElementById(’main’), flags: flags }); app.ports.startCamera.subscribe(function() { initCamera(flags.ids.video) }); あとはこんな感じに Elm 側で呼び出します (誌⾯の都合上 elm format したコードではありま せん)。 *6 詳しくは guide.elm-lang.jp のポートのページをみると良いでしょう https://guide.elm-lang.jp/interop/ ports.html 35
  38. 第 3 章 Elm + Haskell で作る Web アプリ (2019

    年度編) 3.2 フロントエンドを作る メインの Elm コード module Main exposing (main) import AnaQRam.QRCode as QRCode import Browser import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onClick) main : Program Config Model Msg main = Browser.element { init = init , view = view , update = update , subscriptions = \_ -> Sub.none } type alias Config = { ids : { video : String }, size : { width : Int, height : Int } } type alias Model = { config : Config } init : Config -> (Model, Cmd Msg) init config = (Model config, Cmd.none) type Msg = EnableCamera update : Msg -> Model -> (Model, Cmd Msg) update msg model = case msg of EnableCamera -> (model, QRCode.startCamera ()) view : Model -> Html Msg view model = div [] [ video [ id model.config.ids.video, style "background-color" "#000", autoplay True , width model.config.size.width, height model.config.size.height -- iOS のために必要 , attribute "playsinline" "" ] [] , p [] [ button [ onClick EnableCamera ] [ text "Enable Camera" ] ] ] 極めてシンプルな Elm コードですね。ボタンの onClick でイベントハンドラを受け取り、 startCamera ポート関数を呼び出しているだけです。Flags 機能を使って、video タグに必要な id を JavaScript 側と共有しています。これをコンパイルするとこんな感じになります。 36
  39. 第 3 章 Elm + Haskell で作る Web アプリ (2019

    年度編) 3.2 フロントエンドを作る 図 3.2: Step1 での成果物 (黒いところにカメラ画⾯が出⼒されます) Step2. カメラから QR コードを読み取る カメラは起動できたので、いよいよ QR コードを読み取ってみます。QR コードを読み取るのには jsQR という JavaScript のライブラリを使います*7。画像データを JavaScript から Elm に送り、 Elm 側でデコードするという⽅法も考えた (途中まではやってみた) のですが、ちょっと時間的に厳 しいので今回は⼿抜きします。jsQR の使い⽅は極めて簡単で、jsQR というメソッドに ImageData オブジェクト (とサイズ) を渡してあげるだけです。 jsQR の README に載っているサンプルコード const code = jsQR(imageData, width, height, options?); // QR コードがなければ null になるようです if (code) { console.log("Found QR code", code); } ImageData オブジェクトはカメラ画像をいったん Canvas に退避させることで取得できます。 カメラ画⾯を Canvas に退避して画像を取り出す function captureImage(videoId, captureId) { var canvas = document.getElementById(captureId); var video = document.getElementById(videoId); canvas.width = video.videoWidth; canvas.height = video.videoHeight; *7 https://github.com/cozmo/jsQR 37
  40. 第 3 章 Elm + Haskell で作る Web アプリ (2019

    年度編) 3.2 フロントエンドを作る const ctx = canvas.getContext(’2d’); ctx.drawImage(video, 0, 0); return ctx.getImageData(0, 0, video.videoWidth, video.videoHeight); } さて、これを Elm で呼び出してみます。カメラの起動と違い、QR コードをスキャンするボタン を押したら QR コードのデコード結果が返ってきてほしいですね? ですので、今度は外向きのメッ セージと内向きのメッセージを組み合わせましょう。 またポート関数を定義する port module AnaQRam.QRCode exposing (..) import Json.Decode as D exposing (Decoder) import Json.Encode as E type alias QRCode = { data : String } decoder : Decoder QRCode decoder = D.map QRCode (D.field "data" D.string) port startCamera : () -> Cmd msg port captureImage : () -> Cmd msg -- JS とは JSON データでやり取りするのが良いらしい port updateQRCode : (E.Value -> msg) -> Sub msg updateQRCodeWithDecode : (Result D.Error (Maybe QRCode) -> msg) -> Sub msg updateQRCodeWithDecode msg = updateQRCode (msg << D.decodeValue (D.nullable decoder)) Sub a 型は Elm にある副作⽤を扱うもう⼀つの型です (扱い⽅は後述します)。captureImage 関 数は startCamera 関数と同じ外向きのメッセージで、updateQRCode 関数は JavaScript 側から呼 び出す内向きのメッセージです。 内向きのメッセージは JavaScript 側から呼び出す // canvas の id を追加 const flags = { ids: { video: ’video_area’, capture: ’capture_image’ }, size: { width: 300, height: 300 } }; app.ports.captureImage.subscribe(function() { const imageData = captureImage(flags.ids.video, flags.ids.capture); const qrcode = jsQR(imageData.data, imageData.width, imageData.height) 38
  41. 第 3 章 Elm + Haskell で作る Web アプリ (2019

    年度編) 3.2 フロントエンドを作る app.ports.updateQRCode.send(qrcode); // ココ }) これで JavaScript 側の準備は完了です。あとは Elm のコードを更新します。Sub a 型は main 関数の subscribe フィールドに渡します。ガッっと書き換えてしまいます。 module Main exposing (main) import AnaQRam.QRCode as QRCode exposing (QRCode) import Json.Decode exposing (Error, errorToString) main : Program Config Model Msg main = Browser.element { .. -- 割愛 , subscriptions = subscriptions } -- capture を追加 type alias Config = { ids : { video : String, capture : String }, size : { width : Int, height : Int } } type alias Model = { config : Config , qrcode : Maybe QRCode -- QR コードのデコード結果 , error : String -- JSON のデコード失敗結果 } init : Config -> (Model, Cmd Msg) init config = (Model config Nothing "", Cmd.none) type Msg = EnableCamera | CaptureImage | UpdateQRCode (Result Error (Maybe QRCode)) update : Msg -> Model -> (Model, Cmd Msg) update msg model = case msg of ... -- 割愛 CaptureImage -> (model, QRCode.captureImage ()) -- QR コードがなかった場合 (null が返ってくるので) UpdateQRCode (Ok Nothing) -> ({ model | error = "QR code is not found" }, Cmd.none) -- QR コードのデコード成功 UpdateQRCode (Ok qrcode) -> ({ model | qrcode = qrcode, error = "" }, Cmd.none) -- JSON のデコード失敗 UpdateQRCode (Err message) -> ({ model | error = errorToString message }, Cmd.none) 39
  42. 第 3 章 Elm + Haskell で作る Web アプリ (2019

    年度編) 3.2 フロントエンドを作る view : Model -> Html Msg view model = div [] [ video -- 割愛 , p [] [ button [ onClick EnableCamera ] [ text "Enable Camera" ] ] , p [] [ button [ onClick CaptureImage ] [ text "Decode QR" ] ] , canvas [ id model.config.ids.capture, hidden True ] [] -- カメラ画像退避⽤ , viewResult model ] viewResult : Model -> Html Msg viewResult model = if String.isEmpty model.error then p [] [ text ("QR code: " ++ Maybe.withDefault "" (Maybe.map .data model.qrcode)) ] else p [] [ text model.error ] subscriptions : Model -> Sub Msg subscriptions _ = QRCode.updateQRCodeWithDecode UpdateQRCode 結果的に QR コードリーダーができあがりました。コンパイルするとこんな感じ。 図 3.3: Step2 での成果物 (Hello World という QR コードを読み取っています) 40
  43. 第 3 章 Elm + Haskell で作る Web アプリ (2019

    年度編) 3.2 フロントエンドを作る Step3. パズルゲームを作る さて、ここからがアプリロジックの話です (裏を返せば Elm 的におもしろい話ではないです)。関 数型プログラミングらしく、まずはパズルの型を考えてみましょう。 QR コードによってピースのオンオフ パズルには QR コードによるピースのオン (伏字をなくす) 部分と並び替え (アナグラム) の部分が あります。なのでピースの型とその配列を持つパズルの型を定義しましょう。 パズルの型 module AnaQRam.Puzzle exposing (..) type alias Puzzle = { pieces : Array Piece , answer : String } type alias Piece = { hidden : Bool -- 伏字のオンオフ , index : Int -- 元の位置 , char : Char } QR コードには答えの⽂字ではなく数字をエンコードします。そうすることで、どのような答えの ⽂字列であっても同じ QR コードを利⽤できます。index フィールドにはその数字を格納していま す。もちろん、答えを判定するときには char フィールドの値を連結したものと⽐較します。 パズルの成否判定 success : Puzzle -> Bool success puzzle = if String.isEmpty puzzle.answer then False else Array.map pieceToString puzzle.pieces |> Array.toList |> String.concat |> (==) puzzle.answer pieceToString : Piece -> String pieceToString piece = if piece.hidden then "? " else String.fromChar piece.char あとは update 関数の UpdateQRCode のときに該当のピースの hidden フィールドを Fasle に するだけです。このときに Mod をとることで、⽂字列⻑以上の数字の QR コードをスキャンしても 41
  44. 第 3 章 Elm + Haskell で作る Web アプリ (2019

    年度編) 3.2 フロントエンドを作る ちゃんと何かしらのピースをオンできます。 パズルのピースをオンする関数 display : Int -> Puzzle -> Puzzle display idx puzzle = let pIdx = modBy (size puzzle) idx updated = Array.map (displayPiece pIdx) puzzle.pieces in { puzzle | pieces = updated } displayPiece : Int -> Piece -> Piece displayPiece idx piece = if piece.index == idx then { piece | hidden = False } else piece size : Puzzle -> Int size puzzle = Array.length puzzle.pieces ピースを並び替える 並び替えるのは簡単です。ピースを 2 ヵ所タップしたら⼊れ替えるだけです。そのために、Model に⼀つ前にタップした位置 (ここでの位置はピース配列での位置です) を clicked フィールドに保存 します。 update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of ... ClickPiece idx -> updatePiece idx model updatePiece : Int -> Model -> (Model, Cmd Msg) updatePiece idx model = case model.click of Nothing -> ({ model | click = Just idx }, Cmd.none) Just oldIdx -> let updated = Puzzle.swapPiece idx oldIdx model.puzzle in ({ model | click = Nothing, puzzle = updated }, Cmd.none) 残りの部分は普通にプログラミングするだけなので割愛しますが、ClickPiece はピースのビュー のイベントハンドラで呼ばれます。 42
  45. 第 3 章 Elm + Haskell で作る Web アプリ (2019

    年度編) 3.3 サーバサイドを作る 図 3.4: Step3 での成果物 (正解すると Success! と出ます) 3.3 サーバサイドを作る 思いの外、フロントの話にページ数を割いてしまったので実装する内容は極めてシンプルです (実際のコードは matsubara0507/anaqram-web リポジトリの backend ブランチに置いておきます *8)。現状は問題の答えが固定になっているので、サーバにリクエストする形式にしてみましょう。 このときにサーバには⽂字列⻑をリクエストパラメータとして渡すようにします。 Haskell と Elm をつなぐ さて、愚直に実装すれば何とでもやりようはありますが、ここで⼀⼯夫をしてみましょう。可能な 限り、Elm と Haskell でコードの重複をなくすために、Haskell の実装から Elm の実装を⾃動⽣成 してみます。それには以下の 2 つの Haskell パッケージを使います。 • elm-bridge : Haskell の型定義から Elm の型定義と JSON デコーダを⽣成する*9 • servant-elm : Haskell Servant の API 型定義から Elm の API クライアントを⽣成する*10 今回は⾃⾝で定義した型ではなく、組込み⽂字列型を返すので elm-bridge の役割は基本的にあり ません。重要なのは servant-elm です。 *8 https://github.com/matsubara0507/anaqram-web/tree/backend *9 http://hackage.haskell.org/package/elm-bridge-0.5.2 *10 http://hackage.haskell.org/package/servant-elm-0.6.0.2 43
  46. 第 3 章 Elm + Haskell で作る Web アプリ (2019

    年度編) 3.3 サーバサイドを作る Haskell Servant Haskell Servant は Haskell の Web フレームワークの⼀つで、いわゆる RESTful API を型とし て定義できます。 答えの⽂字列を返す API の型 module AnaQRam.API where import Servant type API = "api" :> "sizes" :> Get ’[JSON] [Int] :<|> "api" :> "problem" :> QueryParam’ ’[Required] "size" Int :> Get ’[JSON] String この API 型は 2 つの RESTful API を表現しています。⼀つは api/sizes で対応している⽂字 列⻑のリストを返すもので、もう⼀つは api/problem?size=N で⽂字列⻑ N の⽂字列を返すもの です。Get の部分を Put や Post にすることでほかの HTTP リクエストメソッドを使うこともで きます。ここでは割愛しますが backend ブランチには残りの実装もしているので、プログラムを実 ⾏すると実際に curl localhost:8080/api/sizes などで値が返ってきます。 $ stack exec -- anaqram-web ../config.yaml --verbose 2019-08-13 19:45:58: [info] Please accsess to localhost:8080 @(src/AnaQRam/Cmd.hs:16:3) 2019-08-13 19:46:00: [debug] {"message":"GET: api=/sizes","level":"debug"} @(src/AnaQRam/API.hs:63:3) 2019-08-13 19:46:04: [debug] {"size":4,"message":"GET: api=/problem","level":"debug"} @(src/AnaQRam/API.hs:69:3) 2019-08-13 19:47:38: [debug] {"message":"GET: api=/sizes","level":"debug"} @(src/AnaQRam/API.hs:63:3) servant-elm servant-elm は上記の API 型のような Servant の型定義から Elm の API クライアントを⽣成し てくれます。⽣成するには次のような Haskell コードを記述します。 Elm の API クライアントを⽣成してくれる Haskell コード module Main where import AnaQRam.API (API) import Servant ((:>)) import Servant.Elm (defElmImports, defElmOptions, generateElmModuleWith) 44
  47. 第 3 章 Elm + Haskell で作る Web アプリ (2019

    年度編) 3.3 サーバサイドを作る main :: IO () main = do generateElmModuleWith defElmOptions ["AnaQRam", "Generated", "API"] defElmImports "elm-src" [] (Proxy @ API)) これを実⾏すると、たとえば次のような Elm コードを⽣成してくれます。 ⽣成された Elm コード (抜粋) module AnaQRam.Generated.API exposing (..) ... -- 割愛 getApiProblem : Int -> (Result Http.Error String -> msg) -> Cmd msg getApiProblem query_size toMsg = let params = List.filterMap identity (List.concat [ [ Just query_size |> Maybe.map (String.fromInt >> Url.Builder.string "size") ] ] ) in Http.request { method = "GET" , headers = [] , url = Url.Builder.crossOrigin "" [ "api" , "problem" ] params , body = Http.emptyBody , expect = Http.expectJson toMsg Json.Decode.string , timeout = Nothing , tracker = Nothing } 45
  48. 第 3 章 Elm + Haskell で作る Web アプリ (2019

    年度編) 3.3 サーバサイドを作る 便利! 問題の答えを受け取る さて、あとは⽣成したコードを Elm 側で呼んであげれば良いだけです。適切な Msg 型の値を追加 し、init 関数や update 関数で呼んであげるだけです (view 関数の部分は割愛)。 init : Config -> ( Model, Cmd Msg ) init config = ( Model config Nothing "" [] Puzzle.empty Nothing -- ページを開いたときに可能な⽂字列⻑を取得 , API.getApiSizes FetchWordSizes ) type Msg ... -- 割愛 | FetchWordSizes (Result Http.Error (List Int)) | FetchAnswer (Result Http.Error String) update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = -- model.puzzle.start はパズルゲームがスタートしてるかどうか case (model.puzzle.start, msg) of -- カメラを起動したらパズルゲームスタート (False, EnableCamera) -> ( { model | puzzle = Puzzle.start model.puzzle } , Cmd.batch [ QRCode.startCamera () -- パズルゲームをスタートするときにランダムな答えを取得 , API.getApiProblem (Puzzle.size model.puzzle) FetchAnswer ] ) (False, FetchWordSizes (Ok sizes)) -> ({ model | sizes = sizes }, Cmd.none) (False, FetchWordSizes (Err err)) -> ({ model | error = "can’t fetch sizes: " ++ httpErrorToString err }, Cmd.none) (True, FetchAnswer (Ok "") ) -> ({ model | error = "problem not found." }, Cmd.none) (True, FetchAnswer (Ok answer)) -> (model, Puzzle.shuffle ShufflePuzzle (Puzzle.init answer model.puzzle)) (True, FetchAnswer (Err err)) -> ({ model | error = "can’t fetch problem: " ++ httpErrorToString err }, Cmd.none) ... -- 割愛 46
  49. 第 3 章 Elm + Haskell で作る Web アプリ (2019

    年度編) 3.4 終わりに 簡単ですね! 3.4 終わりに 最後は駆け⾜になりましたがこれで可能な限り重複を減らした Elm + Haskell の Web アプリ ケーションができました。各ステップごとの状態を含め、フロントエンドだけのアプリは GitHub Pages に置いて実際に遊べるようになっています*11。ちなみに、このプログラムはあくまで趣味の もので、弊社に Elm + Haskell の組み合わせで開発しているプロダクトがあるわけではありませ ん (たぶん)。期待させてしまったらすいません笑 *11 フロントだけの最終的なアプリは https://matsubara0507.github.io/anaqram-web です。QR コードは https: //matsubara0507.github.io/anaqram-web/qrcode?size=6 から⽣成できます。 47
  50. 第 4 章 メガベンチャーの新⼈賞を取るまでに やった 7 つのこと 4.1 はじめに こんにちは。minimo

    事業部の遊撃チームで働いている、らりょす (@raryosu) です。 さて私の所属する minimo 事業部はサロンスタッフ直接予約サービス minimo*1を作り、運⽤して いる事業部です。美容師さんやネイリストさん、アイデザイナーさん、エスティシャンの⽅が個⼈で 集客でき、お客さんは⾃分にぴったりのサロンスタッフさんに対して予約できるというサービスで す。サービスの性質上、サービス開発を⾏うプロダクトチームのほかに、ビジネスサイドチームやカ スタマーサポートチーム、マーケティングのチームを有しています。 さて、そんな私は 2019 年 6 ⽉に「mixi AWARD 新⼈部⾨ 優秀賞」を受賞しました。この賞は全 社表彰の⼀部⾨で、社員全員で対象候補者を推薦し、執⾏役員による審議によって 2018 年度新卒の 社員に対して贈られる賞です。受賞理由は以下です。 • 新卒 1 年⽬にもかかわらずさまざまな⽴場の⼈を巻き込み物事を前に進め事業に貢献して いる。 • 職種(エンジニア)の枠にとらわれない⾏動、当事者意識の⾼さを持って業務を推進している。 • 広い視野を持ち、常にサービスや組織が良くなるよう動いている⾏動⼒や周囲を巻き込む⼒を ⾼く評価。 上記は私が仕事をする上で気にかけていることですので、それを評価していただけたことをとても うれしく思っています。そこで今回は、上記の 3 点をもう少し深堀りして、 「どんなことを」 「なぜ」 気にかけて仕事をしているのかを 7 つにまとめてみました。 1. 相⼿の⽴場を理解する。 2.「エンジニア」にとらわれない。 3. ⾃分の意⾒をしっかり持つ。だけど違う考えにしっかり⽿を傾ける。 *1 minimo Web サイト: https://minimodel.jp 49
  51. 第 4 章 メガベンチャーの新⼈賞を取るまでにやった 7 つのこと 4.2 私がやった 7 つのこと

    4. 批判や否定ではなく提案をする。⼀緒に考える。 5. 成果を⾼らかに宣⾔する。 6. 事業に興味を持つ。 7. ポジティブであり続ける。 新⼊社員の皆さんや、エンジニアを志望する皆さんのお役に⽴つことができれば幸いです。 対象とする読者 • 新卒 1 年⽬・2 年⽬のエンジニア • エンジニアを志望している⽅ 4.2 私がやった 7 つのこと 1. 相⼿の⽴場を理解する。 組織にはいろいろな⽴場のメンバーがいます。掲載者さんの⽴場から意⾒をする「ビジネスサイド チーム」 、マーケティングの観点から意⾒をする「マーケティングチーム」 、カスタマーサポートの観 点から意⾒をする「カスタマーサポートチーム」 、そしてデザイナーやエンジニア。当然、各々の観 点や考えが⼀致するとは限りません。そんなときは「なぜ相⼿がそう考えたのか」を考察してみるこ とが重要です。もしくは、 「あなたのやりたいことの意図はこういうことだと理解しましたがあって いますか?」ということをそれとなく聞いてみます。すると、ときに相⼿のさまざまな思惑や意⾒を 聞かせてくれることがあります。⼈は、⾃分に興味を持ってくれるとうれしくなるものです。 そうやっていろいろなメンバーとコミュニケーションを取ることで、お互いを理解して、リスペク トしあって仕事をする環境を作ることができるのです。 ▪コラム: カスタマーサポートと⼀緒に仕事をした⽇ ある⽇ Issue を眺めていると「カスタマーサポート要望」というラベルを発⾒しました。どう やらカスタマーサポートから管理ツールへの要望を Issue に起票し、それにつけられたラベル のようです。当時私は minimo の Web 版サービスをメインで担当していたので管理ツールには 触れたことがありませんでした。しかし、ひとつおもしろそうな Issue があったので、上⻑に 「やってみたい」と相談をし、チャレンジしてみることにしました。 その中で、これまでの業務では触れることのなかったユーザー情報に触れたり、管理ユーザー の権限管理の実装を知ることができたりと、エンジニアリング的興味や minimo のドメイン知識 を深めることにつながりました。また、カスタマーサポートを担当するメンバーにヒアリングを する中で「なぜこの機能が必要とされているのか」とか、 「課題の根本的解決をするためにはど うすればよいのか」を⾃分⾃⾝でも考えながら実装にするかを検討できました。 そして何より、このときにカスタマーサポートのメンバーとコミュニケーションをとって仕事 50
  52. 第 4 章 メガベンチャーの新⼈賞を取るまでにやった 7 つのこと 4.2 私がやった 7 つのこと

    をしたことにより、今でも仲良く仕事をしていますし、⼀緒に飲みに⾏くようにもなりました。 このころは「エンジニアとして⾃分は何ができるか」をメインで考えていましたが、プロダク ト以外のチームのメンバーと⼀緒に仕事をする良い機会でした。以降は、プロダクトのみならず サービス全体を良くするために⾃分は何ができるかを考え、ほかのチームのメンバーとのコミュ ニケーションも積極的に取るようになりました。 2. 「エンジニア」にとらわれない。 加えて私の所属する「遊撃チーム」は「プロダクトに縛られず柔軟に動く」というのが特徴のチー ムで、主にサーバサイドエンジニアが所属しています。当然プロダクト開発(API 設計・開発や Web 版・サロン向け予約管理ツールの開発)もしますが、それ以外にもビジネスサイドチームが欲してい るデータを出しその数値の読み⽅を⼀緒に考えたり、どんな施策を打つと効果が出そうか頭を突き合 わせて考える…みたいな業務も担っています。他にも、カスタマーサポートチームが抱えている「管 理ツール」への不満を拾い上げ、機能追加や導線整理を⾏ったり、マーケティングチームが考えてい るアライアンス施策へのサポートも⾏っています。 「エンジニア」というロールに縛られてしまうとプロダクト開発が華のように感じてしまいますが、 私⾃⾝はエンジニアの視点で施策を考えたり、各チームの⽀援を⾏ったりすることもエンジニアの重 要な仕事だと思っています。もちろん、それを潤滑に進めるためにはいろいろな⽴場の⼈の意⾒や考 えを理解しながらも、⾃分⾃⾝やエンジニアとしての⽴場での意⾒をしっかりと伝えることも⼤切 です。 また、⾃分のエンジニアとしてのスキルを使っていろいろな⽴場の⼈に貢献したことで、周りから の信頼を得ることができ、その結果よりさまざまな⽴場の⼈を巻き込む必要がある⼤きな仕事や、難 易度の⾼い仕事にも取り組もことができるようになりました。 3. ⾃分の意⾒をしっかり持つ。だけど違う考えにしっかり⽿を傾ける。 minimo 事業部は昨年⼤きな組織変更があり、事業責任者の交代と、それに伴い仕事の進め⽅や事 業部の⽅向性が変化したりしました。当時は新卒として現場に配属されてから間がなく、やっとエン ジニアの仕事になれてきたころだったこともあり、私は変化に対して⼤きな不安を感じていました。 具体的にこの部分が不安というわけではなく、どうなるかわからない⽅向に向かっていくことに対し て得体のしれない不安が頭の中で渦巻いていたのです。 こうやって組織が⼤きく変化する中で、サービスの今後について⾃分の考えをしっかり持つ必要が 出てきました。また、⾃分の考えを持ったことで、⾃分の考えと事業部全体の⽅向を照らし合わせた ときに「なぜこの⽅向で進めるのか」をしっかりと⾒ることができるようになりました。もちろんそ の中で不明な点があれば、担当のチームの⼈に話を聞いたり上⻑に聞いたりしてクリアにしていき、 51
  53. 第 4 章 メガベンチャーの新⼈賞を取るまでにやった 7 つのこと 4.2 私がやった 7 つのこと

    ⽅向性に納得できればその実現に向けてできることを全⼒で取り組みます。ふり返ってみると事業部 の組織が変化することにより、⾃分に当事者意識を芽⽣えさせたと感じています。 いままでと変わることに対する不安はどうしてもあるかもしれませんが、否定的になりすぎず、ま ず⾃分の考えをしっかり持つことが重要です。そのうえで、周りの意⾒や考えを聞き、⾃分の考えを さらに蒸留させる。私は組織の変化でそれを学びましたが、もっと早い段階でそれができるように なっていたらより多くのことができていたかもしれません。だからこそ今は⾃分の意⾒をしっかり 持って、いろいろな意⾒に⽿を傾けるように⼼がけています。 4. 批判や否定ではなく提案をする。⼀緒に考える。 「それはおかしくないですか。なんでこういう仕様なんですか。 」という批判を配属当初はよくして いました。でもある時思ったのです。 「その批判に対して何を求めているのだろうか」と。おそらく、 ⾃分の中でより良いと思う仕様と違っているから批判をするのですが、じゃあどんな仕様が良いかと 問われると⾔葉にできない。 それ以来、 「この仕様だとこういう場⾯で問題がありそうですね。 」とか「この部分はどうしてこう いう仕様なのか知りたいです」とか、否定ではなく提案や疑問をぶつけるようにしています。こうす ることで建設的な議論もできるし、相⼿もしっかりと説明をしてくれます。加えて、⾃分でもしっか り考えるので、納得感を持って施策の実装に取り組む事ができます。 より建設的な議論をすることは、よいサービスを作る上で⽋かせない⼯程だと思っています。 5. 成果を⾼らかに宣⾔する。 お互いが常にすべての状況を理解できるわけではありません。仮に私が業務において偉⼤な成果を 残したとしても、遠くの席のメンバーにはなかなか伝わらないのです。しかし、事業やメンバーの成 果は皆で称えたいものです。 私がメインで関わっているのは minimo の Web 版サービスですが、配属当初 Web チームの事 業部内での⽴ち位置は⾮常に⼩さなものでした。そのことが⾮常に気がかりで、当時の事業部⻑に 「Web 版をどうしたいのか」と質問をしたことがあります。 「興味を持ってもらいたいなら、 もっと⽬⽴たないといけない。声を上げていかなければいかない。 」 事業部⻑のその⾔葉を聞いてから私の考えは変わりました。毎⽉の予約数や MAU などの KPI を 週次の定例や Slack で報告、その数字がどうだったかの考察も Slack 上で議論をするようになりまし た。その結果、ずっと Web 版の数字が伸び続けていることも相まって、多くのメンバーが Web 版 に興味を持ってくれるようになり、Web 版に関する多くの議論や提案も⾏われるようになりました。 声を上げることは、必ずしも承認欲求を満たすためのものだけではなく、気付きを与えるための⽅ 法でもあります。もちろん仕事をする上で承認欲求を満たすことも⼤事ですが笑 52
  54. 第 4 章 メガベンチャーの新⼈賞を取るまでにやった 7 つのこと 4.2 私がやった 7 つのこと

    6. 事業に興味を持つ。 エンジニアとして働いていると、⾃分たちがかいたコードでサービスが回っているという錯覚に 陥ってしまうことがあります(私だけでしょうか?) 。しかし、事業全体を⾒渡してみると先に述べ たようにたくさんの⼈が携わっています。 私は、各メンバーが何をしているのか、正直良く知りませんでした。マーケティングのチームのメ ンバーがマーケティングをしているのはわかってはいるのですが、 「マーケティングって何?」とか、 「具体的に何をしているの?」 「何をミッションにしているの?」という点に関してはまったくもって わかっていませんでした。カスタマーサポートやビジネスサイドのチームについても同じことが⾔え ます。 エンジニアがコードを書く上で基本的にはほかのチームのメンバーが何をしているか知っている必 要はあまりないかもしれません。ただ、私の場合はほかのチームのメンバーと⼀緒に仕事をすること が多かったので、まず相⼿を理解するためにいろいろな話をきいてみました。 「何を⽬的にしている チームなのか」 「その中でどんな役割を担っているのか」などです。すると、 (当たり前ですが)全メ ンバーが事業に対して影響を持つ役割を担っているのです。そして、それぞれのチームどうしの事業 における関係性や、ユーザーとの関係性も⾒えてきます。 そういったものが⾒えてくると、仕事の進め⽅も変わってきます。たとえば、何か施策を打ちたい ときにはこのチームの⼈に相談を持ちかけると良さそうとか、早めにこっちのチームの⼈たちにも頭 出しをしておいたほうが良さそうとか。⾃分ひとりが事業に対して興味を持つことで、それが事業全 体をスムーズに動かすためのパワーになるのです。 7. ポジティブであり続ける。 チームの中で仕事をしていると、チームの⽅向性や施策に対して不満や不安を感じることも多々あ るかと思います。そういうときに私はまず、無理やりにでもそのことの「良い側⾯」を意識するよう にしています。 事業部⻑が変わるときも、 「これは良い刺激になるかもしれない」と。当然不安でならないのです が、その不安の根底は⻑が変わることによる組織の変化が読めないことにあります。組織が変化する ということは、今までとは違う環境で⾃分が働くことになります。もちろん不安です。しかし、変化 は必ずしも悪いことではありません。事業部⻑は事業部⻑の考えを持っていて、私達と⼀緒によりよ い世界を作るために組織が変わっていくのです。事業部⻑の考えと私達の考えは異なっているかもし れません。でも、それを⼀緒に考えるよい機会でもあります。そう考えると、事業部⻑が変わること はけっして悪いことではないな、と⾃分の中で整理をすることもできるし、仕事に対して投げやりに なったりいやになったりすることもかなり減ります。 もちろん、もやもやとした何かが⾃分の中あるときに、そのネガティブな側⾯に向き合うこともと ても重要です。しかし、⼀度ポジティブな側⾯を⾒た上でネガティブな⾯にまっすぐ向き合うこと で、もしかしたらそのもやもやも少しは晴れるかもしれません。 53
  55. 第 4 章 メガベンチャーの新⼈賞を取るまでにやった 7 つのこと 4.3 終わりに 4.3 終わりに

    就職してからここまでの 1 年半で、視野がとても⼤きく広がったと感じます。これは、多くのチー ムのメンバーと⼀緒に仕事をする機会があり、その中で何度か壁にぶち当たってきたからこそだと 思っています。そして、壁にぶち当たってきたときに、先輩たちに相談していろいろな考え⽅を教え てもらいました。 環境や状況によって物事のとらえ⽅は⼤きく変わると思います。⾏き詰まったり、苦しくなったり したときには、⼀度周りの⼈に意⾒を求めてみることが最も重要かもしれません。 54
  56. 第 5 章 Bitrise の iOS Auto Provision を導⼊ した話

    最近 In-House アプリの開発に Bitrise を導⼊しました。導⼊理由は 2 点あり、1 つ⽬が開発者が p12 ファイルを個⼈のノート PC に保存するのはセキュリティリスクが⾼いことで、2 つ⽬が社内に ナレッジがありすばやい導⼊が⾒込めたからです。 5.1 本章でのゴール GitHub で管理している iOS アプリを Bitrise で In-House アプリとして書き出す。アプリの書き 出しは xcodebuild コマンドを利⽤するのではなく Automatically manage signing を利⽤します。 5.2 Automatically manage signing とは Xcode 8 から追加された機能で下記の内容を⾃動で⾏ってくれます。 • 署名付き証明書の作成 • App IDs の作成と更新 • プロビジョニングプロファイルの作成と更新 Bitrise では iOS Auto Provision という Workflow を組み込むことで Automatically manage signing の機能を有効にできます。 55
  57. 第 5 章 Bitrise の iOS Auto Provision を導⼊した話 5.3

    導⼊⼿順 5.3 導⼊⼿順 1. Apple Developer アカウントと bitrise のアカウントを連携 Account Setting*1から Apple Developer アカウントとの連携を⾏ってください。連携できると図 5.1 のように連携先の情報が表⽰されます。 図 5.1: Apple Developer アカウント連携時の画⾯ 2. 対象プロジェクトに Apple Developer アカウントの紐付け プロジェクトページのチームタブに"Connected Apple Developer Portal Account"という欄があ るので先程連携したアカウントを選択してください。 図 5.2: 対象プロジェクトに Apple Developer アカウントの紐付け時の画⾯ *1 https://app.bitrise.io/me/profile#/apple_developer_account 56
  58. 第 5 章 Bitrise の iOS Auto Provision を導⼊した話 5.3

    導⼊⼿順 3. p12 証明書のアップロード workflow_editor -> CODE SIGNING IDENTITY に p12 証明書をアップロードしてください。 アップロードできたら図 5.3 のように証明書の情報が表⽰されます。 図 5.3: p12 証明書のアップロード時の画⾯ 4. Workflow の制作 Workflow を作成する際に⼀番気を付けなければいけないことは"Certificate and profile installer" を含んではいけないということです。"Certificate and profile installer"を Workflow 内に含めてし まうと失敗してしまいます。iOS Auto Provision と Xcode Archive & Export for iOS の step の 間に Xcode project の設定を変更するような step を⼊れるのも厳禁です。今回は図 5.4 のような Workflow を構築します。Carthage が含まれていますが、利⽤していなければ外していただいても かまいません。 57
  59. 第 5 章 Bitrise の iOS Auto Provision を導⼊した話 5.3

    導⼊⼿順 図 5.4: Workflow の画⾯ 各 step の設定はだいたい default のものでよいですが、中には詳細な設定が必要なものがあるの でピックアップしました。 iOS Auto Provision で設定すべき値 • Distribution type 58
  60. 第 5 章 Bitrise の iOS Auto Provision を導⼊した話 5.4

    最後に – In-House アプリにしたいので enterprise を選択してください。 • Should the step try to generate Provisioning Profiles even if Xcode managed signing is enabled in the Xcode project? – Automatically manage signing を利⽤するので yes を選択してください。 ちなみにこの辺の設定についての説明は公式ドキュメント*2にも書かれています。 Xcode Archive & Export for iOS で設定すべき値 • Select method for export – In-House アプリにしたいので enterprise を選択してください。 以上の設定で In-House アプリとして Bitrise 上に書き出すことができます。 5.4 最後に この記事*3を参考にし deploygate の Workflow を追加すると、バージョン管理や配信の管理な ど⼿軽にできるのでお勧めです。Bitrise 導⼊初期にいろいろと助⾔をくださった@tdrk18 さんと @kiwi_yuki さん、雑に Workflow を作ったあとにきれいに整備してくれた@Taillook さんにこの場 を借りてお礼申し上げます。ありがとうございました。 *2 https://devcenter.bitrise.io/jp/code-signing/ios-code-signing/ios-auto-provisioning/ *3 Deploy apps to DeployGate from Bitrise: https://devcenter.bitrise.io/deploy/deploy-apps-to-deploygate- from-bitrise/ 59
  61. 第 6 章 SSH 経由なプロキシを作る SSH(Secure Shell)は外部のサーバなどに対してリモートでログインするための⼿段として広く 使⽤されています。しかし、SSH の機能はそれだけにとどまらず、TCP/IP ポートフォワーディン

    グと呼ばれる機能も提供します。 今回は、このポートフォワーディング機能を使って Go ⾔語で簡易的な SSH を経由するプロキシ サーバを作成する例を紹介します。 6.1 OpenSSH におけるポート転送 SSH における TCP/IP のポートフォワーディングについては RFC 4254*1の⼀部として策定され ています。 普段利⽤する SSH 実装は OpenSSH であることが多いと思います。OpenSSH は、前述した仕様 を使ってフォワーディング機能を提供します。 以下に OpenSSH で利⽤できるフォワーディング機能を 3 つ紹介します。 LocalForward クライアント (⾃分⾃⾝) からリモートに対する通信を転送します。図 6.1 ではクライアントか らリモートに対する接続をサーバ経由で⾏っています。以下のような条件のときに使⽤すると便利 です。 • クライアントからサーバには SSH ができる • クライアントからリモートに対しては直接接続できない • サーバからリモートに対しては直接接続できる *1 https://tools.ietf.org/html/rfc4254#section-7 61
  62. 第 6 章 SSH 経由なプロキシを作る 6.1 OpenSSH におけるポート転送 Client Server

    SSH Remote Tunnel 図 6.1: LocalForward RemoteForward サーバからリモートに対する通信を転送します。図 6.2 ではサーバからリモートに対する接続をク ライアント経由で⾏っています。以下のような条件のときに使⽤すると便利です。 • クライアントからサーバには SSH ができる • クライアントからリモートに対しては直接接続できる • サーバからリモートに対しては直接接続できない Client Remote Server SSH Tunnel 図 6.2: RemoteForward DynamicForward クライアント (⾃分⾃⾝) からリモートに対する通信を転送します。ただし、OpenSSH が持ってい る SOCKS プロキシを経由します。OpenSSH は動的に LocalForward に相当することを⾏うため、 ユーザーは転送対象ホストを指定することなく利⽤できます。以下のような条件のときに使⽤すると 便利です。 62
  63. 第 6 章 SSH 経由なプロキシを作る 6.2 プロキシサーバを作る Remote Remote Remote

    Remote Client Tunnel Proxy SOCKS Server SSH 図 6.3: DyanmicForward • LocalForward と同等のシチュエーション • クライアントにあるアプリケーションの通信をサーバ経由にしたい フォワーディングの利点 このようなフォワーディング機能を利⽤することで、SSH サーバを踏み台としてアクセスが制限 されているサーバにアクセスすることや、逆にアクセスさせることができます。しかも、SSH による 安全な通信路の上に転送したい通信を乗せることができます。 特に DynamicForward があれば、SOCKS プロキシとして振る舞うのであらゆる通信を SSH 先経 由にできます。SSH のサービスさえ動かせば、プロキシや VPN を⽤意することなく SSH 先を踏み 台として利⽤できるため、便利なことがあります。 6.2 プロキシサーバを作る 本稿では、Go ⾔語を⽤いて簡易的なプロキシサーバを作成します。Go では、コアとは切り離され てはいるものの、プロジェクトの⼀部として SSH の実装やプロキシが提供されています。また、外 部のライブラリで強⼒なプロキシサーバの実装が開発されています。今回はそれらを⽤いて簡易的な プロキシを作成していきます。 作成するものは OpenSSH の DynamicForward 相当の機能を提供します。パッケージは sshproxy とし、今回紹介するソースコードは https://github.com/atpons/sshproxy として公開してい ます。 63
  64. 第 6 章 SSH 経由なプロキシを作る 6.2 プロキシサーバを作る SSH 接続する 今回はサーバに接続するための情報を以下とします。必要に応じて設定ファイルから読み込んだ

    り、環境変数やコマンドライン引数から取得することも可能です。 const ( ServerUser = "user" // 接続ユーザー ServerHost = "192.0.2.1" // 接続先ホスト ServerPort = "22" // 接続先ポート ServerKeyFile = "/Users/user/.ssh/id_rsa" // 鍵ファイル ) まず、秘密鍵を読み込むための関数を⽤意します。 func LoadPrivateKey(file string) (ssh.AuthMethod, error) { buf, err := ioutil.ReadFile(file) if err != nil { return nil, err } key, err := ssh.ParsePrivateKey(buf) if err != nil { return nil, err } return ssh.PublicKeys(key), nil } 次に、接続する関数を⽤意します。今回は認証に publickey のみを利⽤する設定とします。本来 は HostKeyCallback にホスト鍵の検証を⼊れるべきですが、サンプルのため無効することにしてい ます。 func Connect() (*ssh.Client, error) { authByKey, err := LoadPrivateKey(ServerKeyFile) if err != nil { return nil, err } conf := &ssh.ClientConfig{ User: ServerUser, Auth: []ssh.AuthMethod{ authByKey, 64
  65. 第 6 章 SSH 経由なプロキシを作る 6.2 プロキシサーバを作る }, HostKeyCallback: ssh.InsecureIgnoreHostKey(),

    // サンプルなので無視 Timeout: 10 * time.Second, } conn, err := ssh.Dial("tcp", net.JoinHostPort(ServerHost, ServerPort), conf) if err != nil { return nil, err } return conn, nil } ここで返ってくる ssh.Client には Dial 関数が⽤意されており、これを⽤いて LocalForward と 同等の機能が実現できます。DynamicForward 相当のことではなく、通信を SSH 経由でトンネルし たい場合はこれを Dialer として利⽤できます。サンプルとして SSH サーバ経由で HTTP リクエス トを送信する例を添付してあります。 *2 ▪コラム: HostKeyCallback について クライアント向けに提供するアプリケーションの場合、HostKeyCallback はどのようにした ら良いのでしょうか? 実際、InsecureIgnoreHostKey については It should not be used for production code.*3と書かれています。 このような場合は、クライアントが OpenSSH を利⽤している場合はその known_hosts を⾒ るという⽅法があります。⽅法は crypto/ssh のテストコード*4を参考にすると良いでしょう。 プロキシを作る SOCKS プロキシはすでに実装として公開されている usocksd*5を使⽤します。設定は以下のよう に準備します。 var ( ProxyConf = &usocksd.Config{ Incoming: usocksd.IncomingConfig{ Port: 8080, *2 https://github.com/atpons/sshproxy/blob/a55b18f/cmd/localforward/main.go *3 https://godoc.org/golang.org/x/crypto/ssh#InsecureIgnoreHostKey *4 https://github.com/golang/crypto/blob/d99183c/ssh/example_test.go#L143-L181 *5 https://github.com/cybozu-go/usocksd 65
  66. 第 6 章 SSH 経由なプロキシを作る 6.2 プロキシサーバを作る Addresses: []net.IP{net.ParseIP("127.0.0.1")}, },

    } ) プロキシを起動させるための関数を以下のように⽤意しておきます。 func Start(s *socks.Server) { g := &well.Graceful{ Listen: func() ([]net.Listener, error) { return usocksd.Listeners(ProxyConf) }, Serve: func(lns []net.Listener) { for _, ln := range lns { s.Serve(ln) } err := well.Wait() if err != nil && !well.IsSignaled(err) { os.Exit(1) } }, ExitTimeout: 1, } g.Run() err := well.Wait() if err != nil && !well.IsSignaled(err) { os.Exit(1) } } func Serve(lns []net.Listener) { s := NewServer() for _, ln := range lns { s.Serve(ln) } err := well.Wait() if err != nil && !well.IsSignaled(err) { os.Exit(1) } } func NewServer() *socks.Server { s := usocksd.NewServer(ProxyConf) return s } あとは sshproxy.Start(sshproxy.NewServer()) を呼べばプロキシサーバが作成されます。 66
  67. 第 6 章 SSH 経由なプロキシを作る 6.2 プロキシサーバを作る プロキシが SSH を経由するようにする

    「SSH 接続する」で、通信を SSH 経由でトンネルしたい場合はこれを Dialer として利⽤できま すと紹介しました。usocksd の中でも別で Dialer が定義されており*6、以下を満たすような Dialer を作れば usocksd でその Dialer を利⽤できます。 type Dialer interface { Dial(r *Request) (net.Conn, error) } 今回は ForwardDialer として Dialer を⽤意します。usocksd の Dialer は、*socks.Request を受け取り net.Conn, error を返すようにすれば良いということになります。 type ForwardDialer struct { *ssh.Client } func (f ForwardDialer) Dial(r *socks.Request) (net.Conn, error) { var addr string if len(r.Hostname) > 0 { addr = net.JoinHostPort(r.Hostname, strconv.Itoa(r.Port)) } else { addr = net.JoinHostPort(r.IP.String(), strconv.Itoa(r.Port)) } dialer := contextual.Dialer{SimpleDialer: f.Client} return dialer.DialContext(r.Context(), "tcp", addr) } ここで、contextual*7を使⽤して crypto/ssh パッケージの Client には存在しない DialContext を返すようにしています。 *8 それでは作成した Dialer を使ってプロキシを経由させていきます。SSH 経由でプロキシできるよ うに ssh.Client を受け取り、それを⽤いて Dialer を差し替えてサーバを作成します。 func NewServerOverSSH(client *ssh.Client) *socks.Server { *6 https://github.com/cybozu-go/usocksd/blob/b9e6452/socks/server.go#L44-L46 *7 https://github.com/nbio/contextual *8 https://github.com/golang/go/issues/20288 を参照 67
  68. 第 6 章 SSH 経由なプロキシを作る 6.3 終わりに s := usocksd.NewServer(ProxyConf)

    s.Dialer = ForwardDialer{client} return s } あとは main 関数を sshproxy.Start(sshproxy.NewServerOverSSH(conn)) とすれば SSH を 常に経由する SOCKS プロキシが完成です! 実際に SOCKS プロキシを利⽤できるアプリケーション (Firefox など) に localhost:8080 を SOCKS プロキシとして指定して通信してみてください。正しく SSH 先から接続していることが確 認できるはずです。 6.3 終わりに 今回は OpenSSH の DynamicForward 相当のプロキシを提供するプロキシサーバを作成しました。 作成したプロキシは、DynamicForward で⾃動的に作成される SOCKS プロキシのようなものです が、中に⼊ってアクセス先を制限したり、多段で Dialer を挟んで SSH 先への接続をほかの SOCKS プロキシや SSH サーバを経由*9させたりできます。 今回のプロキシの実装に加え、SSH 接続の際に SOCKS プロキシを前段に挟むようなものを https://github.com/atpons/straightforward として公開していますので、これも合わせてご 覧ください。 *9 SSH 経由にするには OpenSSH の ProxyCommand、 もしくは SOCKS プロキシを経由する場合は ProxyCommand と netcat だけでも可能です。 68
  69. 第 7 章 Makefile マニアクス with Go こんにちは。現在スマートヘルス事業部でココサイズ *1 のサーバサイドエンジニアをしている藤

    ⽥ ( @shumon_84 ) です。 突然ですがみなさん。Go ⾔語を書くときに Makefile を書いていますか? まあ、ほとんどの⼈は Go ⾔語である程度の規模のプログラムを開発する際に Makefile を使⽤し ていると思います。(もし使っていないなら、ぜひ導⼊するべきです。) しかし、C/C++ などから Makefile を覚えた⼈に⽐べて、 Go から Makefile を覚えた⼈は、誤っ た Makefile *2 を書いている⼈が多いように思います。 実際、私が書き換えるまでココサイズで使⽤されていた Makefile も、 Makefile とは名ばかりの シェルスクリプトが使⽤されていました。 せっかく Makefile を使うなら、 Makefile の機能を最⼤限に活⽤し、⽣産性を向上させたいです よね。 というわけで、本章では Gopher が知らなさそうな (だけど Go ⾔語 の開発に役⽴ちそうな) Makefile のちょっとマニアックな仕様について書きたいと思います。 最後に⼀応断りを⼊れておきますが、本章はすでに Makefile の⽂法をある程度知っている⼈向け に書いています。もしあなたが⼀度も Makefile を書いたことがないなら、理解するにはかなり説明 が⾜りないため、なんでもよいので⼩さい Makefile を書いてから、本章を読んでください。 また、ここでは GNU make のみを扱っています。BSD make の場合は微妙に⽂法や機能が違う 場合があるため注意してください。 7.1 Makefile の誤解を解く Gopher の書く Makefile を読んでいると、 Makefile のことを少し誤解しているのかな? と思わ れるような記述がよく⾒受けられます。 まずは、 Makefile の機能の話をする前に、そういった Makefile にまつわるありがちな誤解を解い ておきたいと思います。 *1 https://cococise.com *2 というのはさすがに⾔い過ぎで、 「本来の⼒を引き出せていない Makefile 」という⽅が正確かもしれません 69
  70. 第 7 章 Makefile マニアクス with Go 7.1 Makefile の誤解を解く

    依存関係を適切に列挙していない 依存関係は適切に列挙するべきです。 たとえば、次のような Makefile について考えます。 build: go build generate: go generate この Makefile を使⽤する場合、最初に build するときは事前に generate する必要があるため、 build するためには次のようなコマンドを実⾏する必要があります。 $ make generate $ make build これではもはや Makefile を使う意味はありません。 このようなとき、 build は generate に依存しているため、次のように書けます。 build: generate go build generate: go generate build したい場合は make build を実⾏するだけでよくなります。 .PHONY を使っていない 先ほどの Makefile では、 「build」や「generate」といったファイルが存在していた場合、依存関係 のあるファイルのうちいずれかの mtime が target より新しくなければコマンドは実⾏されません。 $ make build go build $ touch build 70
  71. 第 7 章 Makefile マニアクス with Go 7.1 Makefile の誤解を解く

    $ make build make: ‘build’ is up to date. このような実際のファイル名でない target には .PHONY という特殊 target の依存関係に含めま す。.PHONY の依存関係に列挙された target は、ファイルの存在判定をバイパスし、常にファイルが 存在しないものとして処理します。 build: generate go build generate: go generate .PHONY: build generate 「target == make の引数」ではない 次が最もよく⾒る誤解です。 Makefile に書いた target は、ただ「 make コマンドにどの target を実⾏してもらうかを伝える 識別⼦」というわけではありません。 もちろん、 make には引数で target を伝えるので、これは間違いではないのですが、あくまで target というのは「その rule によって⽣成されるファイル」のことです。(通常は target を⽣成し ない rule の⽅が特殊であり、そのためにわざわざ .PHONY を使います。) 「target == ⽣成するファイル」ということは「make」というコマンドも「make [⽣成するファイ ル]」という英⽂法にのっとったものであることを考えると、理解しやすいはずです。 Makefile の⼤きな特徴である⽣成物と依存ファイル/依存ターゲットのチェックの機能の⼀部しか 使⽤していないため、ほぼ単なるコマンドの羅列になってしまい、各 target は、まるで make のサ ブコマンドかのような振る舞いをします。 これはこれでシンプルで良いのかもしれませんが、 それならわざわざ Makefile を書かずともシェ ルスクリプトで⼗分事⾜ります。 そこで「target はその rule の⽣成物である」という原則を守ると以下のように書けます。(build には hoge.go fuga.go piyo.go generate.go を使⽤し、 generate.go は go generate によって⽣成 されると仮定しています。) GEN_FILES:=generate.go SRC_FILES:=hoge.go fuga.go piyo.go 71
  72. 第 7 章 Makefile マニアクス with Go 7.1 Makefile の誤解を解く

    build: Hoge Hoge: $(SRC_FILES) $(GEN_FILES) go build -tags=build -o Hoge generate: $(GEN_FILES) $(GEN_FILES): $(SRC_FILES) go generate .PHONY: build generate こうすることで Makefile に依存関係の解決を任せることができます。複数回同じ target を実⾏ しても初回しか実⾏せず、かつ依存するファイルに変更があれば実⾏してくれます。 かなり良くなってきましたね。 ワイルドカードに「*」は使⽤するべきではない しかし、この Makefile は、まだ終わりではありません。 今までソースファイルを⾺⿅正直に列挙していましたが、このままはプロジェクトにファイルが増 えるたびに書き⾜す必要があります。そこで、当然ワイルドカードで指定したくなるはずです。 GEN_FILES:=generate.go SRC_FILES:=*.go build: Hoge Hoge: $(SRC_FILES) $(GEN_FILES) go build -tags=build -o Hoge generate: $(GEN_FILES) $(GEN_FILES): $(SRC_FILES) go generate .PHONY: build generate SRC_FILES の指定をアスタリスクを使ったワイルドカードで指定することで、この問題を解決し ました。 実際に実⾏してみると分かりますが、これは⼀⾒正しく動作しているような振る舞いをします。 しかし、*.go にマッチするファイルが 1 つもなかった場合、make は Makefile に従って「*.go」 というファイルを探します。実験してみましょう。 $ ls 72
  73. 第 7 章 Makefile マニアクス with Go 7.2 Makefile 実践テク

    Makefile fuga.go hoge.go piyo.go $ make go generate go build -tags=build -o Hoge $ rm *.go $ make build make: *** No rule to make target ‘*.go’, needed by ‘Hoge’. Stop. Makefile でワイルドカードを使いたいときは、 wildcard 関数 *3 を使います。 GEN_FILES:=generate.go SRC_FILES:=$(wildcard *.go) build: Hoge Hoge: $(SRC_FILES) $(GEN_FILES) go build -tags=build -o Hoge generate: $(GEN_FILES) $(GEN_FILES): $(SRC_FILES) go generate .PHONY: build generate 7.2 Makefile 実践テク さて、よく⾒る誤解が解けたところで、ここからはより実践的なテクニックを紹介します。 実⾏コマンドを隠す make は通常、実⾏するコマンドも表⽰します。 しかし実⾏するコマンドの前に @ を付けると実⾏するコマンドを表⽰しなくなります。 $ cat Makefile task1: echo task1 task2: @echo task2 $ make task1 echo task1 task1 *3 wildcard 関数は Makefile の組込み関数です。組込み関数について、詳しくは後述します。 73
  74. 第 7 章 Makefile マニアクス with Go 7.2 Makefile 実践テク

    $ make task2 task2 ビルド前に、ビルドツールのバージョンを出⼒したいときなどにこのテクニックは便利です。 $ cat Makefile version: @go version $ make version go version go1.13 darwin/amd64 終了ステータスが 0 以外でも続⾏する 通常、make は recipe に書かれたコマンドの終了ステータスが 0 以外の場合は、処理をとめます。 $ cat Makefile task: exit 1 echo task $ make exit 1 make: *** [task] Error 1 場合によっては実⾏の成否を無視してほしいこともあるので、そういうときは実⾏するコマンドの 前に - を付けると異常終了しても make は⾛り続けます。 $ cat Makefile task: -exit 1 echo task $ make exit 1 make: [task] Error 1 (ignored) echo task task また、-i オプションをつけて make を実⾏すると、 Makefile を書き換えることなく、すべてのコ マンドの異常終了を無視できます。 74
  75. 第 7 章 Makefile マニアクス with Go 7.2 Makefile 実践テク

    $ cat Makefile task: exit 1 echo task $ make -i exit 1 make: [task] Error 1 (ignored) echo task task ⾃動変数を活⽤する 巨⼤な Makefile を⾒ると謎の記号がたくさん並んでいて、なんとなく拒絶反応がある⼈って多い と思うんですけど、それはだいたい⾃動変数と後述する Pattern Rules のせいです。 しかし、⾮常に強⼒な機能ですので、ぜひ有名どころは抑えておきましょう。 表 7.1: Makefile の⾃動変数 (⼀部抜粋*4) 変数名 説明 --- $@ target 名 $< 先頭の依存関係 $+ すべての依存関係 $^ すべての依存関係から重複を省いたもの $? 依存関係のうち、 target より新しいもの それぞれ実⾏してみます。 $ cat Makefile task1: echo $@ # target 名 task2: prereq1 prereq2 prereq3 prereq1 echo $< # 先頭の依存関係 echo $+ # すべての依存関係 echo $^ # すべての依存関係から重複を省いたもの task3: hoge fuga piyo echo $? # 依存関係のうち、target より新しいもの *4 https://www.gnu.org/software/make/manual/make.html#Automatic-Variables 75
  76. 第 7 章 Makefile マニアクス with Go 7.2 Makefile 実践テク

    .PHONY: prereq1 prereq2 prereq3 prereq4 $ make task1 echo task1 # target 名 task1 $ make task2 echo prereq1 # 先頭の依存関係 prereq1 echo prereq1 prereq2 prereq3 # すべての依存関係から重複を省いたもの prereq1 prereq2 prereq3 echo prereq1 prereq2 prereq3 prereq1 # すべての依存関係 prereq1 prereq2 prereq3 prereq1 $ ls Makefile fuga hoge piyo task3 $ touch task3 $ touch hoge # task3 より新しいファイルを hoge だけにする $ make task3 echo hoge # 依存関係のうち、target より新しいもの hoge target 名に置換される $@ や、すべての依存関係を列挙してくれる $+ は、なんとなく使い勝⼿が 良さそうなのは分かると思うのですが、それ以外の⾃動変数についは、イマイチ使い道がピンと来な いかもしれません。 しかし、このまま本章を読み進めて⾏くと段々と理解できてくるはずですので、今はひとまずざっ と読み流してください。 組込み関数を活⽤する Makefile でワイルドカードを使⽤したいときは、アスタリスクではなく、wildcard 関数を使って いたのを覚えているでしょうか。 実は wildcard 関数は Makefile の組込み関数*5という機能で、wildcard 関数以外にもさまざま な組込み関数が⽤意されています。 関数は次のように呼び出されます。 $(function arguments) 関数名と引数を空⽩区切りにし、変数と同様に $() で包むことで関数を呼び出すことができます。 Go ⾔語との相性が良さそうな組込み関数を 表 7.2 にまとめました。 他にもいろいろ便利な関数は存在しますが、 shell 関数で好きにシェルコマンドが使えることを *5 わざわざ「組込み」というくらいですので、当然ユーザーが定義する通常の関数も存在します。しかし、組込み関数が ⼗分豊富であり、Go ⾔語のプロジェクトでユーザー定義関数を使うメリットが⼩さいため、本章では扱っていません。 *6 https://www.gnu.org/software/make/manual/make.html#Make-Control-Functions 76
  77. 第 7 章 Makefile マニアクス with Go 7.2 Makefile 実践テク

    表 7.2: 組込み関数 (⼀部抜粋 *6) 関数とその引数 説明 $(subst from,to,text) text に含まれる from を to に置換します。 $ echo $text | sed s/$from/$to/g と同じイメージです。 $(filter pattern...,text) text の中から pattern のいずれかに該当するものだけを抽出します。 $(dir names...) names に与えたパスから、それぞれディレクトリ部分だけを抽出します。 $(notdir names...) names に与えたパスから、それぞれファイル名だけを抽出します。 $(basename names... ) names に与えたパスから、拡張⼦だけを削除します。 $(addsuffix suffix,names...) すべての names の末尾に suffix をつけます。 $(addprefix prefix,names...) すべての names の先頭に prefix をつけます。 $(wildcard pattern...) シェル⾵のワイルドカード指定で該当するファイル⼀覧を返します。 $(shell command) サブシェルを起動し command の実⾏結果を返します。 考えると、このあたりの関数さえ押さえておけば、おおむね問題ありません。 1 つの rule に複数の target 意外と知られていませんが、1 つの rule に複数の target を書くことができます。 複数の target を書いた場合、 それぞれの target はまったく同じ依存関係と、まったく同じ recipe を持ちます。 同じ依存関係と recipe を持つのに target が違うということは「同じ依存関係と recipe から異なる ファイルを⽣成する」という意味を持つのですが、そんな recipe 存在するのかと疑いたくなります。 しかし次のように⾃動変数を使えば、まったく同じ依存関係と recipe を持ちながら、最終的に実 ⾏されるコマンドは異なる場合があります。 $ cat task1 task2 task3: prereq1 prereq2 prereq3 echo $@ $+ .PHONY: prereq1 prereq2 prereq3 $ make task1 echo task1 prereq1 prereq2 prereq3 task1 prereq1 prereq2 prereq3 $ make task2 echo task2 prereq1 prereq2 prereq3 task2 prereq1 prereq2 prereq3 $ make task3 echo task3 prereq1 prereq2 prereq3 task3 prereq1 prereq2 prereq3 77
  78. 第 7 章 Makefile マニアクス with Go 7.2 Makefile 実践テク

    Pattern Rules 今まで、ワイルドカードの指定には wildcard 関数を使っていました。 しかし、 「パッケージごとに go test --coverprofile で .out ファイルを⽣成したい」といった ケースでは wildcard 関数では表現が難しくなります。 次のようなディレクトリ構成のプロジェクトで考えてみます。 図 7.1: ディレクトリ構成 「パッケージごとに go test --coverprofile で .out ファイルを⽣成する」という要件を満たせ る Makefile を書くには、さきほどの複数の target を持つ rule を使うと上⼿に書けそうな感じがし ますが、実際に書いてみるとどうでしょうか。 cover: hoge.out fuga.out piyo.out $(wildcard *.out): $(wildcard [target のワイルドカードがマッチした⽂字列]/*.go) go test ./$(basename $@) -coverprofile=$@ .PHONY: cover 依存関係に [target のワイルドカードがマッチした⽂字列] を書く⼿段がないことに気付きます。 このままでは過酷なシェルスクリプトを書く必要があるでしょう。 このように「target と依存関係 (または recipe ) で同じ⽂字列をマッチさせたい」というときは、 Pattern Rules を使うと良いです。Pattern Rules はワイルドカードとして % を使った⽂字列で、 78
  79. 第 7 章 Makefile マニアクス with Go 7.2 Makefile 実践テク

    wildcard 関数との違いは、 Pattern Rules は複数の % を持つことができない代わりに、target の % でマッチした⽂字列を依存関係の % とも置換できます。recipe で target の % でマッチした⽂字列 を使いたい場合は、⾃動変数の $* を使います。 それでは Pattern Rules を使って、パッケージごとに go test --coverprofile で .out ファイ ルを⽣成する rule を書き換えてみます。 cover: hoge.out fuga.out piyo.out %.out: %/*.go go test ./$* -coverprofile=$@ .PHONY: cover これでスッキリしましたね。 ▪コラム: さらなる⾼みへ %.out の依存関係を⾒ると、wildcard 関数を使わずにアスタリスクを使っています。 これでは前述の通り、ディレクトリ内に*.go にマッチするファイルが存在しなかったとき、 「*.go」という⽂字列で展開されてしまいます。しかし、テスト対象のパッケージに 1 つもソース コードがないという状況は、現実問題としてあり得ないため、これ以上は追求しませんでした。 詳細な解説は省きますが、より完全な Makefile を⽬指したいのであれば、次のように define ディレクティブと eval 関数、call 関数を組み合わせることで可能です。 PKGS:=$(shell go list ./...) cover: $(addsuffix .out,$(PKG)) define coverprof $(1).out: $(wildcard $(1)/*.go) go test $(1) -coverprofile=$$@ endef $(foreach PKG,$(notdir $(PKGS)),$(eval $(call coverprof,$(PKG)))) しかし、 「rule を⽣成する関数で⼤量の rule を⽣成する」というのは、ほとんど黒魔術です。 読み⼿に黒魔術の解読を強要してまで現実的に起こりえないコーナーケースまでカバーしたいの かは、⼀度よく考えるべきです (それに、 「読み⼿」というのは多くの場合将来のあなたです) 79
  80. 第 7 章 Makefile マニアクス with Go 7.2 Makefile 実践テク

    何も⽣成しないけど 依存関係の解決はしたい 今まで「target はその rule で⽣成されるファイルであり、⽣成しない場合は .PHONY にする」と ⾔ってきました。しかし、 .PHONY に指定された target は実在しないファイルと⾒なされるため、 依存関係のあるファイルの mtime にかかわらず、必ず実⾏されてしまいます。 実際には「その rule では何も⽣成しないけど、⼀度実⾏したら依存関係のあるファイルの mtime が変わるまで実⾏したくない」というケースは存在し、go fmt や go vet などが該当します。 そのようなときは、stamp ファイルと呼ばれるファイルを作るテクニックがよく使われます。 stamp ファイルについては、⽂章で説明するよりも Makefile を⾒た⽅が分かりやすいと思います。 fmt.stamp: hoge.go fuga.go piyo.go go fmt $? @touch $@ recipe の最後で fmt.stamp に touch しておくことで、 「最後にその rule を実⾏した時間」として target の mtime を利⽤できるようになっています。$? は依存関係のあるファイルのうち、 target よりも新しいものを空⽩区切りで列挙する⾃動変数ですので、これで「⼀度実⾏したら依存関係のあ るファイルの mtime が変わるまで実⾏しない」が実現できました。 go list Makefile を Go ⾔語で扱う場合、 go list コマンドの存在は⽋かせません。意外と使っている⼈ をあまり⾒ないのですが、⾮常に強⼒なツールですので、ぜひこの機会に覚えてみてください。 go list はソースファイルやパッケージなどを出⼒するためのコマンド *7 で、Go ⾔語の text/template パッケージの書式指定を使うことができ、⾮常に柔軟な出⼒が可能です。 コマンドラインツールでお馴染みの cobra *8 を例にして、go list の機能を⼀部紹介します。 オプションを指定しない場合はカレントディレクトリの import パスを出⼒します。 $ cd $GOPATH/src/github.com/spf13/cobra $ go list github.com/spf13/cobra このプロジェクトに含まれるパッケージの⼀覧がほしいなら、go get と同じ要領で ./... も使 えます。 *7 go のサブコマンド群でパッケージを統⼀されたインタフェースで扱うために go list は⽣まれました。 *8 https://github.com/spf13/cobra 80
  81. 第 7 章 Makefile マニアクス with Go 7.2 Makefile 実践テク

    $ go list ./... github.com/spf13/cobra github.com/spf13/cobra/cobra github.com/spf13/cobra/cobra/cmd github.com/spf13/cobra/doc -json オプションをつけることで、go list が扱っている情報が JSON で出⼒されます。 $ go list -json { "Dir": "/Users/shumon.fujita/go/src/github.com/spf13/cobra", "ImportPath": "github.com/spf13/cobra", "Name": "cobra", # 中略 "strings", "testing", "text/template" ] } ここから -f オプションでテンプレートを使って必要な情報を絞り込みます。たとえば、パッケー ジに含まれるソースファイル⼀覧を取得したい場合、 go list -json の出⼒を眺めると、.GoFiles に⽂字列配列として⼊っていることが分かるため、次のようなテンプレートを書くとソースファイル ⼀覧だけを取り出すことができます。 $ go list -f=’{{join .GoFiles "\n" }}’ . args.go bash_completions.go cobra.go command.go command_notwin.go zsh_completions.go ここにテストファイルも含ませたい場合は、次のようなテンプレートを書きます。 $ go list -f=’{{join .GoFiles "\n"}}{{join .TestGoFiles "\n"}}’ . args.go bash_completions.go cobra.go command.go 81
  82. 第 7 章 Makefile マニアクス with Go 7.2 Makefile 実践テク

    command_notwin.go zsh_completions.goargs_test.go bash_completions_test.go cobra_test.go command_test.go zsh_completions_test.go 極め付けは、ビルドタグまで使えます。 $ go list -tags="windows" -f=’{{join .GoFiles "\n"}}’ . args.go bash_completions.go cobra.go command.go command_win.go zsh_completions.go このように@<go list> は @<go> のサブコマンドなだけあって、かゆいところに⼿がとどく機能 を備えていることが分かってもらえたかと思います。 さきほどの go fmt の Makefile の例も go list があれば、より美しく書くことができます。 SRCS:=$(shell go list -f=’{{range .GoFiles}}{{printf "%s/%s\n" $$.Dir .}}{{end}}’ ./...) fmt.stamp: $(SRCS) go fmt $? @touch $@ go list について、さらに詳細を知りたい場合は https://golang.org/cmd/go/#hdr-List_ packages_or_modules を読むことをお勧めします。 go mod を使った CLI ツールのバージョン固定テクとの併⽤ Go ⾔語はソースコードの字句解析・構⽂解析器が標準ライブラリで提供されている影響で、静的 解析ツールやコードジェネレータが豊富に存在しますが、その結果多くのプロジェクトでは linter を 筆頭に、いくつかのビルドツールを使⽤していると思います。 それらのツール類のバージョンの固定には、Go1.11 以降では go mod を使って管理するのがベス トプラクティス *9 とされています。 このフローに Makefile を追従させることで、さらに⽣産性を向上させていきましょう。 *9 https://github.com/golang/go/issues/25922#issuecomment-412992431 82
  83. 第 7 章 Makefile マニアクス with Go 7.2 Makefile 実践テク

    ここでは次のようなディレクトリ構成で tools.go を使っているとします。 $ tree . ├── Makefile ├── go.mod ├── go.sum └── tools └── tools.go 1 directory, 4 files $ cat tools/tools.go // +build tools package tools import ( _ "golang.org/x/lint/golint" _ "golang.org/x/tools/cmd/goimports" ) ここで、tools.go で管理されているツールのビルド済みバイナリを build_cmd/ というディレク トリを作ってそこに置きたいとします。 go list コマンドを使うことで、tools.go が import しているパッケージを抽出できるため、 build_cmd/ に⼊れるコマンドの⼀覧を取得できます。それさえできれば、ここまでに紹介したテク ニックを駆使することで、次のような Makefile を書くことができます。 BUILD_CMD_DIR:=./build_cmd BUILD_CMD_PKGS:=$(shell go list -f ’{{join .Imports "\n"}}’ -tags=tools ./tools) BUILD_CMDS:=$(addprefix $(BUILD_CMD_DIR)/,$(notdir $(BUILD_CMD_PKGS))) tools: $(BUILD_CMDS) $(BUILD_CMDS): tools/tools.go go.sum go.mod go build -o $@ $(filter %/$(notdir $@),$(BUILD_CMD_PKGS)) .PHONY: tools これで tools.go と go mod のフローに乗ることができたので、今後新しいツールを追加したい場 合は、tools.go に import ⽂を追加するだけです。しかし、それでもまだ tools.go を編集する必要が ありますね。もう少し⼿を加えてさらに⾃動化してみましょう。 tools.go へ簡単に import ⽂を追加できるようにするために、import ⽂を分割します。 83
  84. 第 7 章 Makefile マニアクス with Go 7.2 Makefile 実践テク

    // +build tools package tools import _ "golang.org/x/lint/golint" import _ "golang.org/x/tools/cmd/goimports" こうすることで、import ⽂を tools.go の末尾に付け⾜すだけでよくなったので、 Makefile にも rule を追加します。 BUILD_CMD_DIR:=./build_cmd BUILD_CMD_PKGS:=$(shell go list -f ’{{join .Imports "\n"}}’ -tags=tools ./tools) BUILD_CMDS:=$(addprefix $(BUILD_CMD_DIR)/,$(notdir $(BUILD_CMD_PKGS))) add-tool: ifdef TOOL go get $(TOOL) echo ’import _ "$(TOOL)"’ | sed ’s/@.*"/"/’>> tools/tools.go endif .PHONY: add-tool tools: $(BUILD_CMDS) $(BUILD_CMDS): tools/tools.go go.sum go.mod go build -o $@ $(filter %/$(notdir $@),$(BUILD_CMD_PKGS)) .PHONY: tools 新たに add-tool という target を追加しました。ifdef ディレクティブは TOOL という変数が宣 ⾔されているかどうかで条件分岐するため、TOOL が宣⾔されていなければ、 add-tool は何もしま せん。そのため make add-tool を実⾏しただけでは何も起こりませんが、make コマンドは実⾏時 に変数を定義する機能があるため、 $ make add-tool TOOL=github.com/goadesign/goa/goagen とすることで TOOL が定義され、recipe が実⾏されます。 tools.go を確認すると、 $ cat tools/tools.go // +build tools 84
  85. 第 7 章 Makefile マニアクス with Go 7.2 Makefile 実践テク

    package tools import _ "golang.org/x/lint/golint" import _ "golang.org/x/tools/cmd/goimports" import _ "github.com/goadesign/goa/goagen" と、正しく import できていることが分かります。 また、sed コマンドで @ 以降を import ⽂に含めないようにしているため、 $ make add-tool TOOL=github.com/goadesign/goa/[email protected] のようにバージョンを指定することもできます。 ここまでくると、まるで make が Go ⾔語のエコシステムに取り込まれているかのような使い⽅ ができます。⼿間暇かけて Makefile を整備することの意義が実感できるのではないでしょうか。 make2help を使う だんだん Makefile が複雑になってくると、内部的に呼び出すためだけの rule と、make の第⼆引 数で target を直接指定して使いたい rule の 2 種類が出てくると思います。 しかし、 Makefile を⼀⽬⾒ただけではどの rule をコマンドラインで指定するべきで、またその rule でどんなことをするのか分かりません。 そこで Go ⾔語で書かれた make2help *10 というツールを使うと、⾃動でヘルプを⽣成してくれ るため、開発者に優しい Makefile に⽣まれ変わります。 ヘルプは各 rule の直前の ## から始まるコメント⾏を、その rule のドキュメントとして使⽤し ます。 せっかく Go ⾔語製のツールですので、さきほどのビルドツールが管理できる Makefile を使って みたいと思います。 まずは、Makefile にアノテーションとヘルプを表⽰するための rule を追加します。 BUILD_CMD_DIR:=./build_cmd BUILD_CMD_PKGS:=$(shell go list -f ’{{join .Imports "\n"}}’ -tags=tools ./tools) BUILD_CMDS:=$(addprefix $(BUILD_CMD_DIR)/,$(notdir $(BUILD_CMD_PKGS))) ## ヘルプを表⽰します help: $(BUILD_CMD_DIR)/make2help @make2help $(MAKEFILE_LIST) *10 https://github.com/Songmu/make2help/cmd/make2help 85
  86. 第 7 章 Makefile マニアクス with Go 7.2 Makefile 実践テク

    .PHONY: help ## make add-tool TOOL=[ImportPath] でビルドツールを追加します add-tool: ifdef TOOL go get $(TOOL) echo ’import _ "$(TOOL)"’ | sed ’s/@.*"/"/’>> tools/tools.go endif .PHONY: add-tool ## ビルドツールのバイナリを⽣成します tools: $(BUILD_CMDS) $(BUILD_CMDS): tools/tools.go go.sum go.mod go build -o $@ $(filter %/$(notdir $@),$(BUILD_CMD_PKGS)) .PHONY: tools help という rule を追加し、コマンドラインで指定することが想定されている、help 、add-tool 、 tools にアノテーションを追加しました。 確認してみましょう。 $ make add-tool TOOL=github.com/Songmu/make2help/cmd/make2help go get github.com/Songmu/make2help/cmd/make2help go: finding github.com/Songmu/make2help/cmd/make2help latest go: finding github.com/Songmu/make2help/cmd latest go: finding github.com/Songmu/make2help v0.1.1 go: downloading github.com/Songmu/make2help v0.1.1 go: extracting github.com/Songmu/make2help v0.1.1 go: finding github.com/mattn/go-isatty v0.0.9 go: finding golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 go: finding golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a go: downloading golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 go: downloading github.com/mattn/go-isatty v0.0.9 go: extracting golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 go: extracting github.com/mattn/go-isatty v0.0.9 echo ’import _ "github.com/Songmu/make2help/cmd/make2help"’ | sed ’s/@.*"/"/’>> tools/tools.go $ make help go build -o build_cmd/make2help github.com/Songmu/make2help/cmd/make2help add-tool: make add-tool TOOL=[ImportPath] でビルドツールを追加します help: ヘルプを表⽰します tools: ビルドツールのバイナリを⽣成します make help でヘルプが表⽰され、アノテーションを書いた rule の説明だけが表⽰され、Makefile を使う⽴場の⼈に、この Makefile で何ができるのかを分かりやすく伝えることができます。 86
  87. 第 7 章 Makefile マニアクス with Go 7.2 Makefile 実践テク

    Dry Run これまでに紹介したような、やや複雑な Makefile を書くときは、何度も実⾏してみて期待通りに 動くかどうかを確認しながら書くことになるかと思います。 しかし、そういった試⾏錯誤を重ねていると、書きかけの不完全な Makefile によって依存関係が 破壊されてしまったり、target を最新でなくすためにファイルを削除したり touch したり、といっ たことを繰り返す必要がでてきます。 そういったユースケースをカバーするために、make コマンドは-n オプションで、実⾏するコマン ドを表⽰するだけで実際には実⾏しない Dry Run モードをサポートしています。 $ cat Makefile hoge: touch hoge $ make -n hoge # 実際には実⾏しない touch hoge $ ls Makefile $ make hoge # 実際に実⾏する touch hoge $ ls Makefile hoge 並列実⾏ make は -j オプションで並列実⾏させることができます。 次のような Makefile で実験してみましょう。 task: task1 task2 task3 task4 task1 task2 task3 task4: sleep 1 # $@ .PHONY task task1 task2 task3 task4 make を実⾏すると、1 秒間 sleep するタスクが 4 つ実⾏されます。 まずは並列化なしで実⾏してみます。 87
  88. 第 7 章 Makefile マニアクス with Go 7.2 Makefile 実践テク

    $ time make sleep 1 # task1 sleep 1 # task2 sleep 1 # task3 sleep 1 # task4 real 0m4.049s user 0m0.013s sys 0m0.020s 1 秒間 sleep を 4 回実⾏するので、当然 4 秒かかります。 次に、-j オプションで並列実⾏してみます。 $ time make -j2 # 2 並列 sleep 1 # task1 sleep 1 # task2 sleep 1 # task3 sleep 1 # task4 real 0m2.043s user 0m0.019s sys 0m0.022s $ time make -j3 # 3 並列 sleep 1 # task1 sleep 1 # task2 sleep 1 # task3 sleep 1 # task4 real 0m2.104s user 0m0.025s sys 0m0.041s $ time make -j4 # 4 並列 sleep 1 # task1 sleep 1 # task2 sleep 1 # task3 sleep 1 # task4 real 0m1.032s user 0m0.018s sys 0m0.021s $ time make -j5 # 5 並列 sleep 1 # task1 sleep 1 # task2 sleep 1 # task3 sleep 1 # task4 real 0m1.031s user 0m0.017s 88
  89. 第 7 章 Makefile マニアクス with Go 7.2 Makefile 実践テク

    sys 0m0.023s それぞれの実⾏時間を⽐べると 表 7.3 になります。 表 7.3: 並列化オプションと実⾏時間 並列化オプション 実⾏時間 並列化オプションなし 約 4 秒 -j2 約 2 秒 -j3 約 2 秒 -j4 約 1 秒 -j5 約 1 秒 ⼿軽に⾼速化が期待できる並列化オプションですが、このようにあくまで Makefile に書かれた rule 単位でのタスク並列化であることに注意してください。 1 つの rule の中では逐次実⾏されるため、多くの依存関係が絡むような target を make しなけれ ば、あまり⼤きな効果を得ることはできません。 また、CPU バウンドな処理の場合は、CPU コア数以上の並列度を指定しても⾼速化は期待できま せん。 1 つの target に別々の rule を適⽤する 1 つの target に対して、依存関係によって実⾏する recipe を切り替えてほしいときがあります。 具体的には次のようなケースです。 SOURCE_FILES:=hoge.go piyo.go fuga.go BUILD_CMD_DIR:=./build_cmd goimports: $(BUILD_CMD_DIR)/goimports goimports.stamp goimports.stamp: $(BUILD_CMD_DIR)/goimports # goimports のバージョンが上がったパターン $(BUILD_CMD_DIR)/goimports -l -w ./ @touch $@ goimports.stamp: $(SOURCE_FILES) # ソースコードが変更されたパターン $(BUILD_CMD_DIR)/goimports -l -w $? @touch $@ .PHONY: goimports goimports でフォーマットをかけたいとき、通常なら変更されたソースコードのみフォーマットす れば⼤丈夫ですが、goimports のバージョンを上げたときは、フォーマットのルールが変更されてい る可能性があるため、すべてのファイルに対してフォーマットをかけるべきです。 89
  90. 第 7 章 Makefile マニアクス with Go 7.2 Makefile 実践テク

    しかしこの Makefile には goimports.stamp に複数の rule が存在するため、意図しない挙動 *11 をします。 このような問題は Double-Colon Rules を使うと解決できます。Double-Colon Rules はその名の 通り、target と依存関係の間の「 : 」を「 :: 」にした rule です。 SOURCE_FILES:=hoge.go piyo.go fuga.go BUILD_CMD_DIR:=./build_cmd goimports: $(BUILD_CMD_DIR)/goimports goimports.stamp goimports.stamp:: $(BUILD_CMD_DIR)/goimports # goimports のバージョンが上がったパターン $(BUILD_CMD_DIR)/goimports -l -w ./ @touch $@ goimports.stamp:: $(SOURCE_FILES) # ソースコードが変更されたパターン $(BUILD_CMD_DIR)/goimports -l -w $? @touch $@ .PHONY: goimports goimports.stamp を Double-Colon Rules にしました。 実⾏してみます。 $ make goimports make: ‘goimports’ is up to date. $ touch hoge.go $ make goimports ./build_cmd/goimports -l -w hoge.go $ touch ./build_cmd/goimports $ make goimports ./build_cmd/goimports -l -w ./ 正しく使い分けられていることが分かります。 このように、少し特殊なことができる Double-Colon Rules でしたが、Double-Colon Rules と Single-Colon Rules は、1 つの target には共存できない点には注意してください。 依存関係にディレクトリを含める さまざまな静的解析ツールを使⽤していると、リポジトリのトップディレクトリがだんだん stamp ファイルで溢れ返ってくると思います。そこで stamp ファイルを stamp ディレクトリにまとめて 置くようにすれば、管理の⼿間を減らせます。 *11 ちなみに、このまま実⾏すると make は同じ target に 複数の recipe が存在することを⾒つけ、最後に定義された recipe だけを採⽤し、それ以外の recipe は無視します。 90
  91. 第 7 章 Makefile マニアクス with Go 7.2 Makefile 実践テク

    SRCS:=hoge.go fuga.go piyo.go STAMP_DIR:=./stamp fmt: $(STAMP_DIR)/fmt.stamp $(STAMP_DIR)/fmt.stamp: $(SRCS) $(STAMP_DIR) go fmt $? @touch $@ $(STAMP_DIR): mkdir $@ .PHONY: fmt stamp ファイルを⽣成する rule の依存関係に stamp ディレクトリを含めることで、stamp ディ レクトリを⽣成してから stamp ファイルを⽣成するようにしてみました。 $ make fmt mkdir ./stamp go fmt hoge.go fuga.go piyo.go ./stamp $ touch hoge.go $ make fmt go fmt hoge.go go fmt の引数に ./stamp が⼊っているのが少し気になりますが、おおむね問題なさそうに⾒え ます。 しかしこの Makefile は、stamp ファイルを作る rule が 1 つしかない間は正しく機能しますが、複 数の rule で stamp ファイルを作るようになると、stamp ディレクトリに新たにファイルが作られ るたびに go fmt が実⾏されてしまいます。 $ touch ./stamp/hoge # ./stamp に特に関係ないファイルを作る $ make fmt mkdir ./stamp # <- すでに ./stamp はあるのに作ろうとしてしまう go fmt ./stamp 原因は make の rule の実⾏判定にあります。 make は実⾏しようとしている rule の target と依存関係のあるファイルの中で、target より mtime が新しいファイルがあるかどうかで、 recipe を実⾏するかを決定しています。 この判定⽅法はよくできていて、mtime を⽐較するだけという簡素なルールでありながら、これ までの例で紹介したように、⾮常にきめ細やかで柔軟な制御を可能にしています。 91
  92. 第 7 章 Makefile マニアクス with Go 7.3 終わりに しかし、今回のように依存関係にディレクトリを書き、

    「ディレクトリが存在しなければ、作成し てから recipe を実⾏してほしい」といったケースではどうでしょうか。 ディレクトリの mtime はディレクトリ内のファイルの inode かファイル名が変化すると更新され ます。つまり、stamp/ の中に stamp ファイルが増えるたびに、 make は stamp/ を作ろうとして しまいます。 結果として、依存関係の中に target より新しいものが存在することになり、stamp ファイルを増 やすたびに、本来無関係であるはずの go fmt が実⾏されてしまいます。 こういったケースには order-only prerequisites という機能がぴったりです。order-only prereq- uisites を使うと、make は依存関係の解決に mtime ではなく存在しているかどうかだけを確認し ます。 order-only prerequisites は次のような⽂法になっています。 targets : normal-prerequisites | order-only-prerequisites 依存関係を「 | 」で区切り、左に今まで書いていた normal-prerquisites を書き、右に order-only- prerequisites を書きます。指定⽅法は normal-prerequisites と同様に依存関係のあるファイルを空 ⽩区切りで列挙します。 SRCS:=hoge.go fuga.go piyo.go STAMP_DIR:=./stamp fmt: $(STAMP_DIR)/fmt.stamp $(STAMP_DIR)/fmt.stamp: $(SRCS) | $(STAMP_DIR) go fmt $? @touch $@ $(STAMP_DIR): mkdir $@ .PHONY: fmt ちなみに、今回は使⽤しませんでしたが、order-only-prerequisites に指定したファイル名⼀覧を recipe で使⽤したい場合は、 「 $| 」という⾃動変数を参照することで取得できます。 7.3 終わりに この⻑い章を読んでいただいて、ありがとうございます。 僕はめちゃくちゃ Makefile 好きで、なんでもかんでも Makefile にしてしまうんですけど、多分最 後まで読んでくれた⼈なら、Makefile で全部やりたくなってしまう理由が分かってもらえるかと思 92
  93. 第 7 章 Makefile マニアクス with Go 7.3 終わりに います。

    どんな環境でも⼤概は make ⼊ってますし。 もし本章を読んで Makefile の魅⼒を感じて勉強しようと思った⼈は、 ⽇本語翻訳されている GNU Make のドキュメント *12 が異常に古い (1998 年のもの) ことに注意してください。最後に書いた order-only prerequisites なんかは⽐較的新しい機能で⽇本語翻訳版には載っていないので、結局本 家のドキュメント *13 を参照するのが⼿っ取り早いです。 それではみなさんも、 「ぼくのかんがえたさいきょうの Makeifle 」を作ってみてください。 *12 https://www.ecoop.net/coop/translated/GNUMake3.77/make_toc.jp.html *13 https://www.gnu.org/software/make/manual/make.html 93
  94. 著者紹介 ヤマヤタケシ (第 1 章担当, GitHub: toymany, Twitter: @toyman_jp) 近年はスマホ⽤のゲーム開発プロジェクトに参加しています。趣味でアナログカードゲームを

    作ったり、ゲームアプリを作ったりしています。   津⽥ 恭平 (第 2 章担当, GitHub,Medium,Qiita: flatfisher, Twitter: @canoefishing) スマートヘルス事業部のサーバーサイドエンジニア、GCP と Go ⾔語を使⽤している。GDG と呼ばれる技術コミュニティのオーガナイザーをしており、コミュニティ界隈ではフィッシュ と呼ばれている。釣りが趣味なので「釣り×テクノロジー」といったテーマに取り組むのが 好き。   松原 信忠 (第 3 章担当, GitHub: matsubara0507) 所属はモンストサーバチームで Ruby を書いてる。プログラミングが好きで、普段は推し⾔ 語の Haskell で遊んだり、新しいプログラミング⾔語を勉強したりしている。Haskell-jp や Elm-jp でちょこちょこ活動もしている。   萩原 涼介 (第 4 章担当, GitHub: raryosu, Twitter: @raryosu, note: raryosu) らりょすです。minimo 事業部の遊撃チームの⼀員として働く新卒 2 年⽬エンジニア。最近は 「エンジニア」という役回りにとらわれない働き⽅を意識して働いてます。   栗原 尚弘 (第 5 章担当, Instagram: k__naohiro) 18 新卒です。ライブエクスペリエンス事業本部イベントシステム開発 G でインフラ兼サーバ サイドエンジニアをやっております。⽢いものが⼤好きです。 95
  95. 付録 著者紹介 浅野 ⼤我 (第 6 章担当, GitHub: atpons ,

    Twitter: @atpons) 20 新卒です。現在は内定者アルバイトとしてモンスターストライクのサーバサイドの開発に 携わっています。Ruby や Go を書いています。   藤⽥ 朱⾨ (第 7 章担当, GitHub : shumon84 , Twitter : @shumon_84) スマートヘルス事業部で Go を書いている 19 新卒です。最近はガベージコレクションに興味 があります。   ⼟屋 雅 (デザイナー, Twitter:@miyabit_, note:miyabt) 実は ex-mixi となったのですが、今回もお声がけいただきデザインを担当させていただきまし た。⼤変嬉しいです、ありがとうございます! もっと執筆者の⽅に寄り添ったデザインにし たいなと思い、各章の執筆内容をモチーフにしたイラストを作り、あしらいにしてみました。 学びも愛着も⽣まれる⼀冊になっていただけたら幸いです!   杉⽥ 絵美 (プロジェクト推進・制作進⾏等の庶務, GitHub: esugita, Twitter: @semiemi7) 元エンジニアで、ミクシィでは、Perl を触ったり、アプリを触ったり、新規事業の PM をし たりして、今は、DevRel チームで各種イベントを企画・運営したり、技術的な知⾒のアウト プット活動をサポートしたりしています。   喜多 功次 (制作アシスト, GitHub: kojikita, Twitter: @kojikita) DevRel チームで勉強会やイベントの運営などをしています。元エンジニアで以前は web フ ロント側のサービス開発やアドテクなどを担当していました。JavaScript が好きで、最近は PWA に注⽬しています。 96
  96. mixi tech note #02 2019 年 9 ⽉ 22 ⽇ 初版第

    1 刷 発⾏ 著 者 株式会社ミクシィ 有志 発⾏所 株式会社ミクシィ 印刷所 ⽇光企画   © mixi, Inc.