【メモ】 DockerでMySQLサーバーコンテナを起動する

バージョンを指定してイメージの取得

docker pull mysql:5.7.25

MySQLサーバーコンテナを起動し、DATABASEを作成したりする

  • hoge というDBの作成
  • inari111 というユーザーの作成
  • -eオプションは環境変数を設定
  • -d detachといい、バックグラウンドでコンテナを起動
docker run --name {コンテナ名} -e MYSQL_DATABASE=hoge -e MYSQL_USER=inari111 -e MYSQL_PASSWORD=hogehoge  -e MYSQL_ROOT_PASSWORD={rootユーザーパスワード} -d mysql

コンテナに接続

docker run --link {コンテナ名}:mysql -it --rm mysql bash

-i : ホストの入力をコンテナの標準出力をつなげる
-t : コンテナの標準出力とホストの出力をつなげる

(シェルのような)インタラクティブなプロセスでは、コンテナのプロセスに対して tty を割り当てるために、 -i -t を一緒に使う必要があります。 後の例で出てきますが -i -t は -it と書けます。

Docker run リファレンス

クリーンアップ (--rm)

デフォルトではコンテナを終了しても、コンテナのファイルシステム(の内容)を保持し続けます。これにより、多くのデバッグをより簡単にします(最後の状態を確認できるため)。そして、全てのデータを維持し続けるのがデフォルトです。しかし、短い期間だけ フォアグラウンド で動かしたとしても、これらのコンテナのファイルシステムが溜まり続けます。そうではなく、 コンテナの終了時に、自動的にコンテナをクリーンアップし、ファイルシステムを削除する には --rm フラグを追加します。

コンテナ内でのコマンドの実行が終わったらコンテナを自動で削除するということ。

MySQLに接続

hostはenvコマンドを叩いて表示された MYSQL_PORT_3306_TCP_ADDR を使う

mysql -u inari111 -p hogehoge -h {MYSQL_PORT_3306_TCP_ADDR}

EC2にGoのアプリケーションをデプロイしてレスポンスが返ってくるまで

EC2のインスタンスが起動している前提で進めます。

ビルドする

GOOS=linux GOARCH=amd64 go build

デプロイする

ビルドしたbinaryを配置することにしました。 今回はapp以下に配置します。

scp -i ~/.ssh/hoge.pem {binary名} ec2-user@{IP}:~/app

Nginxのインストール

Nginxはリバースプロキシとして使います。

$ amazon-linux-extras | grep nginx
  4  nginx1.12                available    [ =1.12.2 ]
$ sudo amazon-linux-extras install nginx1.12

起動

$ sudo systemctl start nginx.service

自動起動

$ sudo systemctl enable nginx.service

自動起動の確認

$ systemctl is-enabled nginx.service
enabled

nginx.confの編集

80ポートで受け取って8080ポートのアプリに繋ぐようにします。

server_name  {パブリックIP};

location / {
        proxy_pass http://127.0.0.1:8080;
}

再起動します

sudo service nginx restart

EC2のセキュリティーグループの設定を変更する

EC2のインスタンス作成時にセキュリティーグループは作成されていますが、デフォルトではポート22だけ空いている状態です。
新たにHTTP 80のルールを追加します。

binary実行

cd app

./{binary名}

これでレスポンスを確認できるようになりました。

Cloud BuildでAPIドキュメントの生成を自動化

f:id:inari111:20181215134720p:plain

https://cloud.google.com/cloud-build より引用

ピックアップ Advent Calendar 2018 16日目です。
今日はサーバーサイドエンジニアのinari111がお送りします。
Goを毎日書いていますが、Goの話は出てきません… adventar.org

弊社ではProtocol BuffersというIDL(Interface Definition Language, インタフェース定義言語)を採用しています。
以前は、protocコマンドでProtoファイルからAPIのドキュメントを生成 -> GCS(Google Cloud Storage)にアップロードする作業を手動で行っていました。
手動でドキュメント生成をするのは辛く、そして忘れてしまうこともあるので、この記事ではCloud Buildを使って自動化するまでを解説したいと思います。

Cloud Buildとは

ビルド、テスト、デプロイなどを実行してくれる従量課金制なビルドサービスです。 1日あたり120分まで無料枠があります。
各ビルドステップはDockerコンテナで実行され、Dockerイメージは公式ビルダーcloud-builders-community もあるので必要に応じて使うことができます。

