Posts for: #Kubernetes

ストレッチと読書会で体力を使い果たした

23時に寝て3時と5時に起きつつ7時に起きた。21時半には出張から家に戻ってきて天気がよかったらオフィスへ行ってたんだけど、雨降りだったのでそのまま寝てた。今日もストレッチを終えてから読書会に参加したらその後は眠くなってしまって家に帰って寝ていた。ストレッチと読書会しかやっていないのに一日の体力を使い果たした状態になるぐらいの疲労が蓄積している。

ストレッチ

今日の開脚幅は開始前155cmで、ストレッチ後159cmだった。数字の上では先週と大差ないのだけど、疲労の蓄積で右腰と右股関節周りが大変なことになっている。久しぶりにストレッチを受けていて辛くて耐えきれないところの一歩手前まで到達していた。ふくらはぎとか限界に近かったが、なんとか耐えきった。出張前に懸念していたことでもあったけれど、結果として、出張前のストレッチで復調しつつあった体調は悪化したと言わざるを得ない。たくさん歩いたことや慣れないホテル暮らしで腰と右足に負担がかかってしまった。

オンライン読書会

先月はオフィスの引っ越しで不参加だったため、1ヶ月飛ばしで 第6回『Go言語による分散サービス』オンライン読書会 に参加した。本書の読書会は今日で最後の読書会で次の2章を読んだ。

  • 10章 Kubernetes でローカルにアプリケーションをデプロイ
  • 11章 アプリケーションを Kubernetes でクラウドにデプロイ

10章では kind というローカル k8s クラスターを構築するツールを使って go アプリケーションをデプロイする。データベースのようなデータを永続化するようなサービスのためのリソース種別に StatefulSets がある。私は使ったことがないリソース種別だったのでキーワードを知ることができてよかった。jsonnet というデータテンプレート言語というツールも出てきて、なんだこれは?と思ってびっくりした。これも初見だし、使っている話しも聞いたことなかった。本書でも k8s については書籍を一冊書いても足りないと説明されていて、アプリケーションのデプロイに必要な k8s マニフェストの説明を10章だけでやって、ほとんどの読者は置いてけぼりだと思う。k8s の運用経験のある私が読んでも yaml の正当性なんて動かしてみないとわからないし、k8s はバージョンアップが速いのですぐに陳腐化する可能性もある。helm のパッケージングなどにも触れていてキーワードを知るという意味ではよかったと思う。この2つの章は k8s へのデプロイはこんな感じですよという雰囲気を味わってもらうお気持ち程度の内容だと思う。

15時頃には読み終えて、それから8人ぐらいで雑談していた。私は書籍の組版を自分でやったことはないのだけど、しばたさんは出版社によっては自分で組版までやって pdf で納品しているらしい。現行は tex で管理しているようにみえる。余白の調整や索引作るのも自分でやっているらしい。ある本は後になってから出版社の規定している余白よりも広過ぎてページ数が増えたことに気付いて、その後の本で余白を調整したりしたとのこと。本来はそういうお仕事は編集者のお仕事ではある。とくに索引は数ページになったりするので大変といった話しをされていた。effective java 第3版だと索引だけで9ページある。

軽量のコンテナオーケストレーションツールは存在しない

0時に寝て6時に起きた。冷蔵庫に飲みものなくて不安。

docker の swarm mode

昨日から docker の swarm mode について調べていた。

オンプレにコンテナのアプリケーションをデプロイするにあたり、軽量のコンテナオーケストレーションツールはないかと調べ始めた。Kubernetes vs Docker Swarm vs Nomad - the orchestrator wars continue? を記事を読むと、軽量のオーケストレーションツールと言えるのは swarm mode か Nomad ぐらいしかない。docker に付属しているならまずはそれを調べるべきだろうと調査した。そして swarm mode の採用は見送った。現時点でこの機能が廃止されるというアナウンスはないが、あまり保守されておらず、docker 社も積極的に推進していない。近い将来、機能が廃止される可能性が高いと私は判断した。

docker のドキュメントをみながら3台の ec2 インスタンスでクラスターを構築してみて簡単であることは間違いない。コマンド2つで swarm クラスターを構築できる。一通り触ってみて数台のマシンを管理する軽量のコンテナオーケストレーションツールとしては十分だと私は思うけれど、残念ながらこのツールが求められる業務やビジネスは少ないのだろうと思う。みんな k8s に持ってかれたという感じかな。調べていて読んだ記事など。

