Go言語からkubernetes APIを呼び出すクライアント「client-go」の紹介

これはKubernetes2 Advent Calendar 2017 25日目の記事です。この記事ではclient-goというライブラリを使ってgo言語からkubernetesのAPIを呼び出す方法を紹介します。

とりあえず使ってみるところから、任意のkubernetesクラスタにつなぐ方法、service accountを利用した認証方法、テストの書き方まで紹介します。

Client-goとは

Kubernetes APIを叩くためのGo製ライブラリです。おなじみのkubectlコマンドでも使われています。僕の関わっているサービスでは、kubernetes外のサーバーやkubernetes内部のコンテナからkubernetesを操作したいときに使っています。

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

$ go get k8s.io/client-go/...

インストールに関する詳細はドキュメントを参照して下さい。ちゃんと使うならバージョン管理してね、といったことが書かれています。

まずは叩いてみる

以下の例ではPod一覧を取得しています。なお、実行にはkubectlが必要です(後述しますが、client-go利用の際にkubectlが常に必要になるわけではありません)。

まずはnewClient()でクライアントを生成しています。その際の認証情報は、~/.kube/configファイルから取得しています。これはkubectlが作成するファイルで、接続先サーバーや認証情報が格納されています。

その後、client.CoreV1().Pods("").List()でPod一覧を取得しています。Pods("")の引数ではnamespaceを指定することができ、空文字列にすると、全namespace指定と同等になります。

実行すると、Pod一覧が出力されます。

$ go run get_started.go
kube-addon-manager-minikube
kube-dns-6fc954457d-f58rp
kubernetes-dashboard-jcjjn

接続先kubernetesクラスタを指定する

先程の例では、kubectlの作成したクラスタ/認証情報を使用しました。kubectlは複数のクラスタが存在する場合でもcontextを切り替えて対応できますが、先程はkubectl config current-contextで得られるcontextを自動的に使うようになっていました。

そこで今度は、contextを指定して任意のクラスタに接続できるようにしてみます。こちらも、実行にはkubectlが必要です。

主に変更したのはnewClient()です。clientcmd.ConfigOverrides{}を使用してcontextを指定しています。試したことはないですが接続先サーバーや認証情報の上書きもできるようです。clientcmd.NewDefaultClientConfigLoadingRules()では、クラスタ/認証情報のロード方法を指定しています。デフォルトでは、~/.kube/configの情報がロードされます。これらの情報を元にconfigとclientを作成しています。

実行の際は、引数でcontextを指定します。kubectl config get-contextsで得られるcontext名が指定できます。

$ go run choose_k8s_cluster.go minikube
kube-addon-manager-minikube
kube-dns-6fc954457d-f58rp
kubernetes-dashboard-jcjjn

Kubernetes内部のPodからAPIを叩く

ここまでの例では、kubectlの作成したクラスタ/認証情報を使用してクライアントを作成していました。kubectlが利用できる環境ではここまでの方法でも良いですが、kubernetes内部のPodからAPIを叩くような場合には、kubectlがインストールされていないことがあります。

そのような場合、kubernetesがPodに対して付与するservice accountを使うと便利です。Service accountについてはこのドキュメントが詳しいです。

主に変更したのはnewClient()です。rest.InClusterConfig()でservice accountを利用するための設定をしています。/var/run/secrets/kubernetes.io/serviceaccount/以下のトークンや証明書が使われます。

Pod内のコンテナにログインして実行すると、~/.kube/configのない環境でもPod一覧を取得できることがわかります。 (もしパッケージがない旨のエラーが出たら、client-goをインストールして下さい)

root@golang:/go# ls ~/.kube
ls: cannot access '/root/.kube': No such file or directory
root@golang:/go# go run in_cluster_client.go
golang
kube-addon-manager-minikube
kube-dns-6fc954457d-f58rp
kubernetes-dashboard-jcjjn

テストを書く

最後にテストを書いてみます。client-goに含まれるfakeパッケージを使用すると、テスト用のデータを予め仕込んだ上で各種APIを叩くことができます。

