「Docker」カテゴリーアーカイブ

TensorFlowのチュートリアルをDockerで動かしてみる

AIOpsなどの波に押し寄せられ、機械学習について最初の一歩を恐る恐る踏み出してみました。これから始める方のために一応残しておきます。

  • TensorFlowとは
  • TensorFlowをローカルで動かす
  • TensorFlowのチュートリアルを試す

TensorFlowとは


公式ドキュメントでは、以下の通り記載があります。

TensorFlow は、機械学習向けに開発されたエンドツーエンドのオープンソース プラットフォームです。
引用元:https://www.tensorflow.org

Machine Learning(ML)を使ったアプリケーションを開発・デプロイするために作成されました。

TensorFlowをローカルで動かす


TensorFlowをローカルで動かすには、以下二通りの方法があります。本記事ではリンク先を参考に、コンテナとして実行する方法で動かします。

  • pipコマンドによるインストール
  • Dockerによるコンテナ実行

1. コンテナイメージをpullする


以下のコマンドでTensorFlowのコンテナイメージを取得します。

$ docker pull tensorflow/tensorflow:nightly-py3-jupyter

コンテナイメージにおけるタグ情報のオプションはリンク先をご参照ください。ここでは、python3のサポートとJupyterを含むナイトリービルド(最新ソースコード)のコンテナイメージを取得しています。

ちなみにJupyterとは、プログラミング言語をウェブブラウザ上で実行できるドキュメントツールです。ソースコードと説明文が混在したドキュメントを作成し、ソースコードとその実行結果をインタラクティブに表示することができます。

2. コンテナを起動する


以下のコマンドで、手順1で取得したコンテナイメージからコンテナを起動します。

