GKEでPreemptibleインスタンスを積極的に運用する

これは [Kubernetes3 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/kubernetes3) 22日目の記事です。

GKE上で2~3年ほどシステムを運用しています。運用に付き物なのはコストですね。ここではコスト効率を考えてPreemptibleインスタンスを活用している話を書きます。

## GKEの料金体系

まずGKEがどういう料金体系かを見ていきます。以下にあるとおり、基本的にクラスタを構成するノードがGCEインスタンスと対応しており、その料金がかかるという感じです。

_AnthosやAdvancedでまた変わりそうですがここでは無視します_

https://cloud.google.com/kubernetes-engine/pricing

ではGCEインスタンスの料金がどのように決定されるかというと、基本的にCPUやネットワークなどに対して従量課金される仕組みです。ただしディスカウントの仕組みも用意されており、Commitment priceやPreemptible priceがあります。
AWS(数年使ってないですが)で言うところのリザーブドインスタンスとスポットインスタンスみたいなやつです。

https://cloud.google.com/compute/all-pricing

実際に通常のOn-demand priceとどの程度差があるのか見てみると、Preemptibe priceが非常に安価であることが分かります。

![Screenshot from 2019-12-22 18-53-48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13923/98669b27-4c30-dada-0da0-9705f140ffa2.png)

ただしPreemptibleインスタンスは制約もかなり強く、注意点を把握しておく必要があります。

### Preemptibleインスタンスの制約

雑にまとめると以下のような制約があります。空いてるマシンリソースを少し貸してもらう感じですね。

- 最大24hでインスタンスがシャットダウンする
  - 24h持つかどうかは不明で、途中でシャットダウンもあり得る
- インスタンス起動時に在庫が枯渇していると起動に失敗する
  - 通常のインスタンスも同様だが、恐らく通常インスタンスの方がプールは多くなっていると思われる

## どうやって運用しているか

ではここまでの話を踏まえてどうやってPreemptibleインスタンスを運用しているかですが、主に以下の2つの仕組みを使っています。

- 各deploymentに対して、Node Affinityで通常インスタンスとPreemptibleインスタンスのノードプールをそれぞれ適用する
- Node Affinityで指定する2種類のノードプールで動いているノードを定期的にリフレッシュさせる

それぞれ詳しく見ていきます。

### Node Affinity

まずはNode Affinityの設定ですが、k8s標準で用意されているアレです。ここでは通常インスタンスを `ondemand-pool`、Preemptibleインスタンスを `preemptible-pool` として設定しています。その上でpreemptible-poolの優先度を高くしています。
これにより仮にpreemptible-poolが使えなくなった場合の対応を行っています。

```yaml:deployment.yaml
  template:
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: cloud.google.com/gke-nodepool
                operator: In
                values:
                - preemptible-pool
                - ondemand-pool
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            preference:
              matchExpressions:
              - key: cloud.google.com/gke-nodepool
                operator: In
                values:
                - preemptible-pool
          - weight: 1
            preference:
              matchExpressions:
              - key: cloud.google.com/gke-nodepool
                operator: In
                values:
                - ondemand-pool
```

### ノードのリフレッシュ

次に各ノードプールのリフレッシュです。リフレッシュというのは簡単に言うとノードを殺して実現しています。
ondemand-poolとpreemptible-poolを1つのグループと見立てて、以下のようなことをやっています。

#### preemptible-poolで最もシャットダウンの可能性が高い(=長寿)のインスタンスを殺す

これによりシャットダウン時間にバラつきを持たせることで一度に大量のリソースが使用不可になることを回避しています。

#### ondemand-poolが設定の最低台数以上で動いていれば殺す

これによりpreemptible-poolが一時的に使えなくなり、ondemand-poolが過多となってしまった場合でもpreemptible-poolにリソースを移していくことが可能となります。

### Thyella

これらを実現しているのが https://github.com/takashabe/thyella です。基本的に以下のようにGCPプロジェクトID、クラスタ名と対象のノードプールを設定すればOKです。

```shell:.envrc.sample
export THYELLA_PROJECT_ID=myprojectok
export THYELLA_CLUSTER=mycluster
export THYELLA_NODE_POOLS=ondemand-pool,preemptible-pool
```

あとはこいつをdocker imageなりにしてjob(cronjob)で動かせば定期的にノードがリフレッシュされるようになります。

_元々社内用に書いたやつを公開できる範囲で持ってきたもので、ロギングやメトリクス通知などの便利系社内ライブラリに頼っていた部分を削除しており、このまま運用するにはツラい感じなので年末年始でちゃんと書きたいと思います_

ちなみに非常にアグレッシブな手法でリフレッシュを行っているため、長時間走るようなジョブに適用するのはリスクが大きそうです。もしこの手法を試してみたい場合は、k8sで動かしている前提があるので大抵は大丈夫だと思いますが、途中で死んでも大丈夫なアプリケーションを対象としてください。

#### 類似プロダクト

https://github.com/estafette/estafette-gke-preemptible-killer

Thyellaを作った後に見つけたので正直まともに検証していないんですが、

> Selecting node pool is not supported yet, the code is processing ALL preemptible nodes attached to the cluster, and there is no way to limit it even via taints nor annotations

とあるので、今回やりたかった通常インスタンスが過剰に立ち上がってしまった場合の対応や、ノードプールごとに柔軟な設定はやりづらそうな雰囲気があります。

## おわりに

Preemptibleインスタンスを積極的に活用することでコストを大幅に抑えることに成功しました。ただしPreemptibleインスタンスの制約が厳しいため、運用するためのツールを書いて動かしています。

Thyellaですが、いわゆるChaos Engineeringのようなものなので、GCP自体の稼働状況を見て実行するかどうかといった機構を入れるとより安心に使える気がしています。11月にGCEインスタンスの新規作成が失敗するような[インシデント](https://status.cloud.google.com/incident/zall/19001)も発生していたので…。
ロギングやSlack通知なども含めてPRお待ちしております。