1クール続けるブログ

とりあえず1クール続けるエンジニアの備忘録

ローカル開発環境(apache/tomcat)をコンテナ化+SSL化

ローカルのapache+tomcatで動いているアプリケーションをdockerに乗っけてみました。 SSL込みで設定していきました。Docker for Macを使用しています。

成果物をこちらのリポジトリにあげておきます。

github.com

Apache

ApacheでVirtualHostで3つのFQDNの通信を扱うようにしていきます。SSLの設定も行っていきますが、その際にはmkcertを使用してローカル認証局と鍵の作成を行います。

httpd.confの作成

SSL通信を有効にするため、conf/httpd.confをベースのdockerイメージからコピーしてきて変更を加えていきます。以下の項目のコメントアウトを外します。

#LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
...
#LoadModule ssl_module modules/mod_ssl.so
...
#Include conf/extra/httpd-ssl.conf

今回はVirtualHosts+Proxy Moduleを使っていきたいと思うので下記のコメントアウトも外します。tomcatへの通信をhttpで行うために必要なモジュールもついでに外します。

#Include conf/extra/httpd-vhosts.conf
...
#LoadModule proxy_module modules/mod_proxy.so
...
#LoadModule proxy_http_module modules/mod_proxy_http.so

各VirtualHostsで共有の鍵を使いたいので、httpd-ssl.confに鍵の設定をして、httpd-vhosts.confの設定がhttpd-ssl.confより後にくるように記述の位置を変えます。下記のようになるはずです。

...
# Secure (SSL/TLS) connections
Include conf/extra/httpd-ssl.conf

# Virtual hosts
Include conf/extra/httpd-vhosts.conf
...

鍵の作成

mkcertを使って認証局+鍵の作成を行っていきます。Goで作られているらしいので興味ある方はぜひ。

github.com

brew install mkcert
brew install nss  # Firefoxでアクセスする際には必要のようです
mkcert -install # ローカル認証局作成 Macの方はキーチェーンアクセスを見ると出来ているのが分かる
mkcert localhost "*.example.com " "*.cool.com"
# generateされた下記のファイル2つをdockerコンテキストに配置

httpd-ssl.confの設定

httpd-ssl.confの下記をコメントアウトします。今回は先程生成した2つのpemファイルを利用するためです。

SSLCertificateFile "/usr/local/apache2/conf/server.crt"
...
SSLCertificateKeyFile "/usr/local/apache2/conf/server.key"

そして下記のように記述を加えます。

SSLCertificateFile "/usr/local/apache2/conf/localhost+2.pem"  # 後ほどDockerfileでCOPYするように宣言します
SSLCertificateKeyFile "/usr/local/apache2/conf/localhost+2-key.pem"

httpd-vhosts.confの設定

tomcatのコンテナへajpで通信するように設定を加えます。 通常は静的ファイルはDocumentRoot配下を参照させて、それ以外はtomcatと通信という形になるかと思いますが、今回はとりあえず全部tomcatコンテナに流します。 下記のような形としました。VirtualHostのセクション内です。

   <Directory "/usr/local/apache2/docs/one.cool.com">
        Options Indexes 
        Require all granted
    </Directory>

   <Location /todolist>
        ProxyPass http://app:8080/todolist/
    </Location>

/private/etc/hostsに今回設定したFQDNを追加

本題とは関係ないですが自分のPCからアクセスできるように、/private/etc/hostsに記述しておきます。

127.0.0.1       www.example.com
127.0.0.1       one.cool.com
127.0.0.1       two.cool.com

Dockerfileの作成

設定ファイルと鍵、httpのドキュメントをコンテナにCOPYしポート80番と443番をListenします。イメージはなるべく軽くしたいのでDebian系OSがベースのものでなく、alpineベースのものを選んでいます。

FROM httpd:2.4-alpine

# copy configuration files
COPY --chown=root:www-data conf/httpd.conf /usr/local/apache2/conf
COPY --chown=root:www-data conf/extra /usr/local/apache2/conf/extra

# copy keys for ssl/tls
COPY --chown=root:www-data keys /usr/local/apache2/conf

# copy document
WORKDIR /usr/local/apache2/docs
RUN chown root:www-data /usr/local/apache2/docs
COPY --chown=root:www-data docs .

EXPOSE 80 443

httpdがうまく動いているかテスト

docker-compose.yamlを作成し、下記のようにhttpdの設定を宣言します。

version: '3'
services:
  web:
    container_name: httpd
    build: ./httpd
    ports:
    - "9080:80"
    - "9443:443"

以下のURLにSSLでアクセスできることが確認できました。

Tomcat

kotlin + SpringBootで作成した簡単なアプリケーションをコンテナに乗っけてみます。黒べこ本のサンプルに載っているTodoアプリです。

kotlinのアプリケーションの設定を一部変更

最初にkotlinのアプリケーションのbuild.gradleに変更を加えていきます。今回はwarを作成してそれをデプロイするため、下記のように記述を加えていきます。

apply plugin: 'war'
dependencies {
    providedRuntime("org.springframework.boot:spring-boot-starter-tomcat")  // SpringBoot組み込み以外のTomcatを利用するときに必要みたいです
}

次に、warファイルをTomcatに展開するにあたって必要らしいServletInitializerというclassをエントリポイントのあるclassと同パッケージに配置します。下記のような内容のclassです。

package com.example.todolist

import org.springframework.boot.builder.SpringApplicationBuilder
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer

class ServletInitializer : SpringBootServletInitializer() {

    override fun configure(application: SpringApplicationBuilder): SpringApplicationBuilder {
        return application.sources(TodolistApplication::class.java)
    }
}

Dockerfileの作成

Warに固める作業自体はローカルのIntelliJで行います。Dockerコンテキスト配下にソースを置いてそこに成果物を固めて良いのですが、今回は単純にcopyしてコンテキスト配下に盛ってきたいと思います。いちいちその作業をやるのが面倒くさいのでMakefileを作ります。
※ AutoDeployしてDockerで都度都度検証したい場合には、Volumeマウントして動かしたほうが良いとは思います。

build:
  cp ~/ideaProjects/todolist/build/libs/todolist.war tomcat/build
  docker-compose up --build

run: 
  docker-compose up 

makefileに記述したように、warはtomcat/build配下に格納するので、そのwarをdockerの方にcopyするだけです。なのでDockerfileは下記のようになります。tomcat側もイメージは軽くしたいのでalpineを使います。 またSpringBootは環境変数からコンテキストパスを設定できるのでしておきます。SpringBoot1.x系は別の変数名になるらしいので注意です。

FROM tomcat:9.0.16-jre8-alpine

ENV SERVER_SERVLET_CONTEXT_PATH /todolist
COPY build/todolist.war /usr/local/tomcat/webapps
EXPOSE 8080

動作を検証する