$ docker run -it -p 8888:8888 tensorflow/tensorflow:nightly-py3-jupyter
[I 14:54:21.897 NotebookApp] Writing notebook server cookie secret to /root/.local/share/jupyter/runtime/notebook_cookie_secret
jupyter_http_over_ws extension initialized. Listening on /http_over_websocket
[I 14:54:22.459 NotebookApp] Serving notebooks from local directory: /tf
[I 14:54:22.459 NotebookApp] The Jupyter Notebook is running at:
[I 14:54:22.461 NotebookApp] http://453c162dd9e3:8888/?token=d233277b7f66020bbc79ea155a906ed4c41fc3846634511d
[I 14:54:22.461 NotebookApp]  or http://127.0.0.1:8888/?token=d233277b7f66020bbc79ea155a906ed4c41fc3846634511d
[I 14:54:22.463 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
[C 14:54:22.471 NotebookApp] 
    
    To access the notebook, open this file in a browser:
        file:///root/.local/share/jupyter/runtime/nbserver-1-open.html
    Or copy and paste one of these URLs:
        http://453c162dd9e3:8888/?token=d233277b7f66020bbc79ea155a906ed4c41fc3846634511d
     or http://127.0.0.1:8888/?token=d233277b7f66020bbc79ea155a906ed4c41fc3846634511d

3. ブラウザからログインする


標準出力されたOr copy and paste one of these URLs配下のURLをコピーしてブラウザアクセスすると、以下の画面が表示されます。

TensorFlowのチュートリアルを試す


Dockerで動かしたJupyterを使って、TensorFlowのチュートリアル(分類問題の初歩)を試してみます。このチュートリアル自体は、機械学習のHello Worldに該当するそうです。

1. ipynbファイルを表示する


「tensorflow-tutorials」→「classification.ipynb」をクリックします。

2. 「Run」ボタンをクリックする


「Run」ボタンをクリックし、上から順にソースコードを実行していきます。それぞれの出力結果については、チュートリアルに記載されているので省略します。

チュートリアルで実施している内容をざっくり要約すると以下になります。

以上です。チュートリアルを一通りやるだけでも勉強になりました。

謎のgo starting container process caused “exec: “command”: permission denied”: unknownにハマった話

  • 事象
  • 解決方法
  • 原因について考えてみる

事象


こちらの記事で、SwaggerEditorで生成したGo-Serverをコンテナで動かすDockerfileを作成しました。

FROM golang:latest
# コンテナ作業ディレクトリの変更
WORKDIR /go/src/api
# モジュールのダウンロード
RUN go get -u github.com/gorilla/mux &&\
    go get -u "go.mongodb.org/mongo-driver/mongo"
# ホストOSの ./src の中身を作業ディレクトリにコピー
COPY ./src .
# go build
RUN go build -o api
# API実行コマンドの実行
CMD ["./api"]

ディレクトリ構造は以下の通りです。

$ tree api/
api/
├── Dockerfile
└── src
    ├── api
    │   └── swagger.yaml
    ├── go
    │   ├── README.md
    │   ├── api_user.go
    │   ├── logger.go
    │   ├── model_user.go
    │   └── routers.go
    └── main.go

上記のDockerfileをビルドして実行すると、以下のエラーが発生しました。このようなエラーは大抵ユーザー権限がない場合に出るのですが、rootユーザーでの実行のため不思議でした。

docker: Error response from daemon: OCI runtime create failed: container_linux.go:349: starting container process caused "exec: \"./api\": permission denied": unknown.

念のためgouserに権限を持たせて実行してみましたが、事象は同じでした。

FROM golang:latest
# gouserの作成
RUN useradd -m -s /bin/bash gouser
# コンテナ作業ディレクトリの変更
WORKDIR /go/src/api
RUN chown gouser:gouser /go/src/api
# モジュールのダウンロード
RUN go get -u github.com/gorilla/mux &&\
    go get -u "go.mongodb.org/mongo-driver/mongo"
# ホストOSの ./src の中身を作業ディレクトリにコピー
COPY --chown=gouser:gouser ./src .
# go build
RUN go build -o api &&\
    chown gouser:gouser api
# gouserに変更
USER gouser
# API実行コマンドの実行
CMD ["./api"]

解決方法


rootでの実行なのにエラーが出るので試行錯誤しましたが、結果的には以下の方法で解決しました。

解決方法:ビルドファイルの名前をapiから別の名前に変更する

FROM golang:latest
# コンテナ作業ディレクトリの変更
WORKDIR /go/src/api
# モジュールのダウンロード
RUN go get -u github.com/gorilla/mux &&\
    go get -u "go.mongodb.org/mongo-driver/mongo"
# ホストOSの ./src の中身を作業ディレクトリにコピー
COPY ./src .
# go build
RUN go build -o koratta-api
# API実行コマンドの実行
CMD ["./koratta-api"]

原因について考えてみる


よく考えると、apiという名前のディレクトリが同じ階層にありました。

ディレクトリ名をapi→yamlに変更して再度実行したところ、実行ファイルがapiでもエラーが解消されました。

$ mv src/api src/yaml

以上です。まさかこんなことにハマるなんて。。。

SwaggerEditorで生成したGo API Serverの処理を実装する

  • はじめに
  • APIの全体像
  • MongoDBの作成
    • Dockerfileの作成
    • Dockerビルド
  • Go-Serverの作成
    • APIの実装
      • CreateUserメソッドの実装
      • GetUserByNameメソッドの実装
      • go/api_user.goの全体
    • Dockerfileの作成
    • Dockerビルド
  • APIの確認
    • docker-compose.yamlの作成
    • コンテナの起動
    • curlによるAPIの確認

はじめに


SwaggerEditorはOpenAPI specを様々な言語に変換することができます。以下の記事では、SwaggerEditorを使って簡単なOpenAPI specを作成し、Go-Serverに変換するところまで紹介しました。

MacにSwaggerEditorをインストールしてOpenAPI specを実装する

しかし、あくまでもAPIの雛形であって、中身の処理が実装されているわけではありません。本記事では、上記の記事で作成したGo-Serverに処理を実装します。

APIの全体像


本記事で作成するAPIの全体像について紹介します。

APIの仕様は以下の通りです。DBにはMongoDBを利用し、apiとdbはそれぞれコンテナで起動します。

  • POST /v2/user/create・・・Context-TypeでJson形式のユーザー情報を受け付け、DBに保管します。
  • GET /v2/user/{username}・・・URLに指定したusernameとマッチするレコードをDBから取得し、表示します。

MongoDBの作成


Dockerfileの作成


MongoDBのDockerfileを作成します。

ディレクトリ構造は以下の通りです。

$ tree mongo/
mongo/
├── Dockerfile
├── init.sh
└── users.json

今回は揮発性のDBコンテナにします。コンテナ起動時に初期データ(users.json)を流し込むinit.shも併せて作成します。

1. 初期データの作成


コンテナ起動時に流し込む初期データをjson形式で作成します。

[
  { "username":	"koratta", "email": "koratta@example.com" },
  { "username":	"koratta2", "email": "koratta2@example.com" }
]

2. 初期実行ファイルの作成


1で作成したJsonファイルをMongoDBに保管するためのスクリプトinit.shを作成します。

mongoimport --authenticationDatabase admin --username root --password password --db api --collection users --drop --file /docker-entrypoint-initdb.d/users.json --jsonArray

3. Dockerfileの作成


init.shをコンテナ起動時に実行するmongoベースのDockerfileを作成します。コンテナ起動時に実行するため、/docker-entrypoint-initdb.dにコピーします。

FROM mongo:latest
COPY users.json /docker-entrypoint-initdb.d/
COPY init.sh /docker-entrypoint-initdb.d/

Dockerビルド


上記で作成したDockerfileをビルドし、コンテナイメージを作成します。

$ docker build -t api-mongo-db:latest mongo/

Go-Serverの作成


APIの実装


APIリクエストのルーティング情報は、go/routers.goに記述されています。

~~go/router.goより抜粋~~
var routes = Routes{ Route{ "Index", "GET", "/v2/", Index, }, Route{ "CreateUser", strings.ToUpper("Post"), "/v2/user/create", CreateUser, }, Route{ "GetUserByName", strings.ToUpper("Get"), "/v2/user/{username}", GetUserByName, }, }

/v2/user/createに対してPOSTされたらCreateUserメソッドを、/v2/user/{username}に対してGETされたらGetUserBynameメソッドを呼び出すようになっています。それぞれのメソッドはgo/api_user.goで定義されているので、それぞれを実装します。

CreateUserメソッドの実装


CreateUserメソッドを以下に示します。

func CreateUser(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")

	//ヘッダー情報の取得
	if r.Header.Get("Content-Type") != "application/json" {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	length, err := strconv.Atoi(r.Header.Get("Content-Length"))
	body := make([]byte, length)
	length, err = r.Body.Read(body)
	var insert_record User
	err = json.Unmarshal(body[:length], &insert_record)

	//DBへのInsert
	connect := getConnect()
	_, err = connect.Database("api").Collection("users").InsertOne(context.Background(), insert_record)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	w.WriteHeader(http.StatusOK)
}

9~11行目でPOSTされたJson形式のユーザー情報を取得しています。

	length, err := strconv.Atoi(r.Header.Get("Content-Length"))
	body := make([]byte, length)
	length, err = r.Body.Read(body)

12~13行目でUser構造体のinsert_recordに変換しています。

	var insert_record User
	err = json.Unmarshal(body[:length], &insert_record)

このUser構造体は、OpenAPI specから生成されていて、go/model_user.goに記述されています。

package swagger

type User struct {

	Username string `json:"username,omitempty"`

	Email string `json:"email,omitempty"`
}

16~17行目でMongoDBに保管しています。getConnectメソッドはMongoDBとの接続を確立するメソッドで、go/api_user.goの全体に載せています。

	connect := getConnect()
	_, err = connect.Database("api").Collection("users").InsertOne(context.Background(), insert_record)

GetUserByNameメソッドの実装


GetUserByNameメソッドを以下に示します。

func GetUserByName(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")

	//検索ユーザー情報の取得
	user := strings.Replace(r.URL.Path, "/v2/user/", "", 1)

	//user情報の取得
	var output User
	connect := getConnect()
	filter := bson.D{{"username", user }}
	err := connect.Database("api").Collection("users").FindOne(context.Background(), filter).Decode(&output)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	//Jsonの出力
	json.NewEncoder(w).Encode(output)

	w.WriteHeader(http.StatusOK)
}

5行目で検索条件となるユーザー名を取得しています。今回のAPIの仕様は、URLのパスに指定されたユーザー名の情報を表示するので、URLパスからユーザー名を取得しています。

user := strings.Replace(r.URL.Path, "/v2/user/", "", 1)

9~11行目で、URLに指定されたユーザー名を基にMongoDBから情報を取得しています。

	connect := getConnect()
	filter := bson.D{{"username", user }}
	err := connect.Database("api").Collection("users").FindOne(context.Background(), filter).Decode(&output)

go/user_api.goの全体


user_api.goの全体は以下の通りです。

/*
 * Koratta Test API
 *
 * This is sample api
 *
 * API version: 1.0.0
 * Contact: koratta@example.com
 * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
 */

package swagger

import (
	"net/http"
	"log"
	"time"
	"encoding/json"
	"context"
	"strconv"
	"strings"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"go.mongodb.org/mongo-driver/bson"
)

func CreateUser(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")

	//ヘッダー情報の取得
	if r.Header.Get("Content-Type") != "application/json" {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	length, err := strconv.Atoi(r.Header.Get("Content-Length"))
	body := make([]byte, length)
	length, err = r.Body.Read(body)
	var insert_record User
	err = json.Unmarshal(body[:length], &insert_record)

	//DBへのInsert
	connect := getConnect()
	_, err = connect.Database("api").Collection("users").InsertOne(context.Background(), insert_record)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	w.WriteHeader(http.StatusOK)
}

func GetUserByName(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")

	//検索ユーザー情報の取得
	user := strings.Replace(r.URL.Path, "/v2/user/", "", 1)

	//user情報の取得
	var output User
	connect := getConnect()
	filter := bson.D{{"username", user }}
	err := connect.Database("api").Collection("users").FindOne(context.Background(), filter).Decode(&output)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	//Jsonの出力
	json.NewEncoder(w).Encode(output)

	w.WriteHeader(http.StatusOK)
}

func getConnect() *mongo.Client{
	//MongoDBの認証情報
	credential := options.Credential{
		Username: "root",
		Password: "password",
	}
	//MongoDBへの接続
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	connect, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://db:27017").SetAuth(credential))
	if err != nil { log.Fatal(err) }

	return connect
}

Dockerfileの作成


Dockerfileを作成します。

ディレクトリ構造は以下の通りです。

$ tree api
api
├── Dockerfile
└── src
    ├── api
    │   └── swagger.yaml
    ├── go
    │   ├── README.md
    │   ├── api_user.go
    │   ├── logger.go
    │   ├── model_user.go
    │   └── routers.go
    └── main.go

src配下のファイルは、MacにSwaggerEditorをインストールしてOpenAPI specを実装するで作成したソースコードです。

Dockerfileは以下の通りです。

FROM golang:latest
# gouserの作成
RUN useradd -m -s /bin/bash gouser
# コンテナ作業ディレクトリの変更
WORKDIR /go/src/api
RUN chown gouser:gouser /go/src/api
# モジュールのダウンロード
RUN go get -u github.com/gorilla/mux &&\
    go get -u "go.mongodb.org/mongo-driver/mongo"
# ホストOSの ./src の中身を作業ディレクトリにコピー
COPY --chown=gouser:gouser ./src .
# go build
RUN go build -o koratta-api &&\
    chown gouser:gouser api
# gouserに変更
USER gouser
# API実行コマンドの実行
CMD ["/go/src/api/koratta-api"]

必要なモジュールのgorilla/muxとmongo-driver/mongoをインストールし、gouserに実行権限を与えてgouserで実行するようにしています。root権限でも当然実行できます。

注意点は、main.goのimportパッケージのsw "go"部分です。コンテナの場合デフォルトで$GOPATH=/go/srcなので、上記のDockerfileの場合sw "api/go"になります。

APIの確認


docker-compose.yamlの作成


MongoDBとAPIのコンテナを起動するためのdocker-compose.yamlを作成します。

version: '3'
services:
  db:
    container_name: db
    image: api-mongo-db:latest
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: password
    ports:
      - "27017:27017"
  api:
    container_name: api
    image: api:latest
    depends_on:
      - db
    links:
      - db
    ports:
      - "8080:8080"

コンテナの起動


docker-composeを使ってコンテナを起動します。

$ docker-compose up -d

curlによるAPIの確認


作成したAPIの挙動を確認します。

初期データとして保管されているレコードをAPI経由で確認すると、以下の通り、指定したユーザー名に該当する情報を取得できます。

$ curl -X GET "http://localhost:8080/v2/user/koratta"
{"username":"koratta","email":"koratta@example.com"}

保管されていないユーザーは、当然取得できません。

$ curl -X GET "http://localhost:8080/v2/user/koratta5
$

従って、APIを使ってユーザー情報を保管します。

$ curl -X POST "http://localhost:8080/v2/user/create" -H "accept: application/xml" -H "Content-Type: application/json" -d "{ \"username\": \"koratta5\", \"email\": \"koratta5@example.com\"}"

再度確認すると、ユーザー情報を確認することができます。/v2/user/createのAPI経由でDBに保管できていることが確認できました。

$ curl -X GET "http://localhost:8080/v2/user/koratta5"
{"username":"koratta5","email":"koratta5@example.com"}

以上です。

Istio使ってURLパス単位でリクエスト処理を分けてみる

Istioを使ってURLパス単位で、コンテナアプリケーションを割り振ってみます。

  • コンテナアプリケーションを作成する
    • ソースコードを作成する
    • helloaコンテナイメージを作成する
    • hellobコンテナイメージを作成する
  • コンテナアプリケーションを実行する
  • Istioの設定をする
    • Gatewayを作成する
    • VirtualServiceを作成する
  • 設定を確認する

コンテナアプリケーションを作成する


Istioの割り振り先コンテナアプリケーションを作成します。

今回作成するのは、httpリクエストをポート8080で受け付けると以下の振る舞いを行うアプリケーションです。

    • / ・・・”Hello A” or “Hello B”を表示する
    • /hello ・・・”Hello A” or “Hello B”を表示する

ソースコードを作成する


コンテナアプリケーションで使うソースコードをgolangで作成します。

main.goを以下に示します。マックスハンドラを使って、URL毎に使うhtmlファイルを分けています。

package main

import (
    "log"
    "net/http"
    "html/template"
    "fmt"
)

func main() {
    //index関数をルートディレクトリに指定
    http.HandleFunc("/", index)
    //hello関数をルートディレクトリに指定
    http.HandleFunc("/hello", hello)
    //サーバー起動
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal("ListenAndServe:", nil)
    }
}

func index(writer http.ResponseWriter, r *http.Request) {
  generateHTML(writer, "index")
}

func hello(writer http.ResponseWriter, r *http.Request) {
  generateHTML(writer, "hello")
}

func generateHTML(writer http.ResponseWriter, filenames ...string) {
    var files []string
    for _, file := range filenames {
        files = append(files, fmt.Sprintf("templates/%s.html", file))
    }

    templates := template.Must(template.ParseFiles(files...))
  templates.Execute(writer, nil)
}

main.goを実行するコンテナをビルドするためのDockerfileを以下に示します。

FROM golang:latest

# コンテナ作業ディレクトリの変更
WORKDIR /go/src/web
# ホストOSの ./src の中身を作業ディレクトリにコピー
COPY ./src .
RUN go build -o web
# ウェブアプリケーション実行コマンドの実行
CMD ["./web"]

ディレクトリ構造は以下の通りです。index.htmlとhello.htmlはhelloa, hellobアプリケーション毎に作成します。

$ tree .
.
├── Dockerfile
└── src
    ├── main.go
    └── templates
        ├── hello.html
        └── index.html

helloaコンテナイメージを作成する


httpリクエストを受け付けると”Hello A”を表示するhelloaコンテナイメージを作成します。

以下のコマンドを使って、”Hello A”を表示するindex.htmlとhello.htmlを作成します。

$ echo "Hello A" > index.html
$ echo "Hello A" > hello.html

以下のコマンドでコンテナイメージをビルドして、docker registryに登録します。このdocker registryはMicroK8s上にコンテナとして動いています。docker registryの起動方法は、MicroK8sでアプリケーションを動かすを参考にしてください。

$ docker build -t helloa:latest .
$ docker tag helloa 192.168.64.2:32147/helloa
$ docker push 192.168.64.2:32147/helloa

hellobコンテナイメージを作成する


httpリクエストを受け付けると”Hello B”を表示するhelloaコンテナイメージを作成します。

以下のコマンドを使って、”Hello B”を表示するindex.htmlとhello.htmlを作成します。

$ echo "Hello B" > templates/index.html 
$ echo "Hello B" > templates/hello.html

以下のコマンドでコンテナイメージをビルドして、docker registryに登録します。

$ docker build -t hellob:latest .
$ docker tag hellob 192.168.64.2:32147/hellob
$ docker push 192.168.64.2:32147/hellob

コンテナアプリケーションを実行する


作成したコンテナイメージからコンテナアプリケーションを実行します。

以下のコマンドを使って、Injection済みのnamespaceにhelloaとhellobを実行し、サービスを公開します。Injectionの方法は、Microk8sでIstioによるABテストを試してみる(1/2)を参照してください。

ubuntu@microk8s-vm:~$ microk8s.kubectl run helloa --image=localhost:32147/helloa:latest --port=8080 -n istio-app
pod/helloa created
ubuntu@microk8s-vm:~$ microk8s.kubectl run hellob --image=localhost:32147/hellob:latest --port=8080 -n istio-app
pod/hellob created
ubuntu@microk8s-vm:~$ microk8s.kubectl expose pod helloa --port=8080 -n istio-app
service/helloa exposed
ubuntu@microk8s-vm:~$ microk8s.kubectl expose pod hellob --port=8080 -n istio-app
service/hellob exposed

Istioの設定をする


Gatewayを作成する


全てのホスト名をポート80で受け付けるIstioのGatewayを作成します。

yamlファイルを以下に示します。

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: hello-gateway
spec:
  selector:
    istio: ingressgateway # use Istio default gateway implementation
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*"

以下のコマンドでgatewayリソースを作成します。

ubuntu@microk8s-vm:~$ microk8s.kubectl apply -f gateway.yaml -n istio-app

VirtualServiceを作成する


/helloのリクエストはhellobに、それ以外はhelloaに割り振るVirtualServiceを作成します。

VirtualService全体のyamlファイルを以下に示します。match.uri.prefixを使うことでURL毎に割り振り先を変更できます。

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: hello-switch-url-route
spec:
  hosts:
  - "*"
  gateways:
  - hello-gateway
  http:
  - match:
    - uri:
        prefix: /hello
    route:
    - destination:
        host: hellob
        port:
          number: 8080
  - route:
    - destination:
        host: helloa
        port:
          number: 8080

設定を確認する


以下のcurlコマンドを利用して確認すると、正しくルーティングされていることがわかります。192.168.64.2:31380はIstio Ingressgatewayの公開Serviceで、[multipass-vmのIP]:[NodePort]です。IngressgatewayをNodePortにする方法は、MicroK8sでIstioによるABテストを試してみる(2/2)を参照してください。

$ curl -s http://192.168.64.2:31380/
Hello A
$ curl -s http://192.168.64.2:31380/hello
Hello B

 

以上です。ページなどモジュール単位で分けるようなシステムには使えそうですね!

Jeagerって何?

Jeagerの概要を調査したので、記事を描いてみました。

  • 分散トレーシング
    • 分散トレーシングとは
    • SpanとTraceの関係
    • Propagation
  •  Jeager
    • Jeagerとは 
    • Jeagerを操作してみる 

分散トレーシング


分散トレーシングとは


分散トレーシングとは、マイクロサービスのような分散されたシステムにおいて、障害の検知や原因特定を迅速化するために考えられた、サービス間通信の監視を行う概念です。

SpanとTraceの関係


分散トレーシングの基本概念であるSpanとTraceの関係について以下に示します。

  • Span・・・サービス単位の処理
  • Trace・・・任意のリクエストにおけるSpanの集合体

Propagation


分散トレーシングでは、Span同士でメタデータを伝播(Propagation)させています。以下のように、HttpヘッダーにSpanのメタデータを埋め込み、Spanの関係を把握しています。

参照元)https://github.com/openzipkin/b3-propagation#overall-process

Jeager


Jeagerとは


分散トレーシングを体現したツールは、Zipkin、Hawkularなど様々あります。Jeagerはその中の一つで、Uber社がGo言語で開発した分散トレーシングシステムです。

CNCFにホストされたのがきっかけで、分散トレーシングシステムの代表例でもあります。IstioをOperatorでインストールする場合は、Jeagerも一緒にインストールすることができ、Operatorによって構成されます。

アーキテクチャは以下の通りです。jeager-clientが取得したログをjeager-agentを経由してjeager-collectorが受け取り、DBにログを保管します。ユーザーはGUIを通して、このDBを参照することができます。

参照元)https://www.jaegertracing.io/docs/1.8/architecture/

