投稿者「yosk222」のアーカイブ

Ginを使ったGo API開発の初歩(cookie編)

以下の記事でGinをインストールしてGo APIの実装を始めました。今日はcookieを実装します。

Ginを使ったGo API開発の初歩

  • cookieとは
  • cookieの実装
  • APIの確認

cookieとは


cookieとは、ウェブブラウザに保存される情報のことを指します。

サイトにログイン後、再度同じサイトにアクセスするとログインした状態になっていることがあります。これはcookieとしてユーザー情報が保存されているからです。他にも閲覧したページに関連する広告を出したりと、様々な場面で利用されています。

本記事では、Ginによるcookie操作を確認します。

cookieの実装


Ginによるcookieの設定と取得には以下のメソッドを利用します。それぞれのメソッドの引数・戻り値の型については、gin-GoDocに記載されています。

  • cookieの設定・・・gin.Context.Cookie
  • cookieの取得・・・gin.Context.SetCookie

上記のメソッドを使ったcookie.goを以下に示します。

package main

import "github.com/gin-gonic/gin"

func main() {

    router := gin.Default()

		authorized := router.Group("/admin", gin.BasicAuth(gin.Accounts{
			"koratta":    "koratta",
			"koratta2":   "koratta2",
		}))

		authorized.GET("/login", func(c *gin.Context) {
			user := c.MustGet(gin.AuthUserKey).(string)
			c.SetCookie("user", user, 3600, "/", "localhost", false, true)
		})

		authorized.GET("/hello-cookie", func(c *gin.Context) {
			user, user_cookie_err := c.Cookie("user")
			if user_cookie_err != nil {
				user = "Guest"
			}
			c.JSON(200, gin.H{ "message": "Hello " + user })
		})

    router.Run()
}

/loginにログインしたユーザー情報をcookieに保存し、/hello-cookieにアクセスすると”user”cookieの値を表示します。違いがわかるように、直接/hello-cookieにアクセスした場合は”Guest”が表示されるようにしています。

16行目c.SetCookie("user", user, 3600, "/", "localhost", false, true)の引数について解説します。

  • 第一引数・・・cookieのKeyを指定します。ここでは”user”を指定しています。
  • 第二引数・・・第一引数のKeyに対応するValueを設定します。ここでは、string型変数userを設定しています。userは、15行目のc.MustGet(gin.AuthUserKey).(string)で、ログインしたユーザー名を代入しています。
  • 第三引数・・・cookie情報を保存する時間を指定します。ここでは1時間で設定しています。
  • 第四引数・・・cookie情報が扱える範囲のパスを指定します。ここではルートパスを指定しています。
  • 第五引数・・・cookie情報が扱えるドメインを指定します。ここではlocalhostで指定しています。
  • 第六引数・・・httpsでもcookieを利用するか指定します。trueの場合はhttpsでも利用できます。
  • 第七引数・・・httpリクエストのみcookieを利用できるようにするか指定します。trueの場合JavaScriptからcookieを利用することができません。trueにすることで、クロスサイトスクリプティング(XSS)攻撃を緩和することができます。

20行目のuser, user_cookie_err := c.Cookie("user")は、”user” KeyのValue値を取得しています。Cookieメソッドの戻り値はstring, errorと二つあります。

APIの確認


実際にAPIの挙動を確認してみます。

まずはlocalhost:8080/hello-cookieに直接アクセスします。以下の通り、cookieに保存していないので”Hello Guest”が返ってきます。

$ curl -u koratta:koratta localhost:8080/admin/hello-cookie
{"message":"Hello Guest"}

次に、localhost:8080/loginに一度アクセスしてから、localhost:8080/hello-cookieにアクセスします。/loginでユーザー情報をcookieに設定しているので、以下の通り”Hello koratta”が表示されます。

以上です。

Ginを使ったGo API開発の初歩(ルーターグループ化編)

以下の記事でGinをインストールしてGo APIの実装を始めました。ルーティングをグループ化することができるので、今日はその機能を実装してみたいと思います。

Ginを使ったGo API開発の初歩

  • ルーターグループ化とは
  • ルーターグループ化機能の実装
  • APIの確認

ルーターグループ化とは


Ginにはルーティングをグループ化する機能があります。

この機能によって、同じAPIでもルーティング毎に仕様を変更することができます。よく見かけるのは、API毎にバージョンを分ける使い方です。APIの仕様を突然変更すると、そのAPIを使って自動化しているシステムは影響を受けてしまいます。ユーザーが任意のタイミングで切り替えられるように、バージョン毎に提供することがよくあります。

ルーターグループ化機能の実装


