寝ても覚めてもこんぴうた

プログラム書いたり、ネットワーク設計したり、サーバ構築したり、車いじったり、ゲームしたり。そんなひとにわたしはなりたい。 投げ銭は kyash_id : chidakiyo マデ

モノレポ(mono repository)内でmodで管理しているアプリケーションをGAEにデプロイするために必要な2つのポイント

f:id:chidakiyo:20190719161116j:plain

前回は モノレポ(mono repository)内でgoのmoduleを相対パスで利用する方法 という記事を書きましたが、今回はさらにモノレポで管理しているAppengineの複数のサービスをデプロイする方法になります。

なぜGAEのデプロイの記事が必要?

前回の記事を試した方がいるかいないかは不明ですが、モノレポ内でmodで依存を解決したものをデプロイした場合、modを利用したデプロイ(GO111MODULE=on)ではGCP上のCloudBuild上でコンパイル(依存の解決)を行うため、相対的に管理しているモジュールを取得することができないためエラーになり、デプロイが完了しません。

なので、今回はサンプルも作り、丁寧に説明したいと思います。 サンプルのソースコードはこちらになります。

https://github.com/chidakiyo/benkyo/tree/master/appengine-relative-mod

デプロイするために必要な設定2つ

  1. デプロイ時にはvendorモードでデプロイする
  2. デプロイする際にgo.modファイルが存在してはだめ

上記の2点の対策が必要です。

vendorモードでデプロイするとは?

普通にmodを利用したAppengineのデプロイは以下のようなコマンドを実行します。

GO111MODULE=on gcloud app deploy ./app.yaml

gcloudコマンドをmodで管理する GO111MODULE=on という環境変数の元実行します。 先程の話のように、相対的にモジュールを管理している場合、この方法ではデプロイは失敗します

成功するためには

vendorモードでデプロイする必要があるため、以下のような手続きでコマンドを実行します。

GO111MODULE=on go mod vendor                 // vendorディレクトリを作成する
GO111MODULE=off gcloud app deploy ./app.yaml // Appengineのデプロイを実行
rm -rf ./vendor                              // 一時的に作成したvendorディレクトリを削除する(optional)

つまり、デプロイの際に、vendorディレクトリ配下に相対的に管理しているモジュールのソースコードを持ってきて、 そのファイルごと依存としてGCP側のデプロイ用のCloudBuildにまるごと渡します。

もう一つの嬉しい作用として、vendorモードでデプロイするとGAEのデプロイが若干早くなるようです。

注意: この1点目だけではうまくいきません。

go.modファイルを除外する

デプロイする際に、デプロイするアプリケーションのディレクトリ内にgo.modファイルが存在する場合、 GO111MODULE=off でデプロイしてもCloudBuild側でmodを利用した依存解決をしようとします。

そのため、glcoud app deployコマンドでgo.modファイルを持っていかないように .gloudignore ファイル内に

go.mod

と記述します。 これでCloudBuildのデプロイプロセスではvendorモードとして動作し、Appengineへのデプロイが成功します。

参考まで

Github にモノレポで相対パスでmod管理し、かつ、Appengineにデプロイできる簡単なサンプルを作成してみました。

シンプルなテンプレートエンジンのenvsubstをインストールせずに使う

f:id:chidakiyo:20190719143057j:plain

motivation

色々な仕組みを作るときにテンプレートエンジンが欲しくなることがあると思いますが、 あまり仰々しい感じの仕組みを利用したくない、(or インストールしたくない)ということはあると思います。

kubernetes(k8s)の設定ファイルのパラメータを環境ごとに入れ替えたり、 Appengineのapp.yamlの値を環境ごとに差し替えたい、ということもあるかもしれません。

