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 RegistryRegistered claimsなどで定義されるものを避ける必要があります。
  • Private claims
    • JWTの提供者と利用者で同意された、プライベートなclaim名。Registered claimsPrivate claimsで定義されるものを避ける必要があります。
      ペイロードのサンプルは以下のようになります。
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

このようなペイロードBase64エンコードして、第2パートに含めます。

署名

エンコードされたヘッダーとエンコードされたペイロード等を含めて、署名を作成します。例えば、HMACSHA256をを利用したい場合は以下のようにして署名を作成します。

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が開かれます。

f:id:yuya_hirooka:20210216202307p:plain

プロジェクトを作成する

適当に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はここ からご確認ください。
AWSSDKは利用するモジュールを個別に指定出来るため今回は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(WindowsC:\Users\USERNAME\.aws\credentials)に以下のようなクレデンシャルファイルを置く
[default]
aws_access_key_id = your_access_key_id
aws_secret_access_key = your_secret_access_key

今回は認証情報の明示的な指定して利用するやり方を試してみようと思います。

バケットを操作する

ここまでで諸々の設定は終わったので早速SDKを用いてバケットを操作していきたいと思います。
今回は以下のようなイメージファイルを対象にして、バケットにアップロードダウンロード削除等々をやってみたいと思います。

f:id:yuya_hirooka:20210212112530p:plain

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クライアントのcreateBucketputObjectメソッドを用いることで、バケットの作成と画像のアップロードが行えます。
コードを実行するとMinIOのUIから作成されたバケットとアップロードされた画像を確認することができます。

f:id:yuya_hirooka:20210216230838p:plain

バケットにポリシーを適用する

ポリシーを作成して、クレデンシャル無しで画像をダウンロード出来るようにしてみます。
現状では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つの方法が取れます。

  • JSON形式のポリシーのテキスト文字列を指定する
  • Policy クラスを使用してポリシーを構築する

今回は後者の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)を置いておきます。
今回は以下の画像を置いておきます。

f:id:yuya_hirooka:20210212112530p:plain

$ 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_USERMINIO_ROOT_PASSWORD環境変数から変更することができます。

UIを開いてバケットを確認 & ポリシーの変更

起動までできたので早速UIを開いて見たいと思います。
以下のURLからアクセスできます。

f:id:yuya_hirooka:20210212210016p:plain

ここのAccess KeySercret Keyはそれぞれデフォルト値であるminioadminを入力します。
すると以下のようながめに遷移します。

f:id:yuya_hirooka:20210212210239p:plain

画面からわかるとおりすでにimagesというバケットが作成されています。
このバケットのポリシーを変更しておきます。 サイドメニューのimagesにマウスオーバーすると三点リーダのようなものが表示されるのでそこをクリックすると以下のような選択肢が開かれるので、

f:id:yuya_hirooka:20210212210701p:plain

Edit policyを選択肢ます。
そして以下のようなポリシーを追加するためのダイアログが 表示されるので、今回はそのままAddボタンを押します。

f:id:yuya_hirooka:20210212210817p:plain

これで、サーバの準備は完了です。

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を確認するとバケットが作成されファイルがアップロードされているのが確認できます。

f:id:yuya_hirooka:20210212214013p:plain

mcコマンドで出来ることをざっくりまとめておく

最後にmcコマンドで行えることをまとめておきます。
mcコマンドは第一の引数でざっくりと行なうことが決定されます。 例えばバケットを作成して、ファイルをアップロードしてみるでは、mbmvを使いました。
その他にも以下のようなコマンドがあります。

コマンド 説明
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

ここで、 delaycount両方、もしくは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
    • si : ディスクからスワップインされたメモリの合計値(/s)
    • so : ディスクにスワップされたメモリの合計値(/s)
  • IO
    • bi : ブロックディバイスから受け取ったブロックの数 (blocks/s)
    • bo : ブロックディバイスに送ったブロック数(blocks/s)
  • System
  • 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もつける必要があるみたいです。
これらのヘッダーはマニュアルで転送していくことも可能ですが、ZipkinJaegerのクライアントを使うことで自動的に拡散することも可能なようです。
ちなみに、なぜ、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"