サンプルコードを参考に、ルーターグループ化機能を実装します。

Groupメソッドを使用してv1, v2のルーテンググループを作成し、それぞれが返すmessageの値を変更します。クライアントは、Groupに指定されたURL(以下の場合は/v1,/v2)でルーティングを使い分けます。

package main

import "github.com/gin-gonic/gin"

func helloV1() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.JSON( 200, gin.H{ "message": "hello v1", } )
	}
}

func helloV2() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.JSON( 200, gin.H{ "message": "hello v2", } )
	}
}

func main() {
	r := gin.Default()
	v1 := r.Group("/v1")
	{
		v1.POST("/hello", helloV1() )
	}

	v2 := r.Group("/v2")
	{
		v2.POST("/hello", helloV2() )
	}
	r.Run()
}

APIの確認


ルーティング毎にAPIの出力が変わることを確認します。

$ go run router_group.go

別ターミナルで確認すると、以下の通り/v1/helloと/v2/helloでmessageの値が変わっていることがわかります。

$ curl --request POST localhost:8080/v1/hello
{"message":"hello v1"}
$ curl --request POST localhost:8080/v2/hello
{"message":"hello v2"}

ちなみに、GETリクエストは定義していないのでNotFoundが返ってきます。

$ curl --request GET localhost:8080/v2/hello
404 page not found

以上です。

Ginを使ったGo API開発の初歩(BindQuery編)

以下の記事でGinをインストールしてGo APIの実装を始めました。今日はBindQueryを導入し、フォームに応じて出力を変更するAPIを実装します。

Ginを使ったGo API開発の初歩

  • BindQueryとは
  • BindQueryの実装
  • APIの確認

BindQueryとは


BindQueryとは、golangにおいてクエリパラメータをバインドする機能です。

この機能によってフォームに応じた返答を行うことができます。例えば、日付をフォームに入力したら、その日付のデータのみ返すことができます。このようにBindQueryを使うことで、より使い勝手のいいAPIを作成できるようになります。

BindQueryの実装


BindQueryを実装します。

グループに所属するメンバーを表示するbind_query.goを作成します。存在するグループはgroup1とgroup2を用意し、クライアントはnameフォームの値にグループ名を入力することで表示するグループを切り替えます。指定したグループ名が存在しない場合は、httpリクエストステータスを400にして「There are no groups」を返します。

package main

import "github.com/gin-gonic/gin"

type Group struct {
	Name    string `form:"name"`
}

var GROUP1_MEMBER_LIST = [...] gin.H{ gin.H{"name": "koratta01"}, gin.H{"name": "koratta02"} }
var GROUP2_MEMBER_LIST = [...] gin.H{ gin.H{"name": "ratta01"}, gin.H{"name": "ratta02"} }

var GROUP_MEMBER_LIST = gin.H{
	"group1":  gin.H{ "member": GROUP1_MEMBER_LIST },
	"group2":  gin.H{ "member": GROUP2_MEMBER_LIST },
}

func main() {
	route := gin.Default()
	route.Any("/member", getGroupMember)
	route.Run(":8080")
}

func getGroupMember(c *gin.Context) {
	var group Group
	if c.BindQuery(&group) == nil {
		if member_list, exists := GROUP_MEMBER_LIST[group.Name]; exists {
			c.JSON(200, member_list)
		} else {
			c.String(400, "There are no groups")
		}
	}
}

APIの確認


作成したbind_query.goを実行してAPIを確認します。

$ go run bind_query.go

別ターミナルで以下のコマンドを実行すると、グループ名に所属するメンバーリストが取得できることを確認できます。

$ curl --request GET 'http://localhost:8080/member?name=group1'
{"member":[{"name":"koratta01"},{"name":"koratta02"}]}
$ curl --request GET 'http://localhost:8080/member?name=group2'
{"member":[{"name":"ratta01"},{"name":"ratta02"}]}
$ curl --request GET 'http://localhost:8080/member?name=group3'
There are no groups

bind_query.goのログを確認すると以下の通り、所属するグループだけステータスが200となっています。

[GIN] 2020/07/14 - 22:38:15 | 200 |     199.543µs |             ::1 | GET      "/member?name=group1"
[GIN] 2020/07/14 - 22:38:21 | 200 |      51.177µs |             ::1 | GET      "/member?name=group2"
[GIN] 2020/07/14 - 22:38:26 | 400 |      57.617µs |             ::1 | GET      "/member?name=group3"

Postで送信したときの挙動も確認すると、Getではメンバーリストが返ってきたグループ名でも「There are no groups」が返ってきています。これはBindQueryがPostデータをバインドしないからです。

