jcmdで何ができるかをまとめる
はじめに
jcmdは起動しているJavaプロセスのGCの統計情報をとったり、JFR起動したり諸々のことを行なうのによく使われるツールかと思います。
今まで必要になったコマンドを調べて使うぐらいしかしたことなかったのですが、実際どのくらいのことができるのか、ツールの全体感を把握できてなかったのでまとめてみようかと思います。
また、その中でJVMについても薄く学べればよいかと思います。
基本的にはオラクルのドキュメントやツールのマニュアルを元に書いていますが、あくまで自分の理解をまとめているものと捉えていただけるとありがたいです。
そして、もし間違え等あればご指摘いただけると嬉しいです。
環境
今回、動作確認に用いるJDKやその動作環境は以下のとおりです。
$ java --version openjdk 16 2021-03-16 OpenJDK Runtime Environment (build 16+36-2231) OpenJDK 64-Bit Server VM (build 16+36-2231, mixed mode, sharing) $ uname -srvmpio Linux 5.4.0-80-generic #90-Ubuntu SMP Fri Jul 9 22:49:44 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a LSB Version: core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch Distributor ID: Ubuntu Description: Ubuntu 20.04.2 LTS Release: 20.04 Codename: focal
jcmd
そもそもjcmdってなんなのか?
man
コマンドで出力されるマニュアルにはシンプルに以下のように書かれます。
Sends diagnostic command requests to a running Java Virtual Machine (JVM).
JVMに対して診断用のコマンドを送るためのツールだそうです。このコマンドをつかうとJVMに関する様々な情報を取得できたり、JVMに対する指示を送ったりすることができます。具体的にに何ができるかはあとでみていきます。
基本的にjcmdはJVMが起動しているマシンと同一マシンかつJVMを起動したユーザと同じもしくはユーザグループに所属している必要があるみたいです。
ただ、DiagnosticCommandMBeanインターフェースを使えば外部プロセスからも診断コマンドを送ることが可能になるみたいですが、まずは基本的な使い方をまとめたいのでこのブログでは深くはおいません。
JDKにはjcmd以外にもjstack
やjmap
のようなコマンドも用意されていますが、基本的にはjcmd
1つでなんでもできるみたいです。
基本的な使い方
jcmdコマンドは以下の構成を取ります。
jcmd pid|main-class PerfCounter.print jcmd pid|main-class -f filename jcmd pid|main-class command[ arguments]
使用方法は大きく3つのパターンがありますが、第一引数にJavaのpidもしくはメインクラスの名前が来る点は変わりません。
メインクラス指定を行なうと同じ名前のメインクラスを持つすべてのプロセスの情報を取得するようです。
また、pidに0を指定するとすべてのJavaプロセスに診断コマンドを送信します。
また、引数なし、もしくは-l
オプション使って実行すると実行中のJavaプロセスの一覧を出力します。
$ jcmd -l 11234 com.intellij.idea.Main 11791 jdk.jcmd/sun.tools.jcmd.JCmd -l
上記はIntelliJのpidとjcmd自身のpidが出力されています。
PerfCounter.print
は指定したプロセス(もしくはクラス)のパフォーマンスカウンターを出力します。
$ jcmd 11234 PerfCounter.print 11234: java.ci.totalTime=65102836647 java.cls.loadedClasses=55377 java.cls.sharedLoadedClasses=0 java.cls.sharedUnloadedClasses=0 java.cls.unloadedClasses=178 (省略)
jcmd pid|main-class command[ arguments]
のパターンでは第二引数に診断コマンドを受け取ります。
詳細は後ほどまとめますが、GC.heap_info
をコマンドとして渡すと以下のようにGCの一般的な情報が出力されます。
$ jcmd 11234 GC.heap_info 11234: par new generation total 261120K, used 118880K [0x0000000080000000, 0x0000000091b50000, 0x00000000a9990000) eden space 232128K, 51% used [0x0000000080000000, 0x0000000087418068, 0x000000008e2b0000) from space 28992K, 0% used [0x000000008e2b0000, 0x000000008e2b0000, 0x000000008ff00000) to space 28992K, 0% used [0x000000008ff00000, 0x000000008ff00000, 0x0000000091b50000) concurrent mark-sweep generation total 579960K, used 293996K [0x00000000a9990000, 0x00000000ccfee000, 0x0000000100000000) Metaspace used 373150K, capacity 388822K, committed 394296K, reserved 1388544K class space used 48366K, capacity 54385K, committed 56128K, reserved 1048576K
jcmd pid|main-class -f filename
でファイル名を指定して渡すとふくすうのコマンドをJavaプロセスに送信することができます。
例えば以下のようなファイルを用意します。
$ cat command.txt # コメント GC.heap_info VM.flags
このファイルを以下のようにコマンドに渡してやると、ヒープの情報とフラグの情報を取得することができます。
$ jcmd 11234 -f command.txt 11234: Command executed successfully par new generation total 261120K, used 192941K [0x0000000080000000, 0x0000000091b50000, 0x00000000a9990000) eden space 232128K, 83% used [0x0000000080000000, 0x000000008bc6b410, 0x000000008e2b0000) from space 28992K, 0% used [0x000000008e2b0000, 0x000000008e2b0000, 0x000000008ff00000) to space 28992K, 0% used [0x000000008ff00000, 0x000000008ff00000, 0x0000000091b50000) concurrent mark-sweep generation total 579960K, used 293996K [0x00000000a9990000, 0x00000000ccfee000, 0x0000000100000000) Metaspace used 373154K, capacity 388822K, committed 394296K, reserved 1388544K class space used 48366K, capacity 54385K, committed 56128K, reserved 1048576K -XX:CICompilerCount=2 -XX:ErrorFile=/home/yuya-hirooka/java_error_in_idea_%p.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/yuya-hirooka/java_error_in_idea_.hprof -XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:MaxNewSize=697892864 -XX:MaxTenuringThreshold=6 -XX:MinHeapDeltaBytes=196608 -XX:NewSize=44695552 -XX:NonNMethodCodeHeapSize=5825164 -XX:NonProfiledCodeHeapSize=122916538 -XX:OldSize=89522176 -XX:-OmitStackTraceInFastThrow -XX:ProfiledCodeHeapSize=122916538 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:SoftRefLRUPolicyMSPerMB=50 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseFastUnorderedTimeStamps
コマンドの一覧
jcmdで使えるコマンドには以下のようなものがあります。
(※IntteliJのプロセスに対するリストなので、完璧なリストではないかも知れません)
コマンド名 | 説明 | インパクト |
---|---|---|
Compiler.CodeHeap_Analytics | コード・ヒープ(JITコンパイルされたネイティブコードとスペースマネジメントに必要な関数のストレージ)の分析結果を出力する | 低:ヒープサイズとコンテントに影響される |
Compiler.codecache | コード・ヒープのキャッシュのレイアウトとバインドの情報を出力 | 低 |
Compiler.codelist | コード・ヒープのキャッシュに保存されているコンパイルされた行きているメソッド | 中 |
Compiler.directives_[add | remove | clear | print])|コンパイラー・ディレクティブ(JITコンパイラーへの指示)を追加(add)、最後に追加された指示の削除(remove)、すべての指示の削除(clear)、出力(print)。Json形式のファイルを引数に与える|低 |
Compiler.queue | コンパイルのキューに積まれたメソッドの出力 | 低 |
GC.class_histogram | ヒープの使用率の統計情報を出力 | 高:ヒープサイズとコンテントに影響される |
GC.class_stats | Javaクラスのメタデータに関する統計情報を出力 | 高:ヒープサイズとコンテントに影響される |
GC.finalizer_info | ファイナライザーのキューに関する情報を取得 | 中 |
GC.heap_dump | HPROFフォーマットのJavaのヒープダンプを取得する | 高:ヒープサイズとコンテントに影響される。-all フラグが指定されない場合はフルGCがリクエストされる |
GC.heap_info | ヒープサイズ、利用率などのヒープに関する一般的な情報を出力する。 | 中 |
GC.run | java.lang.System.gc() を実行 |
中 |
GC.run_finalization | java.lang.System.runFinalization() を実行 |
中:コンテントによって影響される |
JFR.check | 記録中のJFRの記録をチェックする | 低 |
JFR.configure | JFRの設定を行なう | 低 |
JFR.dump | JFRの記録をファイルに出力する。<key>=<value> の形式でオプションを指定する。 |
低 |
JFR.start | JFRの記録を開始する | 低〜高:レコードのないようによって影響される |
JFR.stop | JFRの記録を停止する | 低 |
JVMTI.agent_load | JVMTI(JVM Tool Interface。開発ツールや監視ツールで使用されるインターフェース)のネイティブエージェントをロードする | 低 |
JVMTI.data_dump | JVMTIに対するデータダンプのリクエスト | 高 |
ManagementAgent.start | リモートマネジメントエージェントをスタートする。 | 低:影響なし |
ManagementAgent.start_local | ローカルのマネジメントエージェントをスタートする | 低:影響なし |
ManagementAgent.status | マネジメントエージェントのステータスを出力 | 低:影響なし |
ManagementAgent.stop | リモートのマネジメントエージェントを停止する | 低:影響なし |
Thread.print | すべてのスレッドのスタックトレースを出力する | 中:スレッドの数によって影響される |
VM.class_hierarchy | すべてのロードされているクラスのヒエラルキーをツリーで出力する | 中:ロードされているクラスの数によって影響される |
VM.classloader_stats | すべてのクラスローダーの統計情報を出力する | 低 |
VM.classloaders | すべてのクラスローダーをツリーで出力する | 中:クラスローダーの数によって影響される |
VM.command_line | VMを起動する際に実行されたコマンドラインを出力する | 低 |
VM.dynlibs | ロードされたダイナミクなライブラリーを出力する | 低 |
VM.events | VMのイベントログを出力する | 低 |
VM.flags | 現在利用されているVMのフラグを出力する | 低 |
VM.info | VMの環境とステータスを出力する | 低 |
VM.log | ログ、ログの設定を出力する | 低:影響なし |
VM.metaspace | メタスペースに関する統計情報を出力する | 中:ロードされているクラスの数によって影響される |
VM.native_memory | ネイティブメモリの使用率を出力する | 中 |
VM.print_touched_methods | JVMのライフタイムで一度も触れれていないメソッドを出力する。-XX:+LogTouchedMethods を有効化する必要がある |
中 |
VM.set_flag | VMのオプションをセットする | 低 |
VM.stringtable | Stringテーブルをダンプする | 中:コンテントによって影響される |
VM.symboltable | シンボルテーブルをダンプする | 中:コンテントによって影響される |
VM.system_properties | システムプロパティを出力する | 低 |
VM.version | VMのバージョンを出力する | 低 |
VM.uptime | VMの起動時間を出力する | 低 |
参考資料
KtorとKoinの組み合わでWebAPIを作る
はじめに
KotlinでWeb開発するときに、Springが選ばれることが多いと思うのですが、個人的な思いとしてはKotlin由来のライブラリーやフレームワークをなるべく使いたいという気持ちがあります。
KotlinでそのへんをやるにはKtorとWebフレームワークとKoinというDIコンテナを組み合わせて使うのが1つの大きな選択肢となると思います。KoinはKtorのサポートも行ってそうだったのでプロジェクトを作って簡単なWebアプリを作るまでをやってみようかと思います。
やってみる
環境
$ uname -srvmpio Linux 5.4.0-77-generic #86-Ubuntu SMP Thu Jun 17 02:35:03 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a LSB Version: core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch Distributor ID: Ubuntu Description: Ubuntu 20.04.2 LTS Release: 20.04 Codename: focal $ java --version openjdk 11.0.10 2021-01-19 OpenJDK Runtime Environment 18.9 (build 11.0.10+9) OpenJDK 64-Bit Server VM 18.9 (build 11.0.10+9, mixed mode) $ mvn -v Apache Maven 3.6.3 Maven home: /usr/share/maven Java version: 11.0.10, vendor: Oracle Corporation, runtime: /home/someone/.sdkman/candidates/java/11.0.10-open Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-77-generic", arch: "amd64", family: "unix"
プロジェクトを作成
プロジェクトはGenerate Ktor projectを用いて作成します。
今回はMavenを使ってプロジェクトを作成します。
依存としてはRoutingだけ入れています。
Koinの依存を追加する
IDEか何かで、プロジェクトを開いてKoinの依存を追加します。
Pomに以下の依存を付け加えます。
<dependency> <groupId>io.insert-koin</groupId> <artifactId>koin-ktor</artifactId> <version>3.1.2</version> </dependency>
これで、プロジェクトの準備はできました。
諸々の設定を行なう
まずは、Ktorがapplication.conf
を読み込むように修正します。
Application.kt
を以下のように書き換えます。
fun main(args: Array<String>) { embeddedServer(Netty, commandLineEnvironment(args)).start(wait = true) }
次にハンドラーを1つ追加します。
HelloHandler.kt
を作り以下のルーティングの定期を書きます。
import io.ktor.application.* import io.ktor.response.* import io.ktor.routing.* fun Route.hello() { get("/hello") { call.respond("Hello, Koin") } }
このハンドラーをRouteingとして登録します。
再びAplication.ktに戻り以下のように修正します。
import io.ktor.application.* import io.ktor.routing.* import io.ktor.server.engine.* import io.ktor.server.netty.* fun Application.main() { install(CallLogging) routing { hello() } } fun main(args: Array<String>) { embeddedServer(Netty, commandLineEnvironment(args)).start(wait = true) }
application.conf
を作成してハンドラーの設定をモジュールとして読み込むようにします。
あと、今回は必要ないものもありますが、もそもろの設定もしておきます。
ktor {
deployment {
port = 8081
port = ${?APP_PORT}
}
application {
modules = [
dev.hirooka.ApplicationKt.main,
]
}
environment = "test"
environment = ${?KTOR_ENV}
}
$ mvn compile exec:java $ curl localhost:8081/hello Hello, Koin
KoinでDIする
Koinで依存を定義しDIをやってみます。
まずは、以下のようなサービスクラスとデータクラスを作成します。
data class Name(val value: String = "Moheji") interface HelloService { fun greeting(): String } class HelloServiceImlp(private val name: Name) : HelloService { override fun greeting() = "Hello, ${name.value}" }
関係性としては、HelloService
インターフェースをHelloServiceImpl
が実装してName
データクラスに依存しています。
DIの設定を記述していきます。
Application.kt
を以下のように書き換えます。
import io.ktor.application.* import io.ktor.features.* import io.ktor.routing.* import io.ktor.server.engine.* import io.ktor.server.netty.* import org.koin.dsl.module import org.koin.ktor.ext.Koin fun Application.main() { install(DefaultHeaders) install(CallLogging) routing { hello() } } fun Application.koin() { install(Koin) { modules( module { single { Name() } single { HelloServiceImlp(get()) as HelloService } } ) } } fun main(args: Array<String>) { embeddedServer(Netty, commandLineEnvironment(args)).start(wait = true) }
Application.koin()
でモジュールを1つ追加しKoinの依存の設定を記述しています。Application.main()
モジュールで書いても問題はないのですが、設定を分けて置けるとあとから読みやすかったりするので分けました。
上記ではName
データクラスとHelloSerivceImpl
クラスをそれぞれコンテナに入れています、すでにコンテナに入っているものはget()
で取り出すことが可能で、HelloSerivceImpl
のインスタンスを生成する際のName
インスタンスをインジェクションする際に利用しています。
また、HelloSerivceImpl
をHelloSerivce
でキャストすることで利用時にHelloSerivce
方でのコンテナからの取り出しを行えます。
application.conf
を書き換えモジュールを読み込むように変更します。
ktor {
deployment {
port = 8081
port = ${?APP_PORT}
}
application {
modules = [
dev.hirooka.ApplicationKt.main,
dev.hirooka.ApplicationKt.koin
]
}
environment = "test"
environment = ${?KTOR_ENV}
}
それでは最後にDIコンテナに入れたHelloSerivceImpl
をハンドラーから利用します。
ハンドラーを以下のように書き換えます。
import io.ktor.application.* import io.ktor.response.* import io.ktor.routing.* import org.koin.ktor.ext.inject fun Route.hello() { val helloService by inject<HelloService>() get("/hello") { call.respond(helloService.greeting()) } }
コンテナからサービスを取り出す際にはinject
を利用します。
アプリケーションを起動し直して、アクセスします。
$ mvn compile exec:java $ curl localhost:8081/hello -v Hello, Moheji
一通りの使い方はこんな感じですね。
GitHub ActionsでGauge Test(Kotlin)を実行する
はじめに
仕事ではGaugeを使うことが多いのですが、GitHub Actionsを使って動かすにはどうすればいいんだろうかというところに興味が少しわきました。 そもそもGitHub Actionsをそんなに使ったことも無かったのでHello Worldも兼ねてやってみようかと思います。
やってみる
環境
Gauge(といくつかのプラグイン)は事前にインストールしています。
Gaugeのプライグインに関しては今回はgauge-javaだけでよい想定です。
$ uname -srvmpio Linux 5.4.0-77-generic #86-Ubuntu SMP Thu Jun 17 02:35:03 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a LSB Version: core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch Distributor ID: Ubuntu Description: Ubuntu 20.04.2 LTS Release: 20.04 Codename: focal yuya-hirooka@yuya-hirooka:~/source/sleepy $ java --version openjdk 11.0.10 2021-01-19 OpenJDK Runtime Environment 18.9 (build 11.0.10+9) OpenJDK 64-Bit Server VM 18.9 (build 11.0.10+9, mixed mode) $ gauge version Gauge version: 1.3.1 Commit Hash: c76b761 Plugins ------- html-report (4.0.8) java (0.7.15) screenshot (0.0.1) xml-report (0.2.2)
プロジェクトを作成する
まずは、Gaugeのプロジェクトを作成します。
まずは、Maven Javaでプロジェクトを作成し、Kotlinで動作させるように変更します。
gauge init
コマンドでプロジェクトの作成を行います。
$ mkdir gauge-kotlin $ cd gauge-kotlin/ $ gauge init java_maven Initializing template from https://github.com/getgauge/template-java-maven/releases/latest/download/java_maven.zip . Copying Gauge template java_maven to current directory ... Successfully initialized the project. Run specifications with "mvn clean test" in project root.
プロジェクトができたら、src/test/java
とspecs/example.spec
は消してしまって大丈夫です。
Kotlinでコードを記述できるようにするために、Pomに以下の記述を追加します。
<properties> <kotlin.compiler.incremental>true</kotlin.compiler.incremental> <java.version>11</java.version> <kotlin.version>1.5.20</kotlin.version> <kotlin.compiler.jvmTarget>${java.version}</kotlin.compiler.jvmTarget> </properties> <dependencies> // もともとあった依存は省略 <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-stdlib</artifactId> <version>${kotlin.version}</version> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-test</artifactId> <version>${kotlin.version}</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> // もともとあったGaugeのプラグインは省略 <plugin> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-plugin</artifactId> <version>${kotlin.version}</version> <configuration> <args> <arg>-Xjsr305=strict</arg> </args> <jvmTarget>11</jvmTarget> </configuration> <executions> <execution> <id>compile</id> <phase>compile</phase> <goals> <goal>compile</goal> </goals> </execution> <execution> <id>test-compile</id> <phase>test-compile</phase> <goals> <goal>test-compile</goal> </goals> <configuration> <sourceDirs> <source>src/test/kotlin</source> </sourceDirs> </configuration> </execution> </executions> </plugin> </plugins> </build>
次にsrc/test/kotlin
ディレクトリを作成し、以下のクラスを作成します。
import com.thoughtworks.gauge.Step class HelloGitHubActions { @Step("Hello, GitHub Actionの文字列を出力する") fun hello(){ println("Hello, GitHub Action") } }
対応するSpecを記述します。
specs/example.spec
を作成し以下の記述を行います。
# Hello GitHub Actions ## GitHub Actionsに入門する * Hello, GitHub Actionの文字列を出力する
テストを実行します。
$ mvn test (省略) # Hello GitHub Actions ## GitHub Actionsに入門する Hello, GitHub Action ✔ Successfully generated html-report to => /home/yuya-hirooka/source/kotlin/gauge-kotlin/reports/html-report/index.html Specifications: 1 executed 1 passed 0 failed 0 skipped Scenarios: 1 executed 1 passed 0 failed 0 skipped Total time taken: 42ms Updates are available. Run `gauge update -c` for more info. [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 8.569 s [INFO] Finished at: 2021-07-10T15:46:47+09:00
ここまででProjectの準備は完了です。
GitHub Actionsの設定をおこなう
GitHub Actionsの概要
GitHub Actionsは、ほぼはじめてであるためまずは簡単に概要をまとめます。
GitHub Actionsは開発のライフサイクルを自動化してくれるSaaSです。
イベント駆動で、指定されたイベントが発生した際に定義してある一連のコマンドが実行されます。
GitHub Actionsを構成する要素として以下のような概念が存在します。
- Workflow
- リポジトリに追加する。一連の自動化されたプロセスです。1つ以上のJob(後述)で構成され、スケジュールかもしくは設定されたイベントの発起をトリガーに実行されます。Workflowでプロジェクトのビルド、テスト、デプロイ等を行なうことができます。
- Event
- Workflowのトリガーとなるアクティビティです。コミットのPush、Issueの作成 、プルリクなど様々なアクティビティがあります(他のアクティビティに関してはこちらをご覧ください)。
- Job
- 同一のRunnerで実行される一連のStep(後述)の集合です。Workflowが複数Jobを持つ場合はデフォルトでJobを並行で実行します。シーケンシャルにJobを実行させることも可能です。
- Step
- Job内で実行される単一のタスクです。StepはAction(後述)もしくはShellコマンドとなります。
- Action
- Stepに結合されているスタンドアローンなコマンド。Workflow内での最小の構成要素となります。独自アクションの作成が可能ですし、GitHubコミュニティによって提供されるアクションを利用することも可能です。
- Runnler
- Workflowが実行されるGitHub Action Runnerがインストールされているサーバ。自分でホストすることも可能ですし、GitHubでホストされているものを利用することも可能です。Workflowの各Jobは新しい仮想環境で実行されます。自分のRunnerをホストしたい場合はこちらを参照してください。
GitHub ActionsではWorkflowはyamlファイルで定義します。
name: learn-github-actions on: [push] jobs: check-bats-version: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 - run: npm install -g bats - run: bats -v
例えば上記のyamlは以下のように定義されています。
- name:Workflowの名前。Actionsタブに表示される
- on: [push]:Workflowのトリガーとなるうイベント上記の場合Pushされた際に発火される。
- jobs:Workflowを構成する一連のJobをグループ化する
- check-bats-version:Jobの名前。
- runs-on: ubuntu-latest:Jobが実行されるRunnerを定義。上記の場合Ubuntu Linuxのランナーで実行される。
- steps:
check-bats-version
Jobで実行されるStepのグループ化する - uses: アクションの定義
- actions/checkout@v2:コミュニティアクションの v2 を取得するようにジョブに指示。リポジトリをランナーにチェックアウトしてアクションを実行できるようにする。
- actions/setup-node@v1:Nodeのソフトウェアパッケージをインストールしnpmコマンドを利用できるようにしている。
- run: npm install -g bats:Runnnerでコマンドを実行する。上記の場合npmでbatsをインストールしている。
- run: bats -v:Runnerでコマンドを実行する。上記の場合batsのバージョンを表示している。
ベースとなるJavaのVersionを表示するWorkflowを定義する。
一通り、まとめて早速使っていきたいと思います。
まずはリポジトリを作成して先程作成したプロジェクトをPushしておきます。
次にベースとなるWorkflowを定義します。
.github/workflows/gauge-test.yaml
を作成して以下のようなyamlを記述します。
name: gauge-test on: [push] jobs: gauge-test: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - uses: actions/setup-java@v2 with: distribution: 'adopt' java-version: '11' - run: java --version
上記のActionはリポジトリをチェックアウトして、Javaのセットアップを行い、Versionを表示しているだけです。
JavaのセットアップはSetup Java JDKを用いています。
上記の図のように✓のバッチが付いているAcitonはGitHubがアクションの作成者をパートナーオーガナイゼーションとして認めたものになるみたいです。
このアクションは大きく以下のようなことを行ってくれます。
Javaに関してはZulu OpenJDK
、Adopt OpenJDK Hotspot
、Adopt OpenJDK OpenJ9
から選べるみたいで、またそのバージョンは 8
、 11
、15
の中から選択可能なようです(その他の細かいJavaのバージョンはこちらで確認してください)。
このWorkflowをpushしGitHubリポジトリのActions
セクションを確認します。
何度かミスってしまってますが、無視してください。 最新の成功している実行を確認すると以下のように表示されます。
ちゃんとJavaのバージョンが表示されていますね。 クリーンの処理も走っているみたいです。
Gaugeを実行するWorkflowを定義する
ベースとなるWorkflowはできたので、Gauge Testを実行するようにWorkflowを修正します。
name: gauge-test on: [push] jobs: gauge-test: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - uses: actions/setup-java@v2 with: distribution: 'adopt' java-version: '11' - run: curl -SsL https://downloads.gauge.org/stable | sh -s -- --location-[custom path] - run: gauge install java --version 0.7.15 - run: gauge version
先程のWorkflowから gaugeをインストールしてバージョンを表示するように修正しています。
pushしてActionsの実行結果をみてみます。
いい感じにできてるみたいですね。
最後に、テストを実行してみます。
name: gauge-test on: [push] jobs: gauge-test: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - uses: actions/setup-java@v2 with: distribution: 'adopt' java-version: '11' - run: curl -SsL https://downloads.gauge.org/stable | sh -s -- --location-[custom path] - run: gauge install java --version 0.7.15 - run: gauge version - run: mvn test
pushしてActionsの実行結果をみてみます。
テストの実行まで行えましたね。
Skaffoldを用いてローカルでk8sにデプロイするJavaアプリの開発を行なう
はじめに
最近身の回りでSkaffoldという名前をよく聞くようになりまして、ちょっと気になって調べたら面白そうだったし、今後使っていきそうな雰囲気を感じたので、ちょっとさわっておこうかと思います。
Skaffoldとは?
Skaffoldはk8sネイティブなアプリケーションの開発をサポートしてくれるコマンドラインツールです。
k8sに対するBuild、Push、Deploy等をサポートしてくれます。
大まかには以下のような機能や特徴があります。
- ローカルでの開発において、のソースコードの変更を検知して、自動でBuild、Push、Deployまでのサポート。
- ローカルの開発において、ログに対するサポートとポートフォワードのサポート
git clone
とskaffold run
の実行で様々な環境でアプリを動作させることが可能- Skaffoldのprofile, local user config, environment variables, flags などの機能を使って環境ごとの設定を組み込むことが可能
skaffold render
コマンドを用いてKubernetesマニフェストのテンプレートをレンダリングすることによって、GitOpsワークフローをサポート- Clusterは無くクライアント再度のみで独立している
skaffold.yaml
ファイルによって宣言的で、プラガブルな設定が可能
Skaffold自体はCI/CDにおけるワークフローのサポートも行っているようですが、今回のこのブログではローカルでのアプリケーション開発におけるいくつかの機能を試してみたいと思います。
また、今回はJavaアプリケーションで開発を行ってみようと思います。
使ってみる
動作環境
ローカルのクラスタはMinikube(Docker Drive)を用いて構築します。
$ uname -srvmpio Linux 5.4.0-77-generic #86-Ubuntu SMP Thu Jun 17 02:35:03 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a LSB Version: core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch Distributor ID: Ubuntu Description: Ubuntu 20.04.2 LTS Release: 20.04 $ minikube version minikube version: v1.21.0 commit: 76d74191d82c47883dc7e1319ef7cebd3e00ee11 $ kubectl version -o yaml clientVersion: buildDate: "2021-03-18T01:10:43Z" compiler: gc gitCommit: 6b1d87acf3c8253c123756b9e61dac642678305f gitTreeState: clean gitVersion: v1.20.5 goVersion: go1.15.8 major: "1" minor: "20" platform: linux/amd64 serverVersion: buildDate: "2021-05-12T12:32:49Z" compiler: gc gitCommit: 132a687512d7fb058d0f5890f07d4121b3f0a2e2 gitTreeState: clean gitVersion: v1.20.7 goVersion: go1.15.12 major: "1" minor: "20" platform: linux/amd64
Skaffoldのインストール
Skaffoldをインストールするためには以下のコマンドを叩きます。
$ curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-linux-amd64 && \ sudo install skaffold /usr/local/bin/ $ skaffold version v1.27.0
その他、MacやWindows、Dockerでのインストールはこちらをご確認ください。
サンプルアプリを作成しておく
Skaffoldを使ってデプロイやディバグなどの機能を試すために開発対象となるアプリを作っておきます。
特に深い意図はないのですがSpringを使ってやろうかと思います。
Spring Initializrで以下の設定でアプリを作成します。
依存はWebだけを追加してます。
Skaffoldプロジェクトを初期化する
ダウンロードしてきたプロジェクトを解凍して、プロジェクトのルートに移動し以下のコマンドを実行します。
Skaffoldプロジェクトを初期化するには、skaffold init
コマンドを用います。
skaffold init
コマンドは実行するとプロジェクトをスキャンし、以下のようなファイルを見つけるとその構成に合わせた設定を行ってくれます。
ちなみに500MB以上のファイルは無視されるようです。
例えば、こんかいのケースではいくつかの選択肢を提示してくれます。
$ skaffold init --generate-manifests ? Select port to forward for pom-xml-image (leave blank for none): 8080
--XXenableJibInit
フラグや--XXenableBuildpacksInit
フラグを使えば、それぞれJibやBuildpacksを用いた構成を作ることも可能なようです。
--generate-manifests
フラグはマニフェストの生成まで行ってもらうために使用しています。このフラグを使用しない場合は自分で作成したdeployment.yamlを用いることになります。
今回の場合はpom.xml
のみが検知され、Buildpack
を用いた設定がされます。
$ skaffold init --generate-manifests ? Select port to forward for pom-xml-image (leave blank for none): 8080 adding manifest path deployment.yaml for image pom-xml-image apiVersion: skaffold/v2beta18 kind: Config metadata: name: skaffold-sample build: artifacts: - image: pom-xml-image buildpacks: builder: gcr.io/buildpacks/builder:v1 deploy: kubectl: manifests: - deployment.yaml portForward: - resourceType: service resourceName: pom-xml-image port: 8080 deployment.yaml - apiVersion: v1 kind: Service metadata: name: pom-xml-image labels: app: pom-xml-image spec: ports: - port: 8080 protocol: TCP clusterIP: None selector: app: pom-xml-image --- apiVersion: apps/v1 kind: Deployment metadata: name: pom-xml-image labels: app: pom-xml-image spec: replicas: 1 selector: matchLabels: app: pom-xml-image template: metadata: labels: app: pom-xml-image spec: containers: - name: pom-xml-image image: pom-xml-image ? Do you want to write this configuration, along with the generated k8s manifests, to skaffold.yaml? Yes Generated manifest deployment.yaml was written Configuration skaffold.yaml was written You can now run [skaffold build] to build the artifacts or [skaffold run] to build and deploy or [skaffold dev] to enter development mode, with auto-redeploy
skaffold.yaml
は以下のようになります。
apiVersion: skaffold/v2beta18 kind: Config metadata: name: skaffold-sample build: artifacts: - image: pom-xml-image buildpacks: builder: gcr.io/buildpacks/builder:v1 deploy: kubectl: manifests: - deployment.yaml portForward: - resourceType: service resourceName: pom-xml-image port: 8080
上記のskaffold.yaml
ではビルドやデプロイ、ポートフォワードの設定が行われています。
その他、ここで使われていない項目や設定の説明はこちらをご覧ください。
次に進む前にイメージの名前がpom-xml-sample
だとあまりにもあまりになので以下のように書き換えておきます。
また、Buildkitを有効にしておきます。
apiVersion: skaffold/v2beta18 kind: Config metadata: name: skaffold-sample build: artifacts: - image: spring-app buildpacks: builder: gcr.io/buildpacks/builder:v1 deploy: kubectl: manifests: - deployment.yaml portForward: - resourceType: service resourceName: pom-xml-image port: 8080
作成されたk8sのマニフェストのpom-xml-sample
の部分もspring-app
に書き換えておきます。
apiVersion: v1 kind: Service metadata: name: spring-app labels: app: spring-app spec: ports: - port: 8080 protocol: TCP clusterIP: None selector: app: spring-app --- apiVersion: apps/v1 kind: Deployment metadata: name: spring-app labels: app: spring-app spec: replicas: 1 selector: matchLabels: app: spring-app template: metadata: labels: app: spring-app spec: containers: - name: spring-app image: spring-app
devモードをで開発を行なう
前述したとおり、Skaffoldはローカルでの開発に置いてソースコードの変更を検知して、自動でBuild、Push、Deployまでのサポート
までをサポートしてくれます。
devモードで起動するとその機能が利用可能で、以下のコマンドでdevモードで起動します。
$ skaffold dev (省略) Starting test... Tags used in deployment: - spring-app -> spring-app:e0b79f2a42356a8de0ba7b3da0f0f74903c0d9b99ddf1db39ed36a872a90d577 Starting deploy... - service/spring-app created - deployment.apps/spring-app created Waiting for deployments to stabilize... - deployment/spring-app is ready. Deployments stabilized in 1.129 second Press Ctrl+C to exit Watching for changes... [spring-app] [spring-app] . ____ _ __ _ _ [spring-app] /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ [spring-app] ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ [spring-app] \\/ ___)| |_)| | | | | || (_| | ) ) ) ) [spring-app] ' |____| .__|_| |_|_| |_\__, | / / / / [spring-app] =========|_|==============|___/=/_/_/_/ [spring-app] :: Spring Boot :: (v2.5.2) [spring-app] [spring-app] 2021-07-08 17:17:30.213 INFO 20 --- [ main] d.h.s.SkaffoldSampleApplication : Starting SkaffoldSampleApplication v0.0.1-SNAPSHOT using Java 11.0.11 on spring-app-6d5b5c74c4-2q6xs with PID 20 (/workspace/target/skaffold-sample-0.0.1-SNAPSHOT.jar started by cnb in /workspace) [spring-app] 2021-07-08 17:17:30.215 INFO 20 --- [ main] d.h.s.SkaffoldSampleApplication : No active profile set, falling back to default profiles: default [spring-app] 2021-07-08 17:17:31.073 INFO 20 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) [spring-app] 2021-07-08 17:17:31.085 INFO 20 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] [spring-app] 2021-07-08 17:17:31.086 INFO 20 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.48] [spring-app] 2021-07-08 17:17:31.156 INFO 20 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext [spring-app] 2021-07-08 17:17:31.157 INFO 20 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 880 ms [spring-app] 2021-07-08 17:17:31.683 INFO 20 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' [spring-app] 2021-07-08 17:17:31.691 INFO 20 --- [ main] d.h.s.SkaffoldSampleApplication : Started SkaffoldSampleApplication in 2.022 seconds (JVM running for 2.444)
コマンドを実行するとビルドが始まり少し待つとKubernetes上にデプロイされます。
また、devモードで起動するとローカルマシンへのポートフォワードも自動的に行ってくれます
ここまででServiceとDeploymentがローカルのMinikubeで作ったクラスタに作成されリソースが作られている状態でかつホストマシンへのポートフォワードまで行われています。
$ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 7d3h spring-app ClusterIP None <none> 8080/TCP 4m48s $ kubectl get deploy NAME READY UP-TO-DATE AVAILABLE AGE spring-app 1/1 1 1 5m17s
8080
ポートフォワードされているのでcURLでアクセスしてみます。
$ curl localhost:8080 {"timestamp":"2021-07-08T17:28:57.961+00:00","status":404,"error":"Not Found","path":"/"}
現状はコントローラーを作成していないので404が返ってきます。
以下のクラスを作成してコントローラーを1つ作ってみます。
@RestController public class SampleController { @GetMapping("/") public String helle(){ return "Hello, Skaffold"; } }
コードを修正すると自動でビルドが走りクラスターにデプロイされます。
再度、cURLでアクセスすると今度はHello, Skaffold
の文字列が返ってきます。
$ curl localhost:8080 Hello, Skaffold
debugモードで起動して、IntelliJを用いてDebugする
Skaffoldのdebugモードはdevモードと同じように動作しますが、debug用のPodが立ち上がりlanguage runtimeに応じたdebug用のポートがホストにポートフォワードされます。
Javaの場合はJDWPを用いてdebugが可能となるようです。
ここで、debug自動デプロイ機能が無効になるので注意が必要です。
以下のコマンドでdebugモードで起動します。
$ skaffold debug (省略) [spring-app] Picked up JAVA_TOOL_OPTIONS: -agentlib:jdwp=transport=dt_socket,server=y,address=5005,suspend=n,quiet=y (省略) Port forwarding pod/spring-app-7b74d66d9-42wns in namespace default, remote port 5005 -> 127.0.0.1:5005
ログに出力されているように出力されJWDPのポートが5005で公開されてるのがわかります。
InteliJからリモートdebug用のプロセスに接続します。
Run/Debug Configurationを開き左上の+
ボタンからRemote JVM Debug
を選択します。
基本はデフォルトのままの設定で大丈夫ですが、名前の部分だけspring-app
にしておきます。
Apply
を押してIntelliJをDebug実行をすると起動します。
先程のコントローラーにブレークポイントを置いておきます。
この状態で再度cURLでリクエストを投げると置いたブレークポイントで停止することが確認できます。
OperatorSDKでCustom ResourceとCustom Controllerを作る
はじめに
k8sではいくつかの拡張ポイントが用意されています。その中でも、APIをカスタムするためのCustom Resourceや調整ループによってリソースオブジェクトの状態管理を行い、宣言的なAPIを可能にするCustom Controllerがあります。
まえまえからこの2つには興味がありつつも触れることができてなかったのですが、先日k8sのブログに「Writing a Controller for Pod Labels」というのを見かけて、OperatorSDKを使えばいい感じにできそうであるということに気がついたので試してみたいと思います。
OperatorSDKとは
そもそもOperatorとはなんぞやという部分なのですが、 Custom Resourceを使ってアプリケーションとそのコンポーネントを管理するソフトウェアの拡張で、OperatorそのものはControllerパターンに則ってリソースオブジェクトを管理します。
OperatorSDKはその名の通りでOperatorを作る際のSDKです。
Operatorを作る際の高次元なAPIを提供していたり、テスト、ビルド、パッケージングのサポートをしていたりします。
また、Operatorやカスタムリソースを作成する際のベースとなるテンプレートプロジェクトの作成などもしてくれるようです。
OperatorSDKでは以下の方法でOperatorの作成を行えるようです。
今回はGoでOperator(Memcached Operator)を作る方法を試してみます。
基本的にはここをたどって気になったところを深堀りする感じでやっていこうと思います。
使ってみる
環境
動作環境は以下の通り
$ uname -srvmpio Linux 5.4.0-77-generic #86-Ubuntu SMP Thu Jun 17 02:35:03 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a LSB Version: core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch Distributor ID: Ubuntu Description: Ubuntu 20.04.2 LTS Release: 20.04 Codename: focal $ go version go version go1.16 linux/amd64 # インストールに必要っぽい $ gpg --version gpg (GnuPG) 2.2.19 libgcrypt 1.8.5 Copyright (C) 2019 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. (省略) $ minikube version minikube version: v1.21.0 commit: 76d74191d82c47883dc7e1319ef7cebd3e00ee11 $ kubectl version -o yaml clientVersion: buildDate: "2021-03-18T01:10:43Z" compiler: gc gitCommit: 6b1d87acf3c8253c123756b9e61dac642678305f gitTreeState: clean gitVersion: v1.20.5 goVersion: go1.15.8 major: "1" minor: "20" platform: linux/amd64 serverVersion: buildDate: "2021-01-13T13:20:00Z" compiler: gc gitCommit: faecb196815e248d3ecfb03c680a4507229c2a56 gitTreeState: clean gitVersion: v1.20.2 goVersion: go1.15.5 major: "1" minor: "20" platform: linux/amd64
OperatorSDKのインストール
インストトールは以下の3つの方法があります。
今回はInstall from GitHub releaseでインストールしてみます。
インストールにはcurlとgpgのversion 2.0以上が必要みたいです。
バイナリをダウンロードします。
$ echo $ARCH amd64 $ export OS=$(uname | awk '{print tolower($0)}') $ echo $OS linux $ curl -LO https://github.com/operator-framework/operator-sdk/releases/download/v1.9.0/operator-sdk_${OS}_${ARCH}
次にバイナリの検証を行います。
$ gpg --keyserver keyserver.ubuntu.com --recv-keys 052996E2A20B5C7E $ curl -LO https://github.com/operator-framework/operator-sdk/releases/download/v1.9.0/checksums.txt $ curl -LO https://github.com/operator-framework/operator-sdk/releases/download/v1.9.0/checksums.txt.asc $ gpg -u "Operator SDK (release) <cncf-operator-sdk@cncf.io>" --verify checksums.txt.asc gpg: 署名されたデータが'checksums.txt'にあると想定します gpg: 2021年06月18日 08時21分40秒 JSTに施された署名 gpg: RSA鍵8613DB87A5BA825EF3FD0EBE2A859D08BF9886DBを使用 gpg: "Operator SDK (release) <cncf-operator-sdk@cncf.io>"からの正しい署名 [不明の] gpg: *警告*: この鍵は信用できる署名で証明されていません! gpg: この署名が所有者のものかどうかの検証手段がありません。 主鍵フィンガープリント: 3B2F 1481 D146 2380 80B3 46BB 0529 96E2 A20B 5C7E 副鍵フィンガープリント: 8613 DB87 A5BA 825E F3FD 0EBE 2A85 9D08 BF98 86DB
公開鍵自体が信用できるかわからないという警告が出ていますね。 ただ、checksum.txtの検証自体はうまく言ってるみたいなのでここでは先に進もうと思います。
$ grep operator-sdk_${OS}_${ARCH} checksums.txt | sha256sum -c - operator-sdk_linux_amd64: OK
チェックサムもOKみたいです。
実行権限を付与して、Pathがとおっているところに配置します。
$ chmod +x operator-sdk_${OS}_${ARCH} && sudo mv operator-sdk_${OS}_${ARCH} /usr/local/bin/operator-sdk
これでインストールは完了です。
$ operator-sdk version operator-sdk version: "v1.9.0", commit: "205e0a0c2df0715d133fbe2741db382c9c75a341", kubernetes version: "1.20.2", go version: "go1.16.5", GOOS: "linux", GOARCH: "amd64"
プロジェクトを作成する
まずは、プロジェクトを作成します。
$ mkdir operatorsdk-sample $ cd operatorsdk-sample/ $ operator-sdk init --domain hirooka.dev --repo github.com/samuraiball/settings
operator-sdk init
コマンドではGo modulesベースのプロジェクトを作成します。
$GOPATH/src
以外のところでプロジェクトをInitする場合は--repo
フラグでリポジトリを指定する必要があるみたいです。
--domain
フラグではDockerレジストリもしくは、Docker Hubのnamespace(ユーザ名)を指定します。
今回この最初の部分で色々しくってレジストリでもユーザ名でもないものを指定してしまったのですが、Makefileの記述を変えればうまく行くので一旦ここでは先に進みます。
さておきコマンドを実行すると諸々の設定がすんだプロジェクトが出来上がります。
ディレクトリ構造は以下のような感じ。
$ tree . ├── Dockerfile ├── Makefile ├── PROJECT ├── config │ ├── default │ │ ├── kustomization.yaml │ │ ├── manager_auth_proxy_patch.yaml │ │ └── manager_config_patch.yaml │ ├── manager │ │ ├── controller_manager_config.yaml │ │ ├── kustomization.yaml │ │ └── manager.yaml │ ├── manifests │ │ └── kustomization.yaml │ ├── prometheus │ │ ├── kustomization.yaml │ │ └── monitor.yaml │ ├── rbac │ │ ├── auth_proxy_client_clusterrole.yaml │ │ ├── auth_proxy_role.yaml │ │ ├── auth_proxy_role_binding.yaml │ │ ├── auth_proxy_service.yaml │ │ ├── kustomization.yaml │ │ ├── leader_election_role.yaml │ │ ├── leader_election_role_binding.yaml │ │ ├── role_binding.yaml │ │ └── service_account.yaml │ └── scorecard │ ├── bases │ │ └── config.yaml │ ├── kustomization.yaml │ └── patches │ ├── basic.config.yaml │ └── olm.config.yaml ├── go.mod ├── go.sum ├── hack │ └── boilerplate.go.txt └── main.go
config/
ディレクトリには起動時の設定が諸々用意されて、Kustomizeのyamlファイルが入っています。
config/manager
:コントローラーをPodとして起動する設定config/rbac
:作成するコントローラーを操作する際のパーミッションの設定
その他にもCRDやWebhookの設定も入っているようです。
また、main.go
がOperatorのエントリーポイントとなるみたいです。
func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme } func main() { var metricsAddr string var enableLeaderElection bool var probeAddr string flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") opts := zap.Options{ Development: true, } opts.BindFlags(flag.CommandLine) flag.Parse() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, MetricsBindAddress: metricsAddr, Port: 9443, HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "6a59cba3.hirooka.dev", }) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") os.Exit(1) } if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up ready check") os.Exit(1) } setupLog.Info("starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) } }
ここではControllerのセットアップ、実行のトラッキング、キャッシュの制御、CRDのスキーマ登録等を行ってくれるManagerを初期化する処理が書かれています。
ManagerではControllerがResorceを監視するネームスペースを制限することができます。
mgr, err := ctrl.NewManager(cfg, manager.Options{Namespace: namespace})
デフォルトではネームスペースはOperatorが動いているネームスペースのものを監視するようになります。
もしすべてのネームスペースを監視するようにしたい場合はNamespace: ""
と空の文字列を入れる必要があるようです。
ここではなにも触らずに、デフォルトのままで先に進みます。
新しいCRDとControllerを追加する
足場となるコードを自動生成する
次に新しいCRDとControllerを追加します。
以下のコマンドを実行します。
$ operator-sdk create api --group cache --version v1alpha1 --kind Memcached --resource --controller
これで、groupがcache
、versionがv1alpha1
、KindがMemcached
の足場が完成します。
今回深くはおいませんが、groupなどのそれぞれの意味はこちらをご確認ください。
実行してGitでどんな感じの変更が入っているかをみてみます。
$ git status ブランチ master Your branch is up to date with 'origin/master'. コミット予定の変更点: (use "git restore --staged <file>..." to unstage) new file: ../../docker/Dockerfile new file: ../opentracing/deployment.yaml new file: ../opentracing/gateway.yaml new file: ../opentracing/service.yaml new file: ../opentracing/tracing.yaml new file: ../opentracing/virtual-service.yaml modified: PROJECT new file: api/v1alpha1/groupversion_info.go new file: api/v1alpha1/memcached_types.go new file: api/v1alpha1/zz_generated.deepcopy.go new file: config/crd/kustomization.yaml new file: config/crd/kustomizeconfig.yaml new file: config/crd/patches/cainjection_in_memcacheds.yaml new file: config/crd/patches/webhook_in_memcacheds.yaml new file: config/rbac/memcached_editor_role.yaml new file: config/rbac/memcached_viewer_role.yaml new file: config/samples/cache_v1alpha1_memcached.yaml new file: config/samples/kustomization.yaml new file: controllers/memcached_controller.go new file: controllers/suite_test.go modified: go.mod modified: main.go
新しいファイルがいくつかできているのとmain.goなどが書き換わってますね。
Custom Resourceを編集する
ここで、注目すべきはapi/v1alpha1/memcached_types.go
とcontrollers/memcached_controller.go
でこれがCustom ResourceとCustom Controllerのベースのコードとなります。
まずはmemcached_types.go
の方から修正していきます。
Memcached
とそれに関係する構造体が定義されているのがわかります。
type Memcached struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec MemcachedSpec `json:"spec,omitempty"` Status MemcachedStatus `json:"status,omitempty"` }
Memached
構造体が持つMemcachedSpec.Size
という、デプロイされるCustom Resourceの数を設定するフィールドと、MemcachedStatus.Nodes
ではCustom Resourceで作られるPodの名前を保存するフィールドを追加します。
// MemcachedSpec defines the desired state of Memcached type MemcachedSpec struct { //+kubebuilder:validation:Minimum=0 // デプロイされるMemcachedの数 Size int32 `json:"size"` } // MemcachedStatus defines the observed state of Memcached type MemcachedStatus struct { // MemcachedのPodの名前を保存する Nodes []string `json:"nodes"` }
次にMemached
構造体に+kubebuilder:subresource:status
マーカーを追加します。
Status Subresourceを追加することによりControllerが他のCustom Resourceオブジェクトに変更を加えること無くCustom Resourceのステータスを更新できるようになるみたいです。
//+kubebuilder:subresource:status type Memcached struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec MemcachedSpec `json:"spec,omitempty"` Status MemcachedStatus `json:"status,omitempty"` }
*_types.go
は必ず以下のコマンドを実行して、Resource Typeの自動生成されるコードを更新する必要があります。
$ make generate
Makefileで定義されたこのコマンドはcontroller-genを実行して、api/v1alpha1/zz_generated.deepcopy.go
をアップデートします。
また、 SpecやStatusフィースドにCRD validationマーカーが付いている場合、以下のコマンドでそのCRDのマニフェストを生成することができます。
$ make manifests
config/crd/bases/cache.example.com_memcacheds.yaml
に以下のようなCRDのマニフェストが生成されます。
apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.4.1 creationTimestamp: null name: memcacheds.cache.hirooka.dev spec: group: cache.hirooka.dev names: kind: Memcached listKind: MemcachedList plural: memcacheds singular: memcached scope: Namespaced versions: - name: v1alpha1 schema: openAPIV3Schema: description: Memcached is the Schema for the memcacheds API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: MemcachedSpec defines the desired state of Memcached properties: size: description: デプロイされるMemcachedの数 format: int32 minimum: 0 type: integer required: - size type: object status: description: MemcachedStatus defines the observed state of Memcached properties: nodes: description: MemcachedのPodの名前を保存する items: type: string type: array required: - nodes type: object type: object served: true storage: true subresources: status: {} status: acceptedNames: kind: "" plural: "" conditions: [] storedVersions: []
コントローラーの実装
コントローラーの実装はここのものを一旦そのまま使います。
まず、SetUpWithManager
メソッドではManager
がセットアップされ、ControllerがどのようにCustom Resourceを管理するかを設定します。
func (r *MemcachedReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&cachev1alpha1.Memcached{}). Owns(&appsv1.Deployment{}). Complete(r) }
NewControllerManagedBy
はコントローラーのビルダーを提供しており、様々なコントローラーの設定を行なうことができます。
例えば上記の例では以下のような設定を行ってます。
For(&cachev1alpha1.Memcached{}
- MecahedをプライマリーResourceとして指定しています。
Add/Update/Delete
のそれぞれのイベントが発火されたタイミングで調整ループの中でRequest
がMemcachedオブジェクトを操作するために送られます。
- MecahedをプライマリーResourceとして指定しています。
Owns(&appsv1.Deployment{})
Controllerを初期化する際の様々な設定が用意されています。詳細はこちらをご確認ください。
例えば以下のように設定すると、調整ループの最大の並行数を2に設定し特定条件のイベントを無視するようになります。
func (r *MemcachedReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&cachev1alpha1.Memcached{}). Owns(&appsv1.Deployment{}). // WithOptionとWithEventFilerを追加 WithOptions(controller.Options{MaxConcurrentReconciles: 2}). WithEventFilter(ignoreDeletionPredicate()). Complete(r) } func ignoreDeletionPredicate() predicate.Predicate { return predicate.Funcs{ UpdateFunc: func(e event.UpdateEvent) bool { // メタデータが変更されていない場合はUpdateを無視する return e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration() }, DeleteFunc: func(e event.DeleteEvent) bool { // オブジェクトがDeleteとなっている場合はfalseで評価される return !e.DeleteStateUnknown }, } }
WithEventFilter
にPredicateを渡すことでイベントをフィルターすることができるようです。
次に、 Reconcile
で調整ループの実装の部分をみていきます。
func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { memcached := &cachev1alpha1.Memcached{} err := r.Get(ctx, req.NamespacedName, memcached) // 省略 }
Reconcile関数はCustom Resourceを実際のシステムで理想な状態にする役割を持ちます。
イベントが発火されるたびにReconcile関数が呼ばれ、調整が行われます。
Reconcile関数はRequest
を引数として受け取り、Request
はNamespace/Name
の鍵を持っており、リクエストに対応するオブジェクトをLookUpするのに利用されます。
上記の例では、調整リクエストに対応するMemcached
をLookUpしてます。
また、Reconcile関数は以下の戻り値を返すことが可能です。
return ctrl.Result{}, err
:エラーreturn ctrl.Result{Requeue: true}, nil
:エラーなしreturn ctrl.Result{}, nil
:調整の停止return ctrl.Result{RequeueAfter: nextRun.Sub(r.Now())}, nil
:調整をX時間後に再度行なう
最後にContorollerはRBACのパーミンションを持つ必要があります。
以下のようにRBACマーカーを付与します。
//+kubebuilder:rbac:groups=cache.hirooka.dev,resources=memcacheds,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=cache.hirooka.dev,resources=memcacheds/status,verbs=get;update;patch //+kubebuilder:rbac:groups=cache.hirooka.dev,resources=memcacheds/finalizers,verbs=update //+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { //省略 }
以下のコマンドを叩くとconfig/rbac/role.yaml
に上記のマーカーからマニフェストを自動生成してくれます。
$ make manifests
OperatorのイメージをDocker HubにPushする
作成したOperatorのイメージをDockerレジストリにPushしておく必要があります。
Makefileの以下の部分を書き換えます。
-IMG ?= controller:latest +IMG ?= $(IMAGE_TAG_BASE):$(VERSION)
IMAGE_TAG_BASE
でInitのところで指定したDockerレジストリを取得できるみたいです。
しかし、前述の通り私はちょっとinitのところでしくってしまったのでこのブログでは以下のように書き換えます。
IMG ?= hirohiroyuya/sample-controller:latest
次のコマンドでイメージをビルドしてPushします。
$ make docker-build docker-push
クラスターにデプロイする
作ったCDRとControllerをクラスターにデプロイします。
デプロイにはいくつかの方法があるようですが今回はこの中で「Run as a Deployment inside the cluster」の方法を試してみます。
具体的には以下のコマンドを実行します。
$ make deploy
この際クラスターはカレントContextで指定されるものが利用されるみたいです(このブログではminikube
にしてます)。
コマンドの実行が成功するとデフォルトでは<project-name>-system
のネームスペースでOperatorが実行されています。
$ kubectl get deployment -n operatorsdk-sample-system NAME READY UP-TO-DATE AVAILABLE AGE operatorsdk-sample-controller-manager 1/1 1 1 16m
いい感じに動いてくれてるみたいですね。
Memcached Recourceを作成する
Operatorのデプロイまでできたので、いよいよMemcached Resourceをクラスタにデプロイしてみます。
config/samples/cache_v1alpha1_memcached.yaml
を以下のように書き換えます。’
apiVersion: cache.hirooka.dev/v1alpha1 kind: Memcached metadata: name: memcached-sample spec: size: 3
applyしてリソースが作成されていることを確認します。
$ kubectl apply -f config/samples/cache_v1alpha1_memcached.yaml memcached.cache.hirooka.dev/memcached-sample created $ kubectl get deployment NAME READY UP-TO-DATE AVAILABLE AGE memcached-sample 3/3 3 3 91s $ kubectl get pods NAME READY STATUS RESTARTS AGE memcached-sample-6c765df685-6mzjj 1/1 Running 0 2m12s memcached-sample-6c765df685-l9jpr 1/1 Running 0 2m12s memcached-sample-6c765df685-t2wkw 1/1 Running 0 2m12s
いい感じで動いてくれてそうですね!!
Isito、k8sでカナリアデプロイをする
はじめに
先月の5月18日にIstioが1.10のリリースがされたみたいです。
その中でStable Revision Labelsと言う機能を使ってカナリアップデートを行なう例が乗っていて、最初言葉尻を完全に読み間違えていてアプリのカナリアデプロイが容易になると思ったのですがそうでは無く、リビジョン付きの複数のコントロールプレーンをデプロイするサポートが入ったって感じっぽいですね。
まぁ、1.10は置いておいても、k8sやIstioを使ったカナリアリデプロイってやったことなかったので、ちょっと試してみようかと思います。
基本的にはここに乗っているのに基づいてやろうと思います。
かなり古いブログですが、これ以上に新しいものをみつけることができなかったので、もしもっと良いやり方がありそうであればコメントなどで教えてくれると嬉しいです。
やっていく
環境
今回の動作環境は以下のようになってます。
$ uname -srvmpio Linux 5.4.0-74-generic #83-Ubuntu SMP Sat May 8 02:35:39 UTC 2021 x86_64 x86_64 x86_64 GNU/Linu $ lsb_release -a LSB Version: core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch Distributor ID: Ubuntu Description: Ubuntu 20.04.2 LTS Release: 20.04 Codename: focal $ minikube version minikube version: v1.21.0 commit: 76d74191d82c47883dc7e1319ef7cebd3e00ee11 $ kubectl version -o yaml clientVersion: buildDate: "2021-03-18T01:10:43Z" compiler: gc gitCommit: 6b1d87acf3c8253c123756b9e61dac642678305f gitTreeState: clean gitVersion: v1.20.5 goVersion: go1.15.8 major: "1" minor: "20" platform: linux/amd64 serverVersion: buildDate: "2021-01-13T13:20:00Z" compiler: gc gitCommit: faecb196815e248d3ecfb03c680a4507229c2a56 gitTreeState: clean gitVersion: v1.20.2 goVersion: go1.15.5 major: "1" minor: "20" platform: linux/amd64 $ istioctl version client version: 1.10.1 control plane version: 1.10.1 data plane version: 1.10.1 (1 proxies)
また、このブログでは特に明記しない場合k8sのコンテキストはminikube
を使っています。
下準備
下準備として特定の文字列を返すnginxのコンテナを作成しておきます。
まずは、以下のコマンドを実行して、Dockerのコンテキストをminikubeのものに変えておきます。
$ eval $(minikube docker-env)
これでDockerクライアントはMinikubeのコンテキストで動くようになりました。
次に以下の簡単なDokcerfileを用意します。
Dockerfile-v1
FROM nginx:1.19.6 RUN echo "Hello, Canary v1" > /usr/share/nginx/html/index.html
Dockerfile-v2
FROM nginx:1.19.6 RUN echo "Hello, Canary v2" > /usr/share/nginx/html/index.html
それぞれをビルドしておきます。
$ docker build ./ -t hello-nginx-v2 -f Dockerfile-v2 $ docker build ./ -t hello-nginx-v1 -f Dockerfile-v1 $ docker images | grep hello-nginx hello-nginx-v2 latest 9defac4edce6 2 minutes ago 133MB hello-nginx-v1 latest abd78992aeba 8 minutes ago 133MB
最後にあとで使うのでMinikubeのIPを調べておきます。
$ minikube ip 192.168.49.2
これで下準備は完了です。
どんなことを行なうか
今回は、以下のパターンをやってみようかと思います。
・Kubernetesのみでのカナリアデプロイをやってみる
・Istioを使ったカナリアリデプロイをやってみる
k8sを使ったカナリアデプロイ
k8s単体でもカナリアデプロイを行なうこと自体は可能です。 それを行なうには以下の2つの方法取るようです。
selectorを用いたカナリアデプロイ
selectorを用いてカナリアデプロイをやってみようと思います。
こいつの考え方としては、古いバージョンと新しいバージョンのPod数を変更することで、新しいVersionのPodへルーティングされる割合を調整します。
実際にやってみます。
まずは、2つのDeploymentを用意します。
$ kubectl create deployment hello-nginx --image=hello-nginx-v1 --dry-run=client -o yaml > deployment.yaml $ echo --- >> deployment.yaml $ kubectl create deployment hello-nginx --image=hello-nginx-v2 --dry-run=client -o yaml >> deployment.yam
できたデプロイメントが以下のようになります。
apiVersion: apps/v1 kind: Deployment metadata: creationTimestamp: null labels: app: hello-nginx name: hello-nginx spec: replicas: 1 selector: matchLabels: app: hello-nginx strategy: {} template: metadata: creationTimestamp: null labels: app: hello-nginx spec: containers: - image: hello-nginx-v1 name: hello-nginx-v1 resources: {} status: {} --- apiVersion: apps/v1 kind: Deployment metadata: creationTimestamp: null labels: app: hello-nginx name: hello-nginx spec: replicas: 1 selector: matchLabels: app: hello-nginx strategy: {} template: metadata: creationTimestamp: null labels: app: hello-nginx spec: containers: - image: hello-nginx-v2 name: hello-nginx-v2 resources: {} status: {}
それぞれを見分けるためname
をnginx-hello-v1
とnginx-hello-v2
のに変え環境を示すラベル(track)追加します。
また、nginx-hello-v1
の方はレプリカ数も変えておきます。
apiVersion: apps/v1 kind: Deployment metadata: creationTimestamp: null labels: app: hello-nginx track: stable # 環境を示すラベルを追加 name: hello-nginx-v1 # nameにVersionを追加 spec: replicas: 3 # レプリカ数を3に selector: matchLabels: app: hello-nginx strategy: {} template: metadata: creationTimestamp: null labels: app: hello-nginx spec: containers: - image: hello-nginx-v1 name: hello-nginx-v1 resources: {} status: {} --- apiVersion: apps/v1 kind: Deployment metadata: creationTimestamp: null labels: app: hello-nginx track: canary # 環境を示すラベルを追加 name: hello-nginx-v2 # nameにVersionを追加 spec: replicas: 1 selector: matchLabels: app: hello-nginx strategy: {} template: metadata: creationTimestamp: null labels: app: hello-nginx spec: containers: - image: hello-nginx-v2 name: hello-nginx-v2 resources: {} status: {}
deploymentをapplyします。
$ kubectl apply -f deployment.yaml deployment.apps/hello-nginx-v1 created deployment.apps/hello-nginx-v2 created $ kubectl get po NAME READY STATUS RESTARTS AGE hello-nginx-v1-7b8665d769-grsz5 1/1 Running 0 7s hello-nginx-v1-7b8665d769-rdwg5 1/1 Running 0 7s hello-nginx-v1-7b8665d769-zvlsg 1/1 Running 0 7s hello-nginx-v2-86467b54f9-ghdnm 1/1 Running 0 7s
そして、これらのデプロイメントに対してapp: hello-nginx
のラベルに対してルーティングを行なうServiceを記述してやります。
$ kubectl create service nodeport hello-nginx --tcp=8081:80 --dry-run=client -o yaml > service.yaml
できたServiceが以下の通りになります。
apiVersion: v1 kind: Service metadata: creationTimestamp: null labels: app: hello-nginx name: hello-nginx spec: ports: - name: 8081-80 port: 8081 protocol: TCP targetPort: 80 selector: app: hello-nginx type: NodePort status: loadBalancer: {}
applyしてNodePortのポートをチェックしておきます。
$ kubectl apply -f service.yaml $ kubectl get svc/hello-nginx NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE hello-nginx NodePort 10.100.167.221 <none> 8081:32697/TCP 68m
これでおおよそ1/4程度のリクエストがv2にルーティングされるようになっているはずです。
ロードテストツールであるk6で以下のスクリプトを書いてチェックしてみます。
$ cat 100requests.js import http from 'k6/http'; import { sleep, check } from 'k6'; export let options = { vus: 10, duration: '10s', }; export default function () { let res = http.get('http://192.168.49.2:32697'); sleep(1); check(res, { 'v1 responded': (r) => r.body == 'Hello, Canary v1\n', 'v2 responded': (r) => r.body == 'Hello, Canary v2\n', }); }
ここでk6の使い方を詳細には説明しませんが、10並列で10秒ごとに計100回のリクエストを送り、そのレスポンスボディをチェックするスクリプトになっています。
(k6に関しては過去にブログを書いているので、よろしければそちらもみてみてください)
$ docker run --network=host -i loadimpact/k6 run - < 100requests.js /\ |‾‾| /‾‾/ /‾‾/ /\ / \ | |/ / / / / \/ \ | ( / ‾‾\ / \ | |\ \ | (‾) | / __________ \ |__| \__\ \_____/ .io execution: local script: - output: - scenarios: (100.00%) 1 scenario, 10 max VUs, 40s max duration (incl. graceful stop): * default: 10 looping VUs for 10s (gracefulStop: 30s) (省略) ✗ v1 responded ↳ 80% — ✓ 80 / ✗ 20 ✗ v2 responded ↳ 20% — ✓ 20 / ✗ 80 (省略)
ここでは大体20%ぐらいのリクエストがV2に振られたみたいですね。
もう一度テストを実行すると70%、30%の割合になったので、おおよそ25%の割合で新しいVersionのPodにリクエストが割り振られているのがわかります。
Podが少ない状態である程度新しいバージョンの動作に確証が取れてきたらscaleコマンドでPodの数を調整します。
$ kubectl scale --replicas=2 deployment/hello-nginx-v2 deployment.apps/hello-nginx-v2 scaled $ kubectl scale --replicas=2 deployment/hello-nginx-v1 deployment.apps/hello-nginx-v1 scaled $ kubectl get po NAME READY STATUS RESTARTS AGE hello-nginx-v1-7b8665d769-khzfp 1/1 Running 0 67m hello-nginx-v1-7b8665d769-wjfxj 1/1 Running 0 67m hello-nginx-v2-86467b54f9-ghdnm 1/1 Running 0 84m hello-nginx-v2-86467b54f9-nsr99 1/1 Running 0 37s $ docker run --network=host -i loadimpact/k6 run - < 100requests.js (省略) ✗ v1 responded ↳ 50% — ✓ 50 / ✗ 50 ✗ v2 responded ↳ 50% — ✓ 50 / ✗ 50 (省略)
最終的にv1のスケールを0にしてv2を必要な数までスケールさせるとデプロイ完了です。
ロールバックしたい場合は、v2のスケールを0にして、v1のスケールを元の数まで戻せば良いです。
rolloutを用いたカナリアデプロイ
今度はk8sのrollout/pauseコマンドを用いてカナリアデプロイを行ってみようと思います。
考え方としては、selectorと似ており、Podの数を調整することで新しいVersionにルーティングされる割合を少しずつ増やしていき、カナリアデプロイを実現します。
具体的には、imageのアップデートが行われた際に実行されるrollout
を途中でpauseすることでより、比較的少ない新しいVersionのPodを起動して、少しずつデプロイして行きます。
selectorの例で用いていたDeploymentとServiceを削除して次のDeploymentを使用します。
deployment-rp.yaml
$ cat deployment-rp.yaml apiVersion: apps/v1 kind: Deployment metadata: creationTimestamp: null labels: app: hello-nginx name: hello-nginx spec: replicas: 4 selector: matchLabels: app: hello-nginx strategy: {} template: metadata: creationTimestamp: null labels: app: hello-nginx spec: containers: - image: hello-nginx-v1 name: hello-nginx-v1 resources: {} imagePullPolicy: IfNotPresent status: {}
applyしてPodが起動するのを確認します。
$ kubectl apply -f deployment-rp.yaml deployment.apps/hello-nginx create $ kubectl get po NAME READY STATUS RESTARTS AGE hello-nginx-59849bb7d7-cgkhw 1/1 Running 0 40s hello-nginx-59849bb7d7-lz8bf 1/1 Running 0 40s hello-nginx-59849bb7d7-mgrrb 1/1 Running 0 40s hello-nginx-59849bb7d7-vl7lg 1/1 Running 0 40s
次にイメージをアップデートして、すぐにpauseコマンドを実行します。
$ kubectl rollout pause deployment/hello-nginx $ kubectl set image deployment/hello-nginx hello-nginx=hello-nginx-v2 deployment.apps/hello-nginx image updated $ kubectl rollout pause deployment/hello-nginx deployment.apps/hello-nginx paused $ kubectl get po NAME READY STATUS RESTARTS AGE hello-nginx-59849bb7d7-cgkhw 1/1 Running 0 2m46s hello-nginx-59849bb7d7-lz8bf 1/1 Running 0 2m46s hello-nginx-59849bb7d7-mgrrb 1/1 Running 0 2m46s hello-nginx-5d8dc5f978-4nx4g 1/1 Running 0 11s hello-nginx-5d8dc5f978-d5p8s 1/1 Running 0 11s
今回の場合新しいPodが2つ起動されていますね。
この新しいPodはイメージが新しいものが使われているのがわかります。
$ kubectl describe po hello-nginx-5d8dc5f978-4nx4g Name: hello-nginx-5d8dc5f978-4nx4g Namespace: default Priority: 0 Node: minikube/192.168.49.2 Start Time: Sun, 20 Jun 2021 15:11:33 +0900 Labels: app=hello-nginx pod-template-hash=5d8dc5f978 Annotations: <none> Status: Running IP: 172.17.0.8 IPs: IP: 172.17.0.8 Controlled By: ReplicaSet/hello-nginx-5d8dc5f978 Containers: hello-nginx: Container ID: docker://0988812d8fa7b3ccd1eb91c5fe48bbfed265ec0964407b9064e9f3dbb7801bc3 Image: hello-nginx-v2 Image ID: docker://sha256:9defac4edce66768e5023868544942142630a22e35b0770a222b973083fde7da Port: <none> Host Port: <none> State: Running Started: Sun, 20 Jun 2021 15:11:34 +0900 Ready: True Restart Count: 0 Environment: <none> Mounts: /var/run/secrets/kubernetes.io/serviceaccount from default-token-2zfxv (ro) Conditions: Type Status Initialized True Ready True ContainersReady True PodScheduled True Volumes: default-token-2zfxv: Type: Secret (a volume populated by a Secret) SecretName: default-token-2zfxv Optional: false QoS Class: BestEffort Node-Selectors: <none> Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s node.kubernetes.io/unreachable:NoExecute op=Exists for 300s Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 100s default-scheduler Successfully assigned default/hello-nginx-5d8dc5f978-4nx4g to minikube Normal Pulled 100s kubelet Container image "hello-nginx-v2" already present on machine Normal Created 100s kubelet Created container hello-nginx Normal Started 100s kubelet Started container hello-nginx
もし、リリース中に問題があった場合は、rollbackコマンドを実行し、リビジョンを1つ前のものに戻します。
$ kubectl rollout resume deployment/hello-nginx deployment.apps/hello-nginx resumed $ kubectl rollout undo deployment/hello-nginx deployment.apps/hello-nginx rolled back
Istioを使ったカナリアデプロイ
k8sを使ったカナリアデプロイでは限定的で以下のようなカナリアリデプロイを行なう際には課題があります。
Istioを用いることで、上記のような、より複雑な条件でのデプロイを簡単に行なうことができるようになります。
一度、rolloutで使ったdeploymentを削除して、
deployment-istio.yaml
apiVersion: apps/v1 kind: Deployment metadata: creationTimestamp: null labels: app: hello-nginx name: hello-nginx-v1 spec: replicas: 3 selector: matchLabels: app: hello-nginx strategy: {} template: metadata: creationTimestamp: null labels: app: hello-nginx version: v1 spec: containers: - image: hello-nginx-v1 name: hello-nginx-v1 resources: {} imagePullPolicy: IfNotPresent status: {} --- apiVersion: apps/v1 kind: Deployment metadata: creationTimestamp: null labels: app: hello-nginx name: hello-nginx-v2 spec: replicas: 3 selector: matchLabels: app: hello-nginx strategy: {} template: metadata: creationTimestamp: null labels: app: hello-nginx version: v2 spec: containers: - image: hello-nginx-v2 name: hello-nginx-v2 resources: {} imagePullPolicy: IfNotPresent status: {}
selectorのところで用いたdeploymentと少し似ていますが、Podに対して、version
ラベルを追加しているのとPodの数を3つずつ、2つのDeploymentで計6個のPodを起動しています。
$ kubectl apply -f deployment-istio.yaml $ kubectl get po NAME READY STATUS RESTARTS AGE hello-nginx-v1-7b8665d769-bzgzg 1/1 Running 0 99s hello-nginx-v1-7b8665d769-j569c 1/1 Running 0 99s hello-nginx-v1-7b8665d769-wbthk 1/1 Running 0 99s hello-nginx-v2-86467b54f9-d9txr 1/1 Running 0 8s hello-nginx-v2-86467b54f9-hdsx9 1/1 Running 0 99s hello-nginx-v2-86467b54f9-m8gmk 1/1 Running 0 8s
次に外部からアクセスを可能にするため、Gatewayリソースを作成します。
gateway.yaml
apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: hello-nginx spec: selector: istio: ingressgateway servers: - port: number: 80 name: http protocol: HTTP hosts: - "*"
そして、きもとなるVertualServiceとDestinationRouleを作成します。
destination-rule.yaml
apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: hello-nginx spec: host: hello-nginx subsets: - name: v1 labels: version: v1 - name: v2 labels: version: v2
DestinationRouleでPodに付与したversion
のラベルを指定することで、ルーティング先をsubsetとして定義しています。
virtual-service.yaml
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: hello-nginx spec: hosts: - "*" gateways: - hello-nginx http: - route: - destination: host: hello-nginx subset: v1 weight: 90 - destination: host: hello-nginx subset: v2 weight: 10
ここでは、Gatewayに対するどんなHostのリクエストに対してもsubsetのv1
とv2
に対してルーティングを行なうように設定しています。
また、weight
を設定し、リクエストの10%がv2へルーティングされるようにしています。
これで準備は完了です、k6からGateway経由でリクエストを送るようにするためにGatewayのport番号を取得してk6のスクリプトを修正します。
$ kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].nodePort}' 31424
取得したPortでリクエストを送るようにk6のスクリプトを修正したら、テストを実行します。
$ docker run --network=host -i loadimpact/k6 run - < 100requests.js (省略) ✗ v1 responded ↳ 90% — ✓ 90 / ✗ 10 ✗ v2 responded ↳ 10% — ✓ 10 / ✗ 90 (省略)
先ほどと違い、Podの数とは関係なくルーティングが行われていることが確認できました。
より小さなリクエストの数で、新しいVersionの動作確認が取れたらweight
を修正して新しいVersionにのみリクエストが送られていくように変更していきます。
今回は、Podの数は固定で行いましたが、Podの数で割合を調整する必要がなくなったので、Deploymentのオートスケールの機能と合わせて利用することも可能です。
http4sのHTTP Serverを使ってみる
はじめに
FS2に依存するライブラリはいくつかありますが、http4sというHTTPサーバとクライアントを用意してくれているライブラリがあり、ちょっと興味が湧いたので触ってみようかと思います。
http4sはFS2(と、もちろんCats Effects)をベースにしているので、FunctinnalでStreamingでFunctionalというのが特徴みたいです。
やってみる
環境
$ scala --version Scala code runner version 2.13.6 -- Copyright 2002-2021, LAMP/EPFL and Lightbend, Inc. $ sbt --version [info] 1.2.7 sbt script version: 1.5.2
プロジェクトの作成
まずはベースとなるプロジェクトを作成します。
$ sbt new sbt/scala-seed.g8 [info] welcome to sbt 1.5.2 (Oracle Corporation Java 1.8.0_292) [info] loading global plugins from /home/yuya-hirooka/.sbt/1.0/plugins [info] set current project to new (in build file:/tmp/sbt_adb8af4/new/) A minimal Scala project. name [Scala Seed Project]: http4s-example
作成されたプロジェクトのbuild.sbt
に以下の依存を追加します。
libraryDependencies += "org.http4s" %% "http4s-dsl" % "0.21.23", libraryDependencies += "org.http4s" %% "http4s-blaze-server" % "0.21.23", libraryDependencies += "org.http4s" %% "http4s-blaze-client" % "0.21.23",
今回はhttp4sの現在のstableバージョンである0.21.x
系を利用しようと思います。
名前を受け取って挨拶を返すサーバを作成する
最初にhttp4s-blaze-server
を使って、サーバーの方を作成します。
最終的には名前をJsonで受け取って挨拶を返すサーバを作成しようと思います。
helloの文字列を返す
まずは、単純なGETリクエストに対して"hello"という文字列を返すだけのサーバを作ります。
そのためにはRouter
とBlazeServerBuilder
を使います。
Router
を使ってルートの定義を作成します。
import cats.effect._ import org.http4s._ import org.http4s.dsl.io._ import org.http4s.implicits.http4sKleisliResponseSyntaxOptionT import org.http4s.server.Router object SampleRouters { def helloHandler() = IO("hello") val helloRouter = HttpRoutes.of[IO] { case GET -> Root / "hello" => helloHandler().flatMap(Ok(_)) } def createApp = Router("/" -> helloRouter).orNotFound }
HttpRoutes
と http4s-dsl,を使ってルータを定義します。
みてすぐわかるかも知れませんが、パターンマッチングでGETと/hello
のパスにマッチした場合helloHandr()
を呼び出し、その戻り値であるIO("")
をflatMapして取り出しOk()
レスポンスで返すRouterを定義しています。
1番最後の行ではRouter objectを作成策定していますが、ここでは定義したルータのルートに当たるパスを指定することができます。
Routeの作成はできたので、次にアプリケーションのエントリーポイントとなるobjectを作ります。
import cats.effect.{ExitCode, IO, IOApp} import org.http4s.server.blaze.BlazeServerBuilder import scala.concurrent.ExecutionContext.Implicits.global object AppStarter extends IOApp { override def run(args: List[String]): IO[ExitCode] = BlazeServerBuilder[IO](global) .bindHttp(8081, "localhost") .withHttpApp(SampleRouters.createApp) .serve .compile .drain .as(ExitCode.Success) }
http4sは様々なバックエンドをサポートしているようですが(サポートはここを確認してください。
BlazeServerBuilder
を使って、
もし、IOApp以外の場所でBuilderを使いたい場合は以下のimplicit
を定義する必要があります。
implicit val cs: ContextShift[IO] = IO.contextShift(global) implicit val timer: Timer[IO] = IO.timer(global)
アプリケーションを実行するし、cURLでアプリケーションにリクエストを送ってみます。
$ curl localhost:8081/greeting -v * Trying 127.0.0.1:8081... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8081 (#0) > GET /greeting HTTP/1.1 > Host: localhost:8081 > User-Agent: curl/7.68.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Content-Type: text/plain; charset=UTF-8 < Date: Fri, 11 Jun 2021 14:20:49 GMT < Content-Length: 5 < * Connection #0 to host localhost left intact hello
うまく起動できてるみたいですね。
パスパラーメータで値を受け取る
それでは次に、パスパラメータ名前を受け取ってその名前に対して挨拶を返す用にしてみます。
新しく、Routerを作成します。
object SampleRouters { def helloHandler() = IO("hello") val helloRouter = HttpRoutes.of[IO] { case GET -> Root / "hello" => helloHandler().flatMap(Ok(_)) } def greetingSomeone(name: String) = IO(s"Hello, $name") val greetingRouter = HttpRoutes.of[IO] { case GET -> Root / "greeting" / name => helloHandler().flatMap(Ok(_)) } def createApp = Router("/" -> helloRouter, "/v1" -> greetingRouter).orNotFound }
ここではgreetingRouter
を新たに定義してます。
DSLのなかでパスの位置い変数を置くことで、パスパラメータを束縛することができます。
また、上記のようにRouter object作成時には複数のRouterを登録することができます。
$ curl localhost:8081/v1/greeting/Moheji -v * Trying 127.0.0.1:8081... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8081 (#0) > GET /v1/greeting/Moheji HTTP/1.1 > Host: localhost:8081 > User-Agent: curl/7.68.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Content-Type: text/plain; charset=UTF-8 < Date: Fri, 11 Jun 2021 14:46:01 GMT < Content-Length: 13 < * Connection #0 to host localhost left intact Hello, Moheji
Jsonで値を受け取る。Jsonの値を返す
それでは最後にJsonの値を扱うようなPathを追加してみようと思います。
まずですが、http4sでJsonを扱うためには以下の依存を追加する必要があります。
libraryDependencies += "org.http4s" %% "http4s-circe" % "0.21.23" libraryDependencies += "io.circe" %% "circe-generic" % "0.13.0"
circe-generic
の依存はオプショナルっぽいですが、JsonとScalaのクラスとの変換を知れてくれるみたいなので追加しておきます。
それでは追加したhttp4s-circe
とcirce-generic
を使ってgreetingRouter
に新たなRouteを書き加えてみます。
import io.circe.generic.auto._ import io.circe.syntax.EncoderOps import org.http4s._ import org.http4s.circe.CirceEntityCodec._ //他のImportは省略 object SampleRouters { // 他の実証は諸略 case class Name(name: String) case class Greeting(hello: String) val greetingRouter = HttpRoutes.of[IO] { case req@POST -> Root / "greeting" => for { n <- req.as[Name] resp <- Created(Greeting(n.name).asJson) } yield resp case GET -> Root / "greeting" / name => greetingSomeone(name).flatMap(Ok(_)) } def createApp = Router("/" -> helloRouter, "/v1" -> greetingRouter).orNotFound }
POSTリクエストで名前を受け取るためにDSLを使います。
req@
のように記述刷ることでreq
にリクエストの情報をバインドできるみたいです。
リクエストを受け取るためのcase
// リクエスト用のcase class case class Name(name: String) // レスポンス用のcase class case class Greeting(hello: String)
本来はこれらに対してエンコーダーとデコーダーを作ってimplicitとして定義刷る必要があるようですが。
以下の2つをインポートしていればそれぞれが自動でimplicit
されるみたいです。
import io.circe.generic.auto._ import org.http4s.circe.CirceEntityCodec._
それではリクエストを送ってみましょう。
$ curl localhost:8081/v1/greeting -d '{"name":"Moheji"}' -v * Trying 127.0.0.1:8081... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8081 (#0) > POST /v1/greeting HTTP/1.1 > Host: localhost:8081 > User-Agent: curl/7.68.0 > Accept: */* > Content-Length: 17 > Content-Type: application/x-www-form-urlencoded > * upload completely sent off: 17 out of 17 bytes * Mark bundle as not supporting multiuse < HTTP/1.1 201 Created < Content-Type: application/json < Date: Fri, 11 Jun 2021 15:51:56 GMT < Content-Length: 18 < * Connection #0 to host localhost left intact {"hello":"Moheji"}
これでちょっと触った程度ですが一応、サーバができましたね。