Minikubeにkube-prometheusを使ってPrometheusとGrafanaをデプロイする

はじめに

別のことがしたくて、Kubernetesクラスタの監視をPrometheus + Grafanaで行っている環境が欲しくて、いろいろ調べてたらkube-prometheusというのを見つけて、スターも多そうだったのでちょっと触ってみようと思います。
なおこのブログで特に指定がない場合はコンテキストはminikubeが指定されているものとします。

kube-prometheusとは

kube-prometheusはKubernetesクラスターをPrometheusで監視を行ない簡単に運用していくくための諸々が用意されたアセットです。具体的には、以下のようなものが含まれます。

インストールしてみる

環境

動作環境は以下の通り

今回KubernetesクラスタはMinikubeを用いてローカルに構成します。そのVMはデフォルトのDockerを利用します。
ミドルウェア、OSのバージョンは以下のとおりです。

$ uname -srvmpio
Linux 5.4.0-70-generic #78-Ubuntu SMP Fri Mar 19 13:29:52 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


(Minikubeが動いているDocker)
$ docker version

(クライアントは省略)

Server: Docker Engine - Community
 Engine:
  Version:          20.10.5
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       363e9a8
  Built:            Tue Mar  2 20:16:15 2021
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          1.4.4
  GitCommit:        05f951a3781f4f2c1911b05e61c160e9c30eaa8e
 runc:
  Version:          1.0.0-rc93
  GitCommit:        12644e614e25b05da6fd08a38ffa0cfe1903fdec
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0


$ minikube version
minikube version: v1.18.1
commit: 09ee84d530de4a92f00f1c5dbc34cead092b95bc


$ 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: "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

クラスターを構築する

公式のやり方通りにMinikubeを用いてクラスタを作ってみたいと思います。
以下のコマンドを実行します。

 minikube start --kubernetes-version=v1.20.0 --memory=6g --bootstrapper=kubeadm --extra-config=kubelet.authentication-token-webhook=true --extra-config=kubelet.authorization-mode=Webhook --extra-config=scheduler.address=0.0.0.0 --extra-config=controller-manager.address=0.0.0.0

ここで、それぞれのオプションの意味は以下のようになっています。

  • bootstrapper:Bootstrapper(インストール前の環境をチェックするプログラム)の名前。デフォルトはkubeadm(今回明示的にやる必要は無かったかも)
  • --extra-config: key=valueのペアで各コンポーネントに渡される設定。value.で区切られており最初のパートは設定を追加するコンポーネント(kubelet, kubeadm, apiserver, controller-manager, etcd, proxy, schedulerなどが選択可能)、後のパートは渡す設定値を示すようです。具体的にはそれぞれのコンポーネントに対して以下のようなオプションを渡しています。
    • kubeletに対して渡すオプションとして--authentication-token-webhook=tureauthorization-mode=Webhookを渡しています(オプションの意味はこちら)。
    • schedulerに対して0.0.0.0にバインドするように設定
    • controller-managerに対して0.0.0.0にバインドするように設定

全体を通して、すごく重要そうなのはなさそうな印象ですね。ものによっては省略しても行けそうな気がしますが、変なところでつまりたくないので一旦公式に従っておきます。 ちなみに、それぞれのオプションの意味の詳細はここをご確認ください。 また、Kubernetesに関係するコンポーネントに関してはかこちらがよくまとまっています。

kube-prometheusはmetrics APIサーバーを含むたminikubeが持つmetrics-serverは不要となります。
以下のコマンドで、無効化されているか確認します。

$ minikube addons disable metrics-server
🌑  「metrics-server」アドオンは無効化されています

minikubeにkube-prometeusをインストールする

まず、動かしているクラスターのバージョンによって提供されているブランチを選択する必要があるようです。今回クラスターは1.20系で動かしているので、現状ではブランチはrelease-0.7HEADを使う必要がありそうです。その他詳細な対応表に関してはこちらを確認してください。
今回はrelease-0.7を利用したいと思います。
以下のようにソースコードをクローンしてきて、release-07ブランチをチェックアウトします。