Jeagerを操作してみる 


以下の記事で、MicroK8sにIstioをインストールした時に、一緒にインストールされていたJeagerを使って簡単に確認したいと思います。

MicroK8sでistioによるABテストを試してみる(1/2)
MicroK8sでistioによるABテストを試してみる(2/2)

アクセスするとこんな画面でした。キャラクターが可愛い件について。

適当に検索してみると、トレースログ毎に発生日時とレスポンスタイムのグラフに表示されていました。また、それぞれのトレースログの詳細が確認できますし、トレースログ同士を比較する機能までついていました。

トレースログをクリックすると、上で説明したようなTraceとSpanが確認できました。

Httpリクエストヘッダーも確認することができました。

右上の「Trace Timeline」を「Trace Graph」に変更すると、以下のように依存関係が確認できました。

以上です。本当に概要だけですが、参考になれば幸いです。

LinuxでDockerのネットワークサブネットを変更する

LinuxでDockerのネットワークサブネットを変更する方法について紹介します。

  • Dockerのネットワークサブネットを考慮する理由

  • Dockerのネットワークサブネットを変更する方法

Dockerのネットワークサブネットを考慮する理由


dockerサブネットを気にしたことはありますか?
仮想的にアサインされるので外の機器に影響を与えることはないのですが、外のサブネットと重複する場合は少し考慮が必要です。