$ curl --request POST 'http://localhost:8080/member?name=group3'
There are no groups
[GIN] 2020/07/14 - 22:45:10 | 400 |      56.774µs |             ::1 | POST     "/member?name=group3"

以上です。

Ginを使ったGo API開発の初歩(Basic認証編)

以下の記事でGinをインストールしてGo APIの実装を始めました。今日はBasic認証を導入し、ユーザーIDとパスワードが必要なAPIを実装します。

Ginを使ったGo API開発の初歩

  • Basic認証とは
  • Basic認証の実装
  • APIの確認

Basic認証とは


Basic認証とは、httpリクエストにおける認証システムです。以下の画面のようにユーザーIDとパスワードを求められたことがあると思います。あらかじめ登録されたユーザーIDとパスワードを入力するとアクセスできる仕組みです。

Basic認証の実装


実装サンプルを参考に、Basic認証を実装します。

リンク先のサンプルコードでは、認証されたユーザーの秘匿情報を返すように実装されていますが、今回はわかりやすく認証されたら適当な値を返すだけのbasic_auth.goを作成します。

package main

import "github.com/gin-gonic/gin"

func main() {
  r := gin.Default()

  authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{
    "koratta":    "koratta",
    "koratta2":   "koratta2",
  }))

  authorized.GET("/hello", func(c *gin.Context) {
    user := c.MustGet(gin.AuthUserKey).(string)
    c.JSON(200, gin.H{ "message": "Hello " + user })
  })

  r.Run(":8080")
}

gin.Accountsでは、[ユーザー名]:[パスワード]のJson形式で認証情報を定義しています。その認証情報と/adminプレフィックスによってauthorizedグループを作成します。このGroupからauthorized.Getで作成されたエンドポイントは、定義された認証情報を持つユーザーのみがアクセスできます。admin/helloエンドポイントは、ユーザー名をメッセージに追加してJsonフォーマットで表示します。

APIの確認


basic_auth.goを実行して、APIを確認します。

$ go run basic_auth.go 
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:	export GIN_MODE=release
 - using code:	gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /admin/hello              --> main.main.func1 (4 handlers)
[GIN-debug] Listening and serving HTTP on :8080

curlコマンドを使ってAPIにリクエストを行います。-uオプションを使うことでユーザー名とパスワードを入力することができます。以下のように、basic_auth.goに登録されているkoratta, koratta2のみリクエストが返ってきてることがわかります。

$ curl -u koratta:koratta localhost:8080/admin/hello
{"message":"Hello koratta"}
$ curl -u koratta2:koratta2 localhost:8080/admin/hello
{"message":"Hello koratta2"}
$ curl -u koratta3:koratta3 localhost:8080/admin/hello
$ 

Ginのログを確認しても、3回目にリクエストを行ったkoratta3のみ、ステータスコード401を返していることがわかります。

[GIN] 2020/07/12 - 16:23:51 | 200 |     116.147µs |             ::1 | GET      "/admin/hello"
[GIN] 2020/07/12 - 16:24:19 | 200 |        67.2µs |             ::1 | GET      "/admin/hello"
[GIN] 2020/07/12 - 16:24:28 | 401 |      52.325µs |             ::1 | GET      "/admin/hello"

 

Ginを使ったGo API開発の初歩

今日はGinを使って、簡単なGo APIを動かすところまでやってみたいと思います。初歩ということで、まずはインストールとQuick Startを実施します。

  • Ginとは
  • Ginのインストール
  • Ginを動かす

Ginとは


Ginとは、Golangで開発されたWebフレームワークで、パフォーマンスに優れたAPI機能を提供します。(GinのGitHubから引用しています。)

Gin is a web framework written in Go (Golang). It features a martini-like API with performance that is up to 40 times faster thanks to httprouter. If you need performance and good productivity, you will love Gin.

引用元:https://github.com/gin-gonic/gin#contents

日本語訳されたドキュメントが以下リンク先に用意されています。非常にありがたいです。。。

https://gin-gonic.com/ja/docs/

Ginのインストール


以下のコマンドでGinの必要なパッケージをインストールします。インストール後、ソースコードにimport "github.com/gin-gonic/gin"を記述して使えるようになります。

$ go version
go version go1.14.3 darwin/amd64
$ go get -u github.com/gin-gonic/gin

Ginを動かす


GinのQuick Startを実施して、httpリクエストを受け付けるとJsonを返すAPIを作成します。

リンク先のソースコードをコピーして、getting_start.goを作成します。

package main

import "github.com/gin-gonic/gin"

func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

実行すると以下のエラーが発生しました。

