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

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

以上です。

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の使い方も慣れてきました。

NamespaceをプロビジョニングするOperatorを作ってみる(NetworkPolicy編)

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

  • はじめに
  •  Operatorを作成する
    • CRDを作成する
    • Controllerを追加する
  • Operatorをビルドする
  • Operatorをデプロイする
  • Operatorを確認する

はじめに


以下の記事からNamespaceをプロビジョニングするOperatorを作成しています。以下の記事では、リクエストサイズに合わせてResourceQuotaを作成する機能を作成しました。

NamespaceをプロビジョニングするOperatorを作ってみる(サイズ編)

今回は、上記で作成したOperatorにNetworkPolicyを作成する機能を追加します。アプリケーション毎にNetworkPolicy要件は変わりますが、ログ管理やメトリクス管理などの管理系Podからの通信は許可することが多いです。従って、以下の通信要件を持ったNetworkPolicyをプロビジョニングするOperatorを作成します。

  • Ingress・・・カスタムリソースのmanagedの値に応じて、通信要件を変更する。
    • managed: true・・・ラベルが「app=infra」のNamespace内にあるPod(管理系Pod)からの通信を許可する。それ以外は拒否。
    • managed: false・・・全て許可。
  • Egress・・・全て許可。

Operatorを作成する


CRDを作成する


CRで設定できる項目を定義します。pkg/apis/nspro/v1alpha1/nspro_types.goのNsproSpecにManaged bool `json:”managed”`を加えて、運用管理対象か指定できるようにします。

~~省略~~
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"`
}
~~省略~~

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

$ 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関数にはnewNetworkPolicyForNSを呼び出して、リソースを作成する処理を記述します。

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

newNetworkPolicyforNSは以下の通りです。作成方法は他のリソースと同じですが、NetworkPolicyのAPIはvender/k8s.io/api/networking/v1/type.goに記述されています。

func (r *ReconcileNspro) newNetworkPolicyForNS(cr *nsprov1alpha1.Nspro) *networkingv1.NetworkPolicy {
  nsname  := cr.Name
  namespace_labels := map[string]string{}
  if ( cr.Spec.Managed ) {
    namespace_labels["app"] = "infra"
  }
  np := &networkingv1.NetworkPolicy{
    ObjectMeta: metav1.ObjectMeta{
      Name:      nsname + "-np",
      Namespace: nsname,
    },
    Spec: networkingv1.NetworkPolicySpec{
      Ingress: []networkingv1.NetworkPolicyIngressRule{
        {
          From: []networkingv1.NetworkPolicyPeer{
            {
              NamespaceSelector: &metav1.LabelSelector{
                MatchLabels: namespace_labels,
              },
            },
          },
        },
      },
    },
  }
  controllerutil.SetControllerReference(cr, np, r.scheme)
  return np
}

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

Operatorを確認する


デプロイしたOperatorが正常に動くか確認します。

largeサイズの運用管理対象とするNamespaceを作成するカスタムリソースを作成します。以下のyamlを$ kubectl create -f [ファイル名] -n [Operatorのnamespace]で作成します。

apiVersion: nspro.example.com/v1alpha1
kind: Nspro
metadata:
  name: np
spec:
  nssize: large
  managed: true

カスタムリソース作成後、Namespaceが確認できました。

$ kubectl get ns np --show-labels
NAME   STATUS   AGE   LABELS
np     Active   7s    managed=true,size=large

想定しているNetworkPolicyが作成されていることも確認できました。

$ kubectl describe networkpolicy np-np -n np
Name:         np-np
Namespace:    np
Created on:   2020-06-28 15:26:29 +0900 JST
Labels:       <none>
Annotations:  <none>
Spec:
  PodSelector:     <none> (Allowing the specific traffic to all pods in this namespace)
  Allowing ingress traffic:
    To Port: <any> (traffic allowed to all ports)
    From:
      NamespaceSelector: app=infra
  Allowing egress traffic:
    <none> (Selected pods are isolated for egress connectivity)
  Policy Types: Ingress

以上です。実際の挙動確認しようと思いましたが、network-pluginが入ってないと機能しないので断念しました。

NamespaceをプロビジョニングするOperatorを作ってみる(サイズ編)