企業のシステムだと、Proxyサーバーを経由してインターネットへアクセスすることがあります。その時、docker.ioなどインターネットからベースイメージを取得したい場合、dockerでもproxyを指定することになります。しかし、以下のようにdockerサブネットとProxyサーバーのIPアドレスが重複する場合、docker.ioへ到達することができません。ProxyサーバーのIPアドレスを変える訳にもいかないので、dockerのサブネットを変更しなければなりません。

Dockerのネットワークサブネットを変更する方法


ここからDockerのネットワークサブネットを変更する方法について紹介します。と言ってもすぐできます。

以下のコマンドで/lib/systemd/system/docker.serviceを開き、オプション--bip=XXX.XXX.XXX.XXX/XXを追記した後、dockerをリスタートします。

$ vi /lib/systemd/system/docker.service
→「--bip=XXX.XXX.XXX.XXX/XX」を追記
$ systemctl daemon-reload
$ systemctl restart docker

 

以上です!私が実際に仕事で経験した小ネタでした。皆さんもご注意ください。

GoとMongoDBのコンテナでフロントとバックエンドを分けてみる

GoとMongoDBを使って、DBのデータを表示する簡単なWebアプリケーションを作成します。本記事は、過去に投稿したGoコンテナでフロントとバックエンドを分けてみるで作成したWebアプリケーションのデータ格納先をMongoDBにしたものです。