もろもろを構築しておく

今回は以下のような環境を構築して分散トレーシングをやってみようと思います。

f:id:yuya_hirooka:20210131213754p:plain

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を使って作成します。
プロジェクトの設定は以下の通り。

f:id:yuya_hirooka:20210131164559p:plain

依存はRESTEasyRest 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を使って作成します。
設定は以下のように。

f:id:yuya_hirooka:20210131173540p:plain

今回の構成だと、Springのアプリは外部アクセスを行わないため、ヘッダーをフォワーディングする必要が無いので、Spring Webだけで良いのですが、一応フォワーディングのやり方を示すために依存にSleuthZipkin 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を作成して疎通確認する

GatawayVirtualServiceを作っておきます。

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が表示されます。

f:id:yuya_hirooka:20210131210234p:plain

実はローカルで試している際に何度かリクエストを送ってしまったので、すでにデータが存在してしまっていますが、最初はなにも出力されません。
これは、デフォルトではIstioは1%のリクエストのデータをJaergerに送るためです。
以下のコマンドを何度か叩いて、Jaergerにデータを送るようにしておきます。

$ for i in $(seq 1 100); do curl -s -o /dev/null "http://${INGRESS_HOST}:${INGRESS_PORT}/hello"; done

そうするとそれっぽいデータが見れるようになります。

f:id:yuya_hirooka:20210131210628p:plain

ちょっとUIを見てみる

ヘッダーのSearchタブを選択すると以下のような検索用のボックスが表示されていると思います。

f:id:yuya_hirooka:20210131211740p:plain

Serviceistio-ingressgatewayを選択してFind Tracesを押すと右側にトレースされたリクエストが表示されます。

f:id:yuya_hirooka:20210131212213p:plain

僕の環境では4回のサンプリングされたリクエストの情報が表示されます。
グラフのすぐ下のリクエスト(7ff3cb9)をクリックするとリクエストに関する情報がより詳細にみることができます。

f:id:yuya_hirooka:20210131212649p:plain

ヘッダーのSystem Architectureタブを選択するとサービスの依存関係やそれぞれに難解リクエストが送られたかを確認することができます。

f:id:yuya_hirooka:20210131213032p:plain

f:id:yuya_hirooka:20210131213114p:plain

とりあえず動かすところまでできました。

KongのKubernetes Ingress Controllerを試す

はじめに

久しぶりにKongをちょっとお勉強したい気になってきたので、ドキュメントを眺めていたらKubernetes Ingress Controllerなるものを見つけました。面白そうだったので、とりあえず、動かすまでやってみようかと思います。
Kongの基本的なところプラグインの書き方も以前まとめたので興味がある人はよかったら見てみてください。

KongのIngress Controllerについて

KubernetesIngress Controllerでクラスター内でIngressリソースとして動くKongに対して設定と管理を行います。クラスター内のスケーリング、設定の変更、エラーなどのイベントによってKongをアップデートしてくれます。
以下の2つのコンポーネントからなります。

  • Kong本体
  • Controller、Kongの設定を同期する

プラグインを反映させることももちろん可能で、Kongができることは基本的になんでもできるようです。

カスタムリソースについて

いくつかのカスタムリソースが用意されおり、Kongの宣言的な設定を用いてKongの機能を利用することができます。

  • KongPlugin: KongのPluginエンティティ相当の設定を行なうリソース
  • KongIngress: ルーティング、ロードバランシング、ヘルスチェックなど細かなルーティングの設定等を行なうためのリソース
  • KongConsumer: KongのCunsumerエンティティへのマッピング
  • TCPIngress: TCPベースのルーティングを行なうためのリソース。non-HTTPベースのサービスに対して利用可能

使ってみる

今回はMinikube(driver none)にIngressControllerをインストールして、サンプルアプリにプロキシしてみたいと思います。

環境

$ minikube version
minikube version: v1.16.0
commit: 9f1e482427589ff8451c4723b6ba53bb9742fbb1