$ go run getting_start.go 
getting_start.go:3:8: cannot find module providing package github.com/gin-gonic/gin: working directory is not part of a module

ワークディレクトリにmoduleがないというエラーなので、以下のコマンドでモジュールを作成します。

$ go mod init
go: creating new go.mod: module gin
$ ls
getting_start.go	go.mod

再度実行します。

$ go run getting_start.go 
go: finding module for package github.com/gin-gonic/gin
go: found github.com/gin-gonic/gin in github.com/gin-gonic/gin v1.6.3
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:	export GIN_MODE=release
 - using code:	gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /ping                     --> main.main.func1 (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080

localhost:8080/pingにhttpリクエストを送信すると、Jsonを返します。

$ curl localhost:8080/ping
{"message":"pong"}

getting_start.goの出力結果にレスポンスタイムとステータスが記録されていました。

[GIN] 2020/07/12 - 01:45:41 | 200 |      38.361µs |             ::1 | GET      "/ping"

以上です。まずは動かしてみたということで、これから色々触ってみたいと思います。

golangでhttpリクエストフォームの値を扱う

golangに慣れている人にとっては当たり前の処理ですが、一応メモとして書き留めておきたいと思います。

  • リクエストフォームの値を取得する
  • curlコマンドで確認する
    • URLにフォームを入力する
    • –formオプションを利用する

リクエストフォームの値を取得する


golangでformの値を取得するには、FormValueメソッドを利用します。具体的には、以下のように取得したいKeyの値を指定します。

http.Request.FormValue(Key)

テスト用ソースコードを作成してみました。以下のソースコード は、ポート番号8080でhttpリクエストを受け付け、Formに入力されたnameとcountryの値を表示します。

package main

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

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

func index(writer http.ResponseWriter, r *http.Request) {
  fmt.Println( r.FormValue("name") )
  fmt.Println( r.FormValue("country") )
}

curlコマンドで確認する


curlコマンドで確認する場合は、URLに入力するか、–formオプションを利用します。

先に上記で作成したgoファイルを実行しておきます。

$ go run main.go

URLにフォームを入力する


URLにフォームを入力する場合は、?の後に記述します。複数入力する場合は&を利用します。

$ curl --request GET 'http://localhost:8080/?name=koratta&country=japan'

上記コマンドの出力結果は以下の通りです。

koratta
japan

–formオプションを利用する


–formオプションを利用する場合は、–formオプションの後ろにフォームを入力することで確認できます。

$ curl http://localhost:8080 --form "name=koratta" --form "country=korea"

上記コマンドの出力結果は以下の通りです。

koratta
korea

以上です。

OpenShift Service Meshをインストールする方法

  • OpenShift Service Meshとは
  • OpenShift Service Meshのコンポーネント
  • OpenShift Service Meshをインストールする

OpenShift Service Meshとは


OpenShift ServiceMeshとは、OpenShift上で稼働するマイクロアプリケーションのMesh層を接続・管理・監視するためのOpenShiftサービスです。Istioとの違いなど、OpenShift Service Meshを使うメリットは以下のURLで紹介されています。

OpenShift Service Meshを選ぶ理由

OpenShift Service Meshのコンポーネント


OpenShift Service Meshのコンポーネントは以下の通りです。必ずしも全てインストールする必要はなく、オプションで設定できます。

  • Istio・・・全体のトラフィックの管理
  • Kiali・・・トラフィックの可視化
  • Grafana・・・メトリクスの可視化
  • Jeager・・・分散トレースログの管理
  • Elasticsearch・・・分散トレースログの保管先(※Jeagerを利用する場合に、all-in-oneのインメモリ構成かElasticsearchを選択できます。)

特に注意すべき点はElasticsearchで、それなりに大きいリソースを必要としますし、永続ボリューム用のストレージが必要になります。企業では通常予算を決めてから構築しますので、予算からずれる可能性があります。

Elasticsearchの要件は、こちらをご参照ください。

OpenShift Service Meshをインストールする


GUIのOperator HubからIstioだけインストールします。

1.「OperatorHub」タブから「ServiceMesh」を検索する

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

3. オプションを選択する

  • Installation Mode・・・プロジェクトが利用可能な範囲を選択します。
  • Update Channel・・・インストールする対象バージョンを選択します。GA済みの安定したバージョンかTech Previewのバージョンかどうか選択することができます。
  • Approval Strategy・・・Operatorのアップデートを自動か手動で選択できます。

4. 「Installed Operators」タブからServiceMeshのStatusが「Succeeded」であることを確認し、「Istio Service Mesh Controll Plane」をクリックする

インストールが完了するとOperator Podが作成されています。

$ oc get pods -n openshift-operators
NAME READY STATUS RESTARTS AGE
istio-operator-745ffbbcbb-wrhgq 1/1 Running 0 4m36s

5. 「Create ServiceMeshControllPlane」をクリックする

6. `spec.kiali.enabled`、`spec.grafana.enabled`、`spec.tracing.enabled`をtrue→falseに変更する

7. `metadata.namespace`を作成したproject名に変更し、「Create」ボタンをクリックする

以下のコマンドでOpenShift Service Meshをデプロイするプロジェクトを作成します。

$ oc new-project istio-system

作成したプロジェクトをmetadata.namespaceに指定します。

これでインストールが完了しました。IstioのPodがデプロイされていることがわかります。

$ oc get pods -n istio-system
NAME                                     READY     STATUS    RESTARTS   AGE
istio-citadel-5b66678f8-g847h            1/1       Running   0          2m8s
istio-egressgateway-58b5ccd6b7-7flg8     1/1       Running   0          50s
istio-galley-7dddcfffd8-5ssnn            1/1       Running   0          102s
istio-ingressgateway-6b6d8d747-8cdxs     1/1       Running   0          50s
istio-pilot-c77d8b5dd-nlh4f              2/2       Running   0          64s
istio-policy-57845f5d-shhsm              2/2       Running   0          88s
istio-sidecar-injector-64c496848-dh75r   1/1       Running   0          45s
istio-telemetry-656d4d654f-m2cpq         2/2       Running   0          88s
prometheus-5467bd8998-lph9v              2/2       Running   0          119s

以上です。

MacOSにS2IコマンドをインストールしてS2Iビルドする

  • S2Iとは
  • S2Iコマンドのインストール
  • S2Iビルダーイメージのビルド
  • アプリケーションイメージのS2Iビルド

S2Iとは


S2Iとは、Source To Imageの略で、OpenShiftの特徴的な機能の一つです。その名の通り、ソースコードをコンテナイメージに変換します。S2Iビルダーイメージとソースコードからアプリケーションイメージをビルドすることによって、ビルドを自動化するだけでなく、イメージとソースコードの保守を分離することができます。

この機能における具体的なメリットは、以下の公式ドキュメントをご参照ください。
https://access.redhat.com/documentation/ja-jp/openshift_container_platform/4.4/html/builds/understanding-image-builds#build-strategy-s2i_understanding-image-builds

本記事は、S2Iツールをローカルマシン(Mac)にインストールしてローカル上で使ってみます。

S2Iコマンドのインストール


S2IコマンドをインストールしてMac PC上で操作します。

S2Iツールのパッケージは以下のURLからダウンロードできますが、Macではbrewコマンドでインストールができます。
https://github.com/openshift/source-to-image/releases/tag/v1.3.0

以下のコマンドでインストールします。

$ brew install source-to-image
$ s2i version
s2i v1.3.0-dirty

S2Iビルダーイメージのビルド


S2Iコマンドを使ってS2Iビルダーイメージをビルドしてみます。

以下のコマンドで例のようにS2Iビルダーイメージの雛形を作成することができます。指定したdirectory配下には、必要なファイルが作成されます。それぞれのファイルの解説はまた今度記事を書きたいと思いますが、自身で作成したいイメージに合わせてファイルを編集する必要があります。

$ s2i create [image名] [directory名]
例)
$ s2i create python:3.6 test-python
$ tree test-python/
test-python/
├── Dockerfile
├── Makefile
├── README.md
├── s2i
│   └── bin 
│       ├── assemble
│       ├── run
│       ├── save-artifacts
│       └── usage
└── test ←テストソースコードなので、S2Iビルダーイメージのビルドには不要です。
    ├── run
    └── test-app
        └── index.html