以下の流れで紹介します。

  • dbコンテナを作成する
  • searcherコンテナを作成する
    • ソースコードを作成する
    • コンテナイメージを作成する
  • webコンテナを作成する
  • アプリケーションを起動する

dbコンテナを作成する


以下の記事で作成したMongoDBのコンテナを利用します。

mongodbコンテナの起動時に初期データを登録してみる

コンテナ起動時に、recommendデータベースcategoriesコレクションに以下のデータが保管されています。

[
  { "name": "スポーツ" },
  { "name": "文化" },
  { "name": "娯楽" },
  { "name": "食事" },
  { "name": "お酒" },
  { "name": "イベント" },
  { "name": "ドライブ" },
  { "name": "文化" }
]

searcherコンテナを作成する


dbコンテナからデータを取得し、json形式で出力するsearcherコンテナを作成します。

ソースコードを作成する


MongoDBからデータを取得し、json形式で出力するgolangを作成します。
MongoDBからデータを取得する方法は、mongodbからデータを抽出するgolangを書くで解説しています。

package main

import (
    "fmt"
    "log"
    "net/http"
    "encoding/json"
    "time"
    "context"
    "os"
    "github.com/gorilla/mux"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
    "go.mongodb.org/mongo-driver/bson"
)

type category struct {
    Categories []struct{
      Name string `json:"name"`
    }`json:"categories"`
}

