読者です 読者をやめる 読者になる 読者になる

KotlinでgRPC。SSL/TLSを有効にする方法をまとめた。

前回のエントリではgrpc/grpc-javaをベースにkotlinでgRPCを試しました。今回はSSL/TLSを有効にする方法をまとめていきます。grpc/grpc-java/SECURITY.mdを参照しながら進めました。

証明書を準備する

手元に適当な証明書がなかったのでgrpc-go/testdataにある証明書を利用しました。

Subject Alternative Name を確認するとマルチドメイン*.test.google.fr, waterzooi.test.google.be, *.test.youtube.comが定義されていますので、hostsにwaterzooi.test.google.beを追加しました。

openssl x509 -text -in ./server1.pem
127.0.0.1 waterzooi.test.google.be

OpenSSLを有効にする

netty-tcnativeをプロジェクトに追加します。

buildscript {
  repositories {
    mavenCentral()
  }
}

dependencies {
    compile 'io.netty:netty-tcnative-boringssl-static:1.1.33.Fork26'
}

netty-tcnativeはDynamicStaticがあります。このエントリではStaticを利用しました。grpc/grpc-java/SECURITY.mdを参照するとStaticの利用を推奨していますがOpenSSLのセキュリティパッチが提供された場合、Staticではパッチ反映が即座には行われないためプロジェクト方針によってはDynamicの利用を検討する必要があります。

GrpcSslContextsでSslContextを生成する

サーバ側とクライアント側ではGrpcSslContextsに定義されているforServerforClientのメソッドをつかってSslContextを生成します。

サーバ側

(serverBuilder as NettyServerBuilder).sslContext(
        GrpcSslContexts.forServer(
                File(classLoader.getResource("server1.pem").file),
                File(classLoader.getResource("server1.key").file))
                .clientAuth(ClientAuth.OPTIONAL)
                .build())

クライアント側

NettyChannelBuilder.forAddress("waterzooi.test.google.be", 50051)
        .sslContext(
                GrpcSslContexts.forClient()
                        .trustManager(File(classLoader.getResource("ca.pem").file))
                        .build())
        .build()

上記の実装を行えばSSL/TLSが有効になります。

コードを公開しています

このエントリのコードはgithubに公開しています。
SSL/TLSを有効にするにあたり情報が少ない印象をもちました。java or kotlinで実装をする方に少しでも参考になると嬉しいです。

github.com

関連エントリ

naruto-io.hatenablog.com

KotlinでgRPC。実運用にも活かせるWEBアプリケーション構成で試してみた。

KotlinでgRPCを試していきます。protocol buffersがkotlinに対応していないのでjavaに生成したものを使います。次のようなアプリケーション構成でKotlinを使ったgRPC通信を試してみました。

アプリケーション構成

f:id:n_soushi:20170413103848p:plain

  • エンドクライアントからのアクセスはGateway Serverが窓口となりHTTP/1.1で通信を行います。
  • Gateway ServerのバックエンドにいるgRPC ServerとはgRPC(HTTP/2)で通信を行います。
  • monitoring toolGateway ServergRPC Serverの監視を行いHTTP/1.1で通信を行います。

モチベーション

何度かgRPCについてのエントリをまとめてきました。kotlinでgRPCを試してみたいと感じていたのとSpring Framework 5.0のリリースを控えた状況でSpring Framework 5 on Kotlinを試してみたい欲求がありました。 そのためアプリケーション構成図にあるとおりGateway ServerにはRouter機能を試したいので spring-webfluxでアプリケーションを作りました。

次のエントリではSpring Framework 5.0でReactive Programmingを活用しながらkotlinらしいコードの紹介がされています。

spring.io

またgRPC ServerではHTTP/1.1gRPC(HTTP2)の2つの通信方式を有効にしたいです。Spring Bootでどのように実現するのか?この課題についても理解を深める必要がありました。

そして実戦に向けて実運用をイメージしたアプリケーション構成を構築する必要がありました。


ここからは構築にいたるまでの勘所や課題などについてまとめていきます。

gRPC Server

まずはgRPC Serverからです。
ここでの課題はHTTP/1.1gRPC(HTTP2)の2つの通信方式を有効にすることです。

  • monitoring toolからはヘルスチェックなどの監視リクエストに応えるためにHTTP/1.1で通信を行いたい
  • Gateway Serverとの通信にはgRPC(HTTP2)で通信を行いたい

この課題を解決するために次のspring-boot-starterを使いました。

github.com

こちらを使えばgRPC Serverを実装したクラスに@GRpcServiceをつけるだけでgRPC SeverをSpring Boot上に起動できます。またSpring Boot(spring-boot-starter-web)で起動していますのでHTTP/1.1の通信も有効です。

@GRpcService
class EchoServer : EchoServiceGrpc.EchoServiceImplBase() {

    override fun echoService(request: EchoMessage?, responseObserver: StreamObserver<EchoMessage>?) {
        val msg = EchoMessage.newBuilder().setMessage("echo \\${request?.message}/").build()
        responseObserver?.onNext(msg)
        responseObserver?.onCompleted()
    }
}

レポジトリのREADMEにあるとおりinterceptorの提供(ログ差し込んだり)やServerビルド定義もカスタマイズできます。