hannali dao 雑談

Hannali DAO #02 に参加した。

最初は dao とは何かをみんなで雑談してた。私はお仕事しながら軽く聞いているつもりだったのが、議論に口出しして熱中してた。技術的に私が理解できないことが出てくると、私の認識が誤っていたり新しい気付きがあったりする可能性があるので、ついつい詳細を聞いてみたくなる。hannali dao でトークンをばら撒く戦略をみんなで考えていて dao の宣伝をしたら貢献の1つとみなしてトークンをもらえる。私は twitter でいくつか hannali dao のツィートをしていて、1ツィートが 300 PROG とかで、合計で 1250 PROG のトークンをもらった。PROG というのは hannali dao でのみ使えるトークンね。

その後、ウォレットに名前を付けられる ENS (Ethereum Name Service) というサービスがあるのを教えてもらった。ちょうどいくらか ethereum をもってたので metamask に送金して、metamask で ens の登録 (購入) をやってみた。初めて ethereum を使ってサービスの支払いをやってみた。これで私も web3 を完全に理解したよ。ens で名前を登録 (購入) するのに $22.58 、ドメイン名みたいなものでサブドメインも登録できる。サブドメインを付けるのに $2.96 かかり、その ens を metamask に紐付けるのに $7.49 がかかった。metamask に紐付けると https://etherscan.io/ などで検索したときにウォレットのアドレスではなく、ens で購入した名前が表示されるようになる。トランザクションの履歴に名前が付いてそれが誰なのかが他人にもわかってしまうことがどういった影響を及ぼすのか、プライバシー云々の他に私はまだわかっていない。web3 なので購入した名前は一般公開されているものだけど、その名前は知り合いにしかまだ教えていない。暗号資産のウォレットに名前を付けることの意味や体験などをこれから経験してみる。

簡単な現象の組み合わせ障害

0時に寝て6時に起きた。

eks クラスター障害の原因判明

過去に2回発生していた eks クラスター障害 の原因がようやくわかった。テスト環境も本番環境は5日ごとに再現していて、datadog で k8s のダッシュボードでそれぞれの pod 単位のメモリ使用量をみると datadog-agent の pod がメモリリークしていることに気付いた。そこから当たりをつけて datadog-agent の issue を調べると次のバグに遭遇していた。

ゾンビプロセスが生成されて、それが os のプロセス数上限に達してしまい、それによってプロセス (スレッド) が生成できなくなって、その結果として aws/amazon-vpc-cni-k8saws-node という eks クラスターの管理アプリケーションが動かなくなって、それが動かないと k8s ノードのステータスが NotReady になってしまって、通常の pod のアプリケーションも動かなくなってしまうという現象が発生していた。datadog-agent のアップグレードは私が行ったものだし、その後の k8s ノードの監視や調査で気付きが足りなかったと反省した。

  • datadog-agent の新しいバージョンをテスト環境でもうしばらく検証してもよかった
  • datadog-agent をリソースリークの可能性を私の中の調査対象から外していた
    • 世の中で使われているものに致命的なバグが起きないだろうという先入観があった
  • プロセスを生成できない原因として考えられる背景を調査すべきだった
    • ulimit を確認してリソース制限はないようにみえた
    • プロセス数やゾンビプロセスを調べていなかった
    • kernel に /proc/sys/kernel/pid_max という上限設定があることを知らなかった
  • テスト環境と本番環境で5日程度で落ちるという周期性から気付くべきだった
    • たしかにテスト環境から1日遅れて本番環境で障害が発生していた
    • 周期性があることでリソースリークの可能性は高いとすぐに調査すべきだった
  • datadog で k8s のダッシュボードを調べるべきだった
    • すでに用意されているものがあったのでみようと思えばみえた
  • aws のインフラ要因ではないかと疑っていた
    • ごめんなさい

これは悔しい。自分の無能さや気付きの低さを実感した事件だった。私が注意深く観察していればもう1週間早く気付けた。そのせいで余分な障害と調査に時間を費やした。1つ1つは全く難しくない現象が巧妙に絡みあって隠蔽された結果としての状況に気付けなかった。注意して1つずつ観察して追跡していけばすぐに気付けた。本当に悔しい。