func main() {
    r := mux.NewRouter()
    // localhost:8082/publicでpublicハンドラーを実行
    r.Handle("/public", public)

    //サーバー起動
    if err := http.ListenAndServe(":8082", r); err != nil {
        log.Fatal("ListenAndServe:", nil)
    }
}

var public = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    //MongoDBの認証情報
    credential := options.Credential{
      Username: "root",
      Password: "password",
    }

    //MongoDBへの接続
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    connect, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://db:27017").SetAuth(credential))
    defer connect.Disconnect(ctx)

    //collectionの取得
    collection, err := connect.Database("recommend").Collection("categories").Find(context.Background(), bson.M{})
    if err != nil { log.Fatal(err) }
    var results []bson.M
    if err = collection.All(context.TODO(), &results); err != nil {
      log.Fatal(err)
    }

    //取得したコレクションの_idエントリを削除
    for _, entry_map := range results {
      delete(entry_map, "_id")
    }

    //bson.Mのデータをjson形式に変換
    json_result, err := json.Marshal(results)
    if err != nil {
        fmt.Println("JSON marshal error: ", err)
        return
    }

    //category構造体にJson形式の値を代入
    var output category
    if err := json.Unmarshal(json_result, &output.Categories); err != nil {
        log.Fatal(err)
        os.Exit(1)
    }

    //読み込んだJsonの表示
    json.NewEncoder(w).Encode(output)

})

コンテナイメージを作成する


Dockerfileを書いてコンテナイメージをビルドします。

ディレクトリ構造は以下の通りです。

$ tree .
.
├── Dockerfile
└── src
    └── main.go

Dockerfileは以下の通りです。

FROM golang:latest

# コンテナ作業ディレクトリの変更
WORKDIR /go/src/searcher
# ホストOSの ./src の中身を作業ディレクトリにコピー
COPY ./src .
RUN go get -u github.com/gorilla/mux
RUN go get -u "go.mongodb.org/mongo-driver/mongo"
RUN go build -o searcher
# ウェブアプリケーション実行コマンドの実行
CMD ["./searcher"]

以下のコマンドで作成します。

$ docker build -t searcher:latest .

webコンテナを作成する


webコンテナは以下の記事で作成したwebコンテナを流用します。以下の記事でフロントとバックエンドを分けていたので、修正する必要がありません。

goコンテナでフロントとバックエンドを分けてみる

アプリケーションを起動する


docker-composeを記述してコンテナアプリケーションを実行します。

docker-composeは以下の通りです。

version: '3'
services:
  db:
    container_name: db
    image: mongo-db:latest
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: password
    ports:
      - "27017:27017"
  searcher:
    container_name: searcher
    image: searcher:latest
    depends_on:
      - db
    links:
      - db
    ports:
      - "8082:8082"
  web:
    container_name: web
    image: web:latest
    depends_on:
      - searcher
    links:
      - searcher
    ports:
      - "8080:8080"

以下のコマンドを実行して起動します。