$ ./gradlew clean generateProto bootRun
・・・
2017-04-13 14:48:45.479  INFO 30602 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2017-04-13 14:48:45.482  INFO 30602 --- [           main] o.l.springboot.grpc.GRpcServerRunner     : Starting gRPC Server ...
2017-04-13 14:48:45.528  INFO 30602 --- [           main] o.l.springboot.grpc.GRpcServerRunner     : 'app.grpc.server.EchoServer' service has been registered.
2017-04-13 14:48:45.531  INFO 30602 --- [           main] o.l.springboot.grpc.GRpcServerRunner     : 'app.grpc.server.GreetServer' service has been registered.
2017-04-13 14:48:45.765  INFO 30602 --- [           main] o.l.springboot.grpc.GRpcServerRunner     : gRPC Server started, listening on port 50051.
・・・

8080ポート(HTTP/1.1)と50051ポート(gRPC/HTTP2)の両方が起動ログで確認できます。これでgRPC Serverの課題は解決です。

Gateway Server

次にGateway Serverです。
ここではspring-web-fluxを使いreactor coreベースでアプリケーションが動いています。そしてエンドクライアントからリクエストをgRPC Serverへ渡すgatewayの役割を担います(gRPCクライアントの役割)。
spring-web-fluxを使うとnon-blockingなservletが起動するためgRPCクライントには okhttpを使います。gRPCのクライアントは標準でnettyが使われるためokhttpを指定します。これをしないとreactor coreのアプリケーションとgRPC Serverのnon-blokingなところがバッティングしてしまうようです。

private fun getChannel() = OkHttpChannelBuilder.forAddress(appProperties.grpc.server.hostname, appProperties.grpc.server.port!!)
            // for testing
            .usePlaintext(true)
            .build()

もしGateway Serverエンドクライアントとの通信にgRPCを使いたい場合はGateway ServerにgRPC Serverを置く必要がありバッティング問題を解消しなくてはなりません。これに関してはSpring Framework5.0の正式リリースやマイルストーンの動きを見て試していく必要があり課題として残りました。

protobuf-gradle-plugin

protoclo bufferの生成には次のgradleプラグインを使っています。

github.com

起動時に.protoからprotocol bufferを生成するようにgradleに次ような設定をしています。

protobuf {
    protoc {
        artifact = 'com.google.protobuf:protoc:3.2.0'
    }
    plugins {
        grpc {
            artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
        }
    }
    generateProtoTasks {
        ofSourceSet('main').each { task ->
            task.builtins {
                java {
                    outputSubDir = 'protoGen'
                }
            }
            task.plugins {
                grpc {
                    outputSubDir = 'protoGen'
                }
            }
        }
    }
    generatedFilesBaseDir = "$projectDir/src/"
}

task cleanProtoGen {
    doFirst{
        delete("$projectDir/src/main/protoGen")
    }
}
clean.dependsOn cleanProtoGen

上記の定義により./gradlew clean generateProtoを実行することでprotocol bufferを再生成できます。起動時に最新の.protoからprotocol bufferを使われるように起動コマンドは./gradlew clean generateProto bootRunを使っています。


まとめ

  • 現バージョンのSpring Boot 1.5.2ではLogNet/grpc-spring-boot-starterを使うことでHTTP/1.1gRPC(HTTP2)の共存課題は解決できました。
  • Spring Framework 5ではreactor coreの採用からgRPC Serverとの共存には課題が残りました。今後のjavaのエコシステムなどを使いながら課題解決に取り組みます。

コードを公開しています

今回のKotlinでgRPCを試したコードのすべてはgithubに公開しています。 起動して確認するにはgithubのコードからできますのでREADMEを参照してください。

github.com

supersetをシュッと起動できるDockerfile(認証方式をGoogle API OAuth2に変更)をつくってみた、あと触ってみた所感など

ダッシュボードツールのsupersetをシュッと起動できるDockerfileを作りました。といってもsupersetのDockerfileはgithub.comに見かけるので認証方式をGoogle API OAuth2.0に変更したDockerfileを作りました。あとsupersetを触ってみての感想など導入に向けての所感をまとめたエントリです。

github.com

認証方式をOAuthに変更する方法

supersetは認証方式を変更できます。チームに最適な認証方式を選択できます。標準はDBにID/パスワードを登録する方式になっています。これをOAuthに変更する方法をまとめます。

コンテナ内の環境変数 SUPERSET_HOMEにセットしたディレクトリ配下にsuperset_config.pyを置いてsupersetの環境変数を上書きします。次のように認証方式をAUTH_OAUTHに設定し認証プロバイダの詳細設定を記述します。

import os
from flask_appbuilder.security.manager import AUTH_OAUTH
basedir = os.path.abspath(os.path.dirname(__file__))
AUTH_TYPE = AUTH_OAUTH
OAUTH_PROVIDERS = [
    {'name':'google', 'icon':'fa-google', 'token_key':'access_token',
        'remote_app': {
            'consumer_key':'{GOOGLE_AUTH_CLIENT_ID}',
            'consumer_secret':'{GOOGLE_AUTH_SECRET_KEY}',
            'base_url':'https://www.googleapis.com/plus/v1/',
            'request_token_params':{
              'scope': 'https://www.googleapis.com/auth/userinfo.email'
            },
            'request_token_url':None,
            'access_token_url':'https://accounts.google.com/o/oauth2/token',
            'authorize_url':'https://accounts.google.com/o/oauth2/auth'}
    }
]

認証方式をAUTH_OAUTHに設定した状態でsupersetを起動するとログイン画面で認証するサービスにGoogleが表示されます。