1つだけ言い訳をさせてもらうと、私は本番環境にアクセスできない。だからテスト環境と本番環境で発生している現象が同じかどうかを判断できず、調査を進める確証をもてなかった。

呑み

あまりに悔しかったのと調査してたら遅くなって晩ご飯食べる気力もなかったので気分転換に仲のよい焼き鳥屋さんに寄ってみた。あとから常連客のセブンイレブンの店長さんも来られて、私は初対面かなと思ってたんだけど先方は知っていると言ってたから以前にもカウンターでご一緒していたみたい。何気はなしに3人で2時前ぐらいまで雑談していた。

その店長さんがロレックスを購入しようと考えているという話しになって、資産または投資商品としてのロレックスの話しになった。たまたまヒカキンが1億円で買ったロレックスがいま2億円になっているといった話しがあったそうで、いまがバブルな状態らしいが、ロレックスをはじめとした高級時計の資産価値が上がっているらしい。私は腕時計を身につけないし高級時計もまったく興味はないが、投資商品の1つなんだというところに関心がもてた。

中小企業の社長の一般的な節税方法の1つに外車を買ったり売ったりするという話しがある。儲かったときに経費で外車を買って、赤字のときに外車を売って雑所得に変える。車は社用車として経費で落とせるから可能なことだが、高級時計はどうなのだろうか? 結論から言うと、普通の会社では高級時計は経費にできない。経費の原則は売上を上げるために必要な支出を経費とできる。普通の会社は高級時計で売上を上げることはできない。一方で経費として認められる職業もある。芸能人がそうだという。それは番組のために必要だという理屈で経費で落とせる。おそらくヒカキンも経費で高級時計を購入して、そのことを動画にしているのも仕事で必要だという言い訳作りの目的もあるのだと推測する。

休日の本番障害

夕方から寝ていて何度か起きたものの、そのままずっと寝ていた。あまりないことなんだけど、珍しくたくさん眠れた。

ストレッチ

今日の開脚幅は開始前160cmで、ストレッチ後163cmだった。計測の仕方がややいい加減だった気もしたが、先週より少しよくなったということにしておく。右腰の張りが強いのと肩が前に入りがちなので肩を開いて姿勢を保つように心がけるとよいとアドバイスをいただいた。もう通い始めて1年半ぐらい経つ。トレーナーさんも大半が入れ替わっていて通い始めたときに話しかけてくれた私が知っているトレーナーさんはほとんどいない。1年半も経つと人は変わっていくなというのを実感している。私の最初のトレーナーさんは社内制度で別の店舗の助っ人に行っているのでいなくなった人たちが辞めているわけでもないとは思うけど、1-2年で人が入れ替わってもサービスは継続していかないといけないし、会社ってそういうものだなと実感する機会でもある。

aws インフラの調子が悪い?

1-2週間ぐらい前からテスト環境を含めると複数回発生している eks クラスターの障害 がたまたま土曜日の夜という休日に発生した。いま eks クラスターのインフラの振る舞いを把握しているのは私だけなので、気付いてから指示を出して問題が発生している k8s ノードの削除 (ec2 インスタンスの削除) で復旧させるワークアラウンドで復旧させた。私は本番環境にアクセスできないので詳しい調査はできない。状況を正しく把握できてはいないけれど、k8s ノードが死んだり生き返ったりする不安定な状況に発生しているらしく、k8s ノードを削除して新規に作り直すと復旧することがわかっている。NotReady と Ready を繰り返したりしてアプリケーションの振る舞いが不安定になる。NotReady,SchedulingDisabled になれば、おそらく drain して k8s ノードが入れ替わってくれるのだけど、そうならない不安定な状況があるみたい。これ以上の調査は aws のサポートに問い合わせないとわからない。

k8s ノードの削除方法がわからない

1時に寝て7時に起きた。寝冷えしてお腹痛い。

eks クラスターの障害