$ docker-compose up -d

localhost:8080にアクセスすると以下の結果が得られます。

以上です!

MongoDBコンテナの起動時に初期データを登録してみる

MongoDBをちょっと検証で利用するのに便利な方法をご紹介します。
通常DBコンテナには永続ボリュームをアタッチしますが、DBへの接続方法をクイックに確認したい時は、その処理が煩わしく感じます。そんな時私は、コンテナ起動時に簡単なデータを登録して検証しています。
今回はMongoDBを使って、初期データを投入する方法を紹介します。

  • jsonファイルを作成する
  • jsonファイルを登録するスクリプトを作成する
  • コンテナイメージを作成する
  • コンテナを実行する

jsonファイルを作成する


登録するデータを配列構造で記述したjsonファイル(categories.json)を作成します。

[
  { "name": "スポーツ" },
  { "name": "文化" },
  { "name": "娯楽" },
  { "name": "食事" },
  { "name": "お酒" },
  { "name": "イベント" },
  { "name": "ドライブ" },
  { "name": "文化" }
]

jsonファイルを登録するスクリプトを作成する


コンテナ起動時に、categories.jsonのデータをMongoDBに登録するスクリプト(init.sh)を作成します。
recommendDBのcategoriesコレクションに、/docker-entrypoint-initdb.d/categories.jsonの配列構造データを登録しています。

mongoimport --authenticationDatabase admin --username root --password password --db recommend --collection categories --drop --file /docker-entrypoint-initdb.d/categories.json --jsonArray

mongoimportコマンドのドキュメントは以下URLになります。
https://docs.mongodb.com/manual/reference/program/mongoimport/
--dropは、データ登録時にそのコレクションがあった場合、一度削除してからデータを登録します。
--jsonArrayは、配列構造を扱うためのオプションです。

コンテナイメージを作成する


MongoDBを起動するためのDockerfileを作成します。
ビルド用ディレクトリ構造は以下の通りです。

$ tree .
.
├── Dockerfile
├── categories.json
└── init.sh

Dockerfileは以下の通りです。
mongoベースイメージを取得して、/docker-entrypoint-initdb.dに上記で作成したcategories.jsonとinit.shをコピーしています。/docker-entrypoint-initdb.dにスクリプトを配置することで、コンテナ起動時に実行されます。

FROM mongo:latest
COPY categories.json /docker-entrypoint-initdb.d/
COPY init.sh /docker-entrypoint-initdb.d/

以下のコマンドでコンテナイメージを作成します。

$ docker build -t mongo-test .
Sending build context to Docker daemon  4.096kB
Step 1/3 : FROM mongo:latest
 ---> 3f3daf863757
Step 2/3 : COPY categories.json /docker-entrypoint-initdb.d/
 ---> Using cache
 ---> 209a1bede01c
Step 3/3 : COPY init.sh /docker-entrypoint-initdb.d/
 ---> Using cache
 ---> 68ffc7b95b98
Successfully built 68ffc7b95b98
Successfully tagged mongo-test:latest

コンテナを実行する


コンテナを実行します。今回はdocker-compose.yamlを作成しました。

version: '3'
services:
  db:
    container_name: db
    image: mongo-test:latest
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: password
    ports:
      - "27017:27017"

以下のコマンドで実行します。

$ docker-compose up -d
Creating network "wishy_default" with the default driver
Creating db ... done

以下のコマンドで確認してみましょう。データが登録されていることがわかります。

$ docker exec -it db /bin/bash
root@897eb9caf5c4:/# mongo -uroot -ppassword
> show dbs
admin      0.000GB
config     0.000GB
local      0.000GB
recommend  0.000GB
> use recommend
switched to db recommend
> db.categories.find()
{ "_id" : ObjectId("5ecbe8257399a6df837e7384"), "name" : "スポーツ" }
{ "_id" : ObjectId("5ecbe8257399a6df837e7385"), "name" : "娯楽" }
{ "_id" : ObjectId("5ecbe8257399a6df837e7386"), "name" : "食事" }
{ "_id" : ObjectId("5ecbe8257399a6df837e7387"), "name" : "お酒" }
{ "_id" : ObjectId("5ecbe8257399a6df837e7388"), "name" : "文化" }
{ "_id" : ObjectId("5ecbe8257399a6df837e7389"), "name" : "ドライブ" }
{ "_id" : ObjectId("5ecbe8257399a6df837e738a"), "name" : "文化" }
{ "_id" : ObjectId("5ecbe8257399a6df837e738b"), "name" : "イベント" }
> 

以上です!

Goコンテナでフロントとバックエンドを分けてみる

今日はGo言語で簡単なwebアプリケーションを作成します。webとdbにコンテナを分けて、webページにdbから取得したデータを表示したいと思います。以下のように、マイクロサービスを意識して、コンテナ間通信をhttpリクエストで行いたいと思います。

以下の流れで作成します。

  • dbを作成する
  • webを作成する
    • ソースコードを作成する
      • main.goを作成する
      • layout.htmlを作成する
      • index.htmlを作成する
    • Dockerfileを作成する
  • コンテナを実行する
    • コンテナイメージの作成
    • Docker Composeファイルを作成する
    • コンテナを実行する

dbを作成する

 dbコンテナは以下の記事で作成したものを利用します。ここで作成したのは、httpリクエストを行うとjsonファイルを読み込んで出力するコンテナです。本来であればデータベースも作成しますが、今回はhttpリクエストでやり取りするのをメインにしているので、データはファイルにします。データベースを使うのはまた記事書きたいと思います。