$ docker build -t python-s2i-builder test-python/ 
$ docker images python-s2i-builder REPOSITORY TAG IMAGE ID CREATED SIZE
python-s2i-builder latest 490710255550 3 minutes ago 426MB

今回は自分でS2Iビルダーイメージを作成するのではなく、以下のコマンドでregistry.redhat.ioのイメージを利用してアプリケーションイメージをS2Iビルドします(Red Hatのアカウントが必要になります。)。このように、OpenShiftではS2Iビルダーイメージをサポートしてくれるのが魅力の一つでもあります。

$ docker login registry.redhat.io
Username: 
Password: 
Login Succeeded
$ docker pull registry.redhat.io/rhel8/python-36
Using default tag: latest
latest: Pulling from rhel8/python-36
1a6747857d79: Pull complete 
fc5aa93e3b58: Pull complete 
dd09aac02f79: Pull complete 
8542dbe7d6e8: Pull complete 
ad98eb9d602e: Pull complete 
Digest: sha256:74057ed15aba3ba78ddf061a964b60a4e5c1d0eb8c989ad6831ea086721d9acf
Status: Downloaded newer image for registry.redhat.io/rhel8/python-36:latest
$ docker images registry.redhat.io/rhel8/python-36:latest
REPOSITORY                           TAG                 IMAGE ID            CREATED             SIZE
registry.redhat.io/rhel8/python-36   latest              77c14d37c369        4 weeks ago         781MB
$ docker tag registry.redhat.io/rhel8/python-36:latest python-s2i-builder
$ docker images python-s2i-builder
REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
python-s2i-builder   latest              77c14d37c369        4 weeks ago         781MB