日曜日にテスト環境の eks クラスターで障害が発生していた。k8s ノードが NotReady になっていて、しばらくすると NotReady,SchedulingDisabled に変わって、それから新しい k8s ノードが起動して古いものが削除されて置き換わった。おそらくエラーが発生し始めてから1時間ほどはかかっていたと思う。わりと時間がかかるので明らかに k8s ノードが不調だと人間が判断しているなら ec2 インスタンスの切り替えを早くやりたい。k8s の公式ドキュメントの Use kubectl drain to remove a node from service では次の手順で行うように書いてある。

$ kubectl drain <node name>

drain が正常終了すれば安全に k8s ノードを削除してよいのかな?

$ kubectl delete node <node name>

eks クラスターで障害が発生していたときに drain を実行するとエラーになったのでそのまま delete node したら k8s ノードは削除されたものの、自動的に新しい k8s ノードが起動しなかった。aws のマネジメントコンソールから ec2 インスタンスを調べたら起動したままだったので強制的に ec2 インスタンスを終了させたところ、オートスケールの設定から ec2 インスタンスが起動してきて復旧した。但し、このやり方は k8s が意図した手順ではないようにも思える。軽く調べた範囲では k8s ノードの正しい削除方法 (置き換え方法?) がみつからなかった。そんなことを日曜日に確認していたら月曜日にほぼ同じ現象が本番環境の eks クラスターでも発生した。私は一度経験していたので同僚に指示して経過を観察していた。ここで書いたのと同じような手順で復旧した。おそらく aws 側のなにかのメンテナンス作業でうちの eks クラスターだと k8s ノードが死んでしまうような作業があったのではないか?と疑いをもっている。

アプリケーションとジョブの違いと一時停止

2時に寝て5時に起きた。たった3時間しか寝てないのに夜中に1回は起きている。スポットで9時から会議が入ったのでそれまでに朝のお仕事を終わらせようと思ったら早起きできた。6時前にはオフィスに行ってお仕事を始められた。

アプリケーションからジョブへの移行

k8s の cronjob を活用する前は spring の Scheduled アノテーションで定期実行処理を書いていた。アプリケーションサーバー内の1スレッドが定期実行していた。これはスケールアウト前提のアプリケーションサーバーには向いてなくて、複数のアプリケーションサーバーをスケールアウトさせてデプロイすると、定期処理も複数実行されてしまい、同時実行できない類の処理だと問題になる。k8s の cronjob は同時実行しない設定などもあるため、k8s の cronjob へ移行している。

移行作業の準備をしていてアプリケーションサーバーの pod は replicas を調整することで一時停止の代替となるオペレーションができる。

$ kubectl scale --replicas=0 deployment/my-app
deployment.apps/my-app scaled

移行時のアプリケーションサーバーはこれで無効にしておき、cronjob に入れ替えるといった切り戻し可能な状態で移行できる。cronjob も suspend のフラグを使うことで一時停止できるので検証しながら双方を切り替えることができる。

$ kubectl patch cronjob my-batch-job1 -p '{"spec": {"suspend": true}}}'
cronjob.batch/my-batch-job1 patched

k8s クラスターの service account とロール

23時に寝て6時に起きた。前日は夕方からだらだらしてた。

k8s クラスターの権限管理

初期のクラスターを私が構築したわけではないため、権限周りはあまりよくわかっていない。k8s には2つのユーザーアカウントがある。

  • user account: 人向け
  • service account: アプリケーション向け

In this regard, Kubernetes does not have objects which represent normal user accounts. Normal users cannot be added to a cluster through an API call.

https://kubernetes.io/docs/reference/access-authn-authz/authentication/#users-in-kubernetes

但し、ユーザーアカウントを表すオブジェクトはなく、api 経由でクラスターにユーザーを追加したりはできない。ユーザーは存在しないけど、認証はできる仕組みを私は知らないので関心がある。それはまた今度調べるとして、今回は service account の権限を変更した。service account は pod 内から k8s の api サーバーにアクセスするときの認証などに使われる。デフォルトの権限だと、api サーバーのエンドポイントへの書き込み権限がない (?) ようなので追加する。

In order from most secure to least secure, the approaches are:

  1. Grant a role to an application-specific service account (best practice)
  2. Grant a role to the “default” service account in a namespace
  3. Grant a role to all service accounts in a namespace
  4. Grant a limited role to all service accounts cluster-wide (discouraged)
  5. Grant super-user access to all service accounts cluster-wide (strongly discouraged)

