K8sアプリのE2Eテストを無料でCIする。GitHub ActionsとKindで
K8sアプリ(=Kubernetes上で動くことを前提としたアプリケーション)のEnd-to-Endテスト(=E2Eテスト)をCIの無料枠で実行する方法を説明します。
テストツールの列挙
まずはK8sアプリケーションのテストに使うツールを列挙します。 代表的には次のような選択肢があります。
- fake: client-goのモッククライアント。admission機構が無いなど、本物とは挙動が異なる点も多い
- envtest: controller-runtimeのインテグレーションパッケージ。kube-apiserverとetcdを実際に建てる。APIリソース操作で十分に試験項目を網羅できるなら便利。
- kind: dockerベースのスタンドアローンK8s。マルチノードをサポートしている。K8sの下回りに依存しないアプリケーションなら基本的にこれで問題なし。
- 本物のK8s: コスト大だがどうしても必要なら。
E2Eテストには基本的にkind、あるいは本物のK8sを使うことになるでしょう。プログラムによってはenvtestだけで十分な試験を行えるかもしれません。
テストツールの選定
では選択肢の中からどれを選ぶべきでしょうか。megaconfigmapというプログラムを作ったときの具体的な話を書きます。
このツールは以下の2つで構成されています。
- kubectl-megaconfigmap: サイズの大きなファイルを複数のConfigMapに分割保存するkubectlプラグイン
- combiner: 分割保存されたConfigMapを収集して1つに結合するプログラム
前者のkubectl-megaconfigmapは要するにConfigMapを作るプログラムなので、kube-apiserverさえあれば十分にテストできそうです。
その一方で後者のcombinerはPodのinitContainerとして動かすことを前提としているので、E2EテストにはPodの動作環境が欲しくなります。
これらの条件を踏まえ、kindを使うという方針を決めました。
CIサービスの選定
では次にどこのCIサービスでkindを動かせばよいでしょうか。 収益性ゼロの個人趣味プロダクトなので、できれば課金されたくありません。
無料枠の大きなCIサービスはどこかというと、GitHub Actionsです。 無料でも最大20個のジョブを同時実行できます。 そしてそれぞれのジョブは仮想CPUコアx2・メモリ7GBのマシンで実行されます。
これくらいのスペックがあればkindが普通に動きそうです。 というわけでGitHub Actionsを使うことにします。
engineerd/setup-kind を利用したKind構築
では次にGitHub ActionsのCIにKindを構築する手段を考えてみましょう…としたのですが、 実はGitHub Actionsのマーケットプレイスで配布されている engineerd/setup-kindを GitHub ActionのWorkflowにステップ追加するだけで完了してしまうのでした。便利すぎる…!
name: Kind on: [push] jobs: build: name: E2E Test runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-go@v2 with: go-version: 1.13 id: go - name: Install kubectl-megaconfigmap run: go install -mod=vendor ./cmd/kubectl-megaconfigmap - name: Set up Kind uses: engineerd/setup-kind@v0.3.0 with: version: "v0.7.0" - name: Build Image run: make docker-build TAG=latest - name: Load Image to Kind run: kind load docker-image quay.io/dulltz/megaconfigmap-combiner:latest - name: Run Test run: make e2e
setup-kind適用後のインスタンスではkubectlも普通に叩けるようになっています。 ginkgoを使ったテストコードの例を載せます。
package e2e_test import ( "bytes" "encoding/json" "fmt" "os" "os/exec" "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" ) var _ = Describe("combiner", func() { Describe("creating MegaConfigMap", func() { Context("with dummy file", func() { It("should be combined into one file", func() { By("preparing file") dummyFile := "data.dummy" f, err := os.Create(dummyFile) Expect(err).ShouldNot(HaveOccurred()) defer os.Remove(dummyFile) Expect(f.Truncate(1e6)).ShouldNot(HaveOccurred()) Expect(f.Close()).ShouldNot(HaveOccurred()) stdout, stderr, err := run("kubectl", "megaconfigmap", "create", "my-conf", "--from-file=./"+dummyFile) Expect(err).ShouldNot(HaveOccurred(), "stdout: %s, stderr: %s", stdout.String(), stderr.String()) By("creating a pod with combiner") stdout, stderr, err = run("kubectl", "apply", "-f", "../examples/pod.yaml") Expect(err).ShouldNot(HaveOccurred(), "stdout: %s, stderr: %s", stdout.String(), stderr.String()) Eventually(func() error { stdout, stderr, err := run("kubectl", "exec", "megaconfigmap-demo", "--", "ls", "-lh", "/demo/"+dummyFile) if err != nil { return fmt.Errorf("err: %s, stdout: %s, stderr: %s", err, stdout.String(), stderr.String()) } return nil }, 60*time.Second).ShouldNot(HaveOccurred()) By("delete all partial configmaps if parent have been deleted") stdout, stderr, err = run("kubectl", "delete", "cm", "my-conf") Eventually(func() error { stdout, stderr, err := run("kubectl", "get", "cm", "-o=json") if err != nil { return fmt.Errorf("err: %s, stdout: %s, stderr: %s", err, stdout.String(), stderr.String()) } var cml corev1.ConfigMapList err = json.Unmarshal(stdout.Bytes(), &cml) if err != nil { return fmt.Errorf("failed to unmarshal. err: %s", err) } if len(cml.Items) > 0 { return fmt.Errorf(string(len(cml.Items)) + " configmap remains") } return nil }, 20*time.Second).ShouldNot(HaveOccurred()) By("clean up") stdout, stderr, err = run("kubectl", "delete", "-f", "../examples/pod.yaml") Expect(err).ShouldNot(HaveOccurred(), "stdout: %s, stderr: %s", stdout.String(), stderr.String()) }) }) }) }) func run(first string, args ...string) (*bytes.Buffer, *bytes.Buffer, error) { cmd := exec.Command(first, args...) outBuf := new(bytes.Buffer) errBuf := new(bytes.Buffer) cmd.Stdout = outBuf cmd.Stderr = errBuf err := cmd.Run() return outBuf, errBuf, err }
まとめ
趣味K8aアプリケーションのE2EテストにはGitHub Actions+Kindがいい感じでした。 よりよいやり方があればぜひ教えてください。
趣味GKEのIngressを無料で済ませる
GKEでサービスを外部公開する際には、 GKE Ingress とそのバックエンド GCP Cloud Load Balancing を使用するのがスタンダードです。が、これには費用 ($18/月~) がかかります。
これをCloudflare DNS + Contourで置き換えて、無料で済ませる方法を説明します。ノードは全台プリエンプティブインスタンスで構いません。
この記事はDoxseyさんによる Kubernetes: The Surprisingly Affordable Platform for Personal Projects を発展させた内容になります。 元記事と同様、紹介する構成は趣味利用にとどめてください。
GKEクラスタ作成
まずGKEクラスタを作成してください。3台以上で構築し、プリエンプティブを有効にするのがオススメです。
ちなみにDoxseyさんの記事ではf1-microを使っていますが、 2020年4月18日現在、f1-microではGKEのワーカーノードとして最低限必要なシステムコンポーネントすらまともに動かないようです。 e2-smallにしましょう。
NodeへのHTTP/HTTPSアクセスを許可
ファイアウォール設定で TCP/UDP双方の80・443ポートingressを許可しましょう。 この手順はDoxseyさんの記事に含まれているので、よくわからない方はそちらを参照してください。
ドメイン準備
便宜上、取得したドメイン名をexample.comとします。
Cloudflare DNSの設定
CloudflareのDNSホスティングサービスを使います。 無料から利用できます。この手順もDoxseyさんの記事に含まれているので参照してください。
まずアカウントのホーム画面に移動し、+ Add a site ボタンからサイトを作成します。 前手順で準備したドメイン名を使ってください。
次に作成したサイトのダッシュボードのDNS管理画面に移動します。
Cloudflare DNSで指示される通りに、レジストラで指定しているDNSサーバリストをCloudflare DNSのものに置き換えてください。この置き換えの反映には時間がかかるかもしれません。
この時点では取り敢えずwww.example.comをexample.comにエイリアスするためのCNAMEレコードを作っておきましょう。
kubernetes-Cloudflare-sync のデプロイ
(この手順もDoxseyさんの記事に含まれています)
プリエンプティブインスタンスを使っているとNodeは1日に1度再作成されます。 その際に外部IPが変わってしまうのですが、これを自動的にCloudflare DNSのAレコードに同期するカスタムコントローラkubernetes-Cloudflare-syncがあるのでデプロイしてください。 これを使うとドメイン名からNodeの外部IPを引ける状態が常に維持されます。
デプロイにはCloudflare APIを操作するためのAPIキーが必要になります。詳しくはcalebdoxsey/kubernetes-Cloudflare-syncのREADMEをご覧ください。
Contourをデプロイ
ここまでの手順を行うとドメイン名からノード外部IPを引けるようになっていますが、 そのアクセスをL7制御するコンポーネントがまだデプロイされていません。
Doxseyさんの記事では生のNginx DaemonSetでL7制御しているのですが、 これはあまり使い勝手がよくありません。
そこで生のNginx DaemonSetはやめて、Kubernetesらしく外部アクセス制御するためのIngress Controllerを立てましょう。
今回はIngress ControllerにContourを選びます。 ContourはEnvoyベースのIngress Controllerです。 ダウンタイム無しで設定変更が行える、gRPCを扱える、shadow proxyをサポートしているなどの長所があります。
ここからはContourのデプロイ方法を説明します。バージョンv1.3.0を使用します。
Getting Startedに従いスタンダードにデプロイするとLoadBalancer Serviceを使うのですが、 GKE環境でLoadBalancer Serviceを作ってしまうと前述の課金が発生します。
これを回避するためにHost Networkingデプロイオプションを利用します。 これはEnvoy DaemonSetをホストネットワーク上にデプロイし、Nodeへの80,443アクセスをEnvoyでリッスンするという方式です。
contour/examples/contourのマニフェスト群に以下の変更を加え、applyしてください。
- Envoy用Serviceの
type: LoadBalancer
とexternalTrafficPolicy: Local
の指定を消す - Envoy Podを
hostNetwork: true
にし、dnsPolicy: ClusterFirstWithHostNet
にする - Contourの
serve
コマンドに--envoy-service-http-port=80
と--envoy-service-https-port=443
を追加する
ここまで行うと、HTTPProxyカスタムリソースで任意のServiceをインターネット公開できるようになります。HTTPProxyはIngressリソースの置き換えとなるカスタムリソースです。
ContourはIngressリソースも解釈できますが、Ingressリソースを作成するとGKE Ingress controllerが動作してしまう事故が起きかねないのでHTTPProxyを使うようにしたほうが良いでしょう。
Cert-managerデプロイ
どうせならHTTPSを使用してサービス公開したいのでcert-managerをデプロイしましょう。cert-managerのデプロイはスタンダードなやり方で問題ありません。
ここまでの案内に従うとCloudflare DNSを使用しているはずなので、CloudflareでACME DNS-01チャレンジをするためのIssuer/ClusterIssuerを作成しましょう。CloudflareのAPIトークンを使用します。
HTTP-01チャレンジを選ぶこともできますが、cert-managerでHTTP-01チャレンジを行うとLoadBalancer Serviceが一時的に作られてしまうので、DNS-01チャレンジを利用したほうが良いでしょう。
用意したドメインexample.comについてCertificateリソースを発行し、作成されたSecretをHTTPProxy.spec.tls.secretName
にセットすると、対象のサービスをHTTPSで公開できます。
マニフェスト例を載せておきます。
ClusterIssuer
apiVersion: cert-manager.io/v1alpha2 kind: ClusterIssuer metadata: name: cloudflare-prod spec: acme: email: example@gmail.com privateKeySecretRef: name: cloudflare-account-key server: https://acme-v02.api.letsencrypt.org/directory solvers: - dns01: cloudflare: apiTokenSecretRef: key: api-key name: cloudflare-api-key-secret email: example@gmail.com
Certificate
apiVersion: cert-manager.io/v1alpha2 kind: Certificate metadata: name: example.com spec: commonName: example.com dnsNames: - example.com issuerRef: kind: ClusterIssuer name: clouddns-prod secretName: example-com-prod-tls
HTTPProxy
apiVersion: projectcontour.io/v1 kind: HTTPProxy metadata: name: example spec: routes: - conditions: - prefix: / services: - name: frontend port: 3000 virtualhost: fqdn: example.com tls: secretName: example-com-prod-tls
サブドメイン追加方法
example.comのサブドメイン、たとえばfoo.example.comを追加したい場合の手順を説明します。
Cloudflare DNSにCNAMEレコードを追加し、foo.example.comがexample.comのエイリアスとなるようにしてください。
あとはexample.comのときと同様にHTTPProxyを作るだけです。
他ドメイン追加方法
example.com以外のドメイン、たとえばexample2.comを追加したい場合の手順を説明します。
まずexample.comの時と同様、example2.comをCloudflareにsite追加し、レジストラのDNSサーバをCloudflareのものに切り替えてください。
次にcloudflare-kubernetes-syncを新規でもう1つデプロイしてください。 こうするとexample.comとexample2.comに同じAレコードが割り当てられます。
あとは普通にすでにデプロイ済みのContourでHTTPProxyを作るだけです。
まとめ
GKEでGCPロードバランサとGKE Ingressを使わず、使い勝手を維持したままL7制御するための手順を説明しました。GKEと書きましたが、他のKaaSでも動く気がします。
この記事を書いた後、もう一度この手順を最初から動作確認するのが面倒すぎてやってないので、 なにか書き漏らしがあるかもしれません。 質問があったら書いてください。
あとこの手順はサービスの濫用っぽい気もしなくはないです。だめだったら消すので言ってください。
CloudNative Days Tokyo 2019 感想 #CNDT2019
CloudNative Days Tokyo 2019 というクラウドネイティブコンピューティング系の技術と OpenStack系の技術のカンファレンスに行きました。
印象に残ったセッションをいくつか書きます。
Kubernetesを拡張して日々のオペレーションを自動化する
Kubernetesを拡張して日々のオペレーションを自動化する https://t.co/NzCR6JM7rh 資料を公開しました :) #CNDT2019 #OSDT2019 #RoomB
— ladicle (@Ladicle) July 22, 2019
カスタムコントローラの作り方の話。
最近カスタムコントローラを作ることが増えてきたので。
入門っぽいところから始まって、具体的な開発上の知見まで教えてくれるセッション。 あとスライドがすごいおしゃれだった。
個人的メモ
- Conditions フィールドは配列で作ったほうがいい
- CRD のフィールドは主に以下の二種類がある:
- ズレ検出のためのフィールド
- ズレを修正するためのアクションのオプション。こちらのフィールドは struct にまとめておくと後から変更しやすい
- OwnerReference は親しか引けないので、孫引きをするなら孫引き用のフィールドを用意しとくと計算時間の削減になる
- 公式リソースに対する Finalizer は validation がかかっている。
{{ APIGroup }}/{{ Name }}
という形式にする必要がある - キャッシュは manager 単位なので、同じリソースをみる controller は同じ manager にまとめると効率的
- 後方互換性をサポートするためにこれからは Conversion Webhook が使える
Kubernetes拡張を利用した自作Autoscalerで実現するストレスフリーな運用の世界
本日発表した弊社山本による登壇資料です!
— CyberAgent アドテクスタジオ広報担当 (@ca_adtechstudio) July 22, 2019
ぜひご覧ください😀
「Kubernetes拡張を利用した自作Autoscalerで実現するストレスフリーな運用の世界」#CNDT2019 #OSDT2019 #Kuberneteshttps://t.co/ziOmyXznGl
カスタムコントローラで Bigtable の autoscaler を作った事例。
カスタムコントローラって運用者を置き換える仕組みとして紹介されることが多いけど、 異常検知とか時系列データをもとになにかアクションを起こすような仕組みとの相性がめちゃ良さそうだなとか思った。
そういえば数理的な手法で解を見つけるような仕組みの含まれたカスタムコントローラってまだ無いイメージがある。 Poseidon っていうスケジューラが数理最適化を使うらしい。
Cloud Native Storageが拓くDatabase on Kubernetesの世界
本日の1B3、「Cloud Native Storageが開くDatabase on Kubernetesの未来」の登壇資料です。 #cndt2019 #RoomBhttps://t.co/ygm96pLK1j
— こば(右) (@tzkb) July 22, 2019
k8s以前からあるDBクラスタリング基礎知識の講義から始まり、 いまあるクラウドネイティブなストレージソフトウェアがどのようなパターンを採用しているのかということを説明してくれるセッション。 個人的には一日目の中で一番勉強になった。
ちなみに発表内で紹介されていた kube-fencing の実装が bash だった。k8s 関連の OSS たまに bash で書いてあるからびびる。 github.com
実録!CloudNativeを目指した230日
これから話す「実録!CloudNativeを目指した230日」の資料です🍻 https://t.co/Q6zAz9UIv2 #CNDT2019 #RoomB
— 一輪挿し (@r_takaishi) July 23, 2019
OpenStackベースのプライベートクラウドを使っている会社でk8sを導入していっている話。 ingress-nginx のラウンドロビンがリセットされて一瞬同じPodに負荷が集中するバグの話などがされていた。 オブザーバビリティは重要。
ZOZOTOWNのCloud Native Journey 〜トール・マカベッチのアンサーソング付き〜
本日の登壇資料です / ZOZOTOWNのCloud Native Journey https://t.co/pj2yPP7v9J #CNDT2019
— 岡 大勝 (@okahiromasa) July 23, 2019
ZOZOTOWN のアーキテクチャ刷新の話。 まずはAPI Gatewayを立てましょうなど具体的なアプローチにも触れられていた。 クラウドネイティブ環境への移行は自分の働いている会社でも非常にホットな話題なので観に行った。
この資料が良いらしい。 docs.microsoft.com
あとこの本がいいらしい。 shop.oreilly.com
How cgroup-v2 and PSI impacts CloudNative?
圧を感じたところで自分の資料をえいやと事前公開。 #CNDT2019 #OSDT2019 #RoomF です、 17:40 ~https://t.co/smY8nqg269
— Uchio KONDO 🔫 (@udzura) July 23, 2019
croupの入門的な解説から始まり、cgroup v2 での変更と PSI(Pressure Stall Information) というカーネル機能の解説。 どういう controller が cgroup にあるのかなども教えてくれる内容になっていて、このあたり不勉強の自分にはとても勉強になった。 cgroup v2 と PSI を使うとコンテナごとの負荷状況が取りやすくなるとのこと。
ちなみに node_exporter にも PSI を使う collector が生えてた。 github.com
全体的な感想
カスタムコントローラの話とクラウドネイティブに移行する話が多かった印象。 来年のCNDTではオブザーバビリティやEnvoyを使った事例の話が増えていたりするんだろうか? せっかく自分もk8s中心の基盤チームで働いているんだし、今後なにか発表してみたい。
k8s と k8s の外側(IaaSのマネージドサービスやOpenStack)を繋ぐ部分をカスタムコントローラで作るのは普遍的なアイデアっぽい。 個人的にはカスタムコントローラってどんどん作ってみたいけど、カスタムコントローラで作ることがきちんと意味をなす題材ってあんまり落ちてないな~とかよくおもっていた。なんかいいアイデア思いつきたい。
心残り
無事優秀なワーカーノードになれた私です。マニアックすぎるか?ほんとうにおもしろいか?など悩みながら準備しましたが、ウケてたようでうれしい。 #CNDT2019 / “転職したら Kubernetes だった件 / That Time I Changed Jobs as a Kubernetes. - Speaker Deck” (205 users) https://t.co/xFSs1rO50Q
— ina_ani (@ina_ani) July 23, 2019
このセッション実際に観たかった~
2017年買って良かったもの
総括
今年は社会人になって自由に使えるお金が増えたのでいろんなものを買った。
以下リスト
プロトピック軟膏
毎年たくさん買っている。ヒルドイドと一緒に処方されるのでどんどん使う。
今年はいろいろあって高ストレス環境に突入することも多かったのだが
寛解状態を維持できたのはひとえにこいつを患部に塗りたくったからです。
プロトピックを塗れる程度には強度があってよかった。
ていうかこれからが冬本番なんだよな。
気を抜かずにメンテしていく。
引っ越して新しい病院にいって「プロトピック沢山欲しいです」って言ったら
ニヤニヤしながら「沢山ってどれくらいだよ〜?」と聞かれた。なんなんだよ
ANOVA Precision Cooker
今更なのは分かっているが丁寧な暮らしをしたかったので買った。
ローストビーフと鶏ハムを量産した。
家事能力として評価されなさそうな調理スキルが伸びた。
買うまで気づかなかった利点としては、洗い物が少ないというのがある。
100均で買ったプラスチック製のケースで低温調理しているのだけど、
そろそろもうちょっとマシな器が欲しい気がする。
イームズチェアのリプロダクト
良い椅子を買うまでのつなぎとして新生活開始と同時に購入したが、
おもったよりふつうに使えている。
3000円くらいで買えるので良い。
ロボット掃除機
毎日頑張って掃除してくれる。
かわいい。
ニトリのラグ
寒いから買ったんだけどめっちゃいい。
生活っぽさがでる。
ロボット掃除機もギリギリのぼってくれる。がんばれ!
タマノハダ コンディショナー 000 ラベンダー 540ml | 玉の肌(TAMANOHADA) | リンス・コンディショナー
ランドリン 柔軟剤 クラシックフローラル 600ml
こいつらのお陰でお姉さんみたいな良い匂いがする。
あまりにいい匂いすぎて隣の席の先輩をドキドキさせてしまったかもしれない。
ヒューレット・パッカード HP シングルモニターアーム BT861AA
机が広くなるし邪魔なときは横にずらすとか色々融通が聞くので買ったほうが良い。
PFU Happy Hacking Keyboard Professional BT 英語配列/墨 PD-KB600B
ビジネス系の研修でつらかった時期に金が無いのにかった。
こいつのせいで生活が逼迫されたがこいつを触ってる瞬間は幸せだった。
でもうるさいから会社では Type-S を使ってる。
ていうか HHKB Pro は白い方が好きなんだよな。
直販ならBTT版にも白があるということを買ってから知った。