$ kubectl version -o yaml
clientVersion:
  buildDate: "2021-01-13T13:28:09Z"
  compiler: gc
  gitCommit: faecb196815e248d3ecfb03c680a4507229c2a56
  gitTreeState: clean
  gitVersion: v1.20.2
  goVersion: go1.15.5
  major: "1"
  minor: "20"
  platform: linux/amd64
serverVersion:
  buildDate: "2020-12-08T17:51:19Z"
  compiler: gc
  gitCommit: af46c47ce925f4c4ad5cc8d1fca46c7b77d13b38
  gitTreeState: clean
  gitVersion: v1.20.0
  goVersion: go1.15.5
  major: "1"
  minor: "20"
  platform: linux/amd64

$ docker version
(クライアント略)

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

$ uname -srvmpio
Linux 5.4.0-62-generic #70-Ubuntu SMP Tue Jan 12 12:45:47 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.1 LTS
Release:    20.04
Codename:   focal

アップストリームを作っておく

下準備として、Kongがプロキシすする先のアップストリームを作って置きます。
アップストリームはNginxを用いて2つ作成し、それぞれが自分自身のhostnameを返すようにしておきます。

--dry-runオプションと使ってリソースを作ります。

# deployment.yamlの作成
$ kubectl create deployment nginx-first --image=nginx:1.19.6 --dry-run=client -o yaml > deployment.yaml

$ echo --- >> deployment.yaml 

$ kubectl create deployment nginx-second --image=nginx:1.19.6 --dry-run=client -o yaml >> deployment.yaml


# service.yamlの作成
$ kubectl create service clusterip nginx-first --tcp=8081:80 --dry-run=client -o yaml > service.yaml

$ echo --- >> service.yaml 

$ kubectl create service clusterip nginx-second --tcp=8082:80 --dry-run -o yaml > service.yaml

それぞれできたリソースはここに置いておくので興味があれば確認してみてください。
作成したリソースをApplyしてします。

$ kubectl --context=minikube apply -f deployment.yaml

$ kubectl --context=minikube get po 
NAME                            READY   STATUS    RESTARTS   AGE
nginx-first-d6db6c668-wtmwh     1/1     Running   0          84s
nginx-second-6b8d5c9696-2cj25   1/1     Running   0          84s


$ kubectl --context=minikube apply -f service.yaml
service/nginx-first created
service/nginx-second created

$ kubectl get svc
NAME           TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
kubernetes     ClusterIP   10.96.0.1        <none>        443/TCP    92m
nginx-first    ClusterIP   10.109.148.241   <none>        8081/TCP   44s
nginx-second   ClusterIP   10.105.0.182     <none>        8082/TCP   44s

最後に、それぞれが自分自身のhostnameを返すようにしておきます。

$ kubectl --context=minikube exec -it nginx-first-d6db6c668-wtmwh -- cp /etc/hostname /usr/share/nginx/html/index.html

$ kubectl --context=minikube exec -it nginx-second-6b8d5c9696-2cj25 -- cp /etc/hostname /usr/share/nginx/html/index.html

テストのためにポートフォワードしてそれぞれのNginxにつないで見ます。

# First
$ kubectl --context=minikube port-forward service/nginx-first 8081:8081

$ curl localhost:8081
nginx-first-d6db6c668-wtmwh

# Second
$ kubectl --context=minikube port-forward service/nginx-second 8082:8082

$ curl localhost:8082
nginx-second-6b8d5c9696-2cj25

無事、それぞれのNginxがhostnameを返してくれていますね。
これで準備完了です。

Kubernetes Ingress Controllerのインストール

Minikubeで作ったクラスタにKongのKubernetes Ingress Controllerをインストールします。
ドキュメントに寄るとHelmを使った方法やここからダウンロードしたYamlファイルをアプライスル方法があるみたいです。
今回は後者の方で行こうと思います。

