Knative Servingでサーバーレスを始める

[ラクスAdvent Calendar2021](https://qiita.com/advent-calendar/2021/rakus)の22日目の記事を担当する@t_okkanです。前年のAdvent Calendar以来、1年ぶりのQiitaの投稿です。

最近お仕事でKubernetesを触っています。その中でKubernetes上でサーバーレスプラットフォームを構築できるKnativeを知り、入門してみました。

## Knativeとは

KnativeはKubernetes上にサーバーレスなアプリケーションをデプロイ、実行、管理するためのプラットフォームです。

https://knative.dev/docs/

KubernetesとIstioなどのIngressを組み合わせてコンテナアプリケーションを手軽にデプロイすることができます。Kubernetesを使うことでクラウドプラットフォームに依存しないサーバーレス環境を構築できます。
オープンソースで開発されており、2021年11月にバージョン1.0がリリースされました。[^1]元々は2018年からGoogleによって開発がされていましたが、バージョン1.0に上がると同時にCloud Native Computing Foundationに寄贈されました。[^2]
KnativeはGCPのフルマネージドのコンテナ実行環境であるCloud Runの基盤ソフトウェアとして利用されています。[^3]

すでにCNCFにはAzure Container Appsの基盤である[KEDA](https://keda.sh)が採択されており、KnativeがCNCFに追加されたことによりサーバーレスコンピューティングの標準化が進むのではと期待しています。
今回はそんなKnativeのサーバーレスアプリケーションを公開する機能であるServingについて基本を理解し、ローカル環境で実際にサンプルアプリを動かしてみます。

## Knativeの構成

Knativeは大きく分けて`Knative Serving`と`Knative Eventing`の2つのコンポーネントから構成されています。

https://knative.dev/docs/serving/

https://knative.dev/docs/eventing/

Knative Eventingはイベントドリブンなアーキテクチャでアプリケーションを実行する機能を提供するコンポーネントです。元々はこの2つのコンポーネントに加えて`Knative Build`というCIパイプラインのコンポーネントも存在しましたが、今は切り離されCI/CDパイプラインの構築ツールである[Tekton](https://tekton.dev/)として独立して開発されています。[^4]
今回はKnative Servingについて詳しくみていきます。

### Knative Serving

Knative Servingはサーバーレスアプリケーションを公開し実行する機能を提供するコンポーネントです。コンテナアプリケーションをKubernetesのPodに配置し実行することができます。

Knative Servingの主な役割はオートスケーリングです。その中でも大きな特徴なのは、アクセスがない場合は起動しているPodの数を0にできるゼロスケーリングです。この機能により、エンドポイントにアクセスがあった時にPodを起動しアプリケーションを実行し、処理が終了すればPodを停止しPodの数を0までスケールインできます。そのため、ゼロスケーリングによりサーバーレス（リソースが不要）な状態になります。もちろんアクセスの負荷に合わせてスケールアウトも可能です。
オートスケーリング以外にも、リクエストを決められた割合で特定のPodに割り振るトラフィック機能などもあります。

### Knative Serving の構成

Knative Servingは`Service`、`Route`、`Configuration`、`Revision`から構成されます。それぞれKubernetesのカスタムリソースとして定義する必要があります。

<figure><img src="https://github.com/knative/serving/raw/main/docs/spec/images/object_model.png" /><figcaption><a href="https://knative.dev/docs/serving/">Knative Serving Overview</a>から引用</figcaption></figure>

#### Revision

Revisionはコンテナアプリケーションのコード（コンテナイメージ）と設定ファイル（環境変数など）を管理します。このRevisionをもとにオブジェクトを作成しコンテナアプリケーションのデプロイやオートスケーリングを行います。RevisioinはImmutableであり、後述するConfigurationがRevisionのバージョンを管理しています。

#### Configuration

ConfigurationはRevisionを管理するコンポーネントです。Revisionのコードと設定ファイルを分離し、変更履歴を管理します。新しいアプリケーションがデプロイされると、Configurationが更新され新しいRevisionが作成されます。

#### Route

RouteはRevisionに対するHTTPリクエストのトラフィックの割り振りを管理します。Revisionが複数ある場合は、Revisionごとに割合を指定してトラフィックを分散させることができます。

#### Service

KubernetesのコンポーネントにもServiceがありますが、Knative ServingのServiceとは別物となります。
ServiceはRouteやConfigurationを管理し、Knativeのワークロード全体を制御します。

以上で、Knative Servingの基本的な説明は終わります。続いて実際にローカル環境でKnative Serivigを動かしサーバーレスアプリケーションをデプロイしてみます。

## Knative Servingの実践

### 環境

本記事で利用するソフトウェアやツールは以下のバージョンになります。

- Knative：1.1.0
- Docker：20.10.8
- Node.js：v14.16.0
- kind：0.11.1
- kubectl：1.21.3

### Knativeのインストール

まずは以下の公式のページを参考にKnativeをインストールします。

https://knative.dev/docs/getting-started/

今回はローカル環境のKubernetesクラスターにデプロイしていきます。Kubernetesクラスタはkind（Kubernetes in Dokcer）を利用し構築します。
では、kind、kubectl、Knative（kn）をbrewでインストールします。

```bash
$ brew install kind
$ brew install kubectl
$ brew install kn
```

今回は[Knative quickstart](https://knative.dev/docs/getting-started/#install-the-knative-quickstart-environment)を利用してKnative Servingの環境を構築します。

```bash
$ brew install knative-sandbox/kn-plugins/quickstart
```

本番環境でKnativeを利用する場合は、以下の構築手順を参考にしてください。

https://knative.dev/docs/install/

## Knativeの起動

では必要な環境がそろいましたので、早速KnativeのKubernetesクラスタを構築していきます。以下のコマンドを実行して構築します。

```bash
$ kn quickstart kind
🎉 Now have some fun with Serverless and Event Driven Apps!
$ kind get clusters
knative
```

`kind get clusters`を実行して`knative`というクラスターが作成されていれば完了です。（少し時間がかかります）

### サンプルコンテナアプリの作成

Knative上で動作するサンプルのサーバーレスコンテナアプリケーションを作成します。今回は公式で公開されているNode.jsのサンプルアプリケーションを参考に作成します。

https://github.com/knative/docs/tree/main/code-samples/serving/hello-world/helloworld-nodejs

詳細は折り畳みで非表示にしますので、公式のGithubか以下の折り畳みを表示して作成してください。（Docker Hubでイメージを公開するためアカウントが必要です）

<details><summary>サンプルコンテナアプリの作成</summary><div>

#### Node.jsアプリケーションの作成

以下のGithubを参考にNode.js(Express)のサンプルアプリケーションを作成します。

https://github.com/knative/docs/tree/main/code-samples/serving/hello-world/helloworld-nodejs

Node.jsのプロジェクトを作成し、expressをインストールします。

```bash
$ mkdir knative-service-sample
$ cd knative-service-sample
$ npm inint -y
$ npm install express
```

アプリケーションのコードとなる`index.js`は以下のような実装になります。

- index.js

```javascript:index.js
const express = require('express');
const app = express();

app.get('/', (req, res) => {
  console.log('Knative service sample');

  const target = process.env.TARGET || 'World';
  res.send(`Hello ${target}`);
});

const port = process.env.PORT || 8080;
app.listen(port, () => {
  console.log(`Knative service sample listening on ${port}`);
})
```

`package.json`にnpmのscriptを追加します。

- package.json

```json:package.json
{
  "name": "knative-service-sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.2"
  }
}
```

#### Dockerイメージ作成

こちらも公式のGithubを参考に、Node.jsアプリケーションのDockerイメージをビルドし、Docker Hubにpushします。

- Dockerfile

```dockerfile:Dockerfile
FROM node:12-slim
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --only=production
COPY . ./
CMD [ "npm", "start" ]
```

- .dockerignore

```:.dockerignore
Dockerfile
README.md
node_modules
npm-debug.log
```

イメージをビルドし、Docker Hubにpushします。（`{your name}`のところをご自身のDocker Hubのユーザーネームに変更してください）

```
$ docker build -t {your name}/kind-service-sample .
$ docker push {your name}/kind-service-sample
```

以上でアプリケーションの準備は完了です。

</div></details>

## サーバーレスアプリケーションのデプロイ

では作成したコンテナアプリケーションをKnativeの基盤上にデプロイし、サーバーレスなアプリケーションの実行とゼロスケーリングの確認をしていきます。

まずは対比用に同じコンテナアプリケーションを実行するPodを作成しデプロイします。
以下のようなyamlファイルを作成しPodを作成します。

- pod.yml

```yaml:pod.yml
apiVersion: v1
kind: Pod
metadata:
  name: sample-pod
spec:
  containers:
    - name: sample-pod
      image: docker.io/{your name}/knative-service-sample
      env:
        - name: TARGET
          value: "Pod sample v.1.0"
```

```
$ kubectl apply --filename pod.yml
$ kubectl get pod
NAME         READY   STATUS    RESTARTS   AGE
sample-pod   1/1     Running   0          52s
```

`kubectl get pod`で`sample-pod`と表示されれば完了です。
続いてKnativeでコンテナアプリケーションをデプロイするための設定ファイルを作成します。

- service.yml

```yaml:service.yml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: knative-service-sample
  namespace: default
spec:
  template:
    spec:
      containers:
        - image: docker.io/{your name}/knative-service-sample
          env:
            - name: TARGET
              value: "Knative service sample v.1.0"
```

作成できればKubernetesクラスターにKnative Serviceを作成します。

```bash
$ kubectl apply --filename service.yml
service.serving.knative.dev/knative-service-sample created
```

確認用のコマンドとして以下のコマンドを実行し、defaultのNamespaceに構築されるPodを追跡します。

```bash
$ kubectl get -w pods -n default
NAME                                                       READY   STATUS    RESTARTS   AGE
knative-service-sample-00001-deployment-754d685b84-zjdkd   2/2     Running   0          39s
sample-pod                                                 1/1     Running   0          90s
knative-service-sample-00001-deployment-754d685b84-zjdkd   2/2     Terminating   0          61s
knative-service-sample-00001-deployment-754d685b84-zjdkd   0/2     Terminating   0          91s
```

下記のように`kanative-service-sample`のSTATUSが、起動時のRunnning（コンテナが実行されている状態）からTerminating（コンテナが停止している状態）になっていると思います。
Knativeでは起動後にアクセスがなければ自動でゼロスケーリングします。

Knative Serviceの作成が完了すれば、エンドポイントを確認します。以下のコマンドを実行し`knative-service-sample`のURLを取得します。

```bash
$ kubectl get ksvc knative-service-sample  --output=custom-columns=NAME:.metadata.name,URL:.status.url
NAME                     URL
knative-service-sample   http://knative-service-sample.default.127.0.0.1.sslip.io
```

URLが取得できれば、実際にアクセスしてPod数0からのオートスケーリングとゼロスケーリングを確認します。curlを利用してエンドポイントにアクセスします。

```bash
$ curl http://knative-service-sample.default.127.0.0.1.sslip.io
Hello Knative service sample v.1.0⏎
```

上記のようにExpressのアプリケーションのレスポンスが返ってくれば成功です。

Podの監視の方では以下のように、アクセスが発生すると`knative-service-sample`のPodが生成されSTATUSがPending → Runningに変化していることがわかるかと思います。PendingからRunningまで約1秒で完了できます。初回アクセスは少し遅くなりますが、2回目以降はすぐにレスポンスが返ってきます。
その後しばらくアクセスがないとPodの状態がTerminatingとなり、Pod数が0になります。

```bash
$ kubectl get -w pods -n default
NAME                                                       READY   STATUS    RESTARTS   AGE
knative-service-sample-00001-deployment-754d685b84-zjdkd   2/2     Running   0          39s
sample-pod                                                 1/1     Running   0          90s
knative-service-sample-00001-deployment-754d685b84-zjdkd   2/2     Terminating   0          61s
knative-service-sample-00001-deployment-754d685b84-zjdkd   0/2     Terminating   0          91s
knative-service-sample-00001-deployment-754d685b84-zjdkd   0/2     Terminating   0          91s
knative-service-sample-00001-deployment-754d685b84-zjdkd   0/2     Terminating   0          91s
knative-service-sample-00001-deployment-754d685b84-dzqj2   0/2     Pending       0          0s　　　　　⬅️ アクセスを検知しPodを起動
knative-service-sample-00001-deployment-754d685b84-dzqj2   0/2     Pending       0          0s
knative-service-sample-00001-deployment-754d685b84-dzqj2   0/2     ContainerCreating   0          0s
knative-service-sample-00001-deployment-754d685b84-dzqj2   1/2     Running             0          1s  ⬅️ Podが起動され処理が開始
knative-service-sample-00001-deployment-754d685b84-dzqj2   2/2     Running             0          2s
knative-service-sample-00001-deployment-754d685b84-dzqj2   2/2     Terminating         0          63s ⬅️ アクセスがないためPodを削除
knative-service-sample-00001-deployment-754d685b84-dzqj2   0/2     Terminating         0          94s
knative-service-sample-00001-deployment-754d685b84-dzqj2   0/2     Terminating         0          94s
knative-service-sample-00001-deployment-754d685b84-dzqj2   0/2     Terminating         0          94s
```

## Knativeを終了する

Knativeを使い終わったら以下のコマンドでクラスターを停止します。結構リソースを消費するので利用しない時は停止をお勧めします。

```
$ kn service delete knative-service-sample
```

## 最後に

Knativeをローカル環境に構築し、簡単なサンプルアプリケーションを動かしてみました。今回はKnative Servingだけを触りましたが、Knative Eventingの方も触っていきたいと思います。

[^1]: [Knative 1.0 is out!](https://knative.dev/blog/articles/knative-1.0/)
[^2]: [Knative joining the CNCF](https://knative.dev/blog/steering/knative-cncf-donation/)
[^3]: [マネージド Knative サービスの Cloud Run を一般提供開始](https://cloud.google.com/blog/ja/products/serverless/knative-based-cloud-run-services-are-ga)
[^4]: [Migrating from Knative Build](https://github.com/tektoncd/pipeline/blob/main/docs/migrating-from-knative-build.md)