$ git clone https://github.com/prometheus-operator/kube-prometheus.git

$ cd kube-prometheus/

$ git checkout -b release-0.7 origin/release-0.7

$ git branch
  main
* release-0.7

チェックアウトまでできたら、必要なコンポーネントクラスタにデプロイします。 kube-prometeusのセットアップは2段階で行なう必要があります。
* manifests/setupディレクトリに用意されているマニフェストでCRDs(Custom Resource Definition)とネームスペースを作成する * CDRとネームスペースができたらmanifests配下にあるリソースをデプロイする

具体的には以下のコマンドを実行します。

$ kubectl create -f manifests/setup
$ kubectl create -f manifests/

ちなみにkube-prometeusを削除したい場合は以下のコマンドをを実行します。

$ kubectl delete --ignore-not-found=true -f manifests/ -f manifests/setup

Grafanaのダッシュボードを確認する

デプロイされたGrafanaとPrometheusのダッシュボードにアクセスするためには以下のコマンドでポートフォワードを行います。

$ kubectl --namespace monitoring port-forward svc/prometheus-k8s 9090 & \
 kubectl --namespace monitoring port-forward svc/grafana 3000
Forwarding from 127.0.0.1:9090 -> 9090
Forwarding from [::1]:9090 -> 9090
Forwarding from 127.0.0.1:3000 -> 3000
Forwarding from [::1]:3000 -> 3000

Prometheusはhttp://localhost:9090、Grafanaはhttp://localhost:3000でそれぞれアクセスできます。

Grafanaの画面にアクセスするとデフォルトで諸々の設定を行ってくれたダッシュボードを確認することができます。

f:id:yuya_hirooka:20210330164920p:plain

f:id:yuya_hirooka:20210330165215p:plain

NginxのPodをデプロイしてメトリクスを見てみる

最後に1つネームスペースを作ってNginxのPodをデプロイしてみて、可視化されたメトリクスを確認してみたいと思います。

こちらマニフェストを使ってNginxのPodを1つのレプリケイトで作るDeploymentを作成します。
まずは、ネームスペースを作成します。

$ kubectl create ns nginx-ns

次にDeoloymentを作成します。

$ kubectl -n nginx-ns apply -f https://raw.githubusercontent.com/samuraiball/settings/master/kubernetes/double-nginx-sample/deployment.yaml
deployment.apps/nginx-first created
deployment.apps/nginx-second created

$ kubectl -n nginx-ns get po
NAME                            READY   STATUS    RESTARTS   AGE
nginx-first-d6db6c668-mpbk7     1/1     Running   0          42s
nginx-second-6b8d5c9696-wdt75   1/1     Running   0          42s

この状態でGrafanaの左サイドメニューからSearchを選択し、「pod」で検索します。 検索結果一覧のなかからKubernetes / Compute Resources / Namespace (Pods)を選択します。
画面上部のnamespaceでnginx-nsを選択するとデプロイしたNginxのPodのメトリクスがきちんと

f:id:yuya_hirooka:20210330171048p:plain

MandrelでQuarkusのアプリをネイティブイメージ化する

はじめに

Quarkusのネイティブイメージ化したアプリを作ったことなかったのと、Mandrelという名前自体は聞いていたのですがQuarkusに関係するGraalVMぐらいの理解でしか無かったので、ちょっとまとめて動かしてみようかと思います。
基本的にはQuarkusのガイド(BUILDING A NATIVE EXECUTABLE)に従いつつやりますが、自分が気なったところを少しだけ深ぼってまとめるようにしようと思います。

Mandrelってなんぞ?