次に認証情報を作成したGoogle Developer ConsoleでリダイレクトURLhttp://localhost:8088/oauth-authorized/googleに設定するとGoogle Accountで認証ができます。またGoogle+ Apiからアカウント情報を取得しますのでDeveloper ConsoleでGoogle+ Apiを有効にします。

最後に認証させたいアカウントをfabmanegerを使って作成します。

docker exec -it superset \
  fabmanager create-admin --app superset \
  --username 'Google+ アカウントの表示名(displayName)' \
  --firstname '任意の名' \
  --lastname '任意の姓' \
  --email 'Google アカウントのメールアドレス' \
  --password '任意のパスワード'

Google以外にもTwitterFacebookなどの認証サービスを追加することができます。

詳しくはgithubのレポジトリに公開していますので合わせて確認できます。

GitHub - nsoushi/superset-demo: This repository contains demo using Superset. After begging containers, you can try Superset right now.

supersetが参照するDBを標準のsqliteからmysqlに変更する

標準ではsupersetが参照するDBはsqliteでOS内の$DATA_DIRにデータが格納されます。
これだとコンテナを削除するとダッシュボードの登録設定が消えてしまうのでsupersetのコンテナではない外のmysqlコンテナを起動させて参照させました。
Compose化して次のようにSQLALCHEMY_DATABASE_URI環境変数を変更しています。

SQLALCHEMY_DATABASE_URI = 'mysql://root@mysql:3306/app?charset=utf8mb4'

mysqlのコンテナではマルチバイト文字列も扱えるようにutf8mb4文字コードを有効にしています。 supersetアプリが参照するデータベースURLの末尾に?charset=utf8mb4をつければダッシュボードの名前にマルチバイト文字列が使えるようになります。

Dockerfileの使い方

次のレポジトリのDockerfileでコンテナを起動させるとsupersetが使えるようになります。
superset-demo/superset at master · nsoushi/superset-demo · GitHub

supersetだけを起動したい場合はsuperset-init.sh内の次の行をコメントアウトしてください。

SQLALCHEMY_DATABASE_URI = '${SUPERSET_DB_URI}'

mysqlのコンテナとセットで動かしたい場合はレポジトリのREADMEを参考にdocker-copomseでsupersetとmysqlのコンテナを起動してください。
GitHub - nsoushi/superset-demo: This repository contains demo using Superset. After begging containers, you can try Superset right now.

supersetを触ってみた感想など

最後にsupersetを触ってみた感想をまとめます。
初めてsupersetを触りましたが次のようなダッシュボードを作成することができました。

f:id:n_soushi:20170407141457p:plain

mysqlが提供するworldデータベースをデータソースにして人口やGNPの数値をグラフ化しました。

  • 人口の総数(VisualizationType: BigNumber)
  • 大陸ごとの人口総数(VisualizationType: Distribution - Bar Chart)
  • 大陸ごとの人口総数(VisualizationType: Distribution - Pie Chart)
  • 言語ごとのGNP(VisualizationType: Word Cloud)
  • 大陸ごとのGNP(VisualizationType: Treemap)

worldデータベースには時系列のデータがありませんが、時系列のデータがあれば集計条件に取得範囲時間を設定してダッシュボードを定期的に更新して定点観測することもできます。

グラフの作成手順

特にヘルプなどを見なくても直感的にグラフ作成まで進めます。
グラフ作成手順は次のような流れです。

  • データベースを登録する
  • 登録したデータベースからテーブルを登録する
  • テーブルからグラフ化するカラムを登録する
  • グラフ化に必要なメトリクスを登録する
    • データの総数が必要な場合はCount(*)、カラム値の総数が必要な場合(人口の総数)はSum(Population)などを登録する
  • 登録したテーブルからスライスを登録する
  • グラフを選択する
  • メトリクスとGroup Byするカラムを組み合わせる
  • 例)'Asia',‘Europe'などの大陸ごとに人口総数を出す場合は、メトリクスにSum(Population) を登録してContinentカラムをGroup Byする
  • 登録したグラフをダッシュボードへ登録する

https://raw.githubusercontent.com/nsoushi/superset-demo/master/docs/capture.gif

柔軟にテーブルを定義できる

柔軟にグラフ化したデータテーブルを定義できます。

  • DBにあるテーブルをつかう
  • 複数のテーブルをJoinさせた結果をテーブルとしてつかう
  • SQL LabSQLクエリを実行できる)で実行した結果からダイレクトにグラフ化に進む

ただ、SQL Labからダイレクトにグラフ化に進む方法は手元のバージョン(0.17.3)ではエラーとなりIssueとしても登録されていました。

github.com

SQL Lab

SQL Labでは作成したクエリを実行できます。
実行したクエリは履歴として残ります。後から再度実行できたり、実行結果から直接グラフを作ることもできます。
クエリを書ける人であればSQL Labでグラフ化したいデータの条件でSQLを作りメトリクスとグラフ作成に進むほうが効率が良さそうです。

f:id:n_soushi:20170407145848p:plain

機能権限とセキュリティ

Admin, Alpha, Gamma, sql_labなどのロールが用意されていてテーブルの登録権限、スライスの登録権限、SQL Labだけを使える権限などがあります。

http://airbnb.io/superset/security.html

機能権限に加えてユーザの操作ログや各種メニューへのアクセス権限などを設定することも可能です。
ここらへんはBIツールに必要そうな機能をサポートする姿勢が伺えます。