以下の記事でOperatorを作成する方法がわかりましたので、NamespaceをプロビジョニングするOperatorがあったら、マルチテナントでクラスター運用するのが楽になるかなぁと思って初めてみました。

Operator SDK をインストールしてOperatorを作成する(1/2)
Operator SDK をインストールしてOperatorを作成する(2/2)

  • はじめに
  •  Operatorを作成する
    • プロジェクトを作成する
    • CRDを作成する
    • Controllerを追加する
  • Operatorをビルドする
  • Operatorをデプロイする
  • Operatorを確認する

はじめに


本記事では、サイジングされたNamespaceを作成するOperatorを作成します。サイズはLarge/Medium/Smallを用意し、以下のようにQuotaでサイズを制限します。

  • Large・・・CPU Core 300m, Memory 300Mi
  • Medium・・・CPU Core 200m, Memory 200Mi
  • Small・・・CPU Core 100m, Memory 100Mi

これだけだとyamlを用意しておいた方が早いですが、今後拡張していくので一旦これだけの機能を備えます。

Operatorを作成する


Operator SDKを使ってgolanでOperatorを作成します。

プロジェクトを作成する


Operator SDKのプロジェクトを作成します。

$ operator-sdk new nspro-operator

CRDを作成する


CR用のAPIを追加します。

$ cd nspro-operator
$ operator-sdk add api --api-version=nspro.example.com/v1alpha1 --kind=Nspro

pkg/apis/nspro/v1alpha1/nspro_types.goが作成されるので、CRで設定できる設定項目を定義します。今回は、NsproSpecにNsSize string `json:”nssize”`を加えて、それぞれのサイズが指定できるようにします。

~~省略~~
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"`
}
~~省略~~

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

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

Controllerを追加する


以下のコマンドを実行して、Controllerのテンプレートを作成します。作成が完了すると、pkg/controller/nspro/nspro_controller.go が作成されます。

$ operator-sdk add controller --api-version nspro.example.com/v1alpha1 --kind Nspro

nspro_controllerを以下に示します。

package nspro

import (
    "context"
    nsprov1alpha1 "operator/nspro-operator/pkg/apis/nspro/v1alpha1"
    corev1 "k8s.io/api/core/v1"
    "k8s.io/apimachinery/pkg/api/errors"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/runtime"
    resource "k8s.io/apimachinery/pkg/api/resource"
    "sigs.k8s.io/controller-runtime/pkg/client"
    "sigs.k8s.io/controller-runtime/pkg/controller"
    "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    "sigs.k8s.io/controller-runtime/pkg/handler"
    "sigs.k8s.io/controller-runtime/pkg/manager"
    "sigs.k8s.io/controller-runtime/pkg/reconcile"
    logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
    "sigs.k8s.io/controller-runtime/pkg/source"
)

var log = logf.Log.WithName("controller_nspro")

/**
* USER ACTION REQUIRED: This is a scaffold file intended for the user to modify with their own Controller
* business logic.  Delete these comments after modifying this file.*
 */

// Add creates a new Nspro Controller and adds it to the Manager. The Manager will set fields on the Controller
// and Start it when the Manager is Started.
func Add(mgr manager.Manager) error {
    return add(mgr, newReconciler(mgr))
}

// newReconciler returns a new reconcile.Reconciler
func newReconciler(mgr manager.Manager) reconcile.Reconciler {
    return &ReconcileNspro{client: mgr.GetClient(), scheme: mgr.GetScheme()}
}

// add adds a new Controller to mgr with r as the reconcile.Reconciler
func add(mgr manager.Manager, r reconcile.Reconciler) error {
    // Create a new controller
    c, err := controller.New("nspro-controller", mgr, controller.Options{Reconciler: r})
    if err != nil {
        return err
    }

    // Watch for changes to primary resource Nspro
    err = c.Watch(&source.Kind{Type: &nsprov1alpha1.Nspro{}}, &handler.EnqueueRequestForObject{})
    if err != nil {
        return err
    }

    // TODO(user): Modify this to be the types you create that are owned by the primary resource
    // Watch for changes to secondary resource Pods and requeue the owner Nspro
    err = c.Watch(&source.Kind{Type: &corev1.Namespace{}}, &handler.EnqueueRequestForOwner{
        IsController: true,
        OwnerType:    &nsprov1alpha1.Nspro{},
    })
    if err != nil {
        return err
    }

    return nil
}