アプリケーションイメージのS2Iビルド


上記で作成したS2Iビルダーイメージを使って、アプリケーションイメージをビルドします。

以下のコマンドでアプリケーションイメージをビルドすることができます。

$ s2i build [ソースコード] [S2Iビルダーイメージ名] [アプリケーションイメージ名]

registry.redhat.ioからpullしたS2IビルダーイメージとGit上のサンプルPythonソースコードを使って、python-s2iアプリケーションイメージをビルドします。

$ s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.6/test/setup-test-app/ python-s2i-builder python-s2i
$ docker images python-s2i
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
python-s2i          latest              b07c7f096566        15 seconds ago      789MB

アプリケーションイメージが実行できることを確認できました。

$ docker run --name hello -d -p 8080:8080 python-s2i
ae57680ef2705b92a51727bc0e6d67ce14fca3d6a515abdffc48c9f4f6913bf4
$ curl localhost:8080
Hello from gunicorn WSGI application!

以上です!

DeploymentにおけるRollingとRecreateの違い

KubernetesにおけるDeploymentのデプロイ戦略にはRollingとRecreateがあります。今日はそれぞれの違いについて共有致します。

本記事は、以下のKubernetes公式ドキュメントを参考にしています。
https://kubernetes.io/ja/docs/concepts/workloads/controllers/deployment/

  • Rollingとは
  • Recreateとは
  • RollingとRecreateのユースケース

Rollingとは


Rollingとは、古いPodへのアクセスを新しく起動したPodに切り替えるデプロイ方法です。サービス提供中のPodは、新しいPodが起動するまでの間に停止することがないので、ダウンタイムを最小限に抑えることができます。

Recreateとは


Recreateは、サービス提供中のPod停止後に新しいPodを起動するデプロイ方法です。これまでのサーバー運用と同様に、アップデートにダウンタイムが発生します。

RollingとRecreateのユースケース


RollingとRecreateのユースケースを検討します。

Rollingはダウンタイムを最小限に抑えられる一方で、古いPodと新しいPodが同時に存在することになります。一方で、Recreateは新旧のPodが混在することはありませんが、ダウンタイムがRollingと比較して長い傾向にあります。

整理すると、以下のようにユースケースを分けられます。

  • Rolling・・・ダウンタイムを最小限に抑えたい場合、あるいは新旧Podの同時実行がサポートされているアプリケーションの場合に、利用することが多い。ステートレスなアプリケーションが適用しやすい。
  • Recreate・・・PersistentVolumeがRWOであったり、新旧Podの同時実行がサポートされていないアプリケーションの場合に、利用することが多い。ステートフルなアプリケーションに適用しやすい。

以上です。

NAMESPACEをプロビジョニングするOPERATORを作ってみる(ユーザー権限編)

NamespaceをプロビジョニングするOperatorがあったら、マルチテナントでクラスター運用するのが楽になるかなぁと思い、Operatorを作成しています。本日はNamespaceにおけるユーザーアクセス権限のプロビジョニング機能を作成します。

  • はじめに
  •  Operatorを作成する
    • CRDを作成する
    • Controllerを追加する
  • Operatorをビルドする
  • Operatorをデプロイする
  • Operatorを確認する
    • ユーザーを作成する
    • Contextを作成する
    • CRを作成する
    • 権限を確認する

はじめに


最近、NamespaceをプロビジョニングするOperatorを作成しています。以下の関連記事にあるように、これまでResourceQuotaやNetworkPolicyをプロビジョニングするOperatorを作成しました。

本記事では、このOperatorにアクセス権限をプロビジョニングする機能を追加します。具体的には、CR(Cutom Resouce)に指定されたGroupに所属するユーザーが、Namespaceにアクセスできるように設定します。システムやプロジェクト単位でNamespaceを分割する際、関係ないユーザーが勝手に操作することを防げるので、必ずと言っていいほどインフラチームが実施する設定です。