まとめ

  • RedashはSQLクエリの作成を起点としてダッシュボードを整える流れに比べて、supersetは予め準備されたデータソースを選択します。次に総数や平均など、どんなメトリクスでグラフを作るかを考えダッシュボードを整えます。データソースの選択からダッシュボード登録まで全て画面UIとして提供されているのでクエリを理解していなくても簡単にグラフを作成できます。
  • 必要なデータソースを予め準備するエンジニアと解析する人で役割を分ける運用ができます。解析者はエンジニアが準備してくれたデータソースをもとにメトリクスを作成して長所を活かした役割分担ができます。
  • ロールとセキュリティも担保されているので情報の公開範囲に注意しながら運用できます。
  • グラフの種類が豊富で定期的にグラフが更新されるダッシュボードが作れるので実行したSQL結果をエクセルに持っていきプレゼンしているような状況であれば利用の検討ができそうです。

コードを公開しています

コード全体はgitbubで確認できます。

github.com

go-grpc-prometheusでgRPCのmetricsをPrometeusとGrafanaでモニタリングしてみた

gRPC Ecosystemの1つにgo-grpc-prometheusがあります。今回は「gRPC Ecosystemgo-grpc-prometheusを試してみました」エントリです。

go-grpc-prometheus

github.com

go-grpc-prometheusはgRPCのmetricsをPrometheusでモニタリングできるログ出力をサポートするインターセプターを提供します。

取得できるmetricsはレポジトリのREADMEにまとまっています。
GitHub - grpc-ecosystem/go-grpc-prometheus: Prometheus monitoring for your gRPC Go servers.

gRPC Goはインターセプターをサポートしていますので次のようにClientとServerそれぞれに設定します。

PrometheusでモニタリングしたmetricsをGrafanaでもモニタリングしてみる

go-grpc-prometheusでgRPCのmetricsが取得できるようになります。Prometheusを起動すればmetricsをモニタリングできるようになります。合わせてPrometheusでモニタリングしているmetricsをGrafanaでもモニタリングしてみます。

シンプルなEchoサービスを作る

unary RPCsを利用してシンプルなEchoサービスを作ります。

proto

syntax = "proto3";

option go_package = "protobuf";
package proto;

service EchoService {
  rpc EchoService (Message) returns (Message) {}
}

message Message {
  string message = 1;
}

Server Side

Server sideはgRPCのClientからのリクエストに応えるServer-side of gRPCの役割とPrometeusのためのMetricsを出力する役割の2つが必要です。
1つのPortでHTTP/2 (gRPC)HTTP/1.1のリクエストを解釈する必要があるのでsoheilhy/cmuxを使います。

github.com

ほぼ素の使い方ですがServer-sideのソースは次のようになりました。(コード抜粋。詳細はnsoushi/go-grpc-prometheus-demoにあります。)

func main() {

    // Create the main listener.
    s, err := net.Listen("tcp", fmt.Sprintf(":%s", os.Getenv("GRPC_SERVER_PORT")))
    if err != nil {
        log.Fatal(err)
    }

    // Create a cmux.
    m := cmux.New(s)

    // Match connections in order:
    grpcL := m.Match(cmux.HTTP2HeaderField("content-type", "application/grpc"))
    httpL := m.Match(cmux.HTTP1Fast())

    // gRPC server
    grpcS := grpc.NewServer(
        grpc.UnaryInterceptor(grpc_prometheus.UnaryServerInterceptor),
        grpc.StreamInterceptor(grpc_prometheus.StreamServerInterceptor),
    )
    pb.RegisterEchoServiceServer(grpcS, newGrpcServer())

    // prometheus metrics server
    grpc_prometheus.Register(grpcS)
    httpS := &http.Server{
        Handler: promhttp.Handler(),
    }

    go grpcS.Serve(grpcL)
    go httpS.Serve(httpL)

    m.Serve()
}

unary RPCsのみなのでgrpc.StreamInterceptorは必要ないですがデモのため入れています。

Client Side

Client Sideはブラウザからリクエストを受け取りgRPCのServer-sideへリクエストを送ってくれるエンドポイントとPrometeusのためのmetricsを出力するエンドポイントの2つを用意します。

Client-sideのソースは次のようになりました。(コード抜粋。詳細はnsoushi/go-grpc-prometheus-demoにあります。)

func main() {
    //gRPC connection
    var err error
    conn, err = grpc.Dial(
        fmt.Sprintf("%s:%s", os.Getenv("GRPC_SERVER_HOST"), os.Getenv("GRPC_SERVER_PORT")),
        grpc.WithInsecure(),
        grpc.WithBackoffMaxDelay(time.Second),
        grpc.WithUnaryInterceptor(grpc_prometheus.UnaryClientInterceptor),
        grpc.WithStreamInterceptor(grpc_prometheus.StreamClientInterceptor),
    )
    if err != nil {
        log.Error("Connection error: %v", err)
    }
    defer conn.Close()

    // handle http
    http.Handle("/metrics", promhttp.Handler())
    http.HandleFunc("/echo", echoHandler)
    http.HandleFunc("/", indexHandler)

    // serve http
    http.ListenAndServe(fmt.Sprintf(":%s", os.Getenv("GRPC_CLIENT_PORT")), nil)
}

Prometheusでmetricsを確認する

PrometheusはDockerで起動しました。Dockerで起動するとprometheus.ymlのtargetsにlocalhostとしてもgRPCのServer-sideとClient-sideのホストへはアクセスできないのでdocker-composeを使いコンテナ構成をまとめてホスト解決を行います。