Oracle GraalVM Community Editionのダウンストリームに当たるGraalVMのディストリビューションの1つです。そのメインの目的としては、Quarkusのためにデザインされたネイティブイメージ化の方法を提供することにあります。基本的にアップストリームからの大きな変更はないようですが、Quakusのアプリに不要なものが取り除かれているようです。MandrelはOracle GraalVM CEと同じネイティブイメージ化の能力を提供しますが、polyglotなどのサポートが取り除かれているようです。Mandrelは現在、Linuxコンテナの環境におけるネイティブイメージのビルドだけの利用をが推奨されており、WindowsMacOSに対するネイティブイメージを作成する場合は、 Oracle GraalVMを利用することが推奨されるようです。
理解が足りてない部分があるかもですが、おそらくCI上でのビルドやDockerのマルチステージビルドなどでMandrelを使えば諸々のコストの削減になるのでは無いかと思われます。

ネイティブイメージ化してみる

環境

今回の動作環境は以下のとおりです。

$ 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)

$ uname -srvmpio
Linux 5.4.0-66-generic #74-Ubuntu SMP Wed Jan 27 22:54:38 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

Mandrelに関してはこのブログ内でインストールします。

Mandrelをインストールする

ダウンロードはGitHubここのページからおこなうことが可能です。
具体的には以下の手順でセットアップをおこなします。

$ cd ${YOUR_GRAALVM_INSTALL_DIR}
$ wget https://github.com/graalvm/mandrel/releases/download/mandrel-21.0.0.0-Final/mandrel-java11-linux-amd64-21.0.0.0-Final.tar.gz
$ tar -xf mandrel-java11-linux-amd64-21.0.0.0-Final.tar.gz
$ export JAVA_HOME="$( pwd )/mandrel-java11-21.0.0.0-Final"
$ export GRAALVM_HOME="${JAVA_HOME}"
$ export PATH="${JAVA_HOME}/bin:${PATH}"

QuarkusアプリのネイティブイメージのビルドはQuarkusのMavenラッパーを使って行いますが、その際に自身で指定するGraalVMでのビルドを行いたい場合はPathがとおっているGraalVMに対してnative-imageコマンドがインストールされている必要があります。
通常、native-imageコマンドはgu等を使ってインストールしないと行けなかった気がしますが、Mandrelの場合最初から内包されているようです。

Nativeイメージ化するプロジェクトの作成

Quarkus - Start coding with code.quarkus.ioを使って、 こんな感じの設定でプロジェクトを作成します。
この際にExample CodeはYes, Pleaseを選択肢します(自分でコード書いても良いのですが、ちょいめんどくいさいので)。

f:id:yuya_hirooka:20210310194526p:plain

Exampleのコードを生成を有効にしたので、GreetingResource.javaというハンドラーのコードが生成されているはずです。起動して、/hello-resteasyのパスにアクセスするとHello RESTEasyという文字列が返ってきます。

$ ./mvnw compile quarkus:dev

# 別ターミナルで
$ curl localhost:8080/hello-resteasy
Hello RESTEasy

作成されたプロジェクトのPomをちょっと見てみる

作成されたプロジェクトのPomを見てみると以下のようなプロファイルの設定が記述されているのが確認できます。

    <profile>
      <id>native</id>
      <activation>
        <property>
          <name>native</name>
        </property>
      </activation>
      <build>
        <plugins>
          <plugin>
            <artifactId>maven-failsafe-plugin</artifactId>
            <version>${surefire-plugin.version}</version>
            <executions>
              <execution>
                <goals>
                  <goal>integration-test</goal>
                  <goal>verify</goal>
                </goals>
                <configuration>
                  <systemPropertyVariables>
                    <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
                    <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
                    <maven.home>${maven.home}</maven.home>
                  </systemPropertyVariables>
                </configuration>
              </execution>
            </executions>
          </plugin>
        </plugins>
      </build>
      <properties>
        <quarkus.package.type>native</quarkus.package.type>
      </properties>
    </profile>

Maven Failsafe Pluginを用いたインテグレーションテストの設定が記述されています(詳しくは後述)。

ネイティブイメージをビルドする

Quarkusアプリをネイティブイメージでビルドする場合は以下のコマンドを用いて行います。

$ cd ${YOUR_PROJECT_DIR}
$ ./mvnw package -Pnative

この際にPathがとおっているGraalVMにnative-imageコマンドが無かった場合は、以下のようなログを出力し、Dockerイメージをプルしてきてビルドを行ってくれます。

