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.8
と0.9.1
のterraformを使うのでtfenvを使ってterraformを切り替えながらマイグレーションを行う
1:tfファイルにterraform
セクションを追加してbackends
を設定する
次のように設定しました。
terraform { backend "s3" { bucket = "tfstate-bucket" // 自身のbucket名を設定します } }
- AWSの
access_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ファイル内のremote
がbackend
に置き換わります。 - 次の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
- Terraform 0.9 | HashiCorp
- core: introduce "backends" to replace "remote state" (superset) and fix UX by mitchellh · Pull Request #11286 · hashicorp/terraform · GitHub
- Backends: Migrating From 0.8.x and Earlier - Terraform by HashiCorp
- Backends - Terraform by HashiCorp
- State: Locking - Terraform by HashiCorp
gRPC streamingをつかうとマイクロサービスの責務が整理できるし省コネクションでメリットあるよね、という話
今回はgRPCをマイクロサービス間通信に導入することってメリットあるよね、というエントリです。 定期的に処理を実行してくれるバッチはよくあるものですがバッチの駆動をgRPCを使って次のような構成で動かしました。
Batch Control
とBatch Server
はBidirectional 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'}
クライアントは10
と3
のリクエストを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(); } };
デモ
まとめ
- Bidirectional streamingは1回のコネクションでクライアントとサーバ間で複数回のリクエストとレスポンスを送ることができます。リクエスト/レスポンスの度にコネクションを確率しないので省コネクションのメリットがあります。
- クライアントはgRPCのコネクションを確立してからチャンネルのsubscribeを継続して行っています。キューが送られる度にgRCPのコネクションを繋いでいません。1回のgRCPコネクションを確立するだけでバッチサーバのコントロールが行うことができました。
- gRPCで考えてみましたがHTTP/API/JSONの通信形式であっても権限や責務を1つのサーバに集約させるメリットは受けられます。何よりProtocol Buffers定義による複数言語のサポートとstreaming方式の便利さが運用しているマイクロサービスに嵌まれば導入機会を検討するべきです。引き続きgRPCのメリットを受けられるようなユースケースを考えていきます。
コードを公開しています
コード全体はgitbubで確認できます。
CourseraのMachine Learningから線形回帰を学んだのでまとめてみた
CourseraのMachine Learningを受講しています。時間を見つけてはコツコツ進めて今のところWeek4に差し掛かったところです。Week4ではNeural Networksの話に入り一段とレベルが高くなった印象を受けています。Week1からWeek3までに学んだことを復習する必要があるなと焦りが生まれました。
受講中は配布される資料や動画を見たり他の日本人の方のブログを拝見したりと課題に取り組んできました。このタイミングで復習して整理することでWeek4以降の学習が快適になるのではないかと淡い期待を込めてWeek1からまとめてみます。
Machine Learning - Stanford University | Coursera
こちらが受講しているMachine Learningのコースです。MOOCは好きな時間に進められるし前編英語(動画は日本語字幕あり)なので英語の学習になってオススメです。
どこまでの内容をまとめるか
このエントリではWeek1の内容に触れています。
線形回帰とは?
ある土地の家の価格とその家の部屋数の相関をグラフで表すと以下のようになったとします。
Xが部屋数(RM)でYが価格(MEDV)です。
(出典:Housing Data Set)
※ このデータは部屋数以外に、その土地の犯罪率だったり児童と教師の比率など複数の要素から構成されています。
部屋数が4つの場合や部屋数が7つの場合はグラフから予想できそうです。 このとき頭の中ではグラフに右肩上がりな直線をイメージして予想できますが、この直線を数式から導き出すことを学びました。 この導き方は統計学では回帰分析の一種として線形回帰と呼ばれています。
仮定関数と目的関数
部屋数をX、部屋の価格をYとすると相関を表すグラフを引くための1次関数の式は次のようになります。(懐かしい数式)
Machine Learningのコースではθをつかって次のような式で定義しています。
は仮定関数と呼ばれます。家の価格予想に最適な直線を引くために との数値を変えながらグラフにフィットした仮定関数を導き出します。
つまり右肩上がりの直線のグラフを引くために最適な の a
とb
を決めるということですね。
仮定関数がフィットしているか計算するための関数である目的関数があります。
この目的関数をつかってを最小にする とを導き出します。
最急降下法
目的関数を最小にする方法に最急降下法があります。
ここで微分が出てきます。微分は傾きを求めますがこのアルゴリズムを使い傾きが最小になるまで学習を繰り返していきます。傾きの値が最小になるほどグラフにフィットしたが求められます。 は学習率と呼ばれ数値が大きいほど傾きの変動幅が大きくなりフィットしたデータが得られず、小さいほど学習はゆっくりと進み確実にデータにフィットした値が求められます。
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);
出力したグラフ
データにフィットした直線がプロットできたようです。
出力した数値
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: ) - 数式書くの大変。