version: "3"

services:
  grpcserver:
    container_name: grpcserver
    build: ./server
    ports:
      - 8080:8080
    environment:
      GRPC_SERVER_HOST: grpcserver
      GRPC_SERVER_PORT: 8080

  grpcclient:
    container_name: grpcclient
    build: ./client
    ports:
      - 8081:8081
    environment:
      GRPC_SERVER_HOST: grpcserver
      GRPC_SERVER_PORT: 8080
      GRPC_CLIENT_HOST: grpcclient
      GRPC_CLIENT_PORT: 8081

  prometheus:
    container_name: prometheus
    build: ./prometheus
    ports:
      - 9090:9090
    depends_on:
      - grpcserver
      - grpcclient

  grafana:
    image: grafana/grafana
    ports:
      - "3000:3000"
    depends_on:
      - prometheus
      - grpcserver
      - grpcclient

Prometheusのコンテナを起動してhttp://localhost:9090/graphへアクセスするとgRPCのmetricsが insert metric at cursorのメニューに追加されているのが確認できます。

Grafanaでmetricsを確認する

Grafanaのコンテナもdocker-composeに入れましたのでhttp://localhost:3000/loginへアクセスするとGrafanaのダッシュボードを確認できます。Data Sourceにprometheusを追加してDashboardを作成します。

次のようなServer-sideのダッシュボードを作成しました。 f:id:n_soushi:20170328160648p:plain

gRPCのServer-sideのレスポンス送信数、クライアントからの受信数をGrafanaに設定しました。

nsoushi/go-grpc-prometheus-demografanaフォルダにServer-sideとClient-sideのダッシュボード設定をエクスポートしたJSONがあります。このJSONをインポートするとダッシュボードが簡単に作れます。詳細はレポジトリのREADMEを参照してください。

まとめ

  • go-grpc-prometheusをつかってgRPCのmetricsをPrometheusとGrafanaでモニタリングしました。
  • go-grpc-prometheusの導入はインターセプターを入れるだけなので簡単ではありますが複数のClient-sideとServer-sideの条件での検証、負荷検証などサービスへの導入検証が必要。

コードを公開しています

コード全体はgitbubで確認できます。

github.com

Terraform 0.9がリリース。0.8.xから0.9.xのStateマイグレーション手順をまとめました。

HashiCorpからTerraform 0.9がリリースされました。「よし、最新バージョンにあげよう。」と作業をはじめましたがremoteコマンドが使えない。どうやら0.9からはremoteコマンドが廃止されたようです。このエントリではTerraform 0.9にバージョンアップをして0.8以前のterraform stateをマイグレーションする方法をまとめます。

remoteコマンドの廃止

remoteコマンドが廃止になりました。代わりにbackendsを利用してS3などのremoteにあるtfstateファイルの管理を行います。

remote stateがbackendsに置き換わる過程は次のPull Requestから確認できます。 github.com

0.8以前を利用している場合はbackendを有効にしたtfstateファイルを用意する必要があります。次からは0.8.xまでのリソース状態を保持したまま新機能のbackendを有効にしたtfstateファイルへのマイグレーション手順についてまとめていきます。

マイグレーション手順

次の環境のマイグレーション手順になります。

  • 0.8.8から0.9.1へのバージョンアップする
  • これまではremoteにS3をつかっていて、これからもS3を利用する
  • ロールバックできるように、これまでのtfstateファイルは保持して新しいtfstateファイルを用意する
  • 0.8.80.9.1のterraformを使うのでtfenvを使ってterraformを切り替えながらマイグレーションを行う

1:tfファイルにterraformセクションを追加してbackends を設定する

次のように設定しました。

terraform {
  backend "s3" {
    bucket = "tfstate-bucket" // 自身のbucket名を設定します
  }
}
  • AWSaccess_key, secret_key, regionはそれぞれ環境変数AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGIONを設定しているため省略しています。
  • S3のkeyは必須ですが省略しています。後述するinitコマンドの-backend-config オプションで開発環境や本番環境ごとにS3のkeyを分けているためterraformセクションでは省略します。

※ その他bucketなどのS3の変数はこちらにまとまっています。

2:0.8.8のterraformをつかいremote configをしてtfstateファイルをローカルに同期する

tfenvでインストールしたバージョンリスト

terraform/dev ➤ tfenv list
0.9.1
0.8.8

0.8.8を使いremote configする

terraform/dev ➤ tfenv use 0.8.8
Terraform v0.8.8

terraform/dev ➤ terraform remote config -backend=S3 -backend-config="bucket=tfstate-bucket" -backend-config="key=dev"
Initialized blank state with remote state enabled!
Remote state configured and pulled.
  • S3のkeyは開発環境のdevとしています

3:0.9.1のterraformをつかいinitをしてtfstateファイルをマイグレーションする

terraform/dev ➤ tfenv use 0.9.1
Terraform v0.9.1

terraform/dev ➤ terraform init -backend-config "key=dev"
Initializing the backend...
New backend configuration detected with legacy remote state!
・・・省略・・・
  • 最初のaskでremote stateから変更するか?と聞かれるので yesを入力します。これをすることでtfstateファイル内のremotebackendに置き換わります。
  • 次のaskでremoteのstateをローカルのstateにコピーする?と聞かれるのでローカルのstateを保持したければnoを入力、コピーするのであればyesを入力します。すでにローカルにstateがあるのでnoと入力。