ファイル構成

Protoファイルはアプリケーションのソースコードとは別のリポジトリで管理していて、ファイル構成は以下のようになっています。
今回のゴールは、 proto/admin_api/*.proto proto/core/*.proto proto/user_api/*.proto のどれかに変更があったらProtoファイルからAPIドキュメントを生成し、GCSにアップロードをすることです。

.
├── Makefile
├── README.md
├── _doc
│ └── rpc (生成したドキュメントを置く)
├── _docker
│ └── Dockerfile
├── cloudbuild.yaml (ビルドステップを定義)
├── proto
│   ├── README.md
│   ├── admin_api (Admin API用Protoファイル)
│   │   └── aaa.proto
│   ├── core (Admin API, User API両方でimportするProtoファイル)
│   │   └── bbb.proto
│   └── user_api (User API用Protoファイル)
│       └── ccc.proto
└── rtdb
 └── hoge_db_schema.yaml (Realtime DatabaseのSchema)

トリガーを作成する

GitHubを選択 f:id:inari111:20181215121033p:plain

その後、対象のリポジトリの選択をします。

f:id:inari111:20181215121132p:plain

トリガーの設定を書いていきます。

f:id:inari111:20181215121331p:plain

GitHubにpushしたときに proto/admin_api proto/core proto/user_api 下のどれかのProtoファイルに変更があれば ビルドが実行されるようにしました。
proto/**/*.proto と書いても動かなかったので、下記のように記載しています。

cloudbuild.yamlを作成

cloudbuild.yamlを用意します。CircleCIのconfig.yamlみたいなものです。
各stepをこのように定義しました。

steps:
# Dockerビルド
- name: 'gcr.io/cloud-builders/docker'
  dir: "_docker"
  args: ['build', '-t', 'gcr.io/$PROJECT_ID/{image名}', '.']

# Protoファイルからドキュメント生成
- name: 'gcr.io/$PROJECT_ID/{image名}'
  args: ['make', 'gen-proto-doc']

# GCSにアップロードする
- name: 'gcr.io/cloud-builders/gsutil'
  dir: "_doc/rpc"
  args: ['-m', 'cp', 'index.html', '${_RPC_DOC_PATH}/${BRANCH_NAME}_${SHORT_SHA}.html']

# Container Registryにpush
images: ['gcr.io/$PROJECT_ID/{image名}']

各stepを軽く解説します。
1つ目は、Dockerをビルドします。 dir で指定したディレクトリにあるDockerfileを対象とし、 gcr.io/$PROJECT_ID/{image名} というイメージを作成します。

2つ目は、前stepでビルドしたイメージを使い、 make gen-proto-doc を実行します gen-proto-doc

protoc -Iproto --doc_out=_doc/rpc --doc_opt=html,index.html proto/**/*.proto

を実行するようにMakefileに定義していて、Protoファイルからドキュメントを生成することができます。
結果として _doc/rpc/index.htmlが作成されます。

3つ目は gsutilコマンドを使ってGCSにファイルをアップロードします。 BRANCH_NAMESHORT_SHA はcloudbuild.yaml上で使うことができる変数です。デフォルトで使えるものは他にもあるのでこちらを参照してください。https://cloud.google.com/cloud-build/docs/configuring-builds/substitute-variable-values
独自の変数をトリガーの設定ページから登録することもできます。今回はGCSのパスを_RPC_DOC_PATH として登録していました。

最後の images を書くことで Container Registryにpushできます。不要だったら記載する必要はありません。

ローカルで検証

cloud-build-localをインストール

gcloud components install cloud-build-local

cloud-build-localをインストールしたら、ローカルで実行することができます。
--configでビルドリクエストファイルを指定
--dryrun=falseでビルドを実行
--pushはビルド中に作成されたイメージをContainer Registry にpushするかどうかです
変数に _RPC_DOC_PATH を設定しているので --substitutions=_RPC_DOC_PATH=gs://{バケット名}/{パス} を指定する必要があります。
ローカルから実行する場合に限り、 BRANCH_NAMESHORT_SHA の指定も必要です。 ローカルで実行する場合はこんな感じになります。

