gockでHTTPリクエストをMockする
はじめに
最近はGo言語でWebアプリを書く際に使う諸々のライブラリを試してみているのですが、今はMockサーバをいろいろ見ていました。
Goの場合は標準ライブラリでもhttptestでいろいろ用意されていているみたいです。ただちょっと、そのまま使うには手間が多そうに感じ、ほかを探してるとgockというのが良さげだったのでちょっと試してみようと思います。
gockとは
Go製のHTTPのMockライブラリーです。
以下のような特徴があります。
gockはhttp.Client
で利用されるhttp.DefaultTransport
かもしくはカスタムhttp.Transport
を経由して、HTTPのアウトバウンドのリクエストをモックします。
登録されたMockがFIFOでリクエストにマッチするかの検証が行われ、マッチした場合MockのHTTPレスポンスを返します。
そして、どのMockにもマッチしなかった場合は基本的にはエラーが起こるようです。ただし、リアルネットワークモードが有効化されている場合は実際のリクエストが代わりに実行されます。
このブログではあまり複雑なことはせずにひとまず動かしてみるところまでやってみようかと思います。
使ってみる
環境
プログラムを動かす環境は以下の通り
$ go version go version go1.16 linux/amd64 $ uname -srvmpio Linux 5.4.0-65-generic #73-Ubuntu SMP Mon Jan 18 17:25:17 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
プロジェクトを作る
プロジェクトを作成して、TestifyとGockをインストトールします。
$ go get github.com/stretchr/testify go get: added github.com/stretchr/testify v1.7.0 $ go get -u gopkg.in/h2non/gock.v1 go get: added gopkg.in/h2non/gock.v1 v1.0.16
これで下準備までは環境です。
Pongの文字列を返すMockを作る
準備ができたので、早速使っていこうと思います。
localhost:8081/ping
に対して”pong”
の文字列のレスポンス返す場合は以下のようにします。
import ( "github.com/stretchr/testify/assert" "gopkg.in/h2non/gock.v1" "io" "log" "net/http" "testing" ) const ( MOCK_URL = "localhost:8082" PING_PATH = "/ping" ) func TestName(t *testing.T) { defer gock.Off() gock.New(MOCK_URL). Get(PING_PATH). Reply(200). BodyString("pong") res, err := http.Get("http://" + MOCK_URL + PING_PATH) handleError(err) bodyByte, err := io.ReadAll(res.Body) handleError(err) assert.Equal(t, 200, res.StatusCode) assert.Equal(t, "pong", string(bodyByte)) assert.Equal(t, gock.IsDone(), true) } func handleError(err error) { if err != nil { log.Fatal(err) } }
gock
でMockをたてる場合はgock.New(MOCK_URL)
のように宣言的にMockを定義することができます。また、リクエストのヘッダーやボディ、リクエストパラムでマッピングを作成したい場合はReply(200).
より前のリクエストのパターンを構成するビルダーのメソットチェインの中で、それぞれMatchHeader
、MatchBody
、JSON
(Jsonのリクエストボディを受け取る場合)、MatchHeader
などの関数を呼び出すことで行えます。
例えば、MatchHeader("x-api-version", "1.[0-9]")
のように記述することができ、この記述の場合、ヘッダーにキーがx-api-version
で値が1.(0から9までの数字)
を含むリクエストに対してマッチングします。
レスポンスを作成する場合はReply()
関数を呼び出し、レスポンスボディは今回は文字列を返すのでBodyString
を呼び出しています。JSONの値を返したい場合はJSONメソッドを呼び出して、JSON(map[string]string{"foo": "bar"})
のように記述します。
アサーション部分では、最後のところがポイントでgock.IsDone()
を呼び出すことですべての設定したモックが呼び出されているかの検証を行なうことができます。
GoのDIライブラリWireを試す
はじめに
GoのWebアプリは公私でなんとが作成したことがあったのですが、DIのライブラリをあまり使ったことが無かったなとふと思い。探してみたらGoogle製のアプリ、Wireがなんとなく目立ってたような気がしたので、Hello, worldとしてチュートリアルをやりつつ理解を深めようかと思います。
やってみる
環境
$ go version go version go1.16 linux/amd64 $ uname -srvmpio Linux 5.4.0-65-generic #73-Ubuntu SMP Mon Jan 18 17:25:17 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
Wireの概要
DIをする際の課題の1つが初期化処理が煩雑になりがちというとこがあります。
Wireは構造体、初期化関数、それらの依存関係を定義する関数を作成してやり、wire
コマンドを実行すると、一連の初期化処理を自動生成してくれます。
基本的な流れ
Wireを使う際の基本的な流れは以下のようになります。
- DI対象の構造体を作成
- 構造体の初期化関数(プロバイダー)を作る
- 初期化関数の依存関係を定義するインジェクターを作成
wire
コマンドを使って初期化関数を作成
実際にDIしてみる
DI対象の構造体を作成
まずはDI対象の構造体を3つほどと1つのインターフェース作っておきます。
今回はケーキショップをモデルとした構造体をいくつか作っておきます。
(ちょっとモデリングがいびつかもしれませんが、適当に作っただけなのでご容赦ください)
栗を表す構造体
type Chestnut struct { }
ケーキインターフェースとモンブランを表す構造体
type Cake interface { Eaten() } type MontBlanc struct { Chestnut Chestnut } func (m MontBlanc) Eaten() { fmt.Printf("I'm MontBlanc, How is it?") }
ケーキショップを表す構造体
type CakeShop struct { Cake Cake } func (c CakeShop) Sell() { c.Cake.Eaten() }
構造としては、モンブランは栗(Chesnut)に依存し、ケーキショップはCakeインターフェースを通してモンブランなどのEaten()
関数を呼び出します。
構造体の初期化関数(プロバイダー)を作る
以下のように、作った構造体の初期化関数であるプロバイダーを作成しておきます。
func ChestnutProvider() Chestnut { return Chestnut{} } func MontBlancProvider(c Chestnut) MontBlanc { return MontBlanc{Chestnut: c} } func CakeShopWithMontBlancProvider(c MontBlanc) CakeShop { return CakeShop{Cake: c} }
それぞれ、書いてあるとおりではChestnutProvider()
はChestnut
を作成してMontBlancProvider(c Chestnut)
は受け取った栗を使ってMontBlanc
を作成します。そして、最後のCakeShopWithMontBlancProvider(c MontBlanc)
はCakeインターフェースを実装しているMontBlancを受け取ってCakeShopを作成するプロバイダーになってます。
初期化関数の依存関係を定義するインジェクターを作成
実装としては最後であるインジェクターの作成を行います。
ここでは、プロバイダーの依存関係を定義することができます。
// +build wireinject package config import ( "github.com/google/wire" "github.com/samuraiball/go-sandbox/message" "github.com/samuraiball/go-sandbox/sweets" ) func InitializeCakeShop() sweets.CakeShop { wire.Build( sweets.CakeShopWithMontBlancProvider, sweets.MontBlancProvider, sweets.ChestnutProvider, ) return sweets.CakeShop{} }
Wireでは、wire.Build()
の中でそれぞれの初期化に使うプロバイダーを記述していきます。そして、それぞれのプロバイダーで初期化される構造体が、別のプロバイダーの引数として利用されることになります(これは後ほどもう少し細かく説明します)。
ここで、コンパイルエラーを避けるためにインジェクターの戻り値では空のCakeShop{}
を返しています。このCakeShop{}
は実際は使われることはなく単純に無視されます。なので、ここで値を初期化して入れていたとしても意味がありません(// +build wireinject
を先頭に記述することで、ビルドの対象外にしています)。
wire
コマンドを使って初期化関数を作成
wire
コマンドをインジェクターが定義されているディレクトリーで実行します。するとwire_gen.go
というコードが自動生成されます。
// Code generated by Wire. DO NOT EDIT. //go:generate go run github.com/google/wire/cmd/wire //+build !wireinject package config import ( "github.com/samuraiball/go-sandbox/sweets" ) // Injectors from dependency.go: func InitializeCakeShop() sweets.CakeShop { chestnut := sweets.ChestnutProvider() montBlanc := sweets.MontBlancProvider(chestnut) cakeShop := sweets.CakeShopWithMontBlancProvider(montBlanc) return cakeShop }
ここではインジェクターと同じ名前の関数で、初期化処理のコードが生成されているのが見て取れます。
Wireの基本的な使い方はここまでです。
最後に処理をmain関数から呼び出してみます。
package main import "github.com/samuraiball/go-sandbox/config" func main() { shop := config.InitializeCakeShop() shop.Sell() }
実行結果
$ go build $ ./go-sandbox I'm MontBlanc, How is it?
最後に
今回の例はコンポーネント間の依存関係も複雑では無かったので、そこまで恩恵が得られませんでしたが(と言うより、手数がむしろ増えた気がします)、より複雑なアプリケーションを作成したり、テストをやりだしたりすると、プロバイダーとインジェクターの組み合わせで結構自由にDIの設定を記述できそうな感じがしました。
jwt-goを試す
はじめに
JWTをGoで扱えるライブラリを少し探していて、検索して1番最初に出てきたのがjwt-goが出てきて、スターも多いしIsuueみる限り開発も盛んそうだったので使ってみようと思います。
そもそもJWTってなんぞ?
よく聞くし、なんとなく理解した気ではいたのですが、説明しろと言われるとすっとはできなかったので改めて説明を記述して、まとめることで理解を深めてみようと思います。
JWTはJSON Web Tokenの略でRFC7519で定義されます。セキュアにJSON形式のオブジェクトをピア間で届けるための方法を提供します。JWTはシークレット(HMACアルゴリズム)か、RSAやECDSAと言ったようなpublic/privateキーで電子署名が行われるため、JWTで扱われる情報は信頼のおけるものになります。
JWTは、ピア間で共有されたシークレットキーで暗号化を行なうことも可能です。
JWTは主に以下の2つの目的で利用されます。
- 認可
- 最も一般的な用いられ方です。認証されたユーザは毎回のリクエストにJWTを含むようにします。そうすることで、ユーザはJWTによって許可されるリソースへのアクセスなどを行えるようになります。
- 情報交換
- JWTには電子署名が用い垂れるため、誰が情報を送ってきたのかが明確になります。更に、情報の改ざんが行われていないことも保証できます。
JWTは主に以下の3つパートから成り立ち、それぞれのが.
によって区切られます。
- ヘッダー
- ペイロード
- 署名
イメージとしては以下のような構成になります。
xxxxx.yyyyy.zzzzz
ヘッダー
ヘッダーは主にトークンのタイプ
と署名アルゴリズム
の構成で行われます。
ここで、署名アルゴリズムは以下のようなものがあります。
- HMAC
- SHA256
- RSA
また、ヘッダーのサンプルは以下のようになります。
{ "alg": "HS256", "typ": "JWT" }
このようなヘッダーをBase64エンコードして、第1パートに含めます。
ペイロード
JWTの第2パートはペイロードになります。このペイロードはclaim
を含みます。claim
はユーザの情報や追加情報などです。 claim
には通常以下の3つの種類があります。
Registered claims
iss
(issure)、exp
(JWTの有効期限)、sub
(JWTの件名)、aud
(情報の受信者)などの推奨されるが、必須ではないClaimsです。(詳細はこちらを参照)
Public claims
- 共有されるカスタムな情報。JWTを利用するものの間で取り決めで決定されます。IANA JSON Web Token Registryや
Registered claims
などで定義されるものを避ける必要があります。
- 共有されるカスタムな情報。JWTを利用するものの間で取り決めで決定されます。IANA JSON Web Token Registryや
- Private claims
- JWTの提供者と利用者で同意された、プライベートなclaim名。
Registered claims
やPrivate claims
で定義されるものを避ける必要があります。
ペイロードのサンプルは以下のようになります。
- JWTの提供者と利用者で同意された、プライベートなclaim名。
{ "sub": "1234567890", "name": "John Doe", "admin": true }
このようなペイロードをBase64エンコードして、第2パートに含めます。
署名
エンコードされたヘッダーとエンコードされたペイロード等を含めて、署名を作成します。例えば、HMAC
、SHA256
をを利用したい場合は以下のようにして署名を作成します。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
署名はメッセージの改ざんを検知や、秘密鍵で署名が行われる場合はJWTの送信者が誰であるかの証明も行なうことができます。
使ってみる
少し主題からそれましたが、jwt-goを使ってみたいと思います。
jwt-goはJWTのGo実装で、JWTのパースとバリデーション、そして署名をサポートしています。以下のような署名アルゴリズムをサポートしています。
また自作のものをフックすることも可能です。
環境
今回の動作環境は以下のとおりです。
$ go version go version go1.16 linux/amd64 $ uname -srvmpio Linux 5.4.0-65-generic #73-Ubuntu SMP Mon Jan 18 17:25:17 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
プロジェクトの作成
$ mkdir go-jwt-sample $ cd go-jwt-sample $ go mod init github.com/samuraiball/go-sandbox go: creating new go.mod: module github.com/samuraiball/go-sandbox
作成された、go.sumをみると以下のような感じになってました。
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
現時点でのステイブルの最新バージョンは3.2.x系みたいですね。GitHubから飛べるバージョンのドキュメントが1.x.x系のものだったので少し混乱しましたが、3.x.x系のドキュメントを参考に進めていこうと思います。
private/public keyを作成する
今回利用する鍵を作成しておきます。
今回はRSA形式の公開鍵と暗号鍵を作成し、暗号鍵で署名を行なうことを想定します。
openssl
コマンドを用いてそれぞれの鍵を生成します。
$ openssl genrsa 2048 > private-key.pem $ openssl rsa -in private-key.pem -pubout -out public-key.pem
鍵ができました。
JWTを作成する
ここまでで準備が完了したので、JWTを作っていきます。
今回は独自のClaimsであるuserId
を作ってb5548e70-732d-11eb-971c-57ee55c52577
と言う値を受け渡してみます。
具体的には以下のようにします。
type MyClaims struct { UserId string `json:"userId"` jwt.StandardClaims } func main() { claims := MyClaims{ "b5548e70-732d-11eb-971c-57ee55c52577", jwt.StandardClaims{ Issuer: "issuer", ExpiresAt: time.Now().Add(time.Hour * 3).Unix(), }, } token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) var privateKeyRow = []byte(` -----BEGIN RSA PRIVATE KEY----- (省略) -----END RSA PRIVATE KEY----- `) privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(privateKeyRow) if err != nil { panic(err) } ss, err := token.SignedString(privateKey) if err != nil { panic(err) } fmt.Printf("%v\n", ss) }
独自のClaimsをJWTに含めたい場合まずは構造体を作成します。
type MyClaims struct { UserId string `json:"userId"` jwt.StandardClaims }
ここでは、UerId
というクレイムとjwt.StandardClaims
を使って構造体を作成しています。jwt.StandardClaims
は前述したRegistered Claim Names
の構造体のセットを用意してくれています。
main
関数の中では作成した構造体をインスタンス化しjwt.NewWithClaims(jwt.SigningMethodHS256, claims)
にでトークンを作成しています。
そして、最後に署名ですが、token.SignedString()
を呼び出すことで署名つきのトークンを生成できます。この際に事前に用意しておいた鍵を利用します。RSA形式の秘密鍵を利用する場合はjwt.ParseRSAPrivateKeyFromPEM()
に生の鍵を渡してやってrsa.PrivateKey
を生成してtoken.SignedString()
に渡してやると署名が付与されたトークンが作成されます。
コードを実行すると以下のような結果が出力されます。
$ go build main.go $ ./main eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJiNTU0OGU3MC03MzJkLTExZWItOTcxYy01N2VlNTVjNTI1NzciLCJleHAiOjE2MTM4MDM4MjcsImlzcyI6Imlzc3VlciJ9.rarbCzWG57hVBzuMsxAPRHooG3QrAO1hsr8LCadXN0Z9jWY2y8cFP7xSwvwIqAxWtTBbaV3MPAfmxjb6zsrE482RmBnbhyQl0XI0COQpKp1xshqyQNksFvbD0NBaNdmSEQmiVXp6mXJtf1i38eGf3O9H6UfIepN6WwAGLbgMZ-LEtToNOe_fPZksrIPbGONMybKuMaS4KvqpcZb27epPylm7lWunatZgjZ_KxxHsddjJWNCyIGt4t_8zNs8Oew6oqCqbV38KVz7YanBAe_mLBNvyPELbfgbZrPRZdslRVQwBo49O1_5UytYCiiNwTzSb9b0LNVGES_hm3hcQTnelNQ
作られたトークンのペイロード部分をデコードしてみると以下のような結果が得られました。
$ echo eyJ1c2VySWQiOiJiNTU0OGU3MC03MzJkLTExZWItOTcxYy01N2VlNTVjNTI1NzciLCJleHAiOjE2MTM4MDM4MjcsImlzcyI6Imlzc3VlciJ9 | base64 -d {"userId":"b5548e70-732d-11eb-971c-57ee55c52577","exp":1613803827,"iss":"issuer"}
JWTのパースを行なう
JWTの生成までできたので今度は公開鍵をつかいJWTの署名の検証を行います。 具体的には以下のようなコードで可能になります。
type MyClaims struct { UserId string `json:"userId"` jwt.StandardClaims } func main() { var keyPublicRaw = []byte(` -----BEGIN PUBLIC KEY----- (省略) rQIDAQAB -----END PUBLIC KEY----- `) var tokenString = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJiNTU0OGU3MC03MzJkLTExZWItOTcxYy01N2VlNTVjNTI1NzciLCJleHAiOjE2MTM4MDM4MjcsImlzcyI6Imlzc3VlciJ9.rarbCzWG57hVBzuMsxAPRHooG3QrAO1hsr8LCadXN0Z9jWY2y8cFP7xSwvwIqAxWtTBbaV3MPAfmxjb6zsrE482RmBnbhyQl0XI0COQpKp1xshqyQNksFvbD0NBaNdmSEQmiVXp6mXJtf1i38eGf3O9H6UfIepN6WwAGLbgMZ-LEtToNOe_fPZksrIPbGONMybKuMaS4KvqpcZb27epPylm7lWunatZgjZ_KxxHsddjJWNCyIGt4t_8zNs8Oew6oqCqbV38KVz7YanBAe_mLBNvyPELbfgbZrPRZdslRVQwBo49O1_5UytYCiiNwTzSb9b0LNVGES_hm3hcQTnelNQ" parsedToken, err := jwt.ParseWithClaims(tokenString, &MyClaims{}, func(token *jwt.Token) (interface{}, error) { publicKey, err := jwt.ParseRSAPublicKeyFromPEM(keyPublicRaw) if err != nil { panic(err) } return publicKey, nil }, ) parsedClaims := parseToken.Claims.(*MyClaims) fmt.Println("userId:", decodedClaims.UserId) }
go-jwt
でClaimsのパースを行いかつ署名の検証を行なう場合場jwt.ParseWithClaims()
を使います。 この関数は引数にトークンとClaimsの型、そして、鍵を返すKeyfuncを受け取ります。今回の場合は公開鍵を使って検証してみたいと思います。この検証を行なうことで、鍵を公開しているサーバから送られていることかつ改ざんが行われていないことを検証できます。
パースされたトークンのClaimsはparseToken.Claims.(*MyClaims)
のようにしてキャストすることで、構造体のインスタンス化が行えます。
このプログラムの実行結果は以下のとおりです。
$ go build main.go $ ./main userId: b5548e70-732d-11eb-971c-57ee55c52577
参考資料
S3 AWS SDK for JavaでMinIOのバケットとオブジェクトを操作する
はじめに
前回の記事でMiniIOを動かしてみたのですが、Javaのクライアントをいくつか試して見たいと思って、この記事ではAamazon SDKを使ってみようと思います。
前回同様MiniIOはDockerを用いて立てます。
使ってみる
環境
今回のプログラムを動かす環境は以下の通りです。
$ java --version openjdk 15 2020-09-15 OpenJDK Runtime Environment (build 15+36-1562) OpenJDK 64-Bit Server VM (build 15+36-1562, mixed mode, sharing) $ mvn -v Apache Maven 3.6.3 Maven home: /usr/share/maven Java version: 15, vendor: Oracle Corporation, runtime: /home/yuya-hirooka/.sdkman/candidates/java/15-open Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-65-generic", arch: "amd64", family: "unix" $ 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 $ uname -srvmpio Linux 5.4.0-65-generic #73-Ubuntu SMP Mon Jan 18 17:25:17 UTC 2021 x86_64 x86_64 x86_64 GNU/Lin
MiniIOを立てて置く
前述の通り、MiniIOをDockerで起動します。
以下のコマンドを実行します。
$ docker run -p 9000:9000 minio/minio server /data You are running an older version of MinIO released 2 days ago Update: Run `mc admin update` Endpoint: http://172.17.0.2:9000 http://127.0.0.1:9000 Browser Access: http://172.17.0.2:9000 http://127.0.0.1:9000 Object API (Amazon S3 compatible): Go: https://docs.min.io/docs/golang-client-quickstart-guide Java: https://docs.min.io/docs/java-client-quickstart-guide Python: https://docs.min.io/docs/python-client-quickstart-guide JavaScript: https://docs.min.io/docs/javascript-client-quickstart-guide .NET: https://docs.min.io/docs/dotnet-client-quickstart-guide Detected default credentials 'minioadmin:minioadmin', please change the credentials immediately using 'MINIO_ROOT_USER' and 'MINIO_ROOT_PASSWORD'
localhost:9000にアクセスし、ログインすると(アクセスキーとシークレットキーは両方共minioadmin
)以下のUIが開かれます。
プロジェクトを作成する
適当にMavenプロジェクトを作成し、Pomに以下の依存を追加します。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>aws-s3</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>15</maven.compiler.source> <maven.compiler.target>15</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>com.amazonaws</groupId> <artifactId>aws-java-sdk-s3</artifactId> </dependency> <dependency> <groupId>jakarta.xml.bind</groupId> <artifactId>jakarta.xml.bind-api</artifactId> <version>2.3.2</version> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>com.amazonaws</groupId> <artifactId>aws-java-sdk-bom</artifactId> <version>1.11.327</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> </project>
AWS SDKにはBOMが用意されており、複数モジュール導入する場合でも互換性のあるバージョンを利用することができます。上記のように、dependencyManagementに
aws-java-sdk-bom`を追加してやることでこのBOMを利用することができます。
最新のバージョンのBOMはここ
からご確認ください。
AWSのSDKは利用するモジュールを個別に指定出来るため今回はs3のモジュールだけを依存に追加しています。
もし、すべてのモジュールを依存に追加したい場合はBOMを利用せずに以下の依存をPomに追加することで行なうことができます。
<dependencies> <dependency> <groupId>com.amazonaws</groupId> <artifactId>aws-java-sdk</artifactId> <version>1.11.327</version> </dependency> </dependencies>
AWS SDKはJava7以上で利用することが可能です。環境のところでも示しましたが、今回はJava15でSDKクラアントを試してみようと思います。
Java9以降に関しては、JAXBも入れる必要があるようなので依存に追加しています。
クレデンシャル情報をセットする
AWS 認証情報の設定には以下のような方法があります。
環境変数(
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY
)として設定する認証情報の明示的な指定を行なう
BasicAWSCredentials awsCreds = new BasicAWSCredentials("access_key_id", "secret_key_id"); AmazonS3 s3Client = AmazonS3ClientBuilder.standard() .withCredentials(new AWSStaticCredentialsProvider(awsCreds)) .build();
~/.aws/credentials
(WindowsはC:\Users\USERNAME\.aws\credentials
)に以下のようなクレデンシャルファイルを置く
[default] aws_access_key_id = your_access_key_id aws_secret_access_key = your_secret_access_key
今回は認証情報の明示的な指定
して利用するやり方を試してみようと思います。
バケットを操作する
ここまでで諸々の設定は終わったので早速SDKを用いてバケットを操作していきたいと思います。
今回は以下のようなイメージファイルを対象にして、バケットにアップロードダウンロード削除等々をやってみたいと思います。
S3クライアントの作成
まずは、ASKのS3クライアントを作成します。
public static void main(String[] args) { AmazonS3ClientBuildern endpointConfiguration = new AwsClientBuilder.EndpointConfiguration("http://localhost:9000/", Regions.DEFAULT_REGION.name()); AWSCredentials credentials = new BasicAWSCredentials("minioadmin", "minioadmin"); final AmazonS3 s3Client = AmazonS3ClientBuilder.standard() .withEndpointConfiguration(endpointConfiguration) .withPathStyleAccessEnabled(true) .withCredentials(new AWSStaticCredentialsProvider(credentials)) .build(); }
S3のクライアントを作成するためにはAmazonS3ClientBuilder
を利用します。withRegion()
メソッドにリージョンを指定しますが、今回はローカルのMinIOにつなぎに行きたいため、AwsClientBuilder.EndpointConfiguration
を利用して、エンドポイントの設定を行います(ちょっと、ドキュメント見つけられ無かったんですが、ここで指定するリージョンに関してはなんでも良さそうな感じがします)。
クレデンシャルの情報はBasicAWSCredentials
で作成することができます。MinIOのデフォルトのACCESS_KEY_ID/SECRET_ACCESS_KEY
はそれぞれminioadmin
なのでその設定を行っています。
これで、クライアントの作成ができました。今後は特に明示時なければここでインスタンス化したクライアント
バケットを作成してオブジェクトをアップロードする
それでは、まずはバケットを作成してオブジェクトをアップロードしてみます。
bucket01
という名前のバケットを作成して前述の画像をアップロードを行なうには以下のようなコードを書きます。
public class Main { //クライアント作成は省略 String bucketName = "bucket01"; if (!s3Client.doesBucketExistV2(bucketName)) { Bucket bucket = s3Client.createBucket(bucketName); } s3Client.putObject(bucketName, "henoheno.png", new File("/path/to/imageDir/henoheno.png")); } }
エラーハンドリングなどは省略していますがS3クライアントのcreateBucket
とputObject
メソッドを用いることで、バケットの作成と画像のアップロードが行えます。
コードを実行するとMinIOのUIから作成されたバケットとアップロードされた画像を確認することができます。
バケットにポリシーを適用する
ポリシーを作成して、クレデンシャル無しで画像をダウンロード出来るようにしてみます。
現状ではcURL等を用いた画像のダウンロードを行おうとすると、以下のように403の認可エラーが返ってきます。
$ curl http://localhost:9000/bucket01/henoheno.png -v * Trying 127.0.0.1:9000... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 9000 (#0) > GET /bucket01/henoheno.png HTTP/1.1 > Host: localhost:9000 > User-Agent: curl/7.68.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 403 Forbidden < Accept-Ranges: bytes < Content-Length: 303 < Content-Security-Policy: block-all-mixed-content < Content-Type: application/xml < Server: MinIO < Vary: Origin < X-Amz-Request-Id: 16643FECF975B0A1 < X-Xss-Protection: 1; mode=block < Date: Tue, 16 Feb 2021 14:14:13 GMT < <?xml version="1.0" encoding="UTF-8"?> * Connection #0 to host localhost left intact <Error><Code>AccessDenied</Code><Message>Access Denied.</Message><Key>henoheno.png</Key><BucketName>bucket01</BucketName><Resource>/bucket01/henoheno.png</Resource><RequestId>16643FECF975B0A1</RequestId><HostId>7f44b0c0-57d7-4511-ac50-bc78b99478aa</HostId></Error>y
AWS SDK を用いてPolicyを設定する場合S3クライアントのsetBucketPolicy
メソッドを利用すると実行行えます。この場合、以下の2つの方法が取れます。
今回は後者のPolicyクラスを用いるやり方を試してみようと思います。
具体的には以下のようなコードを記述します。
public static void main(String[] args) { //クライアントの作成とバケット&オブジェクトの作成省略 String bucketName = "bucket01"; Statement statement = new Statement(Statement.Effect.Allow) .withPrincipals(Principal.AllUsers) .withActions(S3Actions.GetObject) .withResources(new Resource( "arn:aws:s3:::" + bucketName + "/*")); s3Client.setBucketPolicy(bucketName, new Policy().withStatements(statement).toJson()); }
上記のコードで作成されているポリシーはすべてのユーザに対して、Getのリクエストを許可しています。
再度、cURLで画像をダウンロードすると今度はきちんと画像がダウンロード出来ることが確認できます。
$ curl http://localhost:9000/bucket01/henoheno.png -v --output henoheno.ping % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying 127.0.0.1:9000... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 9000 (#0) > GET /bucket01/henoheno.png HTTP/1.1 > Host: localhost:9000 > User-Agent: curl/7.68.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Accept-Ranges: bytes < Content-Length: 18968 < Content-Security-Policy: block-all-mixed-content < Content-Type: image/png < ETag: "6524a1d06fc27ef8a835abd32bb7c34c" < Last-Modified: Tue, 16 Feb 2021 14:07:52 GMT < Server: MinIO < Vary: Origin < X-Amz-Request-Id: 166440FB47A5C193 < X-Xss-Protection: 1; mode=block < Date: Tue, 16 Feb 2021 14:33:34 GMT < { [18968 bytes data] 100 18968 100 18968 0 0 3704k 0 --:--:-- --:--:-- --:--:-- 3704k * Connection #0 to host localhost left intact $ ls henoheno.ping
参考資料
MinIOオブジェクトストレージとmcクライアントを動かしてみる
はじめに
S3みたいなオブジェクトストレージで、ローカルで動かせるものが、なにかないか探しているときに、Minioというのがあることを教えてもらって、ちょっとインストールして、画像ファイルがダウンロードできるようになるまでやってみようと思います。
あと少しだけmc
クライアントも使ってみたいと思います。
インストールはDokcerを用いてやろうと思います。
動かしてみる
環境
$ uname -srvmpio Linux 5.4.0-65-generic #73-Ubuntu SMP Mon Jan 18 17:25:17 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 $ docker version Client: Docker Engine - Community Version: 20.10.3 API version: 1.41 Go version: go1.13.15 Git commit: 48d30b5 Built: Fri Jan 29 14:33:21 2021 OS/Arch: linux/amd64 Context: default Experimental: true Server: Docker Engine - Community Engine: Version: 20.10.3 API version: 1.41 (minimum version 1.12) Go version: go1.13.15 Git commit: 46229ca Built: Fri Jan 29 14:31:32 2021 OS/Arch: linux/amd64 Experimental: true containerd: Version: 1.4.3 GitCommit: 269548fa27e0089a8b8278fc4fc781d7f65a939b runc: Version: 1.0.0-rc92 GitCommit: ff819c7e9184c13b7c2607fe6c30ae19403a7aff docker-init: Version: 0.19.0 GitCommit: de40ad0
MinIOサーバを動かしてみる
メディアを用意しておく
初期データとして置いておくメディアファイルを置いておきます。
${YOUR_PROJECT_PATH}/media/images
を作成して、適当な画像(01.png
)を置いておきます。
今回は以下の画像を置いておきます。
$ cd ${YOUR_PROJECT_PATH}/media $ ls
Serverのインストール
前述の通り、Dockerで
以下のコマンドでDocker上で動かすことができます。
$ docker run -v ${pwd}/media:/data -p 9000:9000 minio/minio:RELEASE.2021-02-11T08-23-43Z.hotfix.b3c56b53f server /data Endpoint: http://172.17.0.2:9000 http://127.0.0.1:9000 Browser Access: http://172.17.0.2:9000 http://127.0.0.1:9000 Object API (Amazon S3 compatible): Go: https://docs.min.io/docs/golang-client-quickstart-guide Java: https://docs.min.io/docs/java-client-quickstart-guide Python: https://docs.min.io/docs/python-client-quickstart-guide JavaScript: https://docs.min.io/docs/javascript-client-quickstart-guide .NET: https://docs.min.io/docs/dotnet-client-quickstart-guide Detected default credentials 'minioadmin:minioadmin', please change the credentials immediately using 'MINIO_ROOT_USER' and 'MINIO_ROOT_PASSWORD'
ここで、-v ${YOUR_PROJECT_PATH}/media:/data
で先程画像を配置して置いたディレクトリをコンテナの/data
にマウントしておきます。
ここでマウントされたディレクトリに、設定ファイルも含めたMinIOの諸々が保存されることになります。また、${YOUR_PROJECT_PATH}/media
の配下に作成したimages
が作成されるバケット名となります。
また、ログに出力されている通り、デフォルトでのroot
クレデンシャルは、minioadmin:minadmin
になります。この値はそれぞれMINIO_ROOT_USER
、MINIO_ROOT_PASSWORD
の環境変数から変更することができます。
UIを開いてバケットを確認 & ポリシーの変更
起動までできたので早速UIを開いて見たいと思います。
以下のURLからアクセスできます。
ここのAccess Key
とSercret Key
はそれぞれデフォルト値であるminioadmin
を入力します。
すると以下のようながめに遷移します。
画面からわかるとおりすでにimages
というバケットが作成されています。
このバケットのポリシーを変更しておきます。
サイドメニューのimages
にマウスオーバーすると三点リーダのようなものが表示されるのでそこをクリックすると以下のような選択肢が開かれるので、
Edit policy
を選択肢ます。
そして以下のようなポリシーを追加するためのダイアログが 表示されるので、今回はそのままAdd
ボタンを押します。
これで、サーバの準備は完了です。
cURLデータを取得する
サーバの準備はできたので取得してみようと思います。
以下のコマンドを叩きます。
$ curl localhost:9000/images/01.png --output 01.png % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 18968 100 18968 0 0 18.0M 0 --:--:-- --:--:-- --:--:-- 18.0M $ ll 合計 80 drwxr-xr-x 2 yuya-hirooka docker 4096 2月 12 21:12 ./ drwxrwxrwt 81 root root 53248 2月 12 21:12 ../ -rw-r--r-- 1 yuya-hirooka docker 18968 2月 12 21:12 01.png
mcクライアントを使ってみる
Clientのインストール
MiniIOのクライアントをインストールします。
Dockerを使うことも可能なようですが、今回はバイナリをインストールして利用してみたいと思います。
以下のコマンドを叩きます。
$ wget https://dl.min.io/client/mc/release/linux-amd64/mc $ chmod +x mc $ sudo mv mc /usr/bin/ $ mc --version mc version RELEASE.2021-02-10T07-32-57Z
configエイリアスを登録しておく
ms
ではいくつかのストレージの向き先をエイリアスという形で保存しておくことができます。
新しいエイリアスはmc alias set [ALIAS URL] [ACCESSKEY] [SECRETKEY]
コマンドで追加することができます。
例えば今回作成したサーバのエイリアスを登録しておくには以下のようにコマンドを実行します。
$ mc alias set test http://127.0.0.1:9000 minioadmin minioadmin
登録したエイリアス一覧をみるためにはmc alias ls
コマンドを用いて確認することができます。
$ mc alias ls gcs URL : https://storage.googleapis.com AccessKey : YOUR-ACCESS-KEY-HERE SecretKey : YOUR-SECRET-KEY-HERE API : S3v2 Path : dns local URL : http://localhost:9000 AccessKey : SecretKey : API : Path : auto play URL : https://play.min.io AccessKey : Q3AM3UQ867SPQQA43P2F SecretKey : zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG API : S3v4 Path : auto s3 URL : https://s3.amazonaws.com AccessKey : YOUR-ACCESS-KEY-HERE SecretKey : YOUR-SECRET-KEY-HERE API : S3v4 Path : dns test URL : http://127.0.0.1:9000 AccessKey : minioadmin SecretKey : minioadmin API : s3v4 Path : auto
バケットを作成して、ファイルをアップロードしてみる
バケットを作成するにはmc mb [FLAGS] TARGET [TARGET...]
のコマンドを利用することで行えます。
$ mc mb test/image-01
次に、作成したバケットにイメージをアップロードします。
mc cp [FLAGS] SOURCE [SOURCE...] TARGET
で行なうことができます。
作成したバケットに01.png
をアップロードするには以下のコマンドで行なうことができます。
$ mc mv 01.png test/image-01
UIを確認するとバケットが作成されファイルがアップロードされているのが確認できます。
mcコマンドで出来ることをざっくりまとめておく
最後にmcコマンドで行えることをまとめておきます。
mcコマンドは第一の引数でざっくりと行なうことが決定されます。
例えばバケットを作成して、ファイルをアップロードしてみる
では、mb
とmv
を使いました。
その他にも以下のようなコマンドがあります。
コマンド | 説明 |
---|---|
alias | コンフィグファイルを設定したり削除したりする |
ls | バケットやオブジェクトの一覧を表示する |
mb | バケットを作成する |
rb | バケットを削除する |
cp | オブジェクトをコピーする |
mirror | リモートサイトにオブジェクトをシンクロナイズする |
cat | オブジェクトのコンテンツを表示する |
head | オブジェクトの最初のn行を表示する |
pipe | 標準入力からの入力ストリームをオブジェクトに変換する |
share | 一時的にオブジェクトにアクセスできるようになるURLを生成する |
find | オブジェクトを検索する |
sql | オブジェクトに対してsqlクエリを発行する |
stat | オブジェクトのメタデータを表示 |
mv | オブジェクトを移動させる |
tree | treeフォーマットでオブジェクトを表示する |
du | ディスクの使用量を再帰的にサマライズする |
retention | オブジェクトの保持期間を設定する |
legalhold | リーガルホールドをマネージする |
diff | 2つのオブジェクトの名前、サイズ、データなどのdiffを表示 |
rm | オブジェクトを削除する |
version | バケットのバージョニングをマネージする |
ilm | バケットのライフサイクルをマネージする |
encrypt | バケットの暗号化の設定をマネージする |
event | オブジェクトの通知をマネージします |
watch | オブジェクトの通知イベントを監視する |
undo | PUT/DELETE オペレーションをundoする |
policy | アノニマスユーザのバケットに対するアクセスポリシーを変更する |
tag | バケットとオブジェクトのタグをマネージする |
replicate | サーバーサイドのバケットのレプリケーションを設定する |
admin | MinIOサーバーをマネージする |
update | mcコマンドを最新のリリースにバージョンアップする |
vmstatをつかってみる
はじめに
vmstat
というコマンドは名前は聞いたことあったのですが、あんまり使ったことなかったのでちゃんと使えるようになりたいと思ってちょっとまとめてみようかと思います。
vmstatとは
vmstatコマンドはLinuxなどのOSでプロセス、メモリー、ページング、ブロックIO、ディスク、CPUアクティビティなどの統計情報を取得すためのコマンドです。
最初の出力では、PCがブートされてからの平均値の統計情報が出力され、続く出力はコマンドで指定される期間での情報を出力します。
使い方
動作環境
今回、コマンドを動作させる環境は以下の通り
$ uname -srvmpio Linux 5.4.0-65-generic #73-Ubuntu SMP Mon Jan 18 17:25:17 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
基本的な構文
基本的には以下の構文を取ります。
$ vmstat [options] [delay [count]]
ここで、delay
では情報の出力の間隔、count
では
例えば、以下のような実行では、1秒の間隔での出力を4回繰り返します。
$ vmstat 1 4 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 0 0 0 22358960 1103068 5680548 0 0 11 23 99 10 15 4 80 0 0 0 0 0 22361072 1103068 5678012 0 0 0 0 545 1324 1 1 98 0 0 0 0 0 22351748 1103076 5675704 0 0 0 48 799 2035 3 1 95 1 0 1 0 0 22365932 1103084 5663560 0 0 0 48 1060 2647 4 1 94 0 0
ここで、 delay
とcount
両方、もしくはcount
のみ省略することが可能で、両方を省略した場合、最後に起動してからの平均値を一回のみ出力し、count
を省略した場合は
# 起動時からの平均を一回のみ出力 $ vmstat procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 0 0 0 23076780 1106336 5189788 0 0 10 23 100 18 15 4 81 0 0 # 1秒間に1回停止するまで情報を出力 $ vmstat 1 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 0 0 0 23088852 1106396 5181076 0 0 10 23 100 19 15 4 81 0 0 4 0 0 23061588 1106396 5196904 0 0 0 0 751 1929 3 1 96 0 0 0 0 0 23072540 1106396 5202800 0 0 0 64 2225 5278 7 1 92 0 0 1 0 0 23076960 1106396 5200228 0 0 0 0 1398 2723 3 0 97 0 0 0 0 0 23077212 1106420 5200048 0 0 0 792 675 1625 2 1 98 0 0 0 0 0 23094096 1106420 5193604 0 0 0 0 590 1443 2 0 98 0 0 1 0 0 23098632 1106420 5193620 0 0 0 0 625 1338 2 0 98 0 0 0 0 0 23103672 1106420 5188916 0 0 0 52 592 1369 1 1 98 0 0 ・ ・ ・ ・
各出力項目の意味
以下に基本的な各出力項目の意味をまとめます。
- prpcs
- r : 実行可能な待ちプロセス
- b : 割り込み不可能なスリープ状態のプロセス
- Memory
- swpd : 利用されている仮想メモリの合計値
- free : アイドル状態のメモリの合計値
- buff : バッファーとして利用されているメモリの合計値
- cache : キャッシュとして利用されているメモリの合計値
- inact : インアクティブな状態のメモリの合計値(-a オプション指定時)
- active : アクティブな状態メモリの合計値(-a オプション指定時)
- Swap
- IO
- System
- in : 一秒間に起こる割り込みの数
- cs : 一秒間に起こるコンテキストスイッチの数
- CPU
- us : non-kernelコードを実施している時間
- sy : kernalコードを実施している時間
- id : アイドル時間
- wa : IOの待ち時間(Linux 2.5.41以上はアイドル時間を含む)
- st : Virtual Machineによって取られる時間
オプションの一覧
以下にオプションの一覧をまとめます。
オプション | 説明 |
---|---|
-a, --active | アクティブとインアクティブのメモリの総量を表示 |
-f, --forks | 起動時からのフォークの数を出力。forkとvfork(子プロセスを起動し親プロセスを停止)、clone system callsがカウントされる。作成されたタスクの合計値と同数になる。このオプションを指定指定した場合一回のみの出力が行われる |
-m, --slabs | Slab(カーネルのキャッシュ)情報を出力 |
-n, --one-header | 初回に一回のみのヘッダー表示 |
-d, --disk | ディスクの統計情報を表示 |
-D, --disk-sum | ディスクアプティビティのサマリを出力 |
-p, --partition [device] | パーティションの詳細情報の表示 |
-S, --unit [character] | 出力単位を指定。1000(k)、1024(K)、1000000(m)、1048576 (M) バイト |
-t, --timestamp | タイムスタンプの出力 |
-w, --wide | ワイドアプトプット |
-V, --version | バージョン情報の表示 |
-h, --help | ヘルプの表示 |
Jaegerでk8s+Istio上のアプリ(Quarkus、Spring)を分散トレーシングする
はじめに
分散トレーシングをやる際にJaegerというツールがあって、試してみたいと思って試せていなかったのやってみようと思います。今回はMinikubeで作ったクラスターにIstioをデプロイして、 その環境でのトレーシングを行ってみようと思います。
Istioにおけるトレーシングについて
どんなふうに実現されるか
Istioにおける分散トレーシングがどのように実現されているのかは、ドキュメントにいろいろ書かれてました。
まず、IstioはEnvoyベースのトレーシングを行います。その際にアプリケーションはB3 trace headersなどのヘッダーを転送していく必要があるようです。具体的には以下のようなヘッダーを転送して行くみたいです。
- x-request-id
- x-b3-traceid
- x-b3-spanid
- x-b3-parentspanid
- x-b3-sampled
- x-b3-flags
- b3
さらに、Lightstepを利用する場合はx-ot-span-context
もつける必要があるみたいです。
これらのヘッダーはマニュアルで転送していくことも可能ですが、ZipkinやJaegerのクライアントを使うことで自動的に拡散することも可能なようです。
ちなみに、なぜ、Istio自身がこれらのヘッダーをフォワーディングできないのかについてですが、アプリケーションのアウトバウンドリクエストがどのインバウンドリクエストによって発生したものかを特定するすべが、Istio側には存在しないからです。
また、Envoyベースのトレーシングに置いてEnvoyは以下のようなことを行ってくれます。
- リクエストIDとトレーシングヘッダー(B3 Header等)を生成し送信する
- リクエストとレスポンスのメタデータからTrace Spanを生成する
- トレーシングバックエンドにSpanを送信する
- プロキシ先のアプリケーションにヘッダーを送信する
Jaegerとは
Jaeger JaergerはDapper、OpenZipkinにインスパイヤーされた分散トレーシングシステムです。マイクロサービスに置いて以下のような用途で用いられます。
- 分散トレーシングのモニタリング
- 根本原因解析
- サービスの依存解析
- パフォーマンス、レイテンシの最適化
また、以下のようなコンポーネントで構成されます。
- Goで作られたバックエンドコンポーネント
- React UI
- ストレージ
- Cassandra 3.4+
- Elasticserch 5.x, 6.x, 7.x
- Kafka
- メモリ
やってみる
環境
今回はKubernetesのクラスターはMinikube(driver=none)を用います。
$ minikube version minikube version: v1.16.0 commit: 9f1e482427589ff8451c4723b6ba53bb9742fbb1 $ kubectl version -o yaml clientVersion: buildDate: "2020-10-15T01:52:24Z" compiler: gc gitCommit: 62876fc6d93e891aa7fbe19771e6a6c03773b0f7 gitTreeState: clean gitVersion: v1.18.10 goVersion: go1.13.15 major: "1" minor: "18" platform: linux/amd64 serverVersion: buildDate: "2020-10-15T01:43:56Z" compiler: gc gitCommit: 62876fc6d93e891aa7fbe19771e6a6c03773b0f7 gitTreeState: clean gitVersion: v1.18.10 goVersion: go1.13.15 major: "1" minor: "18" $ istioctl version client version: 1.8.2 control plane version: 1.8.2 data plane version: 1.8.2 (1 proxies) $ uname -srvmpio Linux 5.4.0-64-generic #72-Ubuntu SMP Fri Jan 15 10:27:54 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux $ docker version (Client略) Server: Docker Engine - Community Engine: Version: 20.10.2 API version: 1.41 (minimum version 1.12) Go version: go1.13.15 Git commit: 8891c58 Built: Mon Dec 28 16:15:19 2020 OS/Arch: linux/amd64 Experimental: true containerd: Version: 1.4.3 GitCommit: 269548fa27e0089a8b8278fc4fc781d7f65a939b runc: Version: 1.0.0-rc92 GitCommit: ff819c7e9184c13b7c2607fe6c30ae19403a7aff docker-init: Version: 0.19.0 GitCommit: de40ad0 $ 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 --version 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-65-generic", arch: "amd64", family: "unix"
もろもろを構築しておく
今回は以下のような環境を構築して分散トレーシングをやってみようと思います。
Jaegerでトレーシングするのは以下の3つになります。
利用するアプリはSpringとQuarkusと利用していますが、この記事に置いては深い意味は無く、別で試したことがあったので採用しました。
アプリのロジックに関しても複雑なことは市内想定で、QuarkusアプリがSpringアプリに対して、Hello, Tracing
の文字列を取得してそのままフロント側に返すようにしようと思います。
Sidecar InjectionをTrueにしてネームスペースを作成
まずは、IstioのインジェクションをTrueにしておきます。
今回は余計な複雑さをなくすために、新たにネームスペースを作成はせずにクラスタのdefault
に対してインジェクションをTrueにします。
以下のコマンドを実行します。
$ kubectl --context=minikube label namespace default istio-injection=enabled namespace/default labeled
minikubeのコンテクストでイメージをビルドするように設定
今回はローカルでビルドしたしたイメージを使うようにしておきます。
いかのコマンドでminikubeのコンテクストでイメージをビルドするように設定します。
$ eval $(minikube docker-env)
サンプルアプリケーションを作成しコンテナ化
Quarkusのアプリを作成
まずはQuarkusの方を作っていこうと思います。
プロジェクトはQuarkus - Start coding with code.quarkus.ioを使って作成します。
プロジェクトの設定は以下の通り。
依存はRESTEasy
とRest Client
だけ追加しておきます。
プロジェクトが作成できたら、まずはHTTPクライアントを作成します。
@RegisterRestClient @RegisterClientHeaders public interface GreetingClient { @GET @Path("/hello") String fetchHello(); }
基本的には、なんの変哲の無いRestClientですがひとつだけポイントがあります。
前述の通り、Istioを使った分散トレーシングではヘッダーを転送していく必要があります。そのフォワーディングを行なうために、@RegisterClientHeadersを利用しています。このヘッダーはデフォルトで、指定されたJAX-RSのインバウンドリクエストヘッダーをアウトバウンドのリクエスト時に付与することができます。
RestClientの設定と転送するヘッダー設定を以下のようにappllication.properties
に記述しておきます。
quarkus.http.port=8081 dev.hirooka.GreetingClient/mp-rest/url=http://${SPRING_SERVICE:localhost:8082} dev.hirooka.GreetingClient/mp-rest/scope=javax.inject.Singleton org.eclipse.microprofile.rest.client.propagateHeaders=x-request-id,x-b3-traceid,x-b3-spanid,x-b3-parentspanid,x-b3-sampled,x-b3-flags,b3,x-ot-span-context
設定まで記述できたら、次はハンドラーを記述します。今回はHTTPクライアントをハンドラーから直接利用するようにします。
@Path("/hello") public class GreetingResource { @Inject @RestClient GreetingClient greetingClient; static final Logger logger = Logger.getLogger(GreetingResource.class); @GET @Produces(MediaType.TEXT_PLAIN) public String hello(@Context HttpHeaders headers) { logger.info(headers.getRequestHeaders()); return greetingClient.fetchHello(); } }
これでQuarkusの方のアプリは完成しました。
最後に作ったアプリをDockerイメージ化しておきます。
$ ./mvnw package $ docker build -f src/main/docker/Dockerfile.jvm -t quarkus/open-tracing-jvm . $ docker images | grep quarkus quarkus/open-tracing latest f2c29c0c8e21 2 minutes ago 385MB
Springのアプリを作成
次にSpringのアプリを作成します。
プロジェクトはSpring Initializrを使って作成します。
設定は以下のように。
今回の構成だと、Springのアプリは外部アクセスを行わないため、ヘッダーをフォワーディングする必要が無いので、Spring Web
だけで良いのですが、一応フォワーディングのやり方を示すために依存にSleuth
とZipkin Client
を追加してます。
なにはともあれ、まずはコントローラーを作成します。
@RestController public class GreetingController { private final Logger logger = LoggerFactory.getLogger(GreetingController.class); @GetMapping("/hello") String greeting(@RequestHeader Map<String, String> header) { logger.info(header.toString()); return "Hello, Tracing"; } }
今回必要な部分はこれだけです。
設定は以下の通り
server.port=8082 spring.zipkin.enabled=false
Spring Sleuth
はデフォルトでlocalhost:9411
にメトリクスを送信してしまうので、今回は無効化して置きます。
Springも外部へのHTTPコールを行いヘッダーを転送したい場合はspring.sleuth.propagation-keys
を以下のように設定すれば良さそうです。
spring.sleuth.propagation-keys=x-request-id,x-b3-traceid,x-b3-spanid,x-b3-parentspanid,x-b3-sampled,x-b3-flags,b3,x-ot-span-context
x-b3-traceid
などのB3のヘッダーなどはこの値をセットしていない場合は、Spring Sleuth
が自分で生成した値をヘッダーで利用してしまうみたいです。(もしかしたらもっといい方法があるかも...)
まぁ、今回はspring.sleuth.propagation-keys
に関しては置いておいて、できたアプリをDockerイメージ化します。
$ mvn spring-boot:build-image -Dspring-boot.build-image.imageName=spring/open-tracing $ docker images | grep spring/open-tracing spring/open-tracing latest 0dbe73ec4359 41 years ago 268MB
イメージが作成されました。
DeploymentとServiceを作成してmimikubeにデプロイする
それではDeploymentとServiceを作成してminikubeにデプロイしておきます。
まずはベースとなるdeployment.yamlを作成します。
$ kubectl create deployment quarkus-app --image=quarkus/open-tracing --dry-run=client -o yaml > deployment.yaml echo --- >> deployment.yaml $ kubectl create deployment spring-app --image=spring/open-tracing --dry-run=client -o yaml >> deployment.yaml
できた、deplyment.yamlのそれぞれのDeploymentにimagePullPolicy: IfNotPresent
(ローカルイメージを使用するようにするため)とquarkus-app
には環境変数SPRING_SERVICE
を記述しておきます。
apiVersion: apps/v1 kind: Deployment metadata: creationTimestamp: null labels: app: quarkus-app name: quarkus-app spec: replicas: 1 selector: matchLabels: app: quarkus-app strategy: {} template: metadata: creationTimestamp: null labels: app: quarkus-app spec: containers: - image: quarkus/open-tracing name: open-tracing imagePullPolicy: IfNotPresent env: - name: SPRING_SERVICE value: spring-app:8082 resources: {} status: {} --- apiVersion: apps/v1 kind: Deployment metadata: creationTimestamp: null labels: app: spring-app name: spring-app spec: replicas: 1 selector: matchLabels: app: spring-app strategy: {} template: metadata: creationTimestamp: null labels: app: spring-app spec: containers: - image: spring/open-tracing name: open-tracing imagePullPolicy: IfNotPresent resources: {} status: {}
次にサービスを作成します。
$ kubectl create service clusterip quarkus-app --tcp=8081:8081 --dry-run=client -o yaml > service.yaml $ echo --- >> service.yaml $ kubectl create service clusterip spring-app --tcp=8082:8082 --dry-run=client -o yaml > service.yaml
Serviceは特にいじることは無いので、作ったマニフェストをapplyしていきます。
$ kubectl --context=minikube apply -f deplyment.yaml $ kubectl --context=minikube apply -f service.yaml
GatawayとVirtualSerciceを作成して疎通確認する
Gataway
とVirtualService
を作っておきます。
gateway.yaml
apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: quarkus-app-gateway spec: selector: istio: ingressgateway servers: - port: number: 80 name: http protocol: HTTP hosts: - "*"
virtual-service.yaml
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: quarkus-app-vs spec: hosts: - "*" gateways: - quarkus-app-gateway http: - match: - uri: prefix: /hello route: - destination: port: number: 8081 host: quarkus-app
上記のyamlをapplyしておきます。
$ kubectl --context=minikube apply -f gateway.yaml $ kubectl --context=minikube apply -f virtual-service.yaml
cUrlを使って疎通確認を行います。
$ export INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].nodePort}' $ export INGRESS_HOST=$(minikube ip) $ curl ${INGRESS_HOST}:${INGRESS_PORT}/hello -v * Trying 192.168.49.2:30019... * TCP_NODELAY set * Connected to 192.168.49.2 (192.168.49.2) port 30019 (#0) > GET /hello HTTP/1.1 > Host: 192.168.49.2:30019 > User-Agent: curl/7.68.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < content-length: 14 < content-type: text/plain;charset=UTF-8 < x-envoy-upstream-service-time: 23 < date: Sun, 31 Jan 2021 11:28:48 GMT < server: istio-envoy < * Connection #0 to host 192.168.49.2 left intact Hello, Tracing
ここまででようやく準備完了です。
Jaergerを動かす
さて、ようやくですが。
Jaegerをローカルで動かしておきます。
今回はMinikubeのクラスターにデプロイして動かします。
外部のJaergerにメトリクスを送信する場合は、--set values.global.tracer.zipkin.address=<jaeger-collector-address>:9411
をIstioのインストール時に設定しておけば、任意のJaergerにデータを送信することができます。
それでは、Jargerをデプロイします。
$ kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.8/samples/addons/jaeger.yaml
次のコマンドで、UIを動かします。
$ istioctl dashboard jaeger http://localhost:16686
出力される。アドレスにアクセスすると以下の様なUIが表示されます。
実はローカルで試している際に何度かリクエストを送ってしまったので、すでにデータが存在してしまっていますが、最初はなにも出力されません。
これは、デフォルトではIstioは1%のリクエストのデータをJaergerに送るためです。
以下のコマンドを何度か叩いて、Jaergerにデータを送るようにしておきます。
$ for i in $(seq 1 100); do curl -s -o /dev/null "http://${INGRESS_HOST}:${INGRESS_PORT}/hello"; done
そうするとそれっぽいデータが見れるようになります。
ちょっとUIを見てみる
ヘッダーのSearch
タブを選択すると以下のような検索用のボックスが表示されていると思います。
Service
をistio-ingressgateway
を選択してFind Traces
を押すと右側にトレースされたリクエストが表示されます。
僕の環境では4回のサンプリングされたリクエストの情報が表示されます。
グラフのすぐ下のリクエスト(7ff3cb9
)をクリックするとリクエストに関する情報がより詳細にみることができます。
ヘッダーのSystem Architecture
タブを選択するとサービスの依存関係やそれぞれに難解リクエストが送られたかを確認することができます。
とりあえず動かすところまでできました。