ServiceAccount permissions

セキュアな順番として上から自分たちの環境にあうかどうかを判断すればよい。私の場合、わざわざ新規に service account を作るほどの要件ではないため、2番目の default の service account にロールを与えることにした。RoleBinding というキーワードがあるので既存のクラスターロールから edit という権限を与える。

$ kubectl create rolebinding default-edit-role --clusterrole=edit --serviceaccount=default:default --namespace default
rolebinding.rbac.authorization.k8s.io/default-edit-role created

ここでは default-edit-role という RoleBinding を作成した。

$ kubectl get rolebinding default-edit-role -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  creationTimestamp: "2022-08-08T08:27:12Z"
  name: default-edit-role
  namespace: default
  resourceVersion: "152660975"
  uid: 6de13b71-3103-448c-805a-66e9400f61c3
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: edit
subjects:
- kind: ServiceAccount
  name: default
  namespace: default

これで cronjob から job を作成する権限が付与された。Write access for Endpoints によると、新規に作成した v1.22 以降のクラスターではエンドポイントに対する write アクセス権限が異なるように記載されている。一方で v1.22 にアップグレードしたクラスターは影響を受けないとも記述されている。私がいま運用しているクラスターは v1.21 から v1.22 にアップグレードしたクラスターなので edit ロールでうまくいったのかもしれない。クラスターのバージョンによって設定が異なるかもしれない。

wiremock を使った kubernetes モックサーバーのテスト拡張

2時に寝て7時に起きて9時までだらだらしてた。

Kubernetes Clients のライブラリ実装

昨日の続き 。ある程度、クライアントの振る舞いを確認できたので自前のライブラリを作ることにした。ライブラリなのでテストをちゃんと書きたいと思って単体テストのやり方を調べてたら Kubernetes Clients のリポジトリにも単体テストはほとんどなくて、どうも e2e テストの方を重視しているようにもみえた。github issues を検索してみたら次の issue をみつけた。

なにかしら単体テストの仕組みを作った方がいいんじゃないかという提案と一緒に issue の作者?かどうかはわからんけど、wiremock を使ったテストのサンプルコードをあげていた。名前だけは聞いたことがあったけど、過去に使ったこともなく、どういうものか全くわかってない。ドキュメントを軽く読んでみたら http モックサーバーらしい。issue の内容を参考にしながら wiremock のドキュメントをみて junit5 のテスト拡張を書いてみた。これが適切な実装かはあまり自信がないけど、こんな感じでモックサーバーとモッククライアントの junit5 のテスト拡張を実装した。これは static なモックサーバーの設定になるので wiremock 自体の起動コストは速く感じた。ライブラリのテストとしては申し分ない。いまのところは自画自賛。

@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface KubernetesApiClient {
}
public class SetupKubernetesWireMock implements BeforeAllCallback, BeforeEachCallback, ExtensionContext.Store.CloseableResource {
    private static final Logger logger = LogManager.getLogger(SetupKubernetesWireMock.class.getName());
    private static int PORT = 8384;
    private static WireMockServer wireMockServer = new WireMockServer(options().port(PORT));
    private static boolean started = false;

    private ApiClient apiClient;

    @Override
    public void beforeAll(ExtensionContext context) throws Exception {
        if (!started) {
            wireMockServer.start();
            started = true;
            this.configureMockClient();
            var basePath = String.format("http://localhost:%d", wireMockServer.port());
            this.apiClient = ClientBuilder.standard().setBasePath(basePath).build();
            logger.info("started kubernetes wiremock: {}", basePath);
            context.getRoot().getStore(GLOBAL).put("SetupKubernetesWireMock", this);
        }
    }

    @Override
    public void beforeEach(ExtensionContext context) throws Exception {
        for (var instance : context.getRequiredTestInstances().getAllInstances()) {
            for (var field : instance.getClass().getDeclaredFields()) {
                if (field.isAnnotationPresent(KubernetesApiClient.class)) {
                    field.setAccessible(true);
                    field.set(instance, this.apiClient);
                }
            }
        }
    }

    @Override
    public void close() throws Throwable {
        wireMockServer.stop();
    }

