GKEでPersitentVolumeを使う
KubernetesでPersitentVolumeを使う方法はドキュメントに書いてありますが、使う環境によって永続ディスクが異なるためにスムーズにいかないことがあります。
そのため、今回はGKEでPersitentVolumeを使う方法を紹介します。
環境
GKE 1.9.7-gke.0
永続ディスクの用意
まずはGCPで永続ディスクを作成します。
GUI、CUIどちらでも大丈夫ですが今回はCUIで作成してみます。
$ gcloud compute --project={your-project} disks create sample-disk --zone=asia-northeast1-b --type=pd-ssd --size=100GB
このようになると思います。
これで永続ディスクの作成は完了です。
PersitentVolumeの作成
ここからはKubernetesの設定に入ります。
Podが永続ディスクを使用するためにはPersitentVolumeだけではなく、PersistentVolumeClaimというものも作成しなければいけません。
PersitentVolumeには先程作成した永続ディスクについての設定を記述します。
また、PersistentVolumeClaimを作成することで、PodがPersitentVolumeを見つけることができるようになります。
apiVersion: v1 kind: PersistentVolume metadata: name: sample-pv labels: app: sample-pv spec: capacity: storage: 100Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain gcePersistentDisk: pdName: sample-disk fsType: ext4 --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: sample-pvc labels: app: sample-pvc spec: accessModes: - ReadWriteOnce storageClassName: "" resources: requests: storage: 100Gi selector: matchLabels: app: sample-pv
Podにマウントする
最後にPodからアクセスできるようにします。
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: sample-deployment spec: ... template: ... spec: containers: - name: sample ... volumeMounts: - name: sample-volume mountPath: /sample volumes: - name: sample-volume persistentVolumeClaim: claimName: sample-pvc securityContext: fsGroup: 2000 runAsNonRoot: true runAsUser: 1000
ポイントはsecurityContext
の部分です。
この記述がないと、permissionでエラーになってPodが起動しなくなってしまいます。
以上がGKEでPersitentVolumeを使用する方法です。
PersitentVolumeのような環境に依存する部分は、具体的なコードがあまりないので誰かの参考になれば嬉しいです。
KubernetesでBlue-Green Deploymentしてみる
今回はサービスを本番で運用していくときに欲しくなるBlue-Green DeploymentをKubernetesでやってみます。
TL;DR
- Serviceのselectorを更新するやり方だと10分程度BlueとGreenがまざる
- Istioを使用すれば瞬時に100%のトラフィックを切り分けられるのでBlue-Green Deploymentができる
環境
- GKE 1.9.7-gke.0
2種類のAPI
BlueとGreenを見分けるために、自身の色を返すAPIを用意します。
package main import ( "fmt" "net/http" ) func handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "blue-api") } func main() { http.HandleFunc("/", handler) http.ListenAndServe(":80", nil) }
そして、このAPIが稼働するDeploymentがこれです。
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: blue-api spec: replicas: 1 template: metadata: labels: app: color-api version: blue spec: containers: - name: blue-api image: asia.gcr.io/hoge/color-api:blue ports: - containerPort: 80 --- apiVersion: extensions/v1beta1 kind: Deployment metadata: name: green-api spec: replicas: 1 template: metadata: labels: app: color-api version: green spec: containers: - name: green-api image: asia.gcr.io/hoge/color-api:green ports: - containerPort: 80
ServiceでB/Gしてみる
KubernetesはServiceでアクセスするPodを見つけているので、そこの設定を書き換えればB/Gできそうです。
apiVersion: v1 kind: Service metadata: name: service-color-api spec: selector: app: color-api version: green # version: blue type: NodePort ports: - protocol: TCP port: 80 targetPort: 80 name: http
versionをblueとgreenで変えてみた結果、このようになりました。
ちょっとわかりづらいですね。
点線の時間で切り替えを行い、Blue(上)からGreen(下)にトラフィックが切り替わるようにServiceを書き換えました。
Serviceの設定を更新したらすべてのトラフィックがGreenに行ってほしいところですが、10分ほど両方のPodにアクセスが行ってしまっています。
これではAPIのバージョニングができていないと不整合がおきてしまい正しく動かない恐れがあります。
なにか設定が漏れているのかもしれません。
原因を知っていれば教えてほしいです...
余談ですが、切り替えた直後に前の色のPodを削除することで、瞬時に全てのトラフィックを新しい色に流すことができます。
瞬時に100%のトラフィックを切り替えたいので別のアプローチとしてサービスメッシュのIstioを導入してみます。
IstioでB/Gする
まずはIstioの導入をします。
公式ドキュメントのステップ2まででIstioの導入は完了です。
Istioで経路の制御をするので、ServiceはBlueにもGreenにも通信ができるようにしておきます。
apiVersion: v1 kind: Service metadata: name: service-color-api labels: app: color-api spec: selector: app: color-api type: NodePort ports: - protocol: TCP port: 80 targetPort: 80 name: http
Istioを使用する場合はIstio用のIngressを通さないと経路制御などができないので、少し設定に変更を入れます。
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: color-ingress annotations: kubernetes.io/ingress.class: "istio" spec: backend: serviceName: service-color-api servicePort: 80
最後に本命の経路制御の設定をいれます。
apiVersion: config.istio.io/v1alpha2 kind: RouteRule metadata: name: color-api spec: destination: name: service-color-api precedence: 1 route: - labels: version: green
今回もselectorを書き換えることで制御できます。
定期的なアクセスがある状態で切り替えるとこのようになりました。
取得できるメトリクスがCPU利用率しかなかったので、あまりいいグラフにはなっていませんが、切り替えた直後からすべてのトラフィックが新しいほうに流れています。
Prometheusを導入後に今度はアクセス数をグラフ化して追記したいとおもいます。
まとめ
サービスメッシュを入れるとトラフィックの正確な制御ができます。
しかし、制御のためのプロキシも追加されてしまうため、余計なリソースを使うことにもなってしまいます。
デメリットも存在しますが、Istioを入れてしまえばB/Gだけでなくカナリアリリースもできるようになるので、
リリースを正確に行うためにIstioを導入するのもありかもしれません。
Kubernetesに負荷をかけオートスケールを観察する
本番環境でKubernetesを使うためには、Kubernetesがどのように動作するか把握していないと安心して運用することができません。
まずはサービスを運用する上で重要になる、高負荷時の動きを確認してみたいと思います。
KubernetesにはHPAとCluster Autoscalerの二種類がありますが、両方の動きを見ていきます。
詳細な解説は他に譲りますが、
HPAは稼働中のPodを増加させることで高負荷に耐え、
Cluster AutoscalerはPodを増加させることができないときにKubernetesが扱えるリソースを追加するものです。
つまり、負荷が高まるとHPAがおこりPodが追加され、それでも負荷が高くHPAが続きKubernetesが管理しているリソースに空きがなくなったときにCluster AutoscalerによってVMが追加され、そこにPodが追加されます。
環境
- GKE 1.9.7-gke.0
実行するアプリケーション
できるだけ手軽に実験したいので、goでリクエストを受け付けて、少し負荷がかかる処理をした後にレスポンスを返すアプリケーションをKubernetesで実行してみます。
Kubernetesの構成は
Ingress -> Service -> Deployment
となっています。
実行するgoのAPIのソースはこのようにしました。
package main import ( "fmt" "net/http" "strconv" ) func handler(w http.ResponseWriter, r *http.Request) { n := 0 for i := 0; i < 1*1000*1000*1000; i++ { n += i } fmt.Fprintf(w, strconv.Itoa(n)+"\n") fmt.Fprintf(w, "my-api") } func main() { http.HandleFunc("/", handler) http.ListenAndServe(":80", nil) }
とても単純なものです。
これをDocker Imageにし、DockerのrunコマンドでAPIサーバーが起動するようにDockerfileを書きます。
FROM golang:1.9.2-alpine3.6 ADD ./server.go ./ RUN go build -o server CMD ["/go/server"]
Imageをアップロードしますが、せっかくGKEを使用するので同じGCP上にあるGoogle Container Registryにアップロードします。
$ docker build -t asia.gcr.io/{your-project-id}/my-api:v1 . $ gcloud docker -- push asia.gcr.io/{your-project-id}/my-api:v1
Kubernetesの設定ファイル
Kubernetesについては省略します。
今回は以下の設定ファイルを使用します。
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: test-ingress annotations: kubernetes.io/ingress.global-static-ip-name: "test-ip" spec: backend: serviceName: service-my-api servicePort: 80 --- apiVersion: v1 kind: Service metadata: name: service-my-api spec: type: NodePort ports: - protocol: TCP port: 80 targetPort: 80 name: http selector: app: my-api version: v1 --- apiVersion: extensions/v1beta1 kind: Deployment metadata: name: my-api spec: replicas: 2 template: metadata: labels: app: my-api version: v1 spec: containers: - name: myapi image: gcr.io/{your-project-id}/myapi:v1 resources: requests: cpu: "500m" limits: cpu: "1500m" ports: - containerPort: 80 --- apiVersion: autoscaling/v1 kind: HorizontalPodAutoscaler metadata: name: my-api namespace: default spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: my-api minReplicas: 2 maxReplicas: 6 targetCPUUtilizationPercentage: 70
オートスケールに関係のあるところだけ解説します。
今回負荷をかけるAPIがあるPodに対してはCPUを1.5個まで使用を許可しています。
このPodが初期値として2台稼働しており、閾値を70としています。
つまり、CPUの使用率が70%を超えたときにPodの追加が行われることになります。
また、公式ドキュメントのautoscaling-algorithmによると、10%の許容値があったり瞬間的に超えただけだとオートスケールしなかったりするようです。
負荷をかける
負荷をかけるツールはいろいろありますが、手軽に使えるhey
を使用します。
このツールで負荷をかけてみた結果、このようになりました。
PodのCPU使用率
ノードのCPU使用率
ピーク時はPodは6個に、ノードは3台にまで増えていますね。
HPAは追加にかかる時間が数秒なので、EC2とかのオートスケールと比べるとだいぶ早いですね。
これは便利そうです。
Webpackでビルドするときにflowtypeで型をチェックする
flowを導入して型のチェックがされるようになっても、開発者がエディタで設定していなかったり、 エディタによっては開いていないソースはチェックの対象になっていなかったりします。
これではせっかく導入したflowが活かせません。
そこでWebpackでビルドと同時にflowの型チェックを走らせてみます。
プラグインの導入
flow-status-webpack-pluginを使用してビルド時にチェックします。
インストールします。
yarn add flow-status-webpack-plugin --dev
あとはwebpack.config.js
に設定を追加すれば完了です。
このファイルは環境によっては異なる場合があるので適宜読み替えてください。
var FlowStatusWebpackPlugin = require('flow-status-webpack-plugin'); module.exports = { ..., plugins: [ ..., new FlowStatusWebpackPlugin({ onSuccess: function (stdout) { console.log('\u001b[32m' + 'Succeeded in type check by flow' + '\u001b[0m'); }, onError: function (stdout) { console.log('\u001b[;41m' + ' Failed in type check by flow ' + '\u001b[0m'); console.error(stdout); } }) ] }
補足
\u001b[32m
などはコンソールの出力に色をつけています。
成功したときは緑、失敗したときは赤になっています。
開発者に認識してもらうことが目的なので、このようなちょっとしたことも大切ですね。
QNAPにTwonky Serverをインストールする
QNAPが公式に提供しているDLNAサーバーはいろいろと貧弱なので、評判の高いTwonky Serverを使えるようにします。
環境
- QNAP: TS-431P
- OS: QTS 4.3.4
Twonky Serverのダウンロード
まずは以下のリンクからダウンロードページに移動します。
次にインストールするバージョンを決めます。
今回は最新バージョンである8.5を選びました。
次に自分の環境にあったビルドを選びます。
ここで正しいものを選ばないと動かないので気をつけてください。
QNAP用には次のビルドが用意されています。
Qnap arm-x09 package Qnap arm-x19 package Qnap arm-x31 package Qnap arm-x41 package Qnap x86 package Qnap x86-64 package
自分の環境を調べます。
別のソフトウェアですが、次のサイトが参考になります。
Kazoo Server ソフト QNAP NAS対応モデル | OLIOSPEC BLOG
私のQNAPはTS-431PですのでQnap arm-x41をダウンロードします。
Twonky Serverのインストール
QNAPにログインしてAppCenterを起動します。
AppCenterの右上にあるアイコンをクリックし、手動インストールの画面で先程ダウンロードしたqpkgを選択し、ダイアログに従っていけばインストールが完了します。
JUnit5をmainから実行する
こんにちは。akaimoです。
今回はJUnit5をmain関数内で使用する方法です。
環境
- Java9
- JUnit5.1.0-M1
- Kotlin1.2
ソース
これでmainからJUnitを動かせます。詳しい解説は後ほど。
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder import org.junit.platform.launcher.core.LauncherFactory import org.junit.platform.launcher.listeners.SummaryGeneratingListener import org.junit.platform.engine.discovery.ClassNameFilter.* import org.junit.platform.engine.discovery.DiscoverySelectors.selectPackage import java.io.PrintWriter fun main(args: Array<String>) { val launcher = LauncherFactory.create() val summary = SummaryGeneratingListener() launcher.registerTestExecutionListeners(summary) val request = LauncherDiscoveryRequestBuilder .request() .selectors(selectPackage("jp.hoge.piyo.test")) .filters(includeClassNamePatterns(".*")) .build() launcher.execute(request) summary.summary.printFailuresTo(PrintWriter(System.out)) summary.summary.printTo(PrintWriter(System.out)) }
これを実行すると次のようなログが出力されます。
Test run finished after 13255 ms [ 3 containers found ] [ 0 containers skipped ] [ 3 containers started ] [ 0 containers aborted ] [ 3 containers successful ] [ 0 containers failed ] [ 4 tests found ] [ 3 tests skipped ] [ 1 tests started ] [ 0 tests aborted ] [ 1 tests successful ] [ 0 tests failed ]
もしテストが失敗していたら、このログの前に失敗したテストと失敗した内容が表示されます。
詳細
testディレクトリ配下に入れIDEなどでテストを実行する場合はテストコードだけを書けば良いですが、main内で実行させる場合は全て自分で実行させなければいけません。
Launcher
JUnit5には、テストを検出したりフィルタリング、実行するためにLauncherというものを使用します。
まずはそのLauncherを作成します。
val launcher = LauncherFactory.create()
Summary
次はSummaryです。
これは名前の通り、テストの内容であったり結果を保持しています。
Summaryを作成して、Launcherに登録します。
val summary = SummaryGeneratingListener() launcher.registerTestExecutionListeners(summary)
Request
Requestでは実行するテストの選択やフィルタリングをします。
selectore
とfilters
を適宜変更して実行したいテストが実行できるようにします。
val request = LauncherDiscoveryRequestBuilder .request() .selectors( selectPackage("jp.hoge.piyo.test"), selectClass(MyTestClass::class.java) ) .filters(includeClassNamePatterns(".*Tests")) .build()
作成したrequestをLauncherに渡せばテストを実行できます。
launcher.execute(request)
結果の表示
最後にテスト結果を表示して終了です。
summary.summary.printFailuresTo(PrintWriter(System.out)) summary.summary.printTo(PrintWriter(System.out))
なお、summary.summary
よりデフォルトで出力される項目に個別にアクセスできるので、任意のフォーマットで出力することもできます。
react-reduxにflowtypeを導入しPropsに型を付ける
flowtypeを導入したとき、reactとreduxをつなぐ部分の情報が少なかったのでまとめます。
flowtypeの導入前
比較として導入前のソースを載せておきます。
import React, {PropTypes, Component} from 'react' import {connect} from 'react-redux' import * as actions from '../actions class SamplePage extends Component { render() { const {title, description, myAction} = this.props return ( <div> <button onClick={myAction}>{title}</button> <p>{description}</p> </div> ) } } SamplePage.propTypes = { title: PropTypes.string, description: PropTypes.string, myAction: PropTypes.func } const mapStateToProps = state => { return { title: state.title, description: state.description } } const mapDispatchToProps = dispatch => { return { myAction: () => dispatch(actions.myAction()) } } export default connect(mapStateToProps, mapDispatchToProps)(SamplePage)
導入後
これが導入後のソースです。propTypesが消えましたが、typeを記述しているので行数はあまり変わっていません。
import React, {Component} from 'react' import {connect} from 'react-redux' import * as actions from '../actions import type {MapStateToProps, MapDispatchToProps} from 'react-redux' type StateToProps = { title: string, description: string } type DispatchToProps = { myAction: () => void } type Props = StateToProps & DispatchToProps class SamplePage extends Component<void, Props, void> { render() { const {title, description, myAction} = this.props return ( <div> <button onClick={myAction}>{title}</button> <p>{description}</p> </div> ) } } const mapStateToProps: MapStateToProps<*, *, StateToProps> = state => { return { title: state.title, description: state.description } } const mapDispatchToProps: MapDispatchToProps<*, *, DispatchToProps> = dispatch => { return { myAction: () => dispatch(actions.myAction()) } } export default connect(mapStateToProps, mapDispatchToProps)(SamplePage)
ポイント
propTypes
が3つのtype
に置き換えられています。
その中で特に大事なところが以下で、
type Props = StateToProps & DispatchToProps
mapStateToProps
とmapDispatchToProps
で生成された2つのオブジェクトのtypeが合成されたものが、
class SamplePage extends Component<void, Props, void> { ...
となりcomponentに渡されていることを明示的に示すことができます。
補足1
MapStateToProps
は以下のように定義されています。
declare type MapStateToProps<S, OP: Object, SP: Object> = (state: S, ownProps: OP) => SP;
ジェネリクスで、SとOPを受け取ってSPを返す関数という型になってますね。
導入後のソースではMapStateToProps<*, *, StateToProps>
のように宣言しています。
Stateのtypeを宣言していれば、MapStateToProps<State, *, StateToProps>
となり、MapStateToProps
内のstateに対して型が適用されるのでSやOPに対しても型の宣言をすることをオススメします。
補足2
2018/02/01現在で最新のflowtypeではComponentに渡すPropsの型を以下のように記述します。
class SamplePage extends Component<Props> { ...