newClient()でのクライアント作成はfake.NewSimpleClientset()を呼ぶだけになりました。addTestData()でテスト用のPodを作成していますが、方法は非テストコードでPodを作成する場合と同じです。今回は省略しましたが、ここでPodの状態や各種ポリシーを設定することもできます。

実行すると、作成したテスト用Podが出力されることがわかります。

$ go run normal_case_test.go
test

今度は、kubernetes APIがエラーを返す場合のテストを書いてみます。

setServerError()API呼び出し時にエラーを返すようにしています。テスト用クライアントは内部にReactionChainと呼ばれるハンドラリストをもっていて、先頭のハンドラから順番にリクエストを処理していきます。上記のコードでは常にエラーを返すハンドラを先頭に挿入しているので、API呼び出しがエラーを返すようになります。ちなみに、PrependReactor()の第一引数でverb、第二引数でresourceを指定できるので、第一引数をlist、第二引数をpodsとすれば、Pod一覧取得のときだけエラーを返すようになります。

実行すると、Pod一覧を取得するAPIがエラーを返して終了します。

$ go run error_case_test.go
2017/12/22 13:03:08 server error
exit status 1

まとめ

この記事ではclient-goの使い方について簡単に紹介しました。ドキュメントはそれほど充実していないですが、kubectlで使われているだけあって、やりたいことは大体できる印象です。Kubernetes APIを使うときはぜひ利用を検討してみて下さい。

GCP User GroupでStackdriverについて発表してきた

だいぶ間が空いてしまった。先日開催されたGCPUG Tokyo DevOps Day September 2017というイベントで発表してきたメモ。

「RocroにおけるGCP活用事例」という発表の後半を担当した。前半は現在開発中のサービスとそのデプロイについて、後半がStackdriverについて、という構成。以下が担当部分の発表資料。

一年ぐらいStackdriverを使ってノウハウが少し貯まってきたところだったので、こういう場でまとまった話ができたのはとても良かった。GCPUG運営の方に感謝。Stackdriverは監視サービスとしてはまだマイナーで、ノウハウとかあまり共有されていない印象があるので、今後もわかったことが出てきたらブログを書くか発表するかしていきたい。

CloudFormation Custom Resourceの意図しない削除を防ぐ

最近はCloudFormationをよく使っています。大変便利なのですが、S3オブジェクトやDynamoDBのItemの作成には対応していないため、Lambda-backed Custom Resourceで作成しています。ただCustom Resourceのライフサイクルは少し独特なところがあって、理解せずに使うと、意図せずResourceが削除されてしまうことがあります(あった)。

意図しない削除を起こさないようにするため、この記事ではいくつかの状況についてCustom Resourceがいつ作られ、いつ更新され、いつ削除されるのかまとめます。Lambda-backed Custom Resourceを想定していますが、SNS-backedでも基本は同じではないかと思います。

PhysicalResourceIDについて

まずは前提として、PhysicalResourceIDについて説明します。PhysicalResourceIDは、Custom Resourceから呼ばれるLambda関数がレスポンスに設定する値の一つです。レスポンスについて説明したドキュメントには以下のように書かれています。

This value should be an identifier unique to the custom resource vendor,
and can be up to 1 Kb in size. The value must be a non-empty string and
must be identical for all responses for the same resource. 

最後のmust be identical for all responses for the same resourceが重要です。ここの意味するところは、Custom Resourceのドキュメントに以下のようにはっきり書かれています。

When AWS CloudFormation receives the response, it compares the
PhysicalResourceId between the old and new custom resources.
If they are different, AWS CloudFormation recognizes the update
as a replacement and sends a delete request to the old resource.

要はPhysicalResourceIDが変わったら古いResourceは削除される、ということですね。

前述のように、PhysicalResourceIDはLambda関数がレスポンスの中に含める値です。レスポンスは、Node.jsの場合はcfn-responseモジュールのsendメソッドで送ることが多いかと思います。PhysicalResourceIDはsendメソッドの5つ目の引数で指定できます。例えば以下のような感じです。