go言語でjsonファイルを読み込んで表示してみる

webを作成する

ソースコードを作成する

main.goを作成する

 ポート8080で受け付けると、dbコンテナから取得したjsonコードをhtml形式で表示するwebのソースコード(main.go)を以下に示します。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "github.com/gorilla/mux"
    "io/ioutil"
    "os"
        "html/template"
)

type category struct {
    Categories []struct{
      Name string `json:"name"`
    }`json:"categories"`
}

func main() {
    r := mux.NewRouter()
    r.Handle("/", index)

    //サーバー起動
    if err := http.ListenAndServe(":8080", r); err != nil {
        log.Fatal("ListenAndServe:", nil)
    }
}

var index = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    url := "http://db:8082/public"
    req, _ := http.NewRequest("GET", url, nil)
    client := new(http.Client)
    resp, _ := client.Do(req)
    defer resp.Body.Close()
    byteArray, _ := ioutil.ReadAll(resp.Body)
    var categories category
    if err := json.Unmarshal(byteArray, &categories); err != nil {
            log.Fatal(err)
            os.Exit(1)
    }
    generateHTML(w, categories, "layout", "index")
})

func generateHTML(writer http.ResponseWriter, data interface{}, filenames ...string) {
    var files []string
    for _, file := range filenames {
        files = append(files, fmt.Sprintf("templates/%s.html", file))
    }

    templates := template.Must(template.ParseFiles(files...))
    templates.ExecuteTemplate(writer, "layout", data)
}

 index = http.HandlerFuncでは、dbコンテナから取得したjsonコードを構造体categoryの変数categoriesに代入し、generateHTML関数を呼び出します。generateHTMLは引数で与えられた名前のhtmlファイル、ここではlayout.htmlとindex.htmlを使ってページを生成します。

layout.htmlを作成する

 generateHTML関数は、layout.htmlとindex.htmlを使ってページを生成します。layout.htmlは以下の通り、body以外のheaderのみを記述しています。layoutを作成することで、それぞれのhtmlのheaderを統一し、ソースコードの重複を回避できます。

{{ define "layout" }}

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Test</title>
  </head>
  <body>
    {{ template "content" . }}
  </body>
</html>

{{ end }}

index.htmlを作成する

 このlayoutを使ったindex.htmlが以下になります。{{ define "content" }} と{{ end }}で囲んだhtml文がlayoutの{{ template "content" . }}、つまりはページの本文に記述されることになります。

{{ define "content" }}
  <center>
    {{ range .Categories }}
        {{ .Name }}<br>
    {{ end }}
  </center>
{{ end }}

Dockerfileを作成する

 コンテナイメージを作成するのに必要なDockerfileを作成します。ビルド用のディレクトリ構造は以下の通りです。

$ tree web
web
├── Dockerfile
└── src
    ├── main.go
    └── templates
        ├── index.html
        └── layout.html

 src配下に作成したソースコードを配置します。このディレクトリ構造に合わせて作成したDockerfileが以下になります。

FROM golang:latest

# コンテナ作業ディレクトリの変更
WORKDIR /go/src/web
# ホストOSの ./src の中身を作業ディレクトリにコピー
COPY ./src .
RUN go get -u github.com/gorilla/mux
RUN go build -o web
# ウェブアプリケーション実行コマンドの実行
CMD ["./web"]

コンテナを実行する

コンテナイメージを作成する

 上記で作成したwebとdbのコンテナイメージを以下のコマンドで作成します。

$ docker build -t web:latest web/
$ docker build -t db:latest db/

Docker Composeファイルを作成する

 webとdbのコンテナイメージからコンテナを実行しますが、webとdb間は通信できる必要があります。そこでdocker-compose.yamlファイルを作成します。以下にその内容を示します。

version: '3'
services:
  db:
    container_name: db
    image: db:latest
    ports:
      - "8082:8082"
  web:
    container_name: web
    image: web:latest
    depends_on:
      - db
    links:
      - db
    ports:
      - "8080:8080"

 docker-compose.yamlはservicesブロックの中に実行するコンテナ毎にブロックを作成します。今回はwebとdbコンテナだけなので、webブロックとdbブロックを作成しました。
 webのmain.goにurl := "http://db:8082/public"とurlを直接指定しました。従ってwebコンテナはホスト名dbをdbコンテナに名前解決できる必要があります。それを定義しているのがlinksです。

コンテナを実行する

 docker-compose.yamlファイルからコンテナを実行するにはdocker-composeコマンドを利用します。docker-compose.yamlを置いたディレクトリで以下のコマンドを実行します。

$ docker-compose up -d
Creating db ... done
Creating web ... done
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS              PORTS                    NAMES
da224d318f33        web:latest          "./web"             About a minute ago   Up About a minute   0.0.0.0:8080->8080/tcp   web
b245c4eb8320        db:latest           "./searcher"        About a minute ago   Up About a minute   0.0.0.0:8082->8082/tcp   db

 docker-compose.yamlで定義した内容で実行されていることがわかります。ブラウザでhttp://localhost:8080/にアクセスしてみましょう。jsonの中身が表示されていることがわかります。

以上です!なんとなくGo言語がわかってきたようなきてないような。。ちゃんと仕組みから理解した方が良さそうなので、またそういう記事も書きたいと思います!ソースコードが汚いのは許してください。。。