cloud-build-local --config=cloudbuild.yaml --dryrun=false --push=false --substitutions=_RPC_DOC_PATH=gs://{バケット名}/{パス},BRANCH_NAME=hoge,SHORT_SHA=aaaaa .

↑を実行するとGCS上にアップロードできました。 f:id:inari111:20181215141458p:plain

開発時の検証やテストはcloud-build-localでだいたい事足りました。

ローカルからビルドを送信

実際にローカルからビルドを送信してリモートでビルドを実行した場合は、以下のコマンドを実行します。

gcloud builds submit --config=cloudbuild.yaml --substitutions=_RPC_DOC_PATH=gs://{バケット名}/{パス},BRANCH_NAME=hoge,SHORT_SHA=aaaaa

ブラウザからビルド実行

ブラウザからも実行可能です。

f:id:inari111:20181215142358p:plain

GitHubへのpushをトリガーにビルド実行

これは書くまでもないですね。
ビルドを実行したくなかったら、[skip ci] または [ci skip] をコミットメッセージに書くとビルドがスキップされます。

最後に

  • Cloud Buildを使い、APIドキュメントの生成を自動化することができました
  • 特定のパスのファイルに変更があった場合のみ処理を実行することができるのは便利です
  • 今回は触れませんでしたが、ドキュメントのURLをSlackに通知するのもやっていく予定です
  • 他の会社でもProtocol Buffers採用してほしい

HHKBのキーキャップをピンクに交換した

インスタの#HHKBタグをフォローしてチェックしていると、海外でキーキャップを交換している方が多く、前々から興味がありました。

Massdrop で見つけて購入を検討していたのですがいつの間にか終わってしまい、 結局AliExpressで購入しました。値段は$32.90 + 送料
海外から配送されるので到着まで10日ほど。

このようにキーキャップとキーキャップを引き抜く工具がついてきました。
(ついていないと思って工具も買っちゃっていた...)

f:id:inari111:20181008221430j:plain

まずは、キーキャップを外します。
半年近く掃除していなかったので、ついでに掃除もしました。

f:id:inari111:20181008221450j:plain

最後にキーキャップを取り付けて完了です。

f:id:inari111:20181008221505j:plain

思ったよりも派手な感じもしますが1週間も使えば慣れました。
ErgoDoxも気になり始めたしキーボードは沼だなー

favclip/testeratorを使ってGAE/Goのテストを高速化する

testeratorはGAE/Goのテストを高速化するライブラリです。
このライブラリを使ってテストの高速化ができたので紹介したいと思います。

contextを必要とするテストでは aetest.NewContext()aetest.NewInstance() を使うかと思いますが、これを使うと goapp test実行時にローカルサーバーが関数ごとに起動し、遅くなってしまいます。

func TestHoge(t *testing.T) {
    ctx, done, err := aetest.NewContext() // ローカルサーバー起動
    if err != nil {
        t.Fatal(err)
    }
    defer done()

    // 略
}

func TestFuga(t *testing.T) {
    inst, err := aetest.NewInstance(&aetest.Options{StronglyConsistentDatastore: true}) // ローカルサーバー起動
    if err != nil {
        return nil, nil, err
    }
    req, err := inst.NewRequest("GET", "/", nil)
    if err != nil {
        inst.Close()
        return nil, nil, err
    }
    ctx := appengine.NewContext(req)
    defer inst.Close()

    // 略
}

テストの数が多くなってくると、結構ストレスが溜まります。。。

ここで使うのが testeratorです github.com

testeratorはテスト間でインスタンスを使い回すことができるため、テストが高速化できるという仕組みです。
では、実際に使っていきます。

import (
    "fmt"
    "os"
    "testing"

    "github.com/favclip/testerator"
    _ "github.com/favclip/testerator/datastore"
    _ "github.com/favclip/testerator/memcache"
    _ "github.com/favclip/testerator/search"
)

func TestMain(m *testing.M) {
    if _, _, err := testerator.SpinUp(); err != nil {
        fmt.Printf("testerator.SpinUp error: %v", err)
        os.Exit(1)
    }

    status := m.Run()

    if err := testerator.SpinDown(); err != nil {
        fmt.Printf("testerator.SpinDown error: %v", err)
        os.Exit(1)
    }

    os.Exit(status)
}

func TestHoge(t *testing.T) {
    instance, ctx, err:= testerator.SpinUp()
    if err != nil {
        t.Fatal(err)
    }
    defer testerator.SpinDown()

    // 略
}

func TestFuga(t *testing.T) {
    instance, ctx, err := testerator.SpinUp()
    if err != nil {
        t.Fatal(err)
    }
    defer testerator.SpinDown()

    // 略
}

これだけです。 TestMainm.Run で各テストを実行していきます。
僕はTestMainを書くのを忘れていて、あれ...全然早くならないぞ...と少し悩んだので忘れないようにしましょう。

If you want to clean up Datastore or Search API or Memcache, You should import above packages.

とGoDocに書いてあるように、必要に応じてimportしておきましょう。

contextやinstanceを必要とする全てのテストを書き換えた結果、改善前の時間より 約30% 短縮することができました

まとめ

  • aetest.NewContext()aetest.NewInstance() はテスト関数ごとにローカルサーバーが起動するので遅い
  • testeratorでインスタンスを使い回すことでテストの時間を30%短縮できた

FirebaseUIでTwitterログイン機能を実装する

FirebaseUIはFirebase Authentication SDK上に構築されるライブラリで、アプリで使用するログイン機能とUIを提供してくれるものです。
メールアドレス、FacebookTwitterGitHub、電話番号といった複数のプロバイダに対応しています。
今回はTwitterでログインできるようにします。

Twitter Appsの作成

https://apps.twitter.com/app/new から新しいAppsを作成します。
Callback URLは後で入力します。

f:id:inari111:20180722221015p:plain

Firebase Authenticationの設定を変更する

Authentication -> ログイン方法 からログインプロバイダの設定を変更します。
今回はTwitterを有効にしました。
APIキーとAPIシークレットは ↑で作成したAppsのものを使いましょう。

f:id:inari111:20180722221353p:plain

コールバックURLをTwitter Appに追加

↑でTwitterを有効にするときに表示されたコールバックURLをTwitter Appに追加します。

コード

Firebaseのドキュメントのコードほぼそのままで動きます。 (僕はconfig間違えてて無駄にハマった。。。) configの値は各自の環境に合わせてください。

<script src="https://www.gstatic.com/firebasejs/5.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/5.0/firebase-auth.js"></script>

<script src='https://cdn.firebase.com/js/client/2.2.1/firebase.js'></script>
<script src="https://cdn.firebase.com/libs/firebaseui/2.5.1/firebaseui.js"></script>
<link type="text/css" rel="stylesheet" href="https://cdn.firebase.com/libs/firebaseui/2.5.1/firebaseui.css" />

<script>
    var config = {
        apiKey: "<API_KEY>",
        authDomain: "<PROJECT_ID>.firebaseapp.com",
        databaseURL: "https://<DATABASE_NAME>.firebaseio.com",
        storageBucket: "<BUCKET>.appspot.com",
        messagingSenderId: "<SENDER_ID>",
    };
    firebase.initializeApp(config);

    var ui = new firebaseui.auth.AuthUI(firebase.auth());

    var uiConfig = {
        callbacks: {
            signInSuccessWithAuthResult: function(authResult, redirectUrl) {
                // User successfully signed in.
                // Return type determines whether we continue the redirect automatically
                // or whether we leave that to developer to handle.
                return true;
            },
            uiShown: function() {
                // The widget is rendered.
                // Hide the loader.
                document.getElementById('loader').style.display = 'none';
            }
        },
        // Will use popup for IDP Providers sign-in flow instead of the default, redirect.
        signInFlow: 'popup',
        signInSuccessUrl: 'http://example.com',// Sign in後の遷移先
        signInOptions: [
            // Leave the lines as is for the providers you want to offer your users.
            firebase.auth.GoogleAuthProvider.PROVIDER_ID,
            firebase.auth.FacebookAuthProvider.PROVIDER_ID,
            firebase.auth.TwitterAuthProvider.PROVIDER_ID,
            firebase.auth.GithubAuthProvider.PROVIDER_ID,
            firebase.auth.EmailAuthProvider.PROVIDER_ID,
            firebase.auth.PhoneAuthProvider.PROVIDER_ID
        ],
        // Terms of service url.
        tosUrl: ''
    };

    ui.start('#firebaseui-auth-container', uiConfig);

</script>
<h1>Welcome to My Awesome App</h1>
<div id="firebaseui-auth-container"></div>
<div id="loader">Loading...</div>

こんな感じで表示されます。 f:id:inari111:20180722220426p:plain

Sign in with Twitter をクリックした後 Authorize app すればログイン完了です。
Firebase Authenticationのコンソールを見るとユーザーが作成されていることがわかります。

f:id:inari111:20180722222559p:plain

まとめ

FirebaseUIを使うと簡単にTwitterログインを実装することができました。
次はFirebase SDKを使ってログイン機能の実装をしていきたいと思います。

BigQueryの集計結果をテーブルに保存した後GCSにエクスポートしてみた

こういう細かいネタもブログに残していこうと思います。
ってブログ書く度に決意しているんだけど続かないですね...

BigQueryの集計結果をテーブルに保存

使うpackageは cloud.google.com/go/bigquery です。このpackageはまだbetaらしいです。
google.golang.org/api/bigquery/v2 でもできるはずですが、cloud.google.com/go/bigqueryのほうが簡潔に書けそうな感じがしました。

コードはこんな感じ
AppEngine上で動かすコードになっていますが、一部変更すれば、AppEngine以外でも動くはずです。

package hoge

import (
    "context"

    "cloud.google.com/go/bigquery"
    "google.golang.org/appengine"
)

// BigQueryのSchema例
type schema struct {
    UserID   string
    UserName string
}

const datasetID = "test"
const tableID = "test"
const query = ""

// 関数名適当で申し訳ない
func bq(ctx context.Context) error {
    client, err := bigquery.NewClient(ctx, appengine.AppID(ctx))
    if err != nil {
        return err
    }

    // 集計結果を保存するDatasetを作成
    d := client.Dataset(datasetID)
    d.Create(ctx, &bigquery.DatasetMetadata{
        Location: "US", // お好みで
    })

    // 集計結果を保存するTable作成
    s, err := bigquery.InferSchema(schema{})
    if err != nil {
        return err
    }
    table := client.Dataset(datasetID).Table(tableID)
    if err := table.Create(ctx, &bigquery.TableMetadata{Schema: s}); err != nil {
        return err
    }

    // Queryの準備
    q := client.Query(query)
    q.WriteDisposition = bigquery.WriteTruncate // お好みで
    q.Dst = &bigquery.Table{
        ProjectID: appengine.AppID(ctx),
        DatasetID: datasetID,
        TableID:   tableID,
    }
    // 最大レスポンスサイズを超えそうだったらtrueにする
    // see: https://cloud.google.com/bigquery/querying-data#large-results
    q.AllowLargeResults = true

    job, err := q.Run(ctx)
    if err != nil {
        return err
    }
    jobStatus, err := job.Wait(ctx)
    if err != nil {
        return err
    }
    if err := jobStatus.Err(); err != nil {
        return err
    }
    return nil
}

BigQueryのテーブル指定してをGCSにexport

CSVファイルにしてexportしてみます

package hoge

import (
    "cloud.google.com/go/bigquery"
    "google.golang.org/appengine"
)

const datasetID = "test"
const tableID = "test"
// export先を指定
const gcsURI = "" // TODO

func export(ctx context.Context) error {
    client, err := bigquery.NewClient(ctx, appengine.AppID(ctx))
    if err != nil {
        return err
    }

    gcsRef := bigquery.NewGCSReference(gcsURI)
    gcsRef.SourceFormat = bigquery.CSV // defaultはCSV
    gcsRef.FieldDelimiter = ","        //defaultは ","
    gcsRef.AllowQuotedNewlines = true

    extractor := client.Dataset(datasetID).Table(tableID).ExtractorTo(gcsRef)
    extractor.DisableHeader = false // お好みで
    job, err := extractor.Run(ctx)
    if err != nil {
        return err
    }
    status, err := job.Wait(ctx)
    if err != nil {
        return err
    }
    if status.Err(); err != nil {
        return err
    }
    return nil
}

まとめ

  • cloud.google.com/go/bigquery を使って集計結果の保存とGCSへのexportをやってみました
  • bigquery packageを初めて使ったので調査のコストは少しありました
  • UTF-8でexportされるのでExcelで開くと文字化けするかも...