[WARNING] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Cannot find the `native-image` in the GRAALVM_HOME, JAVA_HOME and System PATH. Install it using `gu install native-image` Attempting to fall back to container build.

ビルドが完了するとデフォルトではtarget配下に実行可能なバイナリが${project.artifactId}-${project.version}-runnerの名前でできています。 今回作成されたバイナリは以下のように実行することができます。

$ ./target/native-image-1.0.0-SNAPSHOT-runner 
__  ____  __  _____   ___  __ ____  ______ 
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ 
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \   
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/   
2021-03-10 21:12:31,788 INFO  [io.quarkus] (main) native-image 1.0.0-SNAPSHOT native (powered by Quarkus 1.12.1.Final) started in 0.030s. Listening on: http://0.0.0.0:8080
2021-03-10 21:12:31,789 INFO  [io.quarkus] (main) Profile prod activated. 
2021-03-10 21:12:31,789 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy]

ネイティブイメージを使ったテストを実行する

ネイティブイメージ化すると、jarでビルドする際と比較して予想しない問題が起こることがありえます。そのため、ネイティブイメージで動くアプリのインテグレーションテストを行なって置くことが推奨されます。

今回は、Exampleコードの生成を有効化しているためデフォルトで以下のようなNativeGreetingResourceITというクラスが生成されていると思います。

import io.quarkus.test.junit.NativeImageTest;

@NativeImageTest
public class NativeGreetingResourceIT extends  {

    // Execute the same tests but in native mode.
}

@NativeImageTestを付与されたテストクラスにインテグレーションテストを記述しておくと、前述した、PomのMaven Failsafe Pluginの設定で指定されるネイティブイメージを利用したテストを実施することが可能です。

ExampleではGreetingResourceTestを拡張しており、このクラスはGreetingResourceをテストがRest Assuredで行われています。 このテストを実行されるには以下のコマンドを実行します。

$  ./mvnw verify -Pnative

このコマンドを実行すると、ネイティブイメージのビルドが行われ、その後、そのイメージを使ったテストが実施されます。

Goのembedを使う

はじめに

Goの1.16からembed packageがcoreライブラリに追加されています。結構面白い感じの機能だったのでちょっと試してみようかと思います。

go enbedとは

embedを利用すると静的ファイルをGoのプログラムに埋め込み、そこに対するアクセスを提供してくれます。embedでは以下の3つの形式でファイルを読み込むことができます

  • string
  • byte[]
  • FS

ファイルの読み込みはパッケージのディレクトリかもしくはそのサブディレクトリから読み込むことができます。

使ってみる。

環境

動作環境は以下です。

$ go version
go version go1.16 linux/amd64

$ uname -srvmpio
Linux 5.4.0-66-generic #74-Ubuntu SMP Wed Jan 27 22:54:38 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

単体のテキストファイルをstringやbyte[]として読み込む

まずは基本的な使い方として単体のテキストファイルの読み込みをしてみます。
//go:embedディレクティブをコメントとして記述することで利用することができます。
まずは以下のようなテキストファイルをembedディレクティブを記述するプログラムと同じファイル階層に置いておきます。

hello.txt

hello, world.
this is embedded.

このテキストファイルをembedでstringとして、読み込むためには以下のようにします。

package main

import (
    _ "embed"
    "fmt"
)

//go:embed hello.txt
var hello string

func main() {
    fmt.Println(hello)
}

実行してみます。

$ go build -o hello_embed
$ ./hello_embed 
hello, world.
this is embedded.

注目ポイントはふたつで、まずはembedを利用するためにパッケージのブランクインポートが必要です。
もう一つのポイントはembedディレクティブは関数の外側でパッケージグローバルに定義しておく必要があるということで例えばプログラムを以下のように変更するとエラーで落ちます。

func main() {
    //go:embed hello.txt
    var hello string
    fmt.Println(hello)
}
$ go build -o hello_embed
# github.com/samuraiball/go-sandbox
./main.go:10:4: go:embed cannot apply to var inside func

