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-kindGitHub 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.comexample.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用Servicetype: LoadBalancerexternalTrafficPolicy: Localの指定を消す
  • Envoy PodhostNetwork: 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.comexample.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を拡張して日々のオペレーションを自動化する

カスタムコントローラの作り方の話。

最近カスタムコントローラを作ることが増えてきたので。

入門っぽいところから始まって、具体的な開発上の知見まで教えてくれるセッション。 あとスライドがすごいおしゃれだった。

個人的メモ

  • Conditions フィールドは配列で作ったほうがいい
  • CRD のフィールドは主に以下の二種類がある:
    • ズレ検出のためのフィールド
    • ズレを修正するためのアクションのオプション。こちらのフィールドは struct にまとめておくと後から変更しやすい
  • OwnerReference は親しか引けないので、孫引きをするなら孫引き用のフィールドを用意しとくと計算時間の削減になる
  • 公式リソースに対する Finalizer は validation がかかっている。{{ APIGroup }}/{{ Name }} という形式にする必要がある
  • キャッシュは manager 単位なので、同じリソースをみる controller は同じ manager にまとめると効率的
  • 後方互換性をサポートするためにこれからは Conversion Webhook が使える

Kubernetes拡張を利用した自作Autoscalerで実現するストレスフリーな運用の世界

カスタムコントローラで Bigtable の autoscaler を作った事例。

カスタムコントローラって運用者を置き換える仕組みとして紹介されることが多いけど、 異常検知とか時系列データをもとになにかアクションを起こすような仕組みとの相性がめちゃ良さそうだなとか思った。

そういえば数理的な手法で解を見つけるような仕組みの含まれたカスタムコントローラってまだ無いイメージがある。 Poseidon っていうスケジューラが数理最適化を使うらしい。

Cloud Native Storageが拓くDatabase on Kubernetesの世界

k8s以前からあるDBクラスタリング基礎知識の講義から始まり、 いまあるクラウドネイティブなストレージソフトウェアがどのようなパターンを採用しているのかということを説明してくれるセッション。 個人的には一日目の中で一番勉強になった。

ちなみに発表内で紹介されていた kube-fencing の実装が bash だった。k8s 関連の OSS たまに bash で書いてあるからびびる。 github.com

実録!CloudNativeを目指した230日

OpenStackベースのプライベートクラウドを使っている会社でk8sを導入していっている話。 ingress-nginx のラウンドロビンがリセットされて一瞬同じPodに負荷が集中するバグの話などがされていた。 オブザーバビリティは重要。

ZOZOTOWNのCloud Native Journey 〜トール・マカベッチのアンサーソング付き〜

ZOZOTOWNアーキテクチャ刷新の話。 まずはAPI Gatewayを立てましょうなど具体的なアプローチにも触れられていた。 クラウドネイティブ環境への移行は自分の働いている会社でも非常にホットな話題なので観に行った。

この資料が良いらしい。 docs.microsoft.com

あとこの本がいいらしい。 shop.oreilly.com

How cgroup-v2 and PSI impacts CloudNative?

croupの入門的な解説から始まり、cgroup v2 での変更と PSI(Pressure Stall Information) というカーネル機能の解説。 どういう controller が cgroup にあるのかなども教えてくれる内容になっていて、このあたり不勉強の自分にはとても勉強になった。 cgroup v2 と PSI を使うとコンテナごとの負荷状況が取りやすくなるとのこと。

ちなみに node_exporter にも PSI を使う collector が生えてた。 github.com

全体的な感想

カスタムコントローラの話とクラウドネイティブに移行する話が多かった印象。 来年のCNDTではオブザーバビリティやEnvoyを使った事例の話が増えていたりするんだろうか? せっかく自分もk8s中心の基盤チームで働いているんだし、今後なにか発表してみたい。