Operatorを作成する


CRDを作成する


CRで設定できる設定項目を定義します。今回は、pkg/apis/nspro/v1alpha1/nspro_types.goのNsproSpecにAllowedGroup []string `json:”allowedGroup”`を加えて、Namespaceにアクセス可能なグループを指定できるようにします。

Operator SDKのインストール方法は、Operator SDK をインストールしてOperatorを作成する(1/2)で紹介していますので、よろしければご参照ください。

type NsproSpec struct {
    // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
    // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file
    // Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html
    NsSize string `json:"nssize"`
    Managed bool `json:"managed"`
    AllowedGroup []string `json:"allowedGroup"`
}

変更を加えたら、以下のコマンドで必要なコードを再生成します。

$ operator-sdk generate k8s
INFO[0006] Running deepcopy code-generation for Custom Resource group versions: [nspro:[v1alpha1], ]
INFO[0011] Code-generation complete.

Controllerを追加する


ControllerのReconcile関数には、newRolePolicyForNSとnewRoleBindingForNSを呼び出してRole, RoleBindingリソースを作成する処理を記述します。

func (r *ReconcileNspro) Reconcile(request reconcile.Request) (reconcile.Result, error) {
    ~~省略~~
    role := r.newRoleForNS(instance)
    err = r.client.Create(context.TODO(), role)

    group_names := instance.Spec.AllowedGroup
    for i := range group_names {
      rb := r.newRoleBindingForNS(instance, group_names[i])
      err = r.client.Create(context.TODO(), rb)
    }
    ~~省略~~
}

newRoleforNSは以下の通りで、CRで作成するNamespaceの全権限(クラスター権限を除く)を付与します。作成方法は他のリソースと同じですが、RoleとRoleBindingの定義は、vender/k8s.io/api/rbac/v1/type.goに記述されています。

func (r *ReconcileNspro) newRoleForNS(cr *nsprov1alpha1.Nspro) *rbacv1.Role {
  nsname  := cr.Name
  role := &rbacv1.Role {
    ObjectMeta: metav1.ObjectMeta {
      Name:      nsname + "-role",
      Namespace: nsname,
    },
    Rules: []rbacv1.PolicyRule {
      {
        APIGroups: []string{ rbacv1.APIGroupAll },
        Resources: []string{ rbacv1.ResourceAll },
        Verbs: []string{ rbacv1.VerbAll },
      },
    },
  }
  controllerutil.SetControllerReference(cr, role, r.scheme)
  return role
}

newRoleBindingForNSは以下の通りで、newRoleforNSで作成したRoleをCRに指定されたGroupに付与します。

func (r *ReconcileNspro) newRoleBindingForNS(cr *nsprov1alpha1.Nspro, group_name string) *rbacv1.RoleBinding {
  nsname  := cr.Name
  rb := &rbacv1.RoleBinding {
    ObjectMeta: metav1.ObjectMeta {
      Name:      nsname + "-rb",
      Namespace: nsname,
    },
    Subjects: []rbacv1.Subject {
      {
        Kind: "Group",
        Name: group_name,
        APIGroup: "rbac.authorization.k8s.io",
      },
    },
    RoleRef: rbacv1.RoleRef {
      Name: nsname + "-role",
      Kind: "Role",
      APIGroup: "rbac.authorization.k8s.io",
    },
  }
  
  controllerutil.SetControllerReference(cr, rb, r.scheme)
  return rb
}

Operatorをビルドする


作成したOperatorのコンテナイメージをビルドします。

$ operator-sdk build 192.168.64.2:32000/nspro-operator:v1

以下のコマンドでレジストリに登録します。今回はMicroK8s上のコンテナレジストリ上に登録するので、[WorkerNodeのIPアドレス]:[NodePort]/[イメージ名]:[タグ名]のように登録しておきます。

$ docker push 192.168.64.2:32000/nspro-operator:v1

Operatorをデプロイする


Operatorをデプロイします。Operatorの前にCRDを作成しないとapiVersionが利用できないので、CRDから作成します。

$ kubectl create -f deploy/crds/nspro_v1alpha1_nspro_crd.yaml
customresourcedefinition.apiextensions.k8s.io/nspros.nspro.example.com created

続いてdeploy/配下にあるyamlを作成します。operator.yamlだけイメージ名を変更する必要があります。