// blank assignment to verify that ReconcileNspro implements reconcile.Reconciler
var _ reconcile.Reconciler = &ReconcileNspro{}

// ReconcileNspro reconciles a Nspro object
type ReconcileNspro struct {
    // This client, initialized using mgr.Client() above, is a split client
    // that reads objects from the cache and writes to the apiserver
    client client.Client
    scheme *runtime.Scheme
}

// Reconcile reads that state of the cluster for a Nspro object and makes changes based on the state read
// and what is in the Nspro.Spec
// TODO(user): Modify this Reconcile function to implement your Controller logic.  This example creates
// a Pod as an example
// Note:
// The Controller will requeue the Request to be processed again if the returned error is non-nil or
// Result.Requeue is true, otherwise upon completion it will remove the work from the queue.
func (r *ReconcileNspro) Reconcile(request reconcile.Request) (reconcile.Result, error) {
    reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
    reqLogger.Info("Reconciling Nspro")

    // Fetch the Nspro instance
    instance := &nsprov1alpha1.Nspro{}
    err := r.client.Get(context.TODO(), request.NamespacedName, instance)
    if err != nil {
        if errors.IsNotFound(err) {
            // Request object not found, could have been deleted after reconcile request.
            // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
            // Return and don't requeue
            return reconcile.Result{}, nil
        }
        // Error reading the object - requeue the request.
        return reconcile.Result{}, err
    }

    // Define a new Namespace object
    ns := newNsproForCR(instance)

    // Set Nspro instance as the owner and controller
    if err := controllerutil.SetControllerReference(instance, ns, r.scheme); err != nil {
        return reconcile.Result{}, err
    }

    err = r.client.Create(context.TODO(), ns)

  quota := r.newQuotaForNS(instance)
  err = r.client.Create(context.TODO(), quota)

    return reconcile.Result{}, nil
}

func newNsproForCR(cr *nsprov1alpha1.Nspro) *corev1.Namespace {
    nssize := cr.Spec.NsSize
    labels := map[string]string{
        "size": nssize,
    }
    return &corev1.Namespace{
        ObjectMeta: metav1.ObjectMeta{
            Name:      cr.Name,
            Labels:    labels,
        },
    }
}

func (r *ReconcileNspro) newQuotaForNS(cr *nsprov1alpha1.Nspro) *corev1.ResourceQuota {
  nsname  := cr.Name
  size_map := map[string]int64{ "large": 3, "medium": 2, "small": 1 }
  size := size_map[cr.Spec.NsSize]
  cpuCores := resource.NewMilliQuantity(100*size, resource.DecimalSI)
  memorySize := resource.NewQuantity(1*1024*1024*size, resource.BinarySI)
  quota := &corev1.ResourceQuota{
    ObjectMeta: metav1.ObjectMeta{
      Name:      nsname + "-quota",
      Namespace: cr.Name,
    },
    Spec: corev1.ResourceQuotaSpec{
      Hard: corev1.ResourceList{
        "limits.cpu": *cpuCores,
        "limits.memory": *memorySize,
      },
    },
  }
  controllerutil.SetControllerReference(cr, quota, r.scheme)
  return quota
}

作成した部分は、newNsproForCR/newQuotaForNSとそれらの呼び出しです。

newNsproForCRがNamespaceを定義していて、nssizeで指定されたサイズをラベルに貼ってNamespaceを作成する単純な関数です。

newQuotaForNSは、nssizeで指定されたサイズに応じてlimitのサイズを定義します。各サイズに応じて数字をかけています。limits.cpu/limits.memoryは、Quantityを指定する必要があり、package resourceが参考になりました。

Operatorをビルドする


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

$ go mod vendor
$ 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

OperatorのPodがデプロイされます。

$ kubectl get pods -n nspro-operator
NAME                              READY   STATUS    RESTARTS   AGE
nspro-operator-585b76656d-jt4nm   1/1     Running   0          25s

