GKEでPersitentVolumeを使う

KubernetesでPersitentVolumeを使う方法はドキュメントに書いてありますが、使う環境によって永続ディスクが異なるためにスムーズにいかないことがあります。
そのため、今回はGKEでPersitentVolumeを使う方法を紹介します。

環境

GKE 1.9.7-gke.0

永続ディスクの用意

まずはGCPで永続ディスクを作成します。

GUICUIどちらでも大丈夫ですが今回はCUIで作成してみます。

$ gcloud compute --project={your-project} disks create sample-disk --zone=asia-northeast1-b --type=pd-ssd --size=100GB

このようになると思います。

f:id:akaimo3:20180523215724p:plain

これで永続ディスクの作成は完了です。

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で変えてみた結果、このようになりました。

f:id:akaimo3:20180522222711p:plain

ちょっとわかりづらいですね。
点線の時間で切り替えを行い、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を書き換えることで制御できます。
定期的なアクセスがある状態で切り替えるとこのようになりました。

f:id:akaimo3:20180523104428p:plain

取得できるメトリクスが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を使用します。

github.com

このツールで負荷をかけてみた結果、このようになりました。

PodのCPU使用率 f:id:akaimo3:20180515231231p:plain

ノードのCPU使用率 f:id:akaimo3:20180515231244p:plain

ピーク時は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のダウンロード

まずは以下のリンクからダウンロードページに移動します。

http://download.twonky.com/

次にインストールするバージョンを決めます。
今回は最新バージョンである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では実行するテストの選択やフィルタリングをします。
selectorefiltersを適宜変更して実行したいテストが実行できるようにします。

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

mapStateToPropsmapDispatchToPropsで生成された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> { ...