$ kubectl create -f https://bit.ly/k4k8s
namespace/kong created
Warning: apiextensions.k8s.io/v1beta1 CustomResourceDefinition is deprecated in v1.16+, unavailable in v1.22+; use apiextensions.k8s.io/v1 CustomResourceDefinition
customresourcedefinition.apiextensions.k8s.io/kongclusterplugins.configuration.konghq.com created
customresourcedefinition.apiextensions.k8s.io/kongconsumers.configuration.konghq.com created
customresourcedefinition.apiextensions.k8s.io/kongingresses.configuration.konghq.com created
customresourcedefinition.apiextensions.k8s.io/kongplugins.configuration.konghq.com created
customresourcedefinition.apiextensions.k8s.io/tcpingresses.configuration.konghq.com created
serviceaccount/kong-serviceaccount created
Warning: rbac.authorization.k8s.io/v1beta1 ClusterRole is deprecated in v1.17+, unavailable in v1.22+; use rbac.authorization.k8s.io/v1 ClusterRole
clusterrole.rbac.authorization.k8s.io/kong-ingress-clusterrole created
Warning: rbac.authorization.k8s.io/v1beta1 ClusterRoleBinding is deprecated in v1.17+, unavailable in v1.22+; use rbac.authorization.k8s.io/v1 ClusterRoleBinding
clusterrolebinding.rbac.authorization.k8s.io/kong-ingress-clusterrole-nisa-binding created
service/kong-proxy created
service/kong-validation-webhook created
deployment.apps/ingress-kong created

むー、Kubernetesのバージョン1.20.xを使ってると、betaじゃなくなったリソースがいくつかあって警告がいくつか出てしまうみたいですね。
まぁ、この辺は一旦気にせずに先に進めようと思います。

デプロイされたコングにアクセスするためのIPを取得して環境変数(KONG_PROXY_IP)にセットしておきます。
この、今後この環境変数を使ってKongにアクセスします。

export KONG_PROXY_IP=$(minikube service -n kong kong-proxy --url | head -1)

デプロイされたKongにアクセスしてみます。

$ curl -i $PROXY_IP
HTTP/1.1 404 Not Found
Date: Sun, 24 Jan 2021 06:20:54 GMT
Content-Type: application/json; charset=utf-8
Connection: keep-alive
Content-Length: 48
X-Kong-Response-Latency: 0
Server: kong/2.2.1

{"message":"no Route matched with those values"}

Kongから無事レスポンスが返ってきました。
まだなにもKongの設定を行っていないので、NotFoundを返してきますね。

プロキシの設定を記述する

プロキシの設定を記述するには、普通の(?)Ingressのリソースを記述すれば良さそうです。
以下のようなYamlを記述します。

ingress.yaml

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: demo
  annotations:
    kubernetes.io/ingress.class: kong
    konghq.com/strip-path: "true"
spec:
  rules:
  - http:
      paths:
      - path: /first
        backend:
          serviceName: nginx-first
          servicePort: 8081
      - path: /second
        backend:
          serviceName: nginx-second
          servicePort: 8082

今回ちょっとだけトリッキーなのはnginxのコンテキストルートはも問題で、/firstなどのパスがそのままアップストリームにプロキシされると困るのでkonghq.com/strip-path: "true"を付与しました。

早速、リソースをApplyしてアクセスしてみます。

$ kubectl apply -f ingress.yaml 

$ curl -i $PROXY_IP/first
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 28
Connection: keep-alive
Server: nginx/1.19.6
Date: Sun, 24 Jan 2021 07:11:05 GMT
Last-Modified: Sun, 24 Jan 2021 05:57:51 GMT
ETag: "600d0c5f-1c"
Accept-Ranges: bytes
X-Kong-Upstream-Latency: 1
X-Kong-Proxy-Latency: 0
Via: kong/2.2.1

nginx-first-d6db6c668-wtmwh

$ curl -i $PROXY_IP/second
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 30
Connection: keep-alive
Server: nginx/1.19.6
Date: Sun, 24 Jan 2021 07:11:06 GMT
Last-Modified: Sun, 24 Jan 2021 05:59:05 GMT
ETag: "600d0ca9-1e"
Accept-Ranges: bytes
X-Kong-Upstream-Latency: 1
X-Kong-Proxy-Latency: 0
Via: kong/2.2.1

nginx-second-6b8d5c9696-2cj25

それぞれ、きちんとプロキシされているようですね。