4:マイグレーションしたtfstateファイルをS3にアップロードする

マイグレーション後に0.8.8にロールバックするかもしれないので、0.8.8で運用したtfstateファイルを残したいです。そのため新しいS3のkeyをdev0.9と決めマイグレーションしたtfstateファイルをS3にアップロードします。

terraform/dev ➤ aws s3 cp ./.terraform/terraform.tfstate s3://tfstate-bucket/dev0.9

こうすることで開発中のtfstateファイルに影響が及ぶことはなくマイグレーションロールバックができる状態にします。

4:最後にplanを実行して新しいtfstateファイルにリソースの差分がないか確認する

terraform/dev ➤ rm -rf ./.terraform

terraform/dev ➤ tfenv use 0.9.1
Terraform v0.9.1

terraform/dev ➤ terraform init -backend-config "key=dev0.9"
Initializing the backend...
・・・省略・・・

terraform/dev ➤ terraform plan --refresh=false
No changes. Infrastructure is up-to-date.

This means that Terraform did not detect any differences between your
configuration and real physical resources that exist. As a result, Terraform
doesn't need to do anything.
  • 0.9.1からはremote configを使わずinitを使いtfstateファイルをローカルに同期します

まとめ

  • 0.9.1へtfstateファイルのマイグレーション手順をまとめました。
  • 0.8.xまではremote configを利用していましたが、0.9.1からはinitを利用します。
  • backendではtfstateのリソース情報がメモリ上に管理されます。0.8.xまではリソース状態がtfstateファイルを開けば確認できましたがbackendでは確認できません。リソース状態の管理がセキュアになりました。
  • backendはSTATE LOCKINGを機能が有効になります。複数人でapplyを実行した場合にstateをロックし競合を防ぎます。CIなどでapplyが同時に稼働しても安心です。
  • もし0.7.xからのマイグレーションの場合はリソース状態に差分が生まれているのでリソース状態を0.8系に合わせる必要があります。

参考URL

gRPC streamingをつかうとマイクロサービスの責務が整理できるし省コネクションでメリットあるよね、という話

今回はgRPCをマイクロサービス間通信に導入することってメリットあるよね、というエントリです。 定期的に処理を実行してくれるバッチはよくあるものですがバッチの駆動をgRPCを使って次のような構成で動かしました。

f:id:n_soushi:20170317102154j:plain

  • Batch ControlBatch ServerBidirectional gRPC streamingでコネクションする。
  • Batch ControlはRedisのPub/Subで特定のチャンネルを監視する。
  • Batch Controlはチャンネルにキューが投げられたらBatch Serverバッチ処理スタートのリクエストを送る。
  • Batch Serviceはリクエストを受け取りバッチを動かし処理結果をBatch Controlに送る(レスポンスを送る)。
  • チャンネルにキューが投げられる度に上記の流れでバッチを稼働させる。

上記の構成を踏まえ次からはメリットをまとめます。

gRPCをマイクロサービスに導入するとメリットあるよね

キューのRead権限をバッチサーバから剥がせる

キュー駆動でバッチを動かしている場合、例えばAmazon SQSを導入しているとRead権限が必要です。上記の構成であればキューを監視するのはバッチサーバではなくコントロールサーバになります。そのためキューを監視する権限をコントロールサーバに集約できるメリットがあります。

ログ集約サーバへの送信責務もバッチサーバから剥がせる

図のとおりgRPCのBidirectional streamingを使えば複数のレスポンスを送信することができます。バッチ処理結果や各種ログはコントロールサーバへ送り、ログ集約サーバへの送信はコントロールサーバが行います。gRPCで各サービスをつないでおいてログを送り、受けとったクライアントにログの集約を任せる、といった構成は導入メリットの1つな気がします。(ログの送信漏れ考慮は必要ですが)

そもそものgRPCのメリット

そもそものgRPCのメリットがあります。異なる言語のマイクロサービス間の通信でもProtocol Buffersを定義することで容易に通信を確立できますし、streamingの方式を用途に合わせて選択することで省コネクションでマイクロサービス間のやり取りが行えます。

GoとJavaでBidirectional gRPC streamingをつかったデモ

上記の図の構成をもとにgRPCのクライアントをGoサーバをJavaで通信方式はBidirectional streamingを採用してデモを作ってみました。

どのようなバッチサービス?

Bidirectional streamingを採用しているので、リクエストが複数あってレスポンスも複数、または1つのようなサービスを考えました。

結果、数値を受け取り割り算をして商と余りを返すサービスを実装しました。

Redisからキューを送信してクライアントがリクエストとレスポンスを受け取ったイメージです。

# Redis
$ redis-cli
127.0.0.1:6379> PUBLISH my_queue '{"serviceName" : "division", "numbers" : [10, 3]}'
# Client
12:27:50.452 Request : {serviceName:'division', message:'10', time:'time string'}
12:27:50.452 Request : {serviceName:'division', message:'3', time:'time string'}
12:27:50.455 Response: {serviceName:'division', message:'quotient:3', time:'time string'}
12:27:50.456 Response: {serviceName:'division', message:'remainder:1', time:'time string'}

クライアントは103のリクエストを2つ送り、商が3と余りが1の結果を受け取ります。(余りが0であればレスポンスは1つになる)

protoファイル

protoファイルは次のようになりました。

syntax = "proto3";

option go_package = "protobuf";
package proto;