Operatorを確認する


デプロイしたOperatorが正常に動くか確認します。

CRとして、nssizeがsmallのsmall-nsとnssizeがlargeのlarge-nsを作成します。それぞれ$ kubectl create -f [ファイル名] -n [Operatorのnamespace]でリソースを作成します。

apiVersion: nspro.example.com/v1alpha1
kind: Nspro
metadata:
  name: small-ns
spec:
  nssize: small
apiVersion: nspro.example.com/v1alpha1
kind: Nspro
metadata:
  name: large-ns
spec:
  nssize: large

ラベルで正しく分かれていることが分かります。

$ kubectl get ns small-ns -L size
NAME       STATUS   AGE   SIZE
small-ns   Active   19s   small
$ kubectl get ns large-ns -L size
NAME       STATUS   AGE   SIZE
large-ns   Active   7s    large

それぞれのサイズに合ったQuotaが定義されていて、使用量が制限できていることがわかります。

$ kubectl describe quota small-ns-quota -n small-ns
Name:          small-ns-quota
Namespace:     small-ns
Resource       Used  Hard
--------       ----  ----
limits.cpu     0     100m
limits.memory  0     1Mi
$ kubectl describe quota large-ns -n large-ns
Name:          large-ns-quota
Namespace:     large-ns
Resource       Used  Hard
--------       ----  ----
limits.cpu     0     300m
limits.memory  0     3Mi

以上です!半角スペースに気づかなかったりして大分PDに時間を取られてしまいました。。。

Operator SDK をインストールしてOperatorを作成する(2/2)

Operator SDKをMacOSにインストールしてgolangでOperatorを作成します。以下の記事の続きでは、Operator SDKのインストールとソースコードの作成を行いました。本記事ではOperatorのビルドとデプロイを実施します。

Operator SDK をインストールしてOperatorを作成する(1/2)

  • Operatorのビルド
  • Operatorのデプロイ
    • CRDの作成
    • Opearatorのデプロイ
    • CRの作成
    • CRの更新

Operatorのビルド


以下のコマンドでOperatorのビルドを実施します。「Operator build complete. 」が出力されれば完了です。

$ go mod vendor
$ operator-sdk build 192.168.64.2:32000/hello-operator:v1
INFO[0008] Building OCI image 192.168.64.2:32000/hello-operator:v1 
Sending build context to Docker daemon  118.1MB
Step 1/7 : FROM registry.access.redhat.com/ubi7/ubi-minimal:latest
 ---> 7d397bf59678
Step 2/7 : ENV OPERATOR=/usr/local/bin/hello-operator     USER_UID=1001     USER_NAME=hello-operator
 ---> Using cache
 ---> d4043ef43b60
Step 3/7 : COPY build/_output/bin/hello-operator ${OPERATOR}
 ---> a40f16af6556
Step 4/7 : COPY build/bin /usr/local/bin
 ---> e758ef7bd8d0
Step 5/7 : RUN  /usr/local/bin/user_setup
 ---> Running in fc181e29564c
+ mkdir -p /root
+ chown 1001:0 /root
+ chmod ug+rwx /root
+ chmod g+rw /etc/passwd
+ rm /usr/local/bin/user_setup
Removing intermediate container fc181e29564c
 ---> 8b5e7541c344
Step 6/7 : ENTRYPOINT ["/usr/local/bin/entrypoint"]
 ---> Running in 903d3d9e95d9
Removing intermediate container 903d3d9e95d9
 ---> b4fff90c4f99
Step 7/7 : USER ${USER_UID}
 ---> Running in 8333de254bce
Removing intermediate container 8333de254bce
 ---> 6a813292199d
Successfully built 6a813292199d
Successfully tagged 192.168.64.2:32000/hello-operator:v1
INFO[0022] Operator build complete.

イメージ名は、[multipass-vmのIPアドレス]:[Docker RegistryのNodePort]/[イメージ名]:[タグ]にしています。今回はMicroK8s上のDocker Registryコンテナにpushするので、multipass-vmのIPアドレスとしています。

以下のコマンドでMicroK8s上のDocker Registryにpushしておきます。