docker-comopseに下記を追記し、make buildしてみます。[https://one.cool.com:9443/todo]にアクセスしてみるとアプリケーションの画面が表示されました。

  app:
    container_name: tomcat
    build: ./tomcat
    ports:
    - "8080:8080"

f:id:jrywm121:20190218021406p:plain
表示された画面

まとめ

アプリケーションをBuildするところからDockerでできるように今度する。マルチステージビルドが使えそう。

SpinnakerのPipeline Expressionsで他stageの情報を取ってくる

SpinnakerのPipeline Expressions

KubernetesAmazon ECSを絡めたCI/CDでは既にデファクト感のあるSpinnakerですが、日本語の記事が少ないのと公式ドキュメント自体も不足はしてないのですが若干物足りない部分があります。KubernetesのBlue/Green Deployを構築した際にPipeline Expressionsで他stageの情報を取ってくるということをしたんですが、ちょっと分かりづらかったので記事に残しておきます。

とはいえ下記のページ見ていただければ十分とも思います。

Pipeline Expressions Guide - Spinnaker

やりたいこと

以下のような流れでパイプラインを構築したいと考えました。項目はStageの単位と対応しています。 providerはKubernetes V2です。

  • 新しいReplicaSetをDeploy(ただしPod数は1)。
  • Enableし新しいReplicaSetにもTrafficが流れるようにする。
  • manual judgement。このままデプロイを続けるかRollbackするか選択させる。
  • Continue
    • 新しいReplicaSetを本番相当までスケールアウト
    • 古いReplicaSetをDisableしTrafficが流れないようにする
    • 古いReplicaSetのサイズを0にする
  • Rollback
    • 新しいReplicaSetをDisableにする
    • 新しいReplicaSetをdeleteする <- ここが問題になった

ScaleやEnableは、対象をstaticとdynamicに分かれます。staticは固定値かつ既に存在するReplicaSetしか選べないので今回の用途に合いません。Dynamicでは、NewestやSecond Newest等の選択肢があります。逆にそれらの選択肢からしか選べません。常に現在動いているReplicaSetが最新のものである前提でないとPipelineの設定がうまくいきません。そのためRollbackではReplicaSetを削除する必要があります。
このDeleteのstageなんですが、ScaleやEnableと違って削除するリソースをNewestやSecond Newestから選べません。namespace/リソースタイプに加え、リソースの名前もしくはラベルを直接指定する必要があります。リソースの名前を指定するのが手っ取り早そうなのですが、なぜかPipeline Expressionsを記述してSaveしても消えてしまったので、タグの方で実装しました。

Pipeline ExpressionsでDeploy時のラベル情報をとってくる

Spinnakerはデプロイ時にこちらが指定したマニフェストに更に情報を付加するので、最終成果物からラベルを取ってくる必要性があります。 結論から言うと以下です。

${ #stage("Deploy new ReplicaSet").context["outputs.manifests"][0].metadata.labels["moniker.spinnaker.io/sequence"].toString() }

Key名moniker.spinnaker.io/sequenceだけ取ってきていますが、他のラベルに関しては固定値のため取ってくる必要性がありません。 この情報は、http://localhost:8084/pipelines/${PIPELINE_ID}で取ってこれるJSONをベースにアクセスしていきます。PIPELINE_IDは、パイプライン実行結果のDetailを選択し展開されたウィンドウの右下のSourceをクリックして飛ぶURLに含まれています…というか先程のURLそのものになっています。 #stage("ステージ名")でstages配列の中の指定したstageのJSONをルートにしてアクセスできます。outputs.manifestsのようにKeyに.(ドット)が含まれる場合にはmap形式でアクセスしないとだめです。

Pipeline Expressionsをテストする

以下のようにcurlを叩くことでテストできます。

curl  http://localhost:8084/pipelines/${PIPELINE_ID}/evaluateExpression \
-H "Content-Type: text/plain" \
--data '${ #stage("Deploy new ReplicaSet").context["outputs.manifests"][0].metadata.labels["moniker.spinnaker.io/sequence"].toString() }'

まとめ

お家でSpinnkerを試せるような環境が作れれば良いのだけれど…。 お家でKubernetes触りたいときには、Katacodaで今まで動かしていたので、minikube+Spinnakerに自分のPCのスペックが足りるかどうか。

試して理解 Linuxのしくみ 読みました 1章~3章

gihyo.jp

上記を読んだので学んだことをアウトプットしたいと思います。大学の授業で低レイヤのことを学んできませんでした。そのため、本書でLinuxに関して体系的に学べたの非常に良かったです。
本書の中で手を動かすパートに関しては、C言語Pythonの代わりにGo言語を使って試してみてます。また、PCはMacを使っているのですが、Linuxの環境で試したいという気持ちが有ったので、Dockerで仮想的にDebian系のOS上で動かすようにしています。リポジトリは以下になります。

github.com

第1章概要

ハードウェアの動作の流れ

1.外部デバイスから処理を依頼される

2.メモリに存在する命令を読み出してCPUにおいて実行
3.出力する

  • HDDやSSDに書き込む
  • ネットワークを介して別のコンピュータに転送する
  • 出力デバイスを介して人間に見せる

4.1に戻る

コンピュータの起動

  1. BIOSUEFIと呼ばれるハードウェア組込ソフトウェアによるハードウェア初期化処理
  2. 起動させるOSを選択するブートローダが動作
  3. ストレージデバイスからOSを読み出す コンピュータにとってストレージデバイスは欠かせない存在

2章ユーザモードで実現する機能

ユーザモードとカーネルモード

バイスの操作やプロセス管理、メモリ管理、プロセススケジューラなど、通常のプロセスから実行できると困る、OSの核となる処理をまとめたプログラムをカーネルという。
これらは特権モード、即ちカーネルモードで動作する。
OS=カーネルではなく、カーネル以外にもユーザモードで動作する様々なプログラムから構成される。
ユーザモードのプロセス処理から、システムコールを通じてカーネルの処理を呼び出す。それを呼び出すのは、プロセス固有のコードやそれが使用するライブラリ(OS提供のもの・そうでないもの)

CPUのモード遷移

システムコールを発行すると、CPU割り込みが発生。これによって、CPUでは、ユーザモードからカーネルモードに遷移して、カーネルの処理を行う。終わったらユーザモードに戻る。

システムコールの呼び出し

sh main.sh -d 02-syscall-and-non-kernel-os/hello
上述のリポジトリにて、上のコマンドを実行すると以下のような結果を得られました。

-------------------------PROGRAM START-------------------------
hello world
--------------------------PROGRAM END--------------------------
-------------------------SYSCALL START-------------------------
execve("/go/bin/app", ["/go/bin/app"], [/* 7 vars */]) = 0 <0.000401>
arch_prctl(ARCH_SET_FS, 0x54c6b0)       = 0 <0.000087>

***(省略)*******************************************************

fcntl(2, F_GETFL)                       = 0x1 (flags O_WRONLY) <0.000139>
write(1, "hello world\n", 12)           = 12 <0.000149>  
exit_group(0)                           = ?
+++ exited with 0 +++
--------------------------SYSCALL END--------------------------

下から3行目のwriteがデータを画面やファイルに出力する「write()」システムコール。省略している部分もシステムコールだが、それはプログラムの開始処理が発行するもの。

ユーザモードとカーネルモードの割合

自前のアプリケーションを走らせない場合

以下のようにUbuntuの環境でsarコマンドを発行してみた

docker run --rm -it ubuntu:xenial /bin/bash
# apt-get update
# apt-get install -y sysstat
# sar -P ALL 1 1

結果は以下のように得られた

12:36:45        CPU     %user     %nice   %system   %iowait    %steal     %idle
12:36:46        all      0.00      0.00      0.50      0.00      0.00     99.50
12:36:46          0      0.00      0.00      0.00      0.00      0.00    100.00
12:36:46          1      0.00      0.00      0.99      0.00      0.00     99.01

Average:        CPU     %user     %nice   %system   %iowait    %steal     %idle
Average:        all      0.00      0.00      0.50      0.00      0.00     99.50
Average:          0      0.00      0.00      0.00      0.00      0.00    100.00
Average:          1      0.00      0.00      0.99      0.00      0.00     99.01

各行が1つのコアに対応する。今回は殆どがidleモードとなっている。

  • %user -> ユーザモード
  • %nice -> ユーザモード
  • %system -> カーネルモード
  • %idle -> 何もしていない時間

自前の無限ループするだけのアプリケーションを走らせた場合

sh main.sh -d 02-syscall-and-non-kernel-os/loop
上述のリポジトリにて、上のコマンドを実行すると以下のような結果を得られました。

Linux 4.9.93-linuxkit-aufs (aad086a81679)    01/14/19    _x86_64_    (2 CPU)

13:18:18        CPU     %user     %nice   %system   %iowait    %steal     %idle
13:18:19        all     50.00      0.00      0.50      0.00      0.00     49.50
13:18:19          0      0.00      0.00      1.00      0.00      0.00     99.00
13:18:19          1    100.00      0.00      0.00      0.00      0.00      0.00

Average:        CPU     %user     %nice   %system   %iowait    %steal     %idle
Average:        all     50.00      0.00      0.50      0.00      0.00     49.50
Average:          0      0.00      0.00      1.00      0.00      0.00     99.00
Average:          1    100.00      0.00      0.00      0.00      0.00      0.00

CPUコア1上でユーザプロセス、すなわちloopプログラムが常に動作していることがわかる。

自前の親プロセス取得を無限ループするアプリケーションを走らせた場合

sh main.sh -d 02-syscall-and-non-kernel-os/ppidloop
上述のリポジトリにて、上のコマンドを実行すると以下のような結果を得られました。

Linux 4.9.93-linuxkit-aufs (4bbb02f02797)    01/14/19    _x86_64_    (2 CPU)

13:26:00        CPU     %user     %nice   %system   %iowait    %steal     %idle
13:26:01        all     20.71      0.00     30.30      0.00      0.00     48.99
13:26:01          0     41.00      0.00     59.00      0.00      0.00      0.00
13:26:01          1      0.00      0.00      1.02      0.00      0.00     98.98

Average:        CPU     %user     %nice   %system   %iowait    %steal     %idle
Average:        all     20.71      0.00     30.30      0.00      0.00     48.99
Average:          0     41.00      0.00     59.00      0.00      0.00      0.00
Average:          1      0.00      0.00      1.02      0.00      0.00     98.98

CPUコア0上でppidloopプログラムを41%の割合で実行し、親プロセス取得のシステムコールを59%の割合で実行していた。

システムコールのラッパー関数

システムコールC言語などの高級言語から直接呼び出せない。アーキテクチャ依存(amd64とかarmとか?)のアセンブリコードを使って呼び出す必要がある。もしシステムコールをラップする関数を内包するライブラリ群がなければ、アーキテクチャ依存のアセンブリソースを書かなくてはいけない。

標準Cライブラリ

GNUプロジェクトが提供するglibcLinuxの標準Cライブラリとして使用する。glibcシステムコールをラップする関数を内包している。またPOSIXという規格に定義されている関数も提供。
プログラムがどのようなライブラリをリンクしているかは以下でlddコマンドでわかる。

% docker run --rm -it golang:1.11.4 /bin/bash
# which go
/usr/local/go/bin/go
# ldd /usr/local/go/bin/go
    linux-vdso.so.1 (0x00007ffef2965000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fe2c2ba2000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe2c2803000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fe2c2dbf000)

第3章プロセス管理

fork関数

同じプロセスの処理を複数のプロセスに分ける際に呼び出される(例:Apacheで子プロセスを生成する)。 子プロセス用メモリ領域を作成し、親プロセスのメモリをコピーする。fork関数はカーネルモードからユーザモードに処理が再び移るときに、親プロセスには子プロセスのプロセスIDを、子プロセスには0を返す。これを利用して親子で処理を分岐させる。

sh main.sh -d 03-process-management/fork
上述のリポジトリにて、上のコマンドを実行すると以下のような結果を得られました。 fork()されたときに同じプログラムが複製されて2個同時に動いているような感覚。

I'm parent! my pid is 7 and the pid of my child is 11.
I'm child! my pid is 11.

execve関数

全く別のプログラムを生成するときに呼び出される(例:bashからsleepコマンド実行)。 ます実行ファイルを読み出し、その後現在のプロセスのメモリを上書く。 実行ファイルの中身 * プロセスの実行に必要なコード、データ * コードを含むデータ領域のオフセット(ベースアドレスに加えられるアドレスの数)-> ファイルの情報 * コードを含むデータ領域のサイズ * コードを含むデータ領域のメモリマップ開始アドレス -> コンピュータのメモリのどこにマッピングするか * 最初に実行する命令のメモリアドレス(エントリポイント)

Linuxの実行ファイルはELFというフォーマットを使用する。
エントリポイントのアドレスを得るには「-h」オプション、ファイル内オフセット、サイズ、メモリマップ開始アドレスを得るには「-S」オプション

% docker run --rm -it ubuntu:xenial /bin/bash
# apt-get update
# apt-get install binutils
# readelf -h /bin/sleep
ELF Header:
------(省略)---------------------------------------------
  Entry point address:               0x401760
------(省略)---------------------------------------------

# readelf -S /bin/sleep
There are 29 section headers, starting at offset 0x7370:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
------(省略)--------------------------------------------------------
  [14] .text             PROGBITS         00000000004014e0  000014e0
       0000000000003319  0000000000000000  AX       0     0     16
------(省略)--------------------------------------------------------
  [25] .data             PROGBITS         00000000006071c0  000071c0
       0000000000000074  0000000000000000  WA       0     0     32
------(省略)--------------------------------------------------------

Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

全く新規のプロセスを新規生成する場合には、親となるプロセスからfork()を発行して、復帰後に子プロセスがexec()を発行する。 sh main.sh -d 03-process-management/execve
上述のリポジトリにて、上のコマンドを実行すると以下のような結果を得られました。fork()の直後にexec()が動いています。

sh main.sh -d 03-process-management/execve
I'm parent! my pid is 6 and the pid of my child is 10.
I'm child! my pid is 10.
hello

第4章プロセススケジューラ

シュガーラッシュ2感想

シュガーラッシュ2(吹替版)をMX4Dで観に行きました。面白かったので感想書いてみようかなと思っています。Twitterに書いて終わりにしようとも思ったのですが、ちょっと文字数が足りなかったようです。
ネタバレ少なめで書いていきますが、多少はありますのでお気をつけください。
出ている企業とかじっくり見たかったので、配信されるようになったらレンタルして一時停止しながら見ようと思います。

インターネット周りの描写

今回の舞台はインターネットなので、インターネットに関する表現が沢山出てきます。

f:id:jrywm121:20181225023559p:plain
インターネット

Wifiからインターネットに出ていくとき

インターネットに出ていくときとか、ちゃんとカプセル化されるんですよね。「これ絶対ヘッダーついてますよね!送信元(先?)IPアドレス言ってたし、これ絶対ヘッダ付いてるってことですよね!?」と思いながら觀ていました。ラルフがカプセルから顔だけはみ出ちゃっているのは、1フレームの最大サイズにおさまらなかったからなのかなと思ったりしました。

f:id:jrywm121:20181225023403p:plain
パケット?状態のラルフ

途中で加速してスピード早くなったのは、電話回線から光回線に変わったからなんですかね?(詳しくないので知ってる方いたら教えてください)

インターネットの世界

インターネットの世界では、Googleエンパイアステートビル的な一番大きなビルに描かれてます。やっぱりインターネットの中で一番存在感があるというのは世界共通認識のようです。ラルフがゴーグルのお店と勘違いしてたのに笑いました。

f:id:jrywm121:20181225023651p:plain
Google

他にもTwittterやInstagramAmazonなど日本でも馴染みのある企業が沢山並んでましたね。FBとInstagramは資本関係にあるだけに、近いところに立地があるのかとかを見ておけばよかったです。IPアドレスで立地決めているという線もあるかもしれないです。

インターネットならではの表現とかもユニークでした。
人間はインターネットの中ではアバターとして表現されるのですが、そのアバターがインターネットを移動する際に乗っているのがブラウザらしいです。終盤もろもろ有って、その乗り物が破壊されたりしたときに、物理世界ではブラウザがクラッシュしているような描写になっていましたね。みんな同じように見えましたが、もしかしてFirefoxChromeなどで違いが表現されていたのかもと思うと夢があります。

f:id:jrywm121:20181225023811p:plain
ブラウザ

検索バーの自動補完、WEBサイト広告のアニメ映像への落とし込み方とかも面白く感じる要素が多かったです。実際のeBayは特に広告などにあうこと無く、各商品のページにいけました。

作品で大きなカギを握るWEBサイトが、ゲーム以外だとeBayとohmy.disney.comとbuzzzTubeになるのですが、buzzzTubeだけは架空のサイトみたいですね。仕組み的にはほぼYouTubeなのではという感じでしたが、YouTube自体のインターネットの街に存在していたので、名前だけ変更して登場させたというわけでもなさそうです。
結構たくさんのIT企業出てたけど、netflixが見つからなくて、これはディズニーがnetflixからディズニー作品を引き上げて自分でストリーミングサービスやるって言ってたことと関係あるのか…?と勘ぐってしまう部分も。

キャラ周り

オールスター感

MARVELとスターウォーズ持ってるだけあって出せるキャラの幅が広いのもオールスター感を感じてよかったです。
ズートピアの監督さんということでズートピアのキャラは多めになるのかなと思いきやニックだけの出演だったと思います。なんにせよ、一時停止しながら見ないと分からない所ではあります。C3POがなにげにちょっと美味しい役回りだったのがちょっと嬉しかったです。

f:id:jrywm121:20181225024029p:plain

プリンセス

プリンセスの条件のくだりとかも面白くて、ディズニーにおけるプリンセス像の変化なんかも、他に沢山作品見てれば感じられたかもと思ってちょっと悔しい気持ち。 ディズニープリンセス系の論文って調べれば結構あるみたいで、一番新しそうなものを一個つまんで読んでみたけどかなり面白いです。こういうこと研究してらっしゃる方もいるんですね。 https://human.kyotogakuen.ac.jp/pdf/association/2017/i2017_01.pdf

ディズニースタジオ作品たくさん見てらっしゃる方は、最後あたりのシーンとかかなり胸熱ではないでしょうか。

おまけ

MX4D

4Dで觀たのは初めてでしたが、思ったよりかなり揺れます。なみなみと盛ってあるポップコーンはまず食べられないと思います。ただ、揺れなども最初のほうはかなり気になりますが、物語を觀ているうちに徐々に気にならなくなります。ただ、映画が始まる前までの予告編でも揺れるのが想定外で、予告編ってだいたい動きのあるシーンの塊だったりするのでガンガン連続で揺れます。

普通に映画を見る料金(1800円)+ 4Dの料金(1200円) って感じでした。一種のアトラクション的な要素があると思えば妥当な価格設定かなと思います。

勢いで帰ってきてモアナを觀た

ヴァネロペのプリンセス像というのが自分の想像するディズニープリンセスとはかなり変わっていて、どういう変遷を通ってディズニーはこのようなディズニープリンセスにしたのかなと気になり、モアナを帰ってきてから觀ました。 作品としてとてもおもしろかったです。2時間があっという間に感じられましたし、音楽もとても良かったです。映画觀ながら3回は泣きました。

f:id:jrywm121:20181225024205p:plain

まとめ

また一時停止しながらじっくり見たい作品です。 ほぼ映画を見ない生活を送っているので、これを機にちょっとずつディズニー周りから履修してみようかなと思いました。

映画「シュガー・ラッシュ:オンライン」の感想 #シュガラお題



sponsored by 映画「シュガー・ラッシュ:オンライン」(12月21日公開)

Go言語によるWebアプリケーション開発まとめ・感想 チャットアプリ編

オライリー出版のGo言語によるWebアプリケーション開発を読んだ感想 チャット部分編

オライリーから出版されているGo言語によるWebアプリケーション開発を読みました。1章から3章までがWebsocketを使用したチャットアプリケーション開発となるのですが、その部分を読んだまとめ・感想を書いていきます。

www.oreilly.co.jp

github.com

webサーバ公開の基本

アクセスパスと処理を結びつけます。
パスは完全一致ではなく前方一致となるため、注意が必要となります。/を登録した場合は/appとアクセスしても関連付けた処理が実行されます。 結びつけたあとに、ListenAndServeメソッドを使ってWebサーバを開始します。

http.HundleFuncでパスと関数を紐づける

この後に挙げるhttp.Handleとは違い、Handlerのインターフェースを実装した構造体を用意する必要がありません。
その代わり、状態などを持たせることが出来ないため注意です。

http.HandleFunc("/auth/", loginHandler) 

http.HandleでパスとHandlerを紐づける

Handlerのインターフェースを実装した構造体を用意した上で、http.Handle関数の第二引数に渡してあげる。Handler(https://golang.org/pkg/net/http/#Handler)は、ServeHTTP(w http.ResponseWriter, r *http.Request)メソッドのみを持つインターフェースになっている。http.Handle関数にて指定したリクエストパスにマッチするリクエストが来た場合に、ServeHTTPメソッドの中に書いた処理が実行される。

下の例は、返却するhtmlファイルに埋め込むデータオブジェクトを作成し、そのデータオブジェクトをコンパイル済のHtmlテンプレートにapplyする処理。ただし、初回リクエストが来たときにテンプレートファイルをコンパイルする。初回だけの実行を保証するための実装としては、sync.Onceを使用している。起動時にコンパイルしないこの方法を、遅延初期化として本では取り上げられており、セットアップ処理に時間がかかる場合や利用頻度の低い機能では効果的と説明されています。ただファイルが無いなどの状態を検知できないため、template.Mustを使用してグルーバル変数にセットする方法が好まれることもあるとのこと。

http.Handle("/login", &templateHandler{filename: "login.html"})

// テンプレートの読込と出力を受け持つ型。テンプレートのコンパイルは一度で良い。
type templateHandler struct {
    once     sync.Once // 複数のgoroutineが呼び出したとしても、引数として渡した関数が一度しか実行されない。
    filename string
    temp1    *template.Template //コンパイルされたテンプレートへの参照を保持
}

// 同じsync.Onceの値を使用しなくてはいけないためレシーバは必ずポインタでなくてはならない
func (t *templateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    t.once.Do(func() {
        t.temp1 =
            template.Must(template.ParseFiles(filepath.Join("templates", t.filename)))
    })
    data := map[string]interface{}{
        "Host": r.Host,
    }
    if authCookie, err := r.Cookie("auth"); err == nil {
        data["UserData"] = objx.MustFromBase64(authCookie.Value)
    }
    t.temp1.Execute(w, data)
}

template.Mustを使用してグルーバル変数にセットする方法は以下。この方法使用すると、もしエラーがある場合にはpanicを起こし起動しなくなるため、エラーに気づかない事象を避けることができる。

var temp1 = template.Must(template.ParseFiles(filepath.Join("templates", "login.html")))

以下のコマンドでWebサーバを開始する。

if err := http.ListenAndServe(*addr, nil); err != nil {
   log.Fatal("ListenAndServe:", err)
}

HTTP2をつかった通信をしたいのであれば、以下のようになる。

certFile, _ := filepath.Abs("server.crt")
keyFile, _ := filepath.Abs("server.key")

if err := http.ListenAndServeTLS(*addr, certFile, keyFile, nil); err != nil {
   log.Fatal("ListenAndServe:", err)
}

実装としては割れるのは、http.HandleFuncを使うかhttp.Handleを使うかというところだと思うが、遅延初期化を使わない場合には、http.HandleFuncで事足りるのではと私は感じている。

チャットアプリケーションのモデル化

ここでのチャットアプリケーションの要件は、一つの公開されたチャットルームにクライアントは自動的にログインして会話をするというもの。クライアントとの通信を管理するclientとという構造体とクライアントの入退出の管理やメッセージをブロードキャストする役割を持つroomという構造体で構成する。

client

Client構造体は、各クライアントごとに必要となるwebsocketのコネクションと他クライアントからのメッセージを格納するチャネルを持つ。また、ブラウザから送信されるメッセージをRoomに送るためにRoomへの参照を持っている。
clientのメソッドは各クライアントに対応するため、ServeHTTPメソッドの中で実行される。他クライアントからのメッセージを格納するチャネルをクライアント側で持つのは、言わずもがなWebsocketに書き出すWriteメソッドをクライアント側で持つため(おそらくsocketのコネクションを他ソースコードに使わせずチャネルでやり取りしたかった)。

type client struct {
    socket *websocket.Conn // クライアントのためのWebSocket
    send   chan []byte     // messageが送られるバッファ付きチャネル
    room   *room           // 参加しているチャットルーム
}

func (c *client) read() {
    for {                                                                                                // 無限ループ
        if _, msg, err := c.socket.ReadMessage(); err == nil {
            c.room.forward <- msg                                               // クライアントからメッセージを受信したらブロードキャストするためにRoomに送る 
        } else {
            break
        }
    }
    c.socket.Close()
}

func (c *client) write() {
    for msg := range c.send {    // チャネルが閉じられるまで、チャネルから値を繰り返し受信し続ける。channelがクローズされれば、ループから抜ける。 
        if err := c.socket.WriteMessage(websocket.TextMessage, msg); err != nil {
            break   // WebSocketへの書き込みが失敗するとbreakでforループから抜け出し、 Websocketが閉じられる。
        }
    }
    c.socket.Close()
}

room

room構造体は他のクライアントに転送するためのメッセージを保持するチャネルと入退出を受け取るチャネル、クライアント全体のデータを持つ。
外部から利用しやすいように、newRoom()という関数を作る。run()メソッドはgoルーチンでWEBサーバと並列で実行される。チャネルへのメッセージの処理には、selectを使用している。

type room struct {
    forward chan []byte      // 他のクライアントに転送するためのメッセージを保持するチャネル
    join    chan *client     // チャットに参加しようとするクライアント用
    leave   chan *client     // チャットルームから退室しようとする用のチャネル
    clients map[*client]bool //すべてのクライアントが保持
    tracer  trace.Tracer
}

func newRoom() *room {
    return &room{
        forward: make(chan []byte),
        join:    make(chan *client),
        leave:   make(chan *client),
        clients: make(map[*client]bool),
    }
}

func (r *room) run() {
    for {
        // いずれかのチャネルにメッセージが届くとcaseに合わせて処理を実行
        // case節の処理は同時に実行されないので同時にr.clientに変更を加えることはない
        select {
        case client := <-r.join:
            r.clients[client] = true
        case client := <-r.leave:
            delete(r.clients, client)
            close(client.send)  // client側のチャンネルを閉じたのでforループから抜けてwebsocket通信が閉じられる
        case msg := <-r.forward:
            for client := range r.clients {
                select {
                case client.send <- msg:
                    r.tracer.Trace(" -- クライアントに送信されました")
                default:
                    delete(r.clients, client)
                    close(client.send) // client側のチャンネルを閉じたのでforループから抜けてwebsocket通信が閉じられる
                }
            }

        }
    }
}

Roomでの処理をHTTPハンドラ化

HTTPハンドラ化するにあたり、room構造体はServeHTTPメソッドを実装する必要がある。
Upgraderメソッドによりhttp通信はWebsocket通信へアップグレードされます。そして変数socketにwebcsocket通信のコネクションが入ってきます。

func (r *room) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    socket, err := upgrader.Upgrade(w, req, nil)
    if err != nil {
        log.Fatal("ServeHTTP:", err)
        return
    }
    client := &client{
        socket: socket,
        send:   make(chan []byte, messageBufferSize),
        room:   r,
    }

    r.join <- client   // joinチャンネルに生成したclientをpassing
    defer func() { r.leave <- client }()  // client.readはソケット通信でエラーになったらbreakし、そのclientをleaveチャンネルに通知する。
    go client.write()
    client.read()
}

main関数の流れは以下のようになります。

        http.Handle("/room", r)
        http.Handle("/chat", &templateHandler{filename: "chat.html"})
        go r.run()
        // WEBサーバ起動

認証機能

HTTPハンドラのdecoratorパターン

認証機能を作るにあたり、/roomにアクセスした際の処理を認証用のHTTPハンドラを用意しラップします。次に実行する処理を持つハンドラを認証ハンドラの構造体にフィールドに登録できるようにしておきます。認証用のハンドラは、認証が正常に完了したら、認証用ハンドラの次に実行するハンドラのServeHTTP()メソッドを実行することで処理を渡します。

http.Handle("/chat", MustAuth(&templateHandler{filename: "chat.html"}))

func MustAuth(handler http.Handler) http.Handler {
    return &authHandler{next: handler}
}

func (h *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if _, err := r.Cookie("auth"); err == http.ErrNoCookie {
        // 未認証
        w.Header().Set("Location", "/login")
        w.WriteHeader(http.StatusTemporaryRedirect)
    } else if err != nil {
        panic(err.Error())
    } else {
        // 成功。ラップしたハンドラ呼び出し。
        h.next.ServeHTTP(w, r)
    }
}

このWrapするパターンですが、以下のようにWrapperをhttp.HandleFuncを使って記述することも可能です。
(参考元->https://medium.com/@matryer/the-http-handler-wrapper-technique-in-golang-updated-bc7fbcffa702)

http.Handle("/chat", MustAuth(&templateHandler{filename: "chat.html"}))

func MustAuth(h http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r 
                                          *http.Request) {
    if _, err := r.Cookie("auth"); err == http.ErrNoCookie {
        w.Header().Set("Location", "/login")
        w.WriteHeader(http.StatusTemporaryRedirect)
    } else if err != nil {
        panic(err.Error())
    } else {
        h.ServeHTTP(w, r)
    }
  })
}

wrapする側もされる側もHandleFuncにするなら以下のような形です。 (参考元->https://twinbird-htn.hatenablog.com/entry/2016/06/06/001704)

http.HandleFunc("/chat", MustAuth(templateHandleFunc("chat.html")))
func MustAuth(fn http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if _, err := r.Cookie("auth"); err == http.ErrNoCookie {
        w.Header().Set("Location", "/login")
        w.WriteHeader(http.StatusTemporaryRedirect)
    } else if err != nil {
        panic(err.Error())
    } else {
        fn(w, r)
    }
    }
}

OAuth2

この本ではgomniauthというOSSが利用されていたが、gomniauthを使うと何故かobjx: JSON encode failed with: json: unsupported type: func() stringというgomiauthの中で使用されているライブラリの中で落ちるという現象にあい、ここで時間をとるのも…と思い、golang.org/x/oauth2というパッケージを利用しました。
処理の流れとしては、以下の記事を参考にいたしました。

dev.to

var googleOauthConfig = &oauth2.Config{
    RedirectURL:  "http://localhost:8080/auth/callback/google",
    ClientID:     os.Getenv("OAUTH_ID_GOOGLE"),
    ClientSecret: os.Getenv("OAUTH_KEY_GOOGLE"),
    Scopes:       []string{"https://www.googleapis.com/auth/userinfo.profile"}, // https://developers.google.com/identity/protocols/googlescopes#oauth2v2
    Endpoint:     google.Endpoint,
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
    segs := strings.Split(r.URL.Path, "/")
    action := segs[2]
    // provider := segs[3]

    switch action {
    case "login":
        state, expiration := generateStateOauthCookie(w)
        u := googleOauthConfig.AuthCodeURL(state) // 認可サーバへのURL+クライアントIDやリダイレクト先の情報が入ったQuery
        cookie := http.Cookie{Name: "oauthstate", Value: state, Expires: expiration, Path: "/"}
        http.SetCookie(w, &cookie)
        http.Redirect(w, r, u, http.StatusTemporaryRedirect)
    case "callback":
        oauthState, _ := r.Cookie("oauthstate")

        fmt.Println(r.FormValue("state"))
        fmt.Println(oauthState.Value)
        if r.FormValue("state") != oauthState.Value {
            log.Fatalln("invalid oauth google state") // Cookieにセットしたstateとクエリにセットしたstateが合致しないのはCSRF攻撃を受けている場合がある
            return
        }

        // 正しく認可コードを得ることができたためアクセストークンを要求し、そのアクセストークンを使用しリソースサーバにアクセスできる。
        data, err := getUserDataFromGoogle(r.FormValue("code"))
        if err != nil {
            log.Println(err.Error())
            http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
            return
        }

        var profile Profile
        if err := json.Unmarshal(data, &profile); err != nil {
            fmt.Println("error:", err)
        }

        authCookieValue := objx.New(map[string]interface{}{
            "name": profile.Name,
        }).MustBase64()

        // Cookie は、ステートレスな HTTP プロトコルのためにステートフルな情報を記憶する
        // https://developer.mozilla.org/ja/docs/Web/HTTP/Cookies
        http.SetCookie(w, &http.Cookie{
            Name:  "auth",
            Value: authCookieValue,
            Path:  "/"})
        // Headerの方は map[string][]string
        w.Header()["Location"] = []string{"/chat"}
        w.WriteHeader(http.StatusTemporaryRedirect)
    default:
        w.WriteHeader(http.StatusNotFound)
        fmt.Fprintf(w, "アクション%sには非対応です", action)
    }
}

アバター

SNSから取得するパターン、Gravatarから取得するパターン、画像のアップロードしてもらうパターンの3種ありましたが、前の2つは実装自体は難しくなかったため3つ目の画像のアップロード・提供について書いていきます。 具体的な処理から抽象化していく工程も書いていきたかったのですが、言語化するのが難しかった…。

画像のアップロード

http.HandleFunc("/uploader", uploaderHandler)

func uploaderHandler(w http.ResponseWriter, req *http.Request) {
    userId := req.FormValue("userid") // HTMLフォームの隠しフィールドにセットされたユーザーIDを読み取り
    file, header, err := req.FormFile("avatarFile")
    if err != nil {
        fmt.Fprintln(w, err.Error())
        return
    }
    defer file.Close()
    data, err := ioutil.ReadAll(file)
    if err != nil {
        fmt.Fprintln(w, err.Error())
        return
    }
    filename := filepath.Join("avatars", userId+filepath.Ext(header.Filename)) // 元ファイル拡張子(extension)をjoinする
    err = ioutil.WriteFile(filename, data, 0644)
    if err != nil {
        fmt.Fprintln(w, err.Error())
        return
    }
    fmt.Fprintln(w, "success!!")
}

提供するときには組み込みのファイルサーバ機能を使います。

   // ScripPrefixもFileServerもhttp.Handler型を返す。本の2章で紹介されたDecoratorパターン。
    http.Handle("/avatars/",
        http.StripPrefix("/avatars/", // パスの中から接頭辞(/avatars/の部分を削除)。削除しないと./avatars/avatars/filenameにアクセスしようとする
            http.FileServer(http.Dir("./avatars")))) // 静的ファイルの提供ができる

まとめ

本自体は原本が2014年の出版でかなり古いので、新しい情報とかも見つつ整理していくのが大事だなと思いました。 普通WEBアプリケーションを作成するときは標準パッケージではなくGinとか使われていくのだろうなとは思っているのですが…。
ただメルカリさんのAPIサーバのように、net/httpパッケージだけを見て作成されるパターンもあるので、実際は用途によっての使い分けなんですかね。

メルカリさんのAPIサーバの実装に関する記事↓

tech.mercari.com

Kubernetes完全ガイド読んだ感想・まとめ

青山さんのKubernetes完全ガイド読みました

インプレスさんより出版されているKubernetes完全ガイドを読みました。今まで行き当たりばったりでKubernetesを学んできた自分にとって、ものすごく学びが多かったですし、体系だった知識に整理できた一冊でした。図がたくさんあって理解もスムーズだったように思えます。 Kubernetesにこれから触る人や私みたいにその場しのぎで学んできた方は絶対に読むべき一冊だと思います。
※ 私はAmazon ECSとGKEを使い始めて半年くらいです

book.impress.co.jp

今回Kubernetes入門を見て改めて整理出来た項目を中心に挙げていきたいと思います。

kubectl

Contextの切り替え

kubectlで操作する際に、操作対象のクラスターや認証情報が必要になります。実態はデフォルトだと、$HOME/.kube/configに記載されるらしいです。EKSとGKEを使っている場合に、複数の異なる環境にアクセスする場合にはContextの登録・切り替えが必要になります。

# クラスタ名の登録
kubectl config --kubeconfig=config-demo set-cluster development --server=https://1.2.3.4 --certificate-authority=fake-ca-file

# 認証情報の登録
kubectl config --kubeconfig=config-demo set-credentials developer --client-certificate=fake-cert-file --client-key=fake-key-seefile

# コンテキスト名 / クラスタ名 / namespace / 認証情報
kubectl config --kubeconfig=config-demo set-context dev-frontend --cluster=development --namespace=frontend --user=developer
kubectl config --kubeconfig=config-demo set-context dev-storage --cluster=development --namespace=storage --user=developer
kubectl config --kubeconfig=config-demo set-context exp-scratch --cluster=scratch --namespace=default --user=experimenter

# 設定情報を見る
kubectl config --kubeconfig=config-demo view

# 切り替えコマンド
kubectl config --kubeconfig=config-demo use-context dev-frontend

Configure Access to Multiple Clusters - Kubernetes

kubectlで適用される差分

今まで意識してこなかったと同時に、なぜ意識できなかったのか後悔の多いところです。 さらに言えば、kubectl applyのマニフェストのマージ方法というところになります。 削除項目の差分適用は、新しくapplyされるマニフェストと、以前applyされたマニフェストとの比較で行われますが、追加・更新の項目の差分適用は以前applyされたマニフェストではなく、そこからKubernetesが変更を加えたものとの比較になります。つまり、kubectl get deploy xxxxx -o yamlした結果との比較です。 applyした結果、podのテンプレートと変更があれば、ローリングアップデートが起こりますね。以前適応したマニフェストにrollingUpdateについて書いてない状態から、rollingUpdateでmaxSurge25%、minUnAvailable25%ととしても、デフォルト値としてKubernetes側がセットした値とかわらないため、rollingUpdateは起こりません。

また、削除項目の差分適用が以前applyされたマニフェストとの比較ということで、単純にcreateされたマニフェストから、applyで更新すると削除できない項目が出てきてしまいます。createコマンドを使用するときはkubectl create --save-configを使用すること。

マニフェストファイルの設計

結合度の高いリソースである場合、例えばDeploymentとService、HPA、PDBは同じマニフェストファイルに書いても良いかもしれない。ConfigMapやSecretなどの疎結合のリソースに関してはマニフェストファイルを分けるべき。1つのファイルに書く場合には---で区切る。上から順に適用されていくため、書く順番は考慮されるべきである。
kubectl apply -f ./ -R再帰的にマニフェストを適応させることが出来るらしいので、ディレクトリ構造は極力わかりやすい形に持っていくことも可能。

その他

  • Stern(https://github.com/wercker/stern)導入で複数のpodをログを同時に見ることが可能
  • 環境変数KUBE_EDITORで使用するエディタを指定できる。
  • kubectl copyコマンド
  • kubectl port-forward deployment/xxxxxx 8080:3306でkubectlを実行したインスタンス(もしくはローカルマシン)のポート8080にアクセスすると対象Podの3306番のpodに接続される。
  • デバッグ-v=6-v=8を使う。

Workloadsリソース

Podのデザインパターン

  • サイドカー:メインコンテナに機能を追加する。個人的にはkinesisアクセスログを流すflunetdコンテナとかがこれに当たると思いました。
  • アンバサダー:外部システムとのやり取りの代理を行う。HAProxyコンテナのイメージです。AWSでいうならRDSの死活監視を行ってトラフィックを流すようなもの。
  • アダプタ:外部からのアクセスのインターフェース。JavaアプリケーションとかだったらPrometheus Tomcat Exporterのようなイメージでしょうか。

Rolling Updateの処理

マニフェストをapply -> .spec.template以下の構造体のハッシュ値を計算 -> 同じハッシュ値のReplicaSetが既存していなければ作成 -> ローリングアップデート戦略にもとづいて、現在のReplicaSetのPod数を減らし新しいReplicaSetのPod数を増やしていく。ロールバックは、戻すリビジョンを指定しそれに対応するReplicaSetのPod数を増やし、現在のReplicaSetのPod数を減らしていく。kubectl rollout pause deployment sampleでローリングアップデートを止めることも可能。

Daemon Setのアップデート戦略

OndeleteRolling Updateがある。OndeleteはDaemosetのtemplateが変更されてもPodの更新は行われない。更新する場合には手動でPodをdeleteしオートヒーリングでPodを新たに作成する際に新しい定義となる。apiVersionがapps/v1はデフォルトがRolling Updateですが、それより前のバージョンでのデフォルト値はOndeleteなので注意が必要。
参考: Default apps/v1beta2 StatefulSet and DaemonSet strategy to RollingUpdate · Issue #49604 · kubernetes/kubernetes · GitHub

Stateful Setの特徴

業務では使用したこと無いワークロードであるところのStateful Setですが他のワークロードとの違いが顕著であるように見受けられます。以下に列挙していきます。

  • 作成されるPod名のSuffixは数字のインデックスが付与されたのものになる。
  • scaleすると、今あるpodのIndexに+1された名前のPodが作成される。デフォルトでは、Ready状態になってから次のPodを作成し始める。なので増えるときは1つずつ。ただ、podManagementPolicyをPararellにするとdeploymentと同じように複数同時に起動する。
  • Rolling UpdateはpodManagementPolicyにかかわらず1つずつ行われる。partitionを設定することで一部だけを更新することが可能
  • Podの再起動時にも同じPersistentDiskを使用する

その他

  • Podテンプレートのコンテナ実行時のコマンド指定
kubernetes Dockerfile
command ENTRYPOINT
args CMD
  • Pod全コンテナの/etc/hostsを書き換える機能があり、.spec.hostAliasに指定する
  • kubectl applyしたときに--recordオプション必須つけることでアノテーションにchange-causeが記録される

Discovery&LBリソース

Service

kind type description
ClusterIP ClusterIP クラスタ内のみから疎通性があるInternal Networkに作り出される仮想IP。spec.ports.portがClusterIPで待ち受けるIPアドレス。spec.ports.targetPortが転送先コンテナのPort番号。手動でIPを固定する場合には、spec.ClusterIPを指定する。
ExternalIP ClusterIP typeはClusterIP。特定のKubernetesノードで受信したトラフィックをコンテナに転送する。spec.ExternalIPsで受信するノードのIPを列挙する
NodePort NordPort ExternalIPと似ている。ただしこれは、全ノードからトラフィックを受け付けるイメージ。NodePortで利用できるポート範囲は30000~32767
LoadBalancer LoadBalancer Production環境でクラスタ外からトラフィックを受けるときに使用される。Kubernetesクラスタ外に疎通性のある仮想IPを払い出す。上の外部疎通ServiceはノードがSPOFになるが、これは外部のロードバランサを利用するためにノードの障害に強い。GCPAWSでは、spec.loadBalancerSourceRangesに接続を許可する送信元IPアドレス範囲を指定できる。
Headless Service ClusterIP DNSラウンドロビンを使ったエンドポイントの提供。type:ClusterIPかつspec.ClusterIPがNoneのもの。負荷分散に向かない。
ExternalName Service ExternalName クラスタ内からService名で名前解決するとCNAMEが返ってくる。外部サービスと疎結合にするために使用する。
None-Selector Service * 自分で指定したメンバに負荷分散することが出来る。EndPointとServiceを同じ名前で作ることで紐付けられる。typeは自由だがメリットを考えるとClusterIP一択。Selectorには何も指定しない

Ingress

Ingressリソースは一旦作成すると、その後あんまりにも触る機会が無く忘れてしまいがちです。
L7のロードバランシングを行うリソース。L4のServiceと区別するため別のリソースとして扱われている。大別してクラスタ外のロードバランサを利用するものとクラスタ内のIngress用Podをデプロイするものがある。Ingressは事前に作成されたServiceをバックエンドとして転送を行う仕組みになっています。なのでNodePortのリソースをまず用意します。その後に、IngressリソースのserviceNameの項目で結びつけます。L7なので当然パスベースルーティングもマニフェストで指定することが出来ます。https通信を行う場合には別途でSecretを作成する必要性がある。

その他

  • 正式なFQDNを指定せずに、Service名だけでも解決できるのは、/resolve/confsearch [namespace].svc.cluster.localという文言があるため。

Config & Storageリソース

Secret

1つのSecretの中に複数のKey-Value値。 大体は、マニフェストから作成することが考えられる。base64エンコードした値をマニフェストに埋め込みますが、その状態で管理してもセキュリティなど無いに等しいため、kubesec(https://github.com/shyiko/kubesec)と暗号鍵のマネージドサービスを使って暗号化した状態でリポジトリに保存しておくのが良きかと思う。

apiVersion: v1
kind: Secret
metadata:
  name: mysecret
type: Opaque
data:
  username: YWRtaW4=
  password: MWYyZDFlMmU2N2Rm

利用方法も複数ある。環境変数として渡す手段とVolumeとしてマウントする手段があります。Volumeとしてマウントした場合には動的なSecretの変更を行うことができます。また、それぞれ特定のKeyを渡す手段(spec.containers[].env[].valueFrom.secretKeyRef)とすべてのKeyを渡す手段(spec.containers[].envFrom.secretRef)がある。マニフェストに一覧性を持たせたい場合には前者、マニフェストを冗長にしたくない場合には後者を用いると良さげ。

kind type description
Generic Opaque マニフェストから作成するケースが多そう。DBなどの資格情報はこれで管理する。
TLS kubernetes.io/tls kubectl create secret tls custom-tls-cert --key /path/to/tls.key --cert /path/to/tls.crtで作成。keyが秘密鍵でcertが証明書ファイル。
Dockerレジストリ kubernetes.io/dockerconfigjson Registoryサーバと認証情報を引数で指定して作成する。podテンプレートのspec.containers[].imagePullSecretsで名前を指定する。
Service Account kubernetes.io/service-account-token 自動で作成されるもの。利用者が意識することはほとんどない

ConfigMap

ConfigMapも殆どの場合でマニフェストから作成されるケースが多そう。Secretと違いBase64エンコードしない点に注意。数値はダブルクォートで囲む。渡し方はSecretとほぼ変わらず、spec.containers[].env[].valueFrom.secretKeyRef -> spec.containers[].env[].valueFrom.configMapKeyRef / spec.containers[].envFrom.secretRef -> spec.containers[].envFrom.configMapRefとフィールドの名前が変わったくらい。

Persistent Volume Claim

ややこしい名前のリソースたちの違い

  • Volume
    • 予め用意された利用可能なボリューム
      • EmptyDir : ホスト上の領域を一時的に確保して利用できる。podがterminateされると削除される
      • hostPath : ホスト上の領域をコンテナにマッピングする。セキュリティに注意。
      • gcePersistentDisk : 同プロジェクト同ゾーンの複数のGCE VMから読み取り専用でマウントされる。大体は予めデータを置いている想定かと思われる。
  • Persistent Volume
    • 実はConfig & StorageリソースではなくClusterリソース
    • 予め作成しておく必要性がある
    • Dynamic Provisioningを使用しない場合にはラベルを付与すること
    • アクセスモードが複数あるが、大体はReadWriteOnceかReadOnlyManyが許可されていて、ReadWriteManyは許可されていない。
    • Reclaim PolicyでPersistentVolumeClaimが削除された際のPersistent Volumeの挙動を決められる。
  • PersistentVolumeClaim
    • その名の通りPersistent Volumeを要求するリソース。マニフェストに指定した条件にあうPVの割当をおこなう。
    • podテンプレートの定義のspec.volumes[].persistentVolumeClaim.ClaimNameにPVCの名前を指定する。
    • 予めPVを使用しなくても、動的にPVを作成して割り当てるのがDynamic Provisioning。StorageClassを作成してPVCのマニフェストで指定する必要あり。
    • StatefulSetはPVCの定義がマニフェストに含まれている

その他

  • SecretとConfigMap以外からPodテンプレートで環境変数を渡すには
    • 静的設定
env:
     - name: TZ
        value: Asia/Tokyo
  • podの情報を参照 。env[].valueFrom.fieldRef.fieldPathで指定する。
  • コンテナの情報を参照。env[].valueFrom.resourceFieldRef.containerNameでコンテナの名前、env[].valueFrom.resourceFieldRef.resouceで取得するフィールドを指定する。
  • SubPathマウントで特定のディレクトリをルートとしてマウントすることができる。mountPathがコンテナ側・subPathがボリューム側のディレクトリパスの指定となる。

リソース管理とオートスケーリング

requestsとlimit

requests/limitともに各コンテナに定義する。
この値が適正かどうか判断する際に役に立つのが、Nodeごとにリソース状況。これはkubectl describe node hogehogeで見ることが可能。
Requestsを大きくしすぎず、RequestsとLimitsの間に顕著な差を作らないことで適正なスケールアウトができるようになる。

  • requests
    • 使用するリソースの下限を指定するもの。
    • この値をもとにNodeにPodがスケジューリングされる。
  • Limits
    • 使用するリソースの上限を指定するもの。
    • この値を超えないようにPodの使用するリソース量がコントロールされる。
    • メモリの場合はLimitsを超えるとOOMでコンテナプロセスが殺される。

HPA(水平スケーリング)

30秒に1回の頻度でオートスケーリングすべきかのチェックが走る。必要なレプリカ数の計算は以下で計算される。
マニフェストの中でDeploymentを指定する。

desiredReplicas = ceil[currentReplicas * ( currentMetricValue / desiredMetricValue )]

スケールアウトの式 (最大で3分に1回)

avg(currentMetricValue) / desiredMetricValue > 1.1

スケールインの式(最大で5分に1回)

avg(currentMetricValue) / desiredMetricValue < 0.9

その他

  • LimitRangeでリソースの最小・最大値・デフォルト値を設定可能。新規でPodを作成するときに適用される。
  • OOM KillerによってPodを停止される場合には、QoS Class「Best Effort」「Burstable」「Guaranteed」の順で停止される。RequestsとLimitsが指定されていてなおかつ値が同じ場合には「Guaranteed」となり削除されにくくなる。逆にどちらも指定していない場合には、停止される対象になりやすい。
  • ResouceQuotaによってNameSpaceごとのリソース量・数に制限をかけることができる。

コンテナのライフサイクル

ヘルスチェック

Amazon ECSでもELBを使用したヘルスチェックがありますが、それよりもだいぶ柔軟です。ヘルスチェック方式は3種類あり、コマンドベースのチェック(exec)とHTTPベースのチェック(httpGet)、TCPベースのチェック(tcpSocket)がある。HTTPベースのチェックではホスト名やHeaderを設定できたりもする。
Amazon ECSにおける「healthCheckGracePeriodSeconds(ヘルスチェックまでの猶予期間)」にあたるのがinitialDelaySecondsとなります。

  • Liveness Probe
    • 失敗時にはPodを再起動する。メモリリークなどでPodの再起動なしには状態が回復しずらい場合に使用。
    • 成功と判断するまでのチェック回数であるSuccessThresholdは必ず1
  • Readiness Probe

コンテナライフサイクル

restartPolicyはPodに定義するものでPodが停止した場合にどのような挙動を取るか指定したものです。基本的には、終了コードにかかわらず起動するAlwaysなのではと思うのですが、Jobなどは終了コード0以外で再起動するOnFailureになるのではと思います。Neverは再起動を行わないものです。

Init Containers

初期化処理を行うためのコンテナ。InitContainersの処理が終わらないと、spec.containersで指定したコンテナが起動しない。GCSからファイルをGETしてくる必要性があるときなどに使用している。この機能がAmazon ECSにも欲しい…。

postStart/preStop

コンテナの起動・終了時に行われる処理。postStartは、コンテナのENTRYPOINTとほぼ同じタイミングで実行されるため、どうしてもコンテナ内部で処理をしなくてはいけない場合以外はinitContainersに処理をしてもらったほうが良さそう。
preStopの処理の流れは、この本を読む前にこの記事(Kubernetes: 詳解 Pods の終了 - Qiita)で知っていましたが、改めて別の図も交えてだとよりわかりやすいなと思う。spec.terminationGracePeriodSecondsをpreStop処理にかかる時間を考慮した時間にしないと、デフォルトの30秒がたった時点でSIGKILLコマンド、preStop処理の途中だった場合はSIGTERMコマンドが送られ、その2秒後にSIGKILLが送られる。nginxやapacheなどのWEBサーバでGraceful Shatdownを行う場合に、コマンドだと非同期での実行となる場合があるのでsleepコマンドを入れておくことが望ましい。

その他

  • 親リソース(ReplicaSet)が削除された場合には子リソースのPodを削除するという処理がはいる。この親子関係は、マニフェストmetadata.ownerReferenceから読み取ることが可能。ex) Deployment削除 -> ReplicaSet削除 -> pod削除
  • podを即座に削除したい場合
kubectl delete pod hogehoge --grace-period 0 --force

高度なスケジューリング

Node Affinity

Podが特定のノード上でしか実行できないような条件付ができる。

  • requiredDuringSchedulingIgnoredDuringExecution
    • 必須条件のポリシー
    • この条件を満たさないとスケジューリングされない
    • 検証環境でプリエンプティブインスタンスに起動されると困るものは、priemptive:falseのものを指定したりとか
  • preferredDuringSchedulingIgnoredDuringExecution
    • 優先条件のポリシー
    • この条件を満たさない場合でもスケジューリングされる
    • 優先度の重み(weight)を設定することができる

Node AntiAffinity

これは、matchExpressionで指定できるオペレータでNotがついているものを使えば実現できるもの。

Inter-Pod Affinity

基本的にはNode Affinityと同じだが、topologyKeyというのを指定する必要がある。topologyKeyはスケジューリング対象の範囲を示す。これはホストやゾーンなどを指定でき、matchExpressionのじょうけんを満たすPodのホストもしくはゾーンに配置するという意味になる。

TaintsとTolerations

Podが条件を提示してNode側が許可する形のスケジューリング。node affinityと違う点は他にもあり、考慮されるのがスケジューリングのときだけではないという点がある。

まとめ

本当にめっちゃめちゃ良い本だったので買いましょう。

GKEのマスターノードVerが上がったので変更点まとめる

2018年8月20日からGKEのマスターノードのVersionが1.8.xは1.9.7に、1.9.xは1.10.6に上がりました。
ノードの自動アップグレードonにしているクラスターは何もしなくても、ワーカーノードのVersionも上がってることかと思います。 ただ、バージョン上がると急に期待している動作と違う挙動が見られることもあります。 例えば、v1.9.7ではgcePersistentDiskのSubPathマウントがうまくいかず、Podが起動しないという事象が起こり得ます。

本番運用しているシステムでワーカーノードの自動アップグレードはリスクがあります。 そのため、マスターノードがバージョンアップしたタイミングで、新しいバージョンのノードプールを作成し、Podを新バージョンのノードプールで起動するのを確認していく方法を取るというプロジェクトが多いのではと思います。

バージョンアップに際し、変更点を認識することが必要です。ということで、ver.1.9.xになることでどのような変化があるのか調べてみました。 deploymentなどのワークロードがGAになったこともあり、デフォルト値の変更などが気になるところです。

以下を参考にいたしました。 気になったことがあれば順次拾っていきます。

www.mirantis.com

github.com

kubernetes.io

Workloads API

kindがDaemonSet, Deployment, ReplicaSet, StatefulSetのapiVersionがextensions / v1beta2からapps / v1へと変更になります。今後、apps / v1への開発は行われるが、その前のバージョンには行われないようなのでこの機会にマニフェストを変更しておきたいところです。apiVersionを新しくしたマニフェストkubectl applyした後に、kubectl get deployment sample -o yamlしても 、apiVersionはextensions / v1beta1と表示されることがあります。これは、登録したマニフェストがapiServer内部で互換性のあるバージョンに関しては変換されるからです。 そして、kubectl get deployment sample -o yaml --v=6と打つと、/apis/extensions/v1beta1/namespaces/…というリソースにアクセスしていることが分かる。
実は、kubectl get deployments.v1.appsと打つと、表示されるマニフェストのapiVersionはapps/v1となり、オプションで--v=6として再度叩くと、apis/apps/v1/namespaces…というリソースにアクセスしていることが分かる。

kubectl convertを使用して、グループバージョン間でマニフェストを変換できるとのことです。以下のコマンドで変換できます。 kubectl convert -f sample_deployment.yaml -o yaml --output-version='apps/v1'

デフォルト値として、今までセットされていたものが外れているケースもある。.spec.selectorだ。今までは、デフォルト値として.template.metadata.labelsに記載された値をセットしていた。apps/v1からは明確な記載となる。

API Machinery

Admission Control

Admission webhooksがbetaになった。Admission Controllerの拡張機能。 Admission Controllerは認証・認可の後、Objectが永続化する前にクライアントからの要求を受け入れるか判定する仕組み。2種類あり、mutatingはクライアントの要求書き換え、validatingはクライアントの要求を受け入れるかどうかを判断する。 mutatingはマニフェストに、決まったアノテーションを付けたり、別コンテナ(例えばEnvoy)を付与したりできる。validatingは、マニフェストの内容やクライアントによって受容するか決められる。

Admission Webhooksはkube-apiserverにCallback先としてHTTPサーバを登録しておくと、そこにリクエストが飛んで来て、それに対してレスポンスを返すことで、Admission Controlを成立させる機能。

GKEにおいても使えそうな感じがするのですが(Istioがこの仕組み使って自動でEnvoyを配置しているため)、方法がわかりませんでした。

参考:Learn more about Admission Webhooks - Speaker Deck

Custum Resource

Custom Resource Definition (CRD)というのがbetaになった。ユーザ独自で定義したリソースのこと。knativeもこの仕組みを使っているはず。 Controllerの作成に便利なツールも出てきている(kube-builderなど)

GitHub - kubernetes/sample-controller: Repository for sample controller. Complements sample-apiserver

KubernetesのCRD(Custom Resource Definition)とカスタムコントローラーの作成

Kubernetes勉強会第6回 〜Kubernetesの実行と管理、CustomResourceDefinitions(CRD)、Container Runtime Interface(CRI)〜 - 世界中の羊をかき集めて

Extending Kubernetes with Custom Resources and Operator Frameworks - Speaker Deck

Auth

RBAC

組み込み(デフォルト)のadmin/edit/viewロールが定義されていて、これをcluster role aggregationで使うことができる。以下のような形。以下に出てくるcronTabはカスタムリソースのため組み込みのRoleでは制御されていない。ClusterRoleなので、namespace関係なし。Roleであれば、namespaceで分割する。

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: aggregate-cron-tabs-edit
  labels:
    # Add these permissions to the "admin" and "edit" default roles.
    rbac.authorization.k8s.io/aggregate-to-admin: "true"
    rbac.authorization.k8s.io/aggregate-to-edit: "true"
rules:
- apiGroups: ["stable.example.com"]
  resources: ["crontabs"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

CLI

kubectl

  • kubectl cp /tmp/foo <some-namespace>/<some-pod>:/tmp/bar -c <specific-container>でremoteのファイルをlocalにcopyできる
  • kubectl create pdbでデフォルト値をセットしなくなったらしい

Network

IPv6

IPv6対応(alpha)したとのこと。ただ、alphaの機能を使用するためには、 GKEの場合にはクラスター作成時に指定をすることが必要なので、面倒かもしれない。

CoreDNS

CoreDNSがリリース。kube-dnsの代わりに使用することもできる(1.11でデフォルトがCoreDNSだったような)

Other

  • –cleanup-ipvsフラグができてkube-dns起動時に既存のルールをフラッシュするか決められるように
  • ホストの/etc/resolve.confまたは-resolv-confに "options"を追加することでpodのresolve.confに反映させることが出来るようになった

Strorage

  • PersistentVolumePersistentVolumeClaimのサイズは0より大きくなくてはいけない。
  • GCEマルチクラスタでは、ノードを持たないゾーンでPersistentVolumeオブジェクトが動的にプロビジョニングされなくなります。

まとめ

1.9でBetaになった機能は拡張するためのものが多い。よりエンタープライズでも運用できるように改善されてきたイメージです。 ただ、使いどころを間違えると管理がかなり大変になりそうので気をつけなきゃですね。

記事書いてて思いましたが、まだまだ勉強が全然足らないですね…。