読み込んだファイルを[]byteで受け取りたい場合は単に以下のように書き換えるだけで大丈夫です。

//go:embed hello.txt
var hello []byte

func main() {
    fmt.Println(hello)
    fmt.Println(string(hello))
}

実行結果

[104 101 108 108 111 44 32 119 111 114 108 100 46 10 116 104 105 115 32 105 115 32 101 109 98 101 100 100 101 100 46]
hello, world.
this is embedded.

複数ファイルの読み込み

1つ以上の複数ファイルを読み込みたい場合はembed.FSを受け取りの型として利用することができます。
例えばhtmlディレクトリをembedを行なうプログラムファイルと同じ階層に作り以下の2つを用意します。

index1.html

<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello, Embed files 1</h1>
</body>
</html>

index2.thml

<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello, Embed files 2</h1>
</body>
</html>

まずindex1.htmlと、index2.htmlを読み込むためには以下のようにします。

//go:embed html/*
var html embed.FS

func main() {

    htmlBytes, err := html.ReadFile("html/index1.html")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf(string(htmlBytes))

}

前述している通り、embed.FSで受け取ります。そして、個別のファイルを受け取る場合は[func (f FS) ReadFile(name string) ([]byte, error)](https://golang.org/pkg/embed/#FS.ReadFile)を利用し、読み込みたいディレクトリ/ファイル名を文字列で渡すと利用することができます。この戻り値は []byteになります。

実行結果は以下の通りになります。

<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello, Embed files 1</h1>
</body>
</html>
----------------------------------------------------
<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello, Embed files 2</h1>
</body>
</html>

ここで、ファイルの読み込みはpath.Matchパターンでファイルを読み込むことができます例えば//go:embed html/*1.htmlに書き換えて先程のコードを実行すると、index1.htmlが読み込まれるようになるため、index2, err := html.ReadFile("html/index2.html")のところで落ちるようになります。

2021/03/05 19:12:12 open html/index2.html: file does not exist

また、embed.FSには他にも、func (f FS) Open(name string) (fs.File, error)func (f FS) ReadDir(name string) ([]fs.DirEntry, error)も定義されており、それぞれfs.Fileでファイルを読み込んだり、[]fs.DirEntryを読み込んだりすることができます。

複数ディレクトリからまとめてファイルを読み込む

embedは複数のディレクトリからファイルを読み込むことも可能です。
先程のindex1.htmlindex2.htmlをそれぞれhtml1html2ディレクトリを作成して格納し、それらを読み込みたい場合は以下のようにします。

//go:embed html1/* html2/*
var html embed.FS

func main() {

    index1, err := html.ReadFile("html1/index1.html")
    // エラーハンドリング省略

    index2, err := html.ReadFile("html2/index2.html")
    // エラーハンドリング省略

    fmt.Print(string(index1))

    fmt.Println()
    fmt.Printf("----------------------------------------------------")
    fmt.Println()

    fmt.Print(string(index2))
}

実行結果は先ほどと同じです。

<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello, Embed files 1</h1>
</body>
</html>
----------------------------------------------------
<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello, Embed files 2</h1>
</body>
</html>

gockでHTTPリクエストをMockする

はじめに

最近はGo言語でWebアプリを書く際に使う諸々のライブラリを試してみているのですが、今はMockサーバをいろいろ見ていました。
Goの場合は標準ライブラリでもhttptestでいろいろ用意されていているみたいです。ただちょっと、そのまま使うには手間が多そうに感じ、ほかを探してるとgockというのが良さげだったのでちょっと試してみようと思います。

gockとは

Go製のHTTPのMockライブラリーです。
以下のような特徴があります。

  • 宣言的なMockの定義
  • ビルトインのJSON/XMLのヘルパー
  • Mockとリアルワールドの切り替え機能
  • gentlemanなどのクライアントとの連携機能

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).より前のリクエストのパターンを構成するビルダーのメソットチェインの中で、それぞれMatchHeaderMatchBodyJSONJsonのリクエストボディを受け取る場合)、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 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

参考資料