$ docker push 192.168.64.2:32000/hello-operator:v1
The push refers to repository [192.168.64.2:32000/hello-operator]
afb9afe8923c: Pushed 
f507dc8f12b6: Pushed 
570d657b8592: Pushed 
da37c371002e: Layer already exists 
e0a4651703ad: Layer already exists 
v1: digest: sha256:ff4b946376cf992ca5fd83208eb0547eb9dc782f7331633f8712f03b8f86aad4 size: 1363

Operatorのデプロイ


上記でビルドしたOperatorをデプロイします。

CRDの作成


kind: Helloが作成できるように、deploy/crds配下に作成された以下のyamlを使ってCRDを作成します。

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: hellos.hello.example.com
spec:
  group: hello.example.com
  names:
    kind: Hello
    listKind: HelloList
    plural: hellos
    singular: hello
  scope: Namespaced
  subresources:
    status: {}
  validation:
    openAPIV3Schema:
      properties:
        apiVersion:
          type: string
        kind:
          type: string
        metadata:
          type: object
        spec:
          type: object
        status:
          type: object
  version: v1alpha1
  versions:
  - name: v1alpha1
    served: true
    storage: true

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

$ kubectl create -f deploy/crds/hello_v1alpha1_hello_crd.yaml -n hello-operator

Operatorのデプロイ


Operatorをデプロイします。Operator SDKでプロジェクトを作成すると、deploy配下にデプロイ用のyamlが作成されます。deploy/operator.yamlのイメージ名を変更して、$ kubectl create -f [ファイル名]でデプロイします。

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

OperatorのステータスがRunningになれば大丈夫です。

$ kubectl get pods -n hello-operator -w
NAME                              READY   STATUS    RESTARTS   AGE
hello-operator-64f7df787d-5tq8f   1/1     Running   0          103s

CRの作成


deploy/crds配下に作成されたyamlを以下の通り編集し、CRを作成します。最初はhelloconfigにhelloaを指定してhelloaアプリケーションを作成します。

apiVersion: hello.example.com/v1alpha1
kind: Hello
metadata:
  name: example-hello
spec:
  # Add fields here
  size: 1
  helloconfig: helloa

以下のコマンドで作成すると、自動でhelloaアプリケーションが作成されます。

$ kubectl create -f hello_v1alpha1_hello_cr.yaml -n hello-operator 
hello.hello.example.com/example-hello created
$ kubectl get pods -n hello-operator -o wide
NAME                              READY   STATUS    RESTARTS   AGE    IP          NODE          NOMINATED NODE   READINESS GATES
example-hello-pod                 1/1     Running   0          16s    10.1.2.19   microk8s-vm   <none>           <none>
hello-operator-64f7df787d-5tq8f   1/1     Running   0          113s   10.1.2.18   microk8s-vm   <none>           <none>

httpリクエストをすると、Hello Aと表示されます。

$ curl 10.1.2.19:8080
Hello A

CRの更新


helloconfigの値をhelloaからhellobに変更して、hellobアプリケーションをデプロイします。以下のコマンドを使ってhelloconfigの値を変更します。

$ kubectl patch hello.hello.example.com/example-hello -n hello-operator --type merge -p '{"spec": {"helloconfig": "hellob"}}'
hello.hello.example.com/example-hello patched

自動でPodが更新されるかと思いましたが、イメージ名の変更なので自動再デプロイはされませんでした。従って、手動でPodを削除して更新します。(Podを削除してもCRは削除されないので、自動で再デプロイされます。)

$ kubectl delete pod example-hello-pod -n hello-operator
pod "example-hello-pod" deleted
$ kubectl get pods -n hello-operator -o wide
NAME                              READY   STATUS    RESTARTS   AGE   IP          NODE          NOMINATED NODE   READINESS GATES
example-hello-pod                 1/1     Running   0          12s   10.1.2.20   microk8s-vm   <none>           <none>
hello-operator-64f7df787d-5tq8f   1/1     Running   0          14m   10.1.2.18   microk8s-vm   <none>           <none>

httpリクエストすると、example-hello-podがhellobアプリケーションであることがわかります。

$ curl 10.1.2.20:8080
Hello B

以上です。まずはOperator SDKの使い方がわかって良かったです。