var response = require('cfn-response');
exports.handler = function(event, context) {
  response.send(event, context, response.SUCCESS, {}, 'PhysicalResourceID');
};

cfn-responseモジュールのドキュメントを見ると、PhysicalResourceIDについては以下のように書かれています。

Optional. The unique identifier of the custom resource that invoked
the function. By default, the module uses the name of the Amazon
CloudWatch Logs log stream that is associated with the Lambda function.

PhysicalResourceIDの指定を省略するとLog stream名が使われるとのことです。これは例えば2017/05/13/[$LATEST]2582d4dedea542868953b34db229dd35のような値で、Lambda関数を更新するたびに変わっているようです。

ここまでのポイントとしては以下です。

  • PhysicalResourceIDはCustom ResourceのIDで、Lambda関数のレスポンスに含めることで指定できる。
  • このIDが変わると、古いCustom Resourceが削除される。
  • PhysicalResourceIDを指定しない場合、Lambda関数が更新されるたびに異なるPhysicalResourceIDが設定される。

ここからは、いくつかの状況について、Custom Resourceがいつ作られ、更新され、削除されるのか見ていきます。

PhysicalResourceIDを指定しない場合

以下のようなPhysicalResourceIDを指定しない場合を考えます。CloudFormationのTemplateはこちらから参照できます。

var response = require('cfn-response');
exports.handler = function(event, context) {
  response.send(event, context, response.SUCCESS, {});
};

まずはスタックの作成をします。以下はCloudFormationのEventsタブのスクショです。

f:id:ks888:20170513133719p:plain

Custom Resrouceが作成されています。

次にスタックの更新をします。何か変更しないと更新できないので、スタックに与えるパラメータを変えています。

f:id:ks888:20170513132222p:plain

Custom Resourceが更新されています。

ここでLambda関数を更新してみます。今回はLambda関数に割り当てるメモリサイズをAWSコンソールから変更しました。再びスタックの更新をします。

f:id:ks888:20170513133729p:plain

Custom Resourceが更新されているのに加え、最後に同Resourceが削除されています。これは、Lambda関数の更新に伴ってPhysicalResourceIDが更新されたからです。

もう一度スタックの更新をしてみます。

f:id:ks888:20170513132248p:plain

Custom Resourceが更新されていますが、Physical IDが変わっています。

最後にスタックを削除します。

f:id:ks888:20170513132257p:plain

Custom Resourceが削除されました。

作成から更新、削除までを一通りみました。PhysicalResourceIDを指定しない場合に重要なのは以下の2点かと思います。

  • Lambda関数を更新するとPhysicalResourceIDが変わるため、古いCustom Resourceが削除される。
  • 古いCustom Resourceが削除されても、新しいCustom ResourceのCreateは呼ばれない。

特に2点目はやや意外な挙動な気がするので、ID指定をしない場合は注意したいところです。

PhysicalResourceIDを指定する場合

以下のようにPhysicalResourceIDを指定する場合を考えます。Templateはこちらから参照できます。

var response = require('cfn-response');
exports.handler = function(event, context) {
  response.send(event, context, response.SUCCESS, {}, 'PhysicalResourceID');
};

まずはスタックの作成をします。

f:id:ks888:20170513133741p:plain

Custom Resrouceが作成されています。

次にLambda関数を更新してみた上で、スタックを更新します。

f:id:ks888:20170513132314p:plain

Custom Resourceが更新されています。今度はResourceは削除されていません。

最後にスタックを削除します。

f:id:ks888:20170513132325p:plain

Custom Resourceが削除されました。

シンプルですね。PhysicalResourceIDを指定しておけば、Lambda関数が更新されてもCustom Resourceは削除されません。

まとめ

PhysicalResourceIDを指定しない場合と指定する場合について、Custom Resourceがいつ作成・更新・削除されるかまとめました。Resourceの意図しない削除を防ぐには、PhysicalResourceIDを常に同じ値にしておくのがよさそうです。他にも削除されるケースをご存知でしたら、ぜひ教えてください。