$ kubectl delete -f deploy/operator.yaml 
deployment.apps "nspro-operator" deleted
$ kubectl create -f deploy/operator.yaml -n nspro-operator
deployment.apps/nspro-operator created
$ kubectl create -f deploy/service_account.yaml -n nspro-operator
serviceaccount/nspro-operator created
$ kubectl create -f deploy/role.yaml -n nspro-operator
role.rbac.authorization.k8s.io/nspro-operator created
$ kubectl create -f deploy/role_binding.yaml -n nspro-operator
rolebinding.rbac.authorization.k8s.io/nspro-operator created

デプロイすると以下のエラーが発生します。

E0701 15:06:25.610185 1 reflector.go:134] sigs.k8s.io/controller-runtime/pkg/cache/internal/informers_map.go:126: Failed to list *v1alpha1.Nspro: nspros.nspro.example.com is forbidden: User "system:serviceaccount:nspro-operator:nspro-operator" cannot list resource "nspros" in API group "nspro.example.com" in the namespace "nspro-operator"

アクセス権限を付与するために、以下の記事でRBAC機能を有効化したことによってOperatorに権限がなくなったことが原因です。

MicroK8sへのユーザー追加と権限付与でハマった話

従って、OperatorにCluster権限を与える必要があります。以下のyamlを使ってnspro-operator ServiceAccountにcluster-adminを付与します。

kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: admin-for-nspro-operator
subjects:
- kind: ServiceAccount
  name: nspro-operator
  namespace: nspro-operator
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io

Operatorを確認する


CRを作成してOperatorの挙動を確認します。

ユーザーを作成する


Operator確認用に、dev1グループに所属するdev1-user01とdev2グループに所属するdev2-user01を作成します。

basic_auth.csv に以下の行を追加します。今回はMicroK8sを利用しているので/var/snap/microk8s/current/credentials/配下にあります。環境毎にパスが異なるので注意してください。

[dev1-user01のパスワード],dev1-user01,dev1-user01,"system:authenticated,developer,dev1"
[dev2-user01のパスワード],dev2-user01,dev2-user01,"system:authenticated,developer,dev2"

Contextを作成する


~/.kube/config を編集して、dev1-user01とdev2-user01で操作する用のContextを作成します。

apiVersion: v1
clusters:
- cluster:
    insecure-skip-tls-verify: true
    server: https://127.0.0.1:16443
  name: microk8s-cluster
contexts:
- context:
    cluster: microk8s-cluster
    user: admin
  name: microk8s
- context:
    cluster: microk8s-cluster
    user: dev1-user01
  name: microk8s-dev1
- context:
    cluster: microk8s-cluster
    user: dev2-user01
  name: microk8s-dev2
current-context: microk8s
kind: Config
preferences: {}
users:
- name: admin
  user:
    password: [adminのパスワード]
    username: admin
- name: dev1-user01
  user:
    password: [dev1-user01のパスワード]
    username: dev1-user01
- name: dev2-user01
  user:
    password: [dev2-user01のパスワード]
    username: dev2-user01

CRを作成する


CRを作成してNamespaceをプロビジョニングします。

dev1グループがアクセスできるdev1-nsとdev2グループがアクセスできるdev2-nsを以下のyamlを使って作成します。

apiVersion: nspro.example.com/v1alpha1
kind: Nspro
metadata:
  name: dev1-ns
spec:
  nssize: large
  managed: true
  allowedGroup: ["dev1"]
apiVersion: nspro.example.com/v1alpha1
kind: Nspro
metadata:
  name: dev2-ns
spec:
  nssize: large
  managed: true
  allowedGroup: ["dev2"]
$ kubectl create -f deploy/crds/dev1-ns.yaml -n nspro-operator
nspro.nspro.example.com/dev1-ns created
$ kubectl create -f deploy/crds/dev2-ns.yaml -n nspro-operator
nspro.nspro.example.com/dev2-ns created
$ kubectl get ns dev1-ns dev2-ns
NAME      STATUS   AGE
dev1-ns   Active   82s
dev2-ns   Active   74s

権限を確認する


dev1-user01とdev2-user01のコンテキストに切り替えて、それぞれの権限を確認すると、以下のようにCRのallowedGroupに指定されたNamespaceのみアクセスできることがわかります。

$ kubectl config use-context microk8s-dev1
Switched to context "microk8s-dev1".
$ kubectl auth can-i get pods -n dev1-ns
yes
$ kubectl auth can-i get pods -n dev2-ns
no
$ kubectl config use-context microk8s-dev2
Switched to context "microk8s-dev2".
$ kubectl auth can-i get pods -n dev1-ns
no
$ kubectl auth can-i get pods -n dev2-ns
yes

以上です。段々Operatorの使い方も慣れてきました。