service MicroService {
  rpc MicroService (stream Request) returns (stream Response) {}
}

message Request {
  string name = 1;
  string message = 2;
  string time = 3;
}

message Response {
  string name = 1;
  string message = 2;
  string time = 3;
}

クライアントのコード(Go)

リクエストを送信してレスポンスを受け取っている通信周りのコードの抜粋です。

※コード全体はgithubにあります。

waitc := make(chan struct{})
go func() {
    for {
        in, err := stream.Recv()
        if err == io.EOF {
            close(waitc)
            return
        }
        if err != nil {
            log.Error("Failed to receive a message : %v", err)
            return
        }
        responseLog.Info("{serviceName:'%s', message:'%s', time:'%s'}", in.Name, in.Message, in.Time)
    }
}()

for {
    message, err := pubSub.ReceiveMessage()
    if err != nil {
        panic(err)
    }
    requests, err := getRequests(message)
    if err != nil {
        panic(err)
    }

    for _, request := range requests {

        requestLog.Info("{serviceName:'%s', message:'%s', time:'%s'}", request.Name, request.Message, request.Time)
        if err := stream.Send(&request); err != nil {
            log.Error("Failed to send a message: %v", err)
        }
    }
}

stream.CloseSend()
<-waitc

サーバのコード(Java

リクエストを受け取りレスポンスを送信している通信周りのコードの抜粋です。

割り算をする数値が分けられて送られてきます。1回目のリクエストでキーを生成してリクエストを保持しながら2回目のリクエストで割った結果を送信しています。

※コード全体はgithubにあります。

return new StreamObserver<Microservice.Request>() {
    public void onNext(Microservice.Request req) {
        Long key = getTime(req);
        Observable.just(req)
                .subscribe(new Observer<Microservice.Request>() {

                    @Override
                    public void onSubscribe(Disposable d) {
                        Log.i("Request", getRequestLog(req));
                    }

                    @Override
                    public void onNext(Microservice.Request request) {
                        if (!routeNumber.containsKey(key)) {
                            routeNumber.put(key, Arrays.asList(req));
                        } else if (routeNumber.get(key).size() == 1) {

                            Microservice.Request prevRequest = routeNumber.get(key).get(0);
                            Integer leftTerm = Integer.parseInt(prevRequest.getMessage());
                            Integer rightTerm = Integer.parseInt(req.getMessage());

                            Integer quotient = leftTerm / rightTerm;
                            Integer remainder = leftTerm % rightTerm;

                            if (remainder == 0) {
                                responses.putIfAbsent(key, Arrays.asList(
                                        getResponse(req.getName(), String.format("quotient:%d", quotient))));
                            } else {
                                responses.putIfAbsent(key, Arrays.asList(
                                        getResponse(req.getName(), String.format("quotient:%d", quotient)),
                                        getResponse(req.getName(), String.format("remainder:%d", remainder))));
                            }
                        } else {
                            Log.w(String.format("waring, unknown state. key:{%s}, value:{%s}", key, routeNumber.get(key)));
                        }
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.e(String.format("onError %s", e.getMessage()));
                    }

                    @Override
                    public void onComplete() {
                        if (responses.containsKey(key)) {
                            Observable.fromIterable(responses.get(key))
                                    .subscribe(res -> {
                                        responseObserver.onNext(res);
                                        Log.i("Response", getResponseLog(res));
                                    });
                            routeNumber.remove(key);
                            responses.remove(key);
                        }
                    }
                });
    }

    public void onError(Throwable t) {
        logger.log(Level.WARNING, "microService cancelled");
    }

    public void onCompleted() {
        responseObserver.onCompleted();
    }
};

デモ

f:id:n_soushi:20170317114216g:plain

まとめ

  • Bidirectional streamingは1回のコネクションでクライアントとサーバ間で複数回のリクエストとレスポンスを送ることができます。リクエスト/レスポンスの度にコネクションを確率しないので省コネクションのメリットがあります。
  • クライアントはgRPCのコネクションを確立してからチャンネルのsubscribeを継続して行っています。キューが送られる度にgRCPのコネクションを繋いでいません。1回のgRCPコネクションを確立するだけでバッチサーバのコントロールが行うことができました。
  • gRPCで考えてみましたがHTTP/APIJSONの通信形式であっても権限や責務を1つのサーバに集約させるメリットは受けられます。何よりProtocol Buffers定義による複数言語のサポートとstreaming方式の便利さが運用しているマイクロサービスに嵌まれば導入機会を検討するべきです。引き続きgRPCのメリットを受けられるようなユースケースを考えていきます。

コードを公開しています

コード全体はgitbubで確認できます。

github.com

CourseraのMachine Learningから線形回帰を学んだのでまとめてみた

CourseraのMachine Learningを受講しています。時間を見つけてはコツコツ進めて今のところWeek4に差し掛かったところです。Week4ではNeural Networksの話に入り一段とレベルが高くなった印象を受けています。Week1からWeek3までに学んだことを復習する必要があるなと焦りが生まれました。

講中は配布される資料や動画を見たり他の日本人の方のブログを拝見したりと課題に取り組んできました。このタイミングで復習して整理することでWeek4以降の学習が快適になるのではないかと淡い期待を込めてWeek1からまとめてみます。

Machine Learning - Stanford University | Coursera

こちらが受講しているMachine Learningのコースです。MOOCは好きな時間に進められるし前編英語(動画は日本語字幕あり)なので英語の学習になってオススメです。

www.coursera.org

どこまでの内容をまとめるか

このエントリではWeek1の内容に触れています。

線形回帰とは?

ある土地の家の価格とその家の部屋数の相関をグラフで表すと以下のようになったとします。

f:id:n_soushi:20170310101210p:plain

Xが部屋数(RM)でYが価格(MEDV)です。

(出典:Housing Data Set

※ このデータは部屋数以外に、その土地の犯罪率だったり児童と教師の比率など複数の要素から構成されています。

部屋数が4つの場合や部屋数が7つの場合はグラフから予想できそうです。 このとき頭の中ではグラフに右肩上がりな直線をイメージして予想できますが、この直線を数式から導き出すことを学びました。 この導き方は統計学では回帰分析の一種として線形回帰と呼ばれています。

仮定関数と目的関数

部屋数をX、部屋の価格をYとすると相関を表すグラフを引くための1次関数の式は次のようになります。(懐かしい数式)

 \displaystyle
Y = aX + b

Machine Learningのコースではθをつかって次のような式で定義しています。

 \displaystyle
h_\theta (X) = \theta_0 + \theta_1(X)

 h_\theta (X)仮定関数と呼ばれます。家の価格予想に最適な直線を引くために \theta_0 \theta_1の数値を変えながらグラフにフィットした仮定関数を導き出します。 つまり右肩上がりの直線のグラフを引くために最適な  aX + babを決めるということですね。

仮定関数がフィットしているか計算するための関数である目的関数があります。

{ \displaystyle
J(\theta_0, \theta_1) = \frac{1}{2m}\sum_{i=1}^{m} (h_\theta (X_i) - Y_i)^2
}

この目的関数をつかって J(\theta_0, \theta_1)を最小にする \theta_0 \theta_1を導き出します。

最急降下法

目的関数を最小にする方法に最急降下法があります。

{ \displaystyle
\theta_j = \theta_j - \alpha \sum_{i=1}^{m} (h_\theta (x^{(i)}) - y^{(i)}) x_j\ ^{(i)}
}

ここで微分が出てきます。微分は傾きを求めますがこのアルゴリズムを使い傾きが最小になるまで学習を繰り返していきます。傾きの値が最小になるほどグラフにフィットした \theta_jが求められます。   \alphaは学習率と呼ばれ数値が大きいほど傾きの変動幅が大きくなりフィットしたデータが得られず、小さいほど学習はゆっくりと進み確実にデータにフィットした値が求められます。

Octaveでプログラム化する

コースの課題ではOctaveを使いプログラミング課題を提出します。 最初に示した家の価格と部屋数のグラフのデータを使い、更にこれまでのアルゴリズムからデータにフィットした直線をグラフにプロットしてみます。

目的関数

function J = computeCost(X, y, theta)
    m = length(y);
    J = sum((X*theta -y).^2) / (2* m);
end

最急降下法

function [theta, J_history] = gradientDescent(X, y, theta, alpha, num_iters)
    m = length(y);
    J_history = zeros(num_iters, 1);

    for iter = 1:num_iters
        theta = theta - alpha / m * X' * (X * theta -y);
        J_history(iter) = computeCost(X, y, theta);
    end
end

目的関数を求めグラフにプロットする。そして部屋数が5つのときの価格を予想する。

%% Initialization
clear ; close all; clc
data = load("housing.txt");
x = data(:, 6);
y = data(:, 14);
m = length(y);

theta = zeros(2, 1);
X = [ones(m, 1), x]

%% Compute Cost
J = computeCost(X, y, theta)

%% Gradient Descent
iterations = 1500;
alpha = 0.01;
[theta J_history] = gradientDescent(X, y, theta, alpha, iterations);

%% Output
fprintf('Initial cost = %f\n', J);
fprintf('Final cost = %f\n', J_history(iterations));
fprintf('Theta found by gradient descent: ');
fprintf('%f %f \n', theta(1), theta(2));

%% Plot data
figure; hold on;
plot(x, y, 'r+', 'LineWidth', 2);
plot(X(:,2), X*theta, '-')
xlabel('RM');
ylabel('MEDV');

%% Predict
fprintf('for RM = 5, MEDV = %f\n', [1, 5] *theta);

出力したグラフ

f:id:n_soushi:20170310115327p:plain

データにフィットした直線がプロットできたようです。

出力した数値

J =  296.07
initial cost 296.073458
final cost 27.131052
Theta found by gradient descent: -5.254087 4.477681
for RM = 5, MEDV = 17.134317

部屋数が5つのときに 17.134317と予想できました。グラフにプロットされたデータとフィットされているようです。

まとめ

  • 最後に出したグラフですが、もう少し角度が急な直線のほうがグラフにフィットしているようです。プロットして上手く行かなければ調整して再度プロットして、の繰り返しが機械学習の大事な工程なのでしょう。
  • 線形回帰はこの先で学ぶ機械学習の知識のベースになっています。データを分類する場合などに流用できます。
  • 今回の学習データは部屋数の1つでしたが部屋数に加え犯罪率児童と教師の比率などの複数の要素からも線形回帰をベースに家の価格を予想できる。(重回帰分析)この内容はWeek2で学びました。
  • MOOCで学び、コースを修了した先輩たちのブログで学び、自分のブログでアウトプットする、など多角的に学ぶことが大事。(ここに書籍を読んで学ぶも入れたい:money_with_wings: )
  • 数式書くの大変。