    private void configureMockClient() throws IOException {
        // add static stubs for mock client
        configureFor("localhost", PORT);
        this.stubForBatchGetCronjobs();
        this.stubForBatchGetSingleCronJob("my-job1");
        this.stubForBatchGetSingleCronJob("my-job2");
        this.stubForBatchGetSingleCronJob("my-job3");
        this.stubForBatchPostJob();
        this.stubForBatchGetJob();
    }

    private byte[] getContents(String name) throws IOException {
        var json = new File(this.getClass().getResource(name).getPath());
        return Files.readAllBytes(Paths.get(json.getPath()));
    }

    private void stubForBatchGetCronjobs() throws IOException {
        var contents = this.getContents("/fixtures/kubernetes/batch/cronjobs.json");
        var path = urlPathEqualTo("/apis/batch/v1/namespaces/default/cronjobs");
        stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(contents)));
    }

    private void stubForBatchGetSingleCronJob(String jobName) throws IOException {
        var contents = this.getContents(String.format("/fixtures/kubernetes/batch/%s.json", jobName));
        var url = String.format("/apis/batch/v1/namespaces/default/cronjobs/%s", jobName);
        var path = urlPathEqualTo(url);
        stubFor(get(path).willReturn(aResponse().withStatus(200).withBody(contents)));
    }

    private void stubForBatchPostJob() throws IOException {
        var name = "/fixtures/kubernetes/batch/post-my-job.json";
        var contents = this.getContents(name);
        var url = "/apis/batch/v1/namespaces/default/jobs";
        var path = urlPathEqualTo(url);
        stubFor(post(path).willReturn(aResponse().withStatus(200).withBody(contents)));
    }

    private void stubForBatchGetJob() throws IOException {
        var name = "/fixtures/kubernetes/batch/get-my-job.json";
        var contents = this.getContents(name);
        var url = "/apis/batch/v1/namespaces/default/jobs/my-job";
        var path = urlPathEqualTo(url);
        stubFor(get(urlPathEqualTo(url)).willReturn(aResponse().withStatus(200).withBody(contents)));
    }
}

Kubernetes Clients のサンプル実装

23時に寝て7時半に起きた。夜中も2回ぐらい起きる。暑さでまいってきた。

ストレッチ

今日の開脚幅は開始前159cmで、ストレッチ後162cmだった。数字は悪くない。いつもはストレッチを受けていると疲労しているところが伸びることで体が軽くなっていく感覚があるのだけど、今日は体全体がだるくてストレッチを受けていてもなんかしんどいなぁとだるさを感じていた。コロナに感染してないと思うけど、夏バテの状態をそのままストレッチにも持ち込んだような感覚があった。腰の張りや肩甲骨の硬さなどが少し気になったかな。トレーナーさんには立ったときの姿勢が少し前よりで重心のバランスがよくないといったアドバイスをされた。とくにどこが悪いというわけでもないのになんかしんどい。

Kubernetes Clients のサンプル実装

Kubernetes Clients の調査 の続き。java クライアントを使って minikube でいくつか動かしてみた。openapi で生成した rest api クライントが提供されている。デフォルト設定でも minikube で普通に動いたのでおそらく裏で $HOME/.kube/config をみたり /var/run/secrets/kubernetes.io/serviceaccount/token を読み込んで認証ヘッダーに設定してくれたりするのだと推測する。

public ApiClient getKubernetesClient() throws IOException {
    var client = Config.defaultClient();
    io.kubernetes.client.openapi.Configuration.setDefaultApiClient(client);
    return client;
}

このクライアントを使って java/kubernetes/docs/ 配下にある api インスタンスを生成する。例えば、cronjob や job を扱うならば BatchV1Api というドキュメントがある。BatchApi だけでも3つのドキュメントがあるのでちょっとやり過ぎな気もする。

  • BatchApi.md
  • BatchV1Api.md
  • BatchV1beta1Api.md

kubectl コマンドで使う cronjob から手動で job を設定するのを実装してみる。

$ kubectl create job --from=cronjob/my-schedule-job my-manual-job

細かい設定はちゃんと調べないといけないけど、一応はこれで動いた。cronjob のオブジェクトを取得して job のオブジェクトを生成して create するだけ。