そんなときに気軽に使えるのがenvsubst。 k8sのconfigにはkustomize( https://kustomize.io/ )という手もありますが。。

envsubstの使い方

envsubstコマンドの基本としては以下のように利用します

テンプレートファイルは以下のイメージです

cat template.txt

${HELLO} ${WORLD}

実行コマンドは以下のようになります

export HELLO=hello
export WORLD=world
envsubst < template.txt // hello world

という感じで、環境変数に定義していた値でtemplateファイルのプレースホルダを置き換えます。

使い方は簡単ですが、envsubstコマンドのインストールが必要です。

インストールせずにenvsubstを利用する

envsubstをインストールしたくないのでDockerをインストールするのか!という突っ込みはありそうですが、 CI/CD環境などでの利用を想定しています。

今回利用するコンテナは supinf/envsubst というものを使ってみます。 DockerhubのURLは https://hub.docker.com/r/supinf/envsubst で、 Githubでは https://github.com/supinf/dockerized-tools/tree/master/cli-tools/envsubst/versions/0.x でどのようなものか確認できます。

利用1 : 環境変数もファイルとして値を与える

以下のようなテンプレートファイルを用意します。

cat template.txt

${A}
  ${B}

環境変数ファイルとして以下のようなファイルを用意します

cat env.txt

A=foo
B=bar

Dockerコマンドでenvsubstを実行すると以下のような結果が得られます。

% docker run --rm -it -v $(pwd):/tmp  supinf/envsubst /tmp/template.txt /tmp/env.txt
foo
  bar

こちら のentrypoint.shを見るとわかりますが、第一引数にはテンプレートファイルを取り、第二引数が存在する場合にはenvファイルを利用するというふるまいになっています。

利用2 : 環境変数を直接渡す

先程のテンプレートファイルをそのまま利用します。

Dockerコマンドで環境変数を渡しながらenvsubstコマンドを以下のように実行します。

% docker run --rm -it -v $(pwd):/tmp -e A=FOO -e B=HOGE supinf/envsubst /tmp/template.txt
FOO
  HOGE

-e オプションで渡した環境変数の値がテンプレートに差し込まれて出力されていることがわかります。

追記 : docker実行したenvsubstから生成ファイルを取得する

docker run --rm -it -v $(pwd):/tmp -e A=FOO -e B=HOGE supinf/envsubst /tmp/template.txt >| config.txt

まとめ

このような形で、環境ごとに設定ファイルのパラメータの変更をしたいなどという場合に、envsubstをインストールせずとも(or インストールできない環境でも)テンプレートファイルの処理が簡単に行なえます。

コンテナでgcloudコマンドを実行するとERROR: gcloud crashed (UnicodeDecodeError): 'ascii' codec can't decode byte 0xe8 in position 1: ordinal not in range(128) みたいなエラーが出る

f:id:chidakiyo:20190719134440j:plain

タイトルの通りで、 コンテナ環境で gcloud コマンドを実行した際 ERROR: gcloud crashed (UnicodeDecodeError): 'ascii' codec can't decode byte 0xe8 in position 1: ordinal not in range(128) のようなエラーが発生してうまく実行できない。

なぜ

glcoudコマンドの中でpythonを実行しているようなのですが、処理するファイルに非asciiな文字列が含まれている場合、pythonのレイヤーでエラーが出ているようです。

これはgcloudコマンド特有の問題ではなく、コンテナ内で各種コマンドを実行した際のあるあるです。 (昔一度ハマったのにまたハマったので記事にしました)

対策

解決方法としては、一番手っ取り早いのは

export LANG=C.UTF-8

のような形で環境変数にLANGを指定してあげれば良いです。

LANG="ja_JP.UTF-8" のような形で設定することもできますが、 その場合には apt-get -y install language-pack-ja みたいな形でlanguage packのインストールが必要そうです。

インストールしていない場合には -bash: warning: setlocale: LC_ALL: cannot change locale (ja_JP.UTF-8) のようなエラーメッセージが出ます

モノレポ(mono repository)内でgoのmoduleを相対パスで利用する方法

f:id:chidakiyo:20190703173656j:plain

goの依存管理ツールはいろいろありますが、最新はGo Modules(mod)を利用することが多くなってきたと思います。 dep(vendor)を利用した依存解決の場合にはgopath配下にあればある程度柔軟に相対的に依存を解決できましたが、 modは普通に利用するとリポジトリにコミットをした状態を期待しているような振る舞いをします。

モノレポで以下のように複数のモジュールを管理している場合には、modでは利用しづらいように思えましたが、 参照しているモジュールをコミットした状態ではなくとも参照したいという要望があったので解決してみました。

project
  ├ a_module
  │  ├ go.mod
  │  └ main.go
  ├ b_module
  │  ├ go.mod
  │  └ hoge
  │     └ say.go
  ...

各モジュールをmod化する

各モジュールをmod化します。 mod init する際に以下のように module 名を設定します。

// a_moduleをmod化
$ cd path/to/project/a_module
$ go mod init a_module

// b_moduleをmod化
$ cd path/to/project/a_module
$ go mod init b_module

作成されたmodファイルは以下のようになります。

a_module/go.mod

module a_module

go 1.12
b_module/go.mod
module b_module

go 1.12

a_moduleからb_moduleを参照する

a_moduleからb_moduleを利用したい場合、a_moduleのmodファイルを以下のように追記します

a_module/go.mod

module a_module

go 1.12

require (
    b_module v0.0.0
)

replace b_module => ../b_module

b_moduleを依存として利用します(require)という記述と、 b_moduleは ../b_module という相対位置にありますという記述が必要になります。 modの相対位置の関係はgo.modファイルがある位置になるようです。

コード側からb_moduleの関数を利用する

先程の記述は依存関係を定義したので、実際にa_moduleのファイルからb_moduleの関数を利用してみます。

a_module/main.go

package main

import (
    "b_module/hoge"
)

func main() {
    hoge.Say() // b_moduleで実装した関数 // Hello
}

main.goが作成できたら go run main.go などで実行します。 実行の際に依存関係の取得が行われます。

ちなみに b_module/hoge/say.go の中身はこんな感じになるかと思います。

package hoge

func Say() string {
    return "Hello"
}

まとめ

このようにmodファイルを構成することでモノレポで管理しているプロジェクトでも 共通のユーティリティモジュールなどを柔軟に参照することができます。

一時 GO111MODULE=off を利用し、vendor(dep) を引き続き利用していましたが、 こちらの方法でも同じように管理できるようになったのでまとめました。

ツッコミどころなどあればコメントください。

次回作の予告

上記のmodでの相対的なモジュール管理はGAEで利用しようとすると、Appengineのbuildのプロセスでは相対的に取得できるはずのモジュールが取得できないため、デプロイがエラーになります。 その部分も回避策があるため次回の記事で書きます。

参考

cmd/go: clarify go.mod documentation for trivial relative modules · Issue #27274 · golang/go · GitHub

go build keeps complaining that: go.mod has post-v0 module path - Stack Overflow

Docker build時にキャッシュを利用しない

Docker build する際に、基本的にはCacheを利用されますが、 それでは困る(毎回ちゃんと処理したい)という場合もあると思います。

そんなときは --no-cache オプションを docker build 時に付与すれば良い。

ま、それだけ。

docker build | Docker Documentation

BQMLのサンプルのqueryitを触ってみた

query-itってなに?

Groovenautsさんが作った、BQMLのサンプルアプリケーション ソースコードもろもろは多分 こちら

こちらのURL からアクセスできたのでメモがてら記事を残します。

使い方

URLでアクセス

http://queryit.magellanic-clouds.com/ にアクセスします。

最初、reCAPTCHAが表示されるのでポチッとしましょう。

f:id:chidakiyo:20190411085732p:plain

メニューを選ぶ

  • wikimediaのイメージから似たものを探す
  • ニューヨークのレンタル自転車の需要予測

の2つから選択できます。

f:id:chidakiyo:20190411085756p:plain

wikimediaを実行してみる

wikimediaのアイコンをクリックしたあとでランダムに画像が3点表示されます。 選択した画像に似た画像をBQを利用して公開画像から探して表示をしてくれます。

3つのうちのどれかを選択してみましょう

f:id:chidakiyo:20190411085813p:plain

実行するとBQでクエリが実行され少し待ち時間があります。

f:id:chidakiyo:20190411085835p:plain

クエリの実行が終了すると画面内にBQが似ていると判定した画像たちが表示されます。 たしかに結構似てますね。

f:id:chidakiyo:20190411085855p:plain

実行したクエリを見る

ShowSQLボタンを押すと実行したクエリが表示されます。 クエリの一部が表示されるので、スクロールして全体を見ることができます。

f:id:chidakiyo:20190411085914p:plain

レンタル自転車の需要予測を実行してみる

メニューからニューヨークのレンタル自転車の需要予測を選択します。

条件を指定するダイアログが表示されますので、 予測したい条件を入力します。

f:id:chidakiyo:20190411085935p:plain

クエリが実行され、しばらく待ちます。

f:id:chidakiyo:20190411085950p:plain

だいたい20秒ぐらいで予測が完了します。 めちゃ速いですね

f:id:chidakiyo:20190411090008p:plain

画面のcloseを選択し、地図上に表示された予測を見てみましょう

f:id:chidakiyo:20190411090030p:plain

先程と同様に ShowSQL ボタンを押すことでクエリを見ることができます。

さいごに

こちらのデモは2年ぐらい前のGoogle Next Tokyoで見たのですが、 BQの可能性を感じたとてもおもしろいサンプルで、今回publicにアクセスできるURLを見つけたので感動をシェアしたくて記事にしました。

BQ便利なのでどんどん使っていきましょう!