k8sk8s の外側(IaaSのマネージドサービスやOpenStack)を繋ぐ部分をカスタムコントローラで作るのは普遍的なアイデアっぽい。 個人的にはカスタムコントローラってどんどん作ってみたいけど、カスタムコントローラで作ることがきちんと意味をなす題材ってあんまり落ちてないな~とかよくおもっていた。なんかいいアイデア思いつきたい。

心残り

このセッション実際に観たかった~

2017年買って良かったもの

総括

今年は社会人になって自由に使えるお金が増えたのでいろんなものを買った。

 

以下リスト

 

プロトピック軟膏

f:id:dulltz:20171227233239p:plain

 

毎年たくさん買っている。ヒルドイドと一緒に処方されるのでどんどん使う。

今年はいろいろあって高ストレス環境に突入することも多かったのだが

寛解状態を維持できたのはひとえにこいつを患部に塗りたくったからです。

プロトピックを塗れる程度には強度があってよかった。

ていうかこれからが冬本番なんだよな。

気を抜かずにメンテしていく。

引っ越して新しい病院にいって「プロトピック沢山欲しいです」って言ったら

ニヤニヤしながら「沢山ってどれくらいだよ〜?」と聞かれた。なんなんだよ

 

 

  

ANOVA Precision Cooker

New: Anova Precision Cooker - WIFI 2nd Gen (900 Watts)

今更なのは分かっているが丁寧な暮らしをしたかったので買った。

ローストビーフと鶏ハムを量産した。

家事能力として評価されなさそうな調理スキルが伸びた。

 

買うまで気づかなかった利点としては、洗い物が少ないというのがある。

 

100均で買ったプラスチック製のケースで低温調理しているのだけど、

そろそろもうちょっとマシな器が欲しい気がする。

 

 

イームズチェアのリプロダクト

三吉 イームズシェルチェアリプロダクト ダイニングチェア WH

良い椅子を買うまでのつなぎとして新生活開始と同時に購入したが、

おもったよりふつうに使えている。

3000円くらいで買えるので良い。

 

 

 

ロボット掃除機

eufy RoboVac 20 (自動掃除機ロボット) 【7つの清掃モード / 最大200分連続稼働 / 自動充電機能搭載 / 専用リモコン & ソニックウォール & 充電ステーション付属】

毎日頑張って掃除してくれる。

かわいい。

 

 

ニトリのラグ

https://www.nitori-net.jp/wcsstore/ec/images/Image/catalog/7226104/646X1000/722610401.jpg

寒いから買ったんだけどめっちゃいい。

生活っぽさがでる。

ロボット掃除機もギリギリのぼってくれる。がんばれ!

 

 

タマノハダ コンディショナー 000 ラベンダー 540ml | 玉の肌(TAMANOHADA) | リンス・コンディショナー

タマノハダ コンディショナー 004 ガーデニア 540ml

 

ランドリン 柔軟剤 クラシックフローラル 600ml

ランドリン 柔軟剤 クラシックフローラル 600ml

こいつらのお陰でお姉さんみたいな良い匂いがする。

あまりにいい匂いすぎて隣の席の先輩をドキドキさせてしまったかもしれない。

  

 

ヒューレット・パッカード HP シングルモニターアーム BT861AA

 

HP(ヒューレット・パッカード) HP(ヒューレット・パッカード) シングルモニターアーム BT861AA

机が広くなるし邪魔なときは横にずらすとか色々融通が聞くので買ったほうが良い。

 

 

PFU Happy Hacking Keyboard Professional BT 英語配列/墨 PD-KB600B

PFU Happy Hacking Keyboard Professional BT 英語配列/墨 PD-KB600B

ビジネス系の研修でつらかった時期に金が無いのにかった。

こいつのせいで生活が逼迫されたがこいつを触ってる瞬間は幸せだった。

でもうるさいから会社では Type-S を使ってる。

ていうか HHKB Pro は白い方が好きなんだよな。

直販ならBTT版にも白があるということを買ってから知った。