var api = new BatchV1Api(this.getKubernetesClient());
var cronJob = api.readNamespacedCronJob(cronJobName, NAMESPACE_DEFAULT, null);
var ann = Map.of("cronjob.kubernetes.io/instantiate", "manual");
var metadata = new V1ObjectMeta().name(newJobName).annotations(ann);
var spec = cronJob.getSpec().getJobTemplate().getSpec();
spec.setTtlSecondsAfterFinished(10);
var job = new V1Job()
        .apiVersion(cronJob.getApiVersion())
        .kind("Job")
        .spec(cronJob.getSpec().getJobTemplate().getSpec())
        .metadata(metadata);
var result = api.createNamespacedJob(NAMESPACE_DEFAULT, job, null, null, null, null);

手動で作成した job の pod は終了後にゴミとして残ってしまうので ttl を設定すれば自動的に削除できることに気付いた。

spec.setTtlSecondsAfterFinished(10);

k8s クラスターの内部、つまり pod 内からリクエストするには ServiceAccount permissions を適切に設定しないといけない。ひとまずローカルの minikube で super user 権限にしたらリクエストはできた。実運用では適切なロールを定義して適切に権限設定しないといけない。

kubectl create clusterrolebinding serviceaccounts-cluster-admin \
  --clusterrole=cluster-admin \
  --group=system:serviceaccounts

Kubernetes Clients の調査

23時に寝て6時に起きた。

k8s cronjob の手動実行

いろんな定期/バッチ処理を k8s の cronjob に置き換えつつある。これまでアプリケーションサーバーでスケジュール実行していたものも本来サーバーである必要はないのでサーバーアプリケーションから cli アプリケーションに移行したりしている。そうやって定期実行ジョブが増えてくると、今度は調査やデバッグ目的で任意のタイミングで実行したくなる。kubectl を使って次のように実行できる。

$ kubectl create job --from=cronjob/my-schedule-job my-manual-job

この cli を実行すると、cronjob のマニフェストから my-manual-job というジョブの pod が生成されて実行される。開発者ならこれでよいのだけど、非開発者も調査や検証目的で実行したい。そのためには非開発者向けのインターフェースを作らないといけない。本当は chatops のように slack apps によるコマンド実行ができるとカッコよいのだけど、k8s クラスターと slack 間の認証やセキュリティの仕組みを作る必要があって、既存の仕組みがないならそこはセキュリティリスクにも成り得るのでちょっと控えたい。そうすると、既存のサーバーアプリケーションの web api のインターフェースで提供できるようにしたい。複数の言語向けに Kubernetes Clients が提供されている。これを使って cronjob の手動実行を実装できそうな気がする。時間があれば週末に軽く調べてみようと思う。

  • python
  • c#
  • javascript
  • java
  • c
  • haskel
  • go
  • ruby

ノードグループと nodeSelector

1時に寝て6時に起きた。

EKS クラスターのノードグループと k8s の nodeSelector

先日 k8s の nodeSelector の調査について書いた。いまテスト環境でその調査結果の運用検証中。当初は k8s の機能だけを使いたいと考えていたが、いま eksctl コマンドで EKS クラスターを管理していて、k8s ノードの実体である ec2 のプロビジョニングはノードグループがもつ起動テンプレートとオートスケールポリシーにより制御される。そのため、ノードグループを分割してそれぞれにノードラベルが必ず付与されるように管理する方が簡単だとわかってきた。要はアプリケーションサーバー向けのノードグループとバッチ処理向けのノードグループの2つを作った。あと覚えておくとよいのが、Adding a custom instance role で任意のポリシーもノードグループの iam ロールに追加できる。設定例としてはこんな感じ。

  iam:
    attachPolicyARNs:
    - arn:aws:iam::${accountId}:policy/my-custom-policy
    - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
    - arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy
    - arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy
    - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore

ノードグループの準備が整ったら nodeSelector を指定した pod をデプロイするだけ。k8s ノードがどんなノードラベルをもっているかは次のようにして確認できる。

$ kubectl get node --show-labels

意図したノードラベルが付与された k8s ノードに pod がデプロイされたかどうかは次のようにして確認できる。

$ kubectl get pod --output wide --field-selector spec.nodeName=${ノードラベルをもつノード名}

これらの環境構築、検証、wiki にドキュメントを書いて本番作業手順もまとめた。一通りきれいにまとまったインフラ作業を完遂できて気分がよい。