Knativeに入門しました

この記事は [OpenSaaS Studio Advent Calendar 2019](https://adventar.org/calendars/3928) の 22 日目の記事です。

初めまして。サイバーエージェント OpenSaaS Studio の@satoshiyamamoto です。コンテンツモデレーションシステム [Orion](https://www.slideshare.net/cyberagent/orion-an-integrated-multimedia-content-moderation-system-for-web-services-102635466) でデータストリーム処理の開発や自社サービスへの導入などを担当しています。Orion のデータストリームは、 Apache Kafka と Spring Cloud Stream で構成されているのですが、Kubernetes への移行がキッカケで Cloud Native なアーキテクチャに興味を持つようになりました。そこで Google Cloud の Cloud Run や Redhat の OpenShift Serverless で採用されている Knative について、バージョン v0.11 を対象に調べてみました。

## Knative とは

Knative は、Google、Pivo​​tal、およびその他の業界リーダーのエンジニアによって開始された新しいオープンソースプロジェクトです。モダンなサーバーレスワークロードをデプロイ、管理するための Kubernetes ベースのプラットフォームで、Serving、Eventing のコンポーネントを含みます。コンテナベースの管理を簡素化することで、ビジネスロジックなどの開発者が重要なことに集中できるようにします。

Knative プロジェクトの概要に触れたところで、Serving、Eventing の各コンポーンエントについて、公式ドキュメントを眺めながら Getting Started をそれぞれ試してみたいと思います。

## Serving

Knative Serving は、Kubernetes と Istio 上に構築され、コンテナのアプリケーションと関数のデプロイをサポートします。Serving は簡単に開始でき、高度なシナリオをサポートするように拡張できます。

Knative Serving プロジェクトは、次のことを可能にします。

- サーバーレスコンテナのすばやい展開
- オートスケーリング
- Istio コンポーネントのルーティングとプログラム可能なネットワーキング
- デプロイされたコードと構成のバージョン管理

AWS の API Gateway + Lambda と同じことが Kubernetes 上で実現できるといった印象でしょうか。

### Serving のリソース

Knative Serving は、オブジェクトのセットを Kubernetes カスタムリソース定義（CRD）として定義します。これらのオブジェクトを使用して、クラスター上のサーバーレスワークロードの動作を定義および制御します。

- Service:
  - Route + Configuration = Service
- Route:
  - ネットワークのエンドポイントを 1 つ以上の Revision にマップ
- Configuration:
  - デプロイメントの望ましい状態を維持
- Revision:
  - コードの変更と Coniguration のスナップショット
  - Configuration の変更で Revision が作成される

![object_model.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35486/da5d8e3c-7fdc-bc40-823f-f74764588513.png)

### Getting Started

[Installing Knative](https://knative.dev/docs/install/)に従い Kubernetes のクラスタに Knative をインストールします。[Configuring DNS](https://knative.dev/docs/install/installing-istio/)の項を参考に xip.io の DNS で istio-ingressgateway の External IP を設定すると動作が確認しやすいと思います。

では、Serving のマニフェストファイルを作成しサンプルアプリケーションをデプロイしてみます。

    # service.yaml
    apiVersion: serving.knative.dev/v1alpha1
    kind: Service
    metadata:
      name: helloworld-go
      namespace: default
    spec:
      template:
        spec:
          containers:
            - image: gcr.io/knative-samples/helloworld-go
              env:
                - name: TARGET
                  value: OpenSaaS Studio!
    ---
    kubectl apply --filename service.yaml

サンプルアプリケーションが正常にデプロイされたかどうか確認します。

    kubectl get ksvc helloworld-go

    NAME            URL                                                LATESTCREATED         LATESTREADY           READY   REASON
    helloworld-go   http://helloworld-go.default.1.2.3.4.xip.io        helloworld-go-96dtk   helloworld-go-96dtk   True

Route で作成されたエンドポイントに HTTP リクエストを送ります

    curl http://helloworld-go.default.1.2.3.4.xip.io
    Hello OpenSaaS Studio!

Kubernetes の標準的なリソースを使用してアプリケーションをデプロイする場合、Deployment、Service、Ingress のマニフェストが必要ですが、それ比較すると Knative Service だけと少ない記述量でアプリケーションがデプロイできました。

続いて Eventing コンポーネントを見ていきます。

## Eventing

Knative Eventing は、Cloud Native 開発の一般的なニーズに対応するように設計されたシステムであり、Event source と Event consumer の遅延バインディングを有効にする基本要素を提供します。

現時点でも様々な Event source が公式から提供されています。

- KubernetesEventSource
- GitHubSource
- GcpPubSubSource
- AwsSqsSource
- ContainerSource
- CronJobSource
- KafkaSource
- CamelSource

### 設計の概要

Knative Eventing は、次のゴールに基づいて設計されています。

1. Knative Eventing サービスは疎結合
2. Event producer と Event consumer は独立している
3. 他のサービスは、Knative のイベントシステムに接続できる
4. サービス間の相互運用性を確保。Knative Eventing は、CNCF サーバーレス WG によって開発された [CloudEvents](https://cloudevents.io) に準拠

### Consumer

複数のタイプのサービスへの配信を可能にするために、Knative Eventing は複数の Kubernetes リソースによって実装できる 2 つの汎用インターフェイスを定義します。

1. Addressable なオブジェクトは、status.address.url フィールドで定義されたアドレスに HTTP 経由で配信されたイベントを受信および確認できます。
2. Callable なオブジェクトは、HTTP 経由で配信されるイベントを受信し、イベントを変換して、HTTP レスポンスで 0 または 1 つの新しいイベントを返すことができます。これらの返されたイベントは、外部イベントソースからのイベントが処理されるのと同じ方法でさらに処理されます。

### Broker と Trigger

v0.5 の時点で、Knative Eventing は Broker および Trigger オブジェクトを定義して、イベントのフィルタリングをより簡単にします。ブローカーは、attribute で選択できるイベントのバケットを提供します。イベントを受信し、1 つ以上の一致する Trigger によって定義された Subscriber (Service) にイベントを転送します。Trigger は、Addressable なオブジェクトに配信されるイベント attribute の filter を記述します。Trigger は、必要な数だけ作成できます。
![broker-trigger-overview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35486/5ea05d34-74d1-27fa-bfd6-88ee14bc8a76.png)

## Event registry

v0.6 の時点で、Knative Eventing は EventType オブジェクトを定義して、Consumer がさまざまな Broker から消費できるイベントのタイプを簡単に発見できるようにします。レジストリは、EventType のコレクションで構成されています。レジストリに格納されている EventType には、他の帯域外の仕組みに頼らずに、Consumer が Trigger を作成するために必要な情報が含まれています。

### Event channel と Subscription

Knative Eventing は、Channel と呼ばれるイベント転送および永続化レイヤーも定義します。各 Channel は個別の Kubernetes カスタムリソースです。イベントは、Subscription を使用してサービスに配信されるか、他の Channel に転送されます。これにより、クラスター内のメッセージ配信が要件に基づいて変化するため、一部のイベントはインメモリ実装によって処理され、他のイベントは Apache Kafka または NATS Streaming を使用して永続化されます。

Eventing はリソースの数が多いので長くなりましたが、諦めずに Getting Started に挑みます。

### Getting Started

[Installing the Knative Eventing component](https://knative.dev/docs/eventing/getting-started/#installing-knative-eventing)の通り Eventing コンポーネントをインストールします。

Getting Started では、event-example 名前空間を作成し Knative リソースをグループ化して整理します。

    kubectl create namespace event-example
    kubectl label namespace event-example knative-eventing-injection=enabled

Broker が実行されているか確認します。

    kubectl --namespace event-example get Broker default
    NAME      READY   REASON   URL                                                        AGE
    default   True             http://default-broker.event-example.svc.cluster.local      17s

2 つの Event consumer、hello-display と goodbye-display を作成します。
まずは hello-display の Deployment と Service から。

    kubectl --namespace event-example apply --filename - << END
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: hello-display
    spec:
    replicas: 1
    selector:
        matchLabels: &labels
        app: hello-display
    template:
        metadata:
        labels: *labels
        spec:
        containers:
            - name: event-display
            # Source code: https://github.com/knative/eventing-contrib/blob/release-0.6/cmd/event_display/main.go
            image: gcr.io/knative-releases/github.com/knative/eventing-sources/cmd/event_display@sha256:37ace92b63fc516ad4c8331b6b3b2d84e4ab2d8ba898e387c0b6f68f0e3081c4

    ---

    # Service pointing at the previous Deployment. This will be the target for event
    # consumption.
    kind: Service
    apiVersion: v1
    metadata:
        name: hello-display
    spec:
        selector:
        app: hello-display
        ports:
        - protocol: TCP
        port: 80
        targetPort: 8080
    END

続いて goodbye-display。

    kubectl --namespace event-example apply --filename - << END
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: goodbye-display
    spec:
    replicas: 1
    selector:
        matchLabels: &labels
        app: goodbye-display
    template:
        metadata:
        labels: *labels
        spec:
        containers:
            - name: event-display
            # Source code: https://github.com/knative/eventing-contrib/blob/release-0.6/cmd/event_display/main.go
            image: gcr.io/knative-releases/github.com/knative/eventing-sources/cmd/event_display@sha256:37ace92b63fc516ad4c8331b6b3b2d84e4ab2d8ba898e387c0b6f68f0e3081c4

    ---

    # Service pointing at the previous Deployment. This will be the target for event
    # consumption.
    kind: Service
    apiVersion: v1
    metadata:
    name: goodbye-display
    spec:
    selector:
        app: goodbye-display
    ports:
    - protocol: TCP
        port: 80
        targetPort: 8080
    END

デプロイされたか確認します。

    kubectl --namespace event-example get deployments hello-display goodbye-display
    NAME              READY   UP-TO-DATE   AVAILABLE   AGE
    hello-display     1/1     1            1           14s
    NAME              READY   UP-TO-DATE   AVAILABLE   AGE
    goodbye-display   1/1     1            1           7s

イベントを適切な Consumer に転送するため Trigger を作成します。Trigger は、CloudEvents の [Context Attributes](https://github.com/cloudevents/spec/blob/v1.0/spec.md#context-attributes) に基づいてフィルターを指定できます。

`type: greeting` のイベントを hello-display に送信する Trigger を作成します。

    kubectl --namespace event-example apply --filename - << END
    apiVersion: eventing.knative.dev/v1alpha1
    kind: Trigger
    metadata:
    name: hello-display
    spec:
    filter:
        attributes:
        type: greeting
    subscriber:
        ref:
        apiVersion: v1
        kind: Service
        name: hello-display
    END

続いて `source: sendoff` のイベントを goodbye-display に送信する Trigger を作成します。

    kubectl --namespace event-example apply --filename - << END
    apiVersion: eventing.knative.dev/v1alpha1
    kind: Trigger
    metadata:
    name: goodbye-display
    spec:
    filter:
        attributes:
        source: sendoff
    subscriber:
        ref:
        apiVersion: v1
        kind: Service
        name: goodbye-display
    END

Trigger が作成されたか確認します

    kubectl --namespace event-example get triggers
    NAME              READY   REASON   BROKER    SUBSCRIBER_URI                                            AGE
    goodbye-display   True             default   http://goodbye-display.event-example.svc.cluster.local/   4s
    hello-display     True             default   http://hello-display.event-example.svc.cluster.local/     3m16s

最後に Event producer の Pod を作成します。curl を使用して個々のイベントを手動で送信し、これらのイベントが正しい Consumer によってどのように受信されるかを見てみます。

    kubectl --namespace event-example apply --filename - << END
    apiVersion: v1
    kind: Pod
    metadata:
    labels:
        run: curl
    name: curl
    spec:
    containers:
        # This could be any image that we can SSH into and has curl.
    - image: radial/busyboxplus:curl
        imagePullPolicy: IfNotPresent
        name: curl
        resources: {}
        stdin: true
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
        tty: true
    END

Broker に HTTP リクエストでイベントを作成します。

    kubectl --namespace event-example attach curl -it
    Defaulting container name to curl.
    Use 'kubectl describe pod/ -n event-example' to see all of the containers in this pod.
    If you don't see a command prompt, try pressing enter.
    [ root@curl:/ ]$

さまざまな type のイベントを作成するために 3 つのリクエストを行います。
`type: greeting` のイベントを作成する最初のリクエスト

    curl -v "http://default-broker.event-example.svc.cluster.local" \
      -X POST \
      -H "Ce-Id: say-hello" \
      -H "Ce-Specversion: 0.3" \
      -H "Ce-Type: greeting" \
      -H "Ce-Source: not-sendoff" \
      -H "Content-Type: application/json" \
      -d '{"msg":"Hello Knative!"}'

Broker がイベントを受信すると、hello-display がアクティブ化され、Event consumer に送信されます。イベントが受信された場合、Event producer は 202 Accepted を受け取ります。

`source: sendoff` のイベントを作成する 2 番目のリクエスト

    curl -v "http://default-broker.event-example.svc.cluster.local" \
      -X POST \
      -H "Ce-Id: say-goodbye" \
      -H "Ce-Specversion: 0.3" \
      -H "Ce-Type: not-greeting" \
      -H "Ce-Source: sendoff" \
      -H "Content-Type: application/json" \
      -d '{"msg":"Goodbye Knative!"}'

good-by-display がアクティブになりました。

`type: greeting` と `source: sendoff` を持つイベントを作成

    curl -v "http://default-broker.event-example.svc.cluster.local" \
      -X POST \
      -H "Ce-Id: say-hello-goodbye" \
      -H "Ce-Specversion: 0.3" \
      -H "Ce-Type: greeting" \
      -H "Ce-Source: sendoff" \
      -H "Content-Type: application/json" \
      -d '{"msg":"Hello Knative! Goodbye Knative!"}'

3 つのイベントが正しく Subscriber に受信されたか確認します。
Pod のラベルを指定して `hello-display` Consumer のログを見ます

    kubectl --namespace event-example logs -l app=hello-display --tail=100
    ☁️  cloudevents.Event
    Validation: valid
    Context Attributes,
      specversion: 0.3
      type: greeting
      source: sendoff
      id: say-hello-goodbye
      time: 2019-12-22T16:29:33.624112047Z
      datacontenttype: application/json
    Extensions,
      knativearrivaltime: 2019-12-22T16:29:33Z
      knativehistory: default-kne-trigger-kn-channel.event-example.svc.cluster.local
      traceparent: 00-aa8665dc43c912ba58c8d710575a595a-8d6f71f444b08f8f-00
    Data,
      {
        "msg": "Hello Knative! Goodbye Knative!"
      }
    ☁️  cloudevents.Event
    Validation: valid
    Context Attributes,
      specversion: 0.3
      type: greeting
      source: sendoff
      id: say-hello-goodbye
      time: 2019-12-22T16:29:33.624112047Z
      datacontenttype: application/json
    Extensions,
      knativearrivaltime: 2019-12-22T16:29:33Z
      knativehistory: default-kne-trigger-kn-channel.event-example.svc.cluster.local
      traceparent: 00-aa8665dc43c912ba58c8d710575a595a-8d6f71f444b08f8f-00
    Data,
      {
        "msg": "Hello Knative! Goodbye Knative!"
      }

`type: greeting` のイベントを受け取ったことが確認できました。同様に `goodbye-display` のログも確認します。

    kubectl --namespace event-example logs -l app=hello-display --tail=100
    ☁️  cloudevents.Event
    Validation: valid
    Context Attributes,
      specversion: 0.3
      type: not-greeting
      source: sendoff
      id: say-goodbye
      time: 2019-12-22T16:40:36.143572583Z
      datacontenttype: application/json
    Extensions,
      knativearrivaltime: 2019-12-22T16:40:36Z
      knativehistory: default-kne-trigger-kn-channel.event-example.svc.cluster.local
      traceparent: 00-12d3b7c70adcc48bc5961cb8bae7576d-20c7194c25944c0b-00
    Data,
      {
        "msg": "Goodbye Knative!"
      }
    ☁️  cloudevents.Event
    Validation: valid
    Context Attributes,
      specversion: 0.3
      type: greeting
      source: sendoff
      id: say-hello-goodbye
      time: 2019-12-22T16:40:42.28921708Z
      datacontenttype: application/json
    Extensions,
      knativearrivaltime: 2019-12-22T16:40:42Z
      knativehistory: default-kne-trigger-kn-channel.event-example.svc.cluster.local
      traceparent: 00-f0c07fbbd7f1f32810c2bdfec1d12ca2-b31ec2a305780987-00
    Data,
      {
        "msg": "Hello Knative! Goodbye Knative!"
      }

## まとめ

Knative は、業界全体が参画するオープンソースプロジェクトでベンダーニュートラルなサーバーレス環境を Kubernetes 上に構築します。

Serving コンポーネントは、簡素なマニフェストでアプリケーションを簡単にデプロイできトラフィックベースでオートスケールできます。

Eventing コンポーネントは、Event source、Event consumer を組み合わせることで Kubernetes 上にイベントドリブンなシステムを構築できます。Consumer は、様々な[プログラミング言語](https://github.com/cloudevents)で Addressable または Collable なオブジェクトを実装することができます。 さらに Trigger のフィルタにより逐次発生するイベントを適切な Consumer に伝搬させることが出来ます。

最後に、本当は CronJobSource、KafkaSource に挑戦したかったのですが、Istio の知識が乏しく [cluster-local-gateway の問題](https://github.com/knative/eventing/issues/1973)を解決できず、サンプルアプリケーションを動かすことができませんでした。少し残念な結果となってしまったので、これらについては冬休みの宿題にしたいと思います。

また、Knative に興味を持った方にオススメの書籍とスライドを紹介させて頂きます。

- [Knative の歩き方 Kubernetes から Serverless を訪ねて 第 2 版 #技術書典](https://booth.pm/ja/items/1309468)
- Knative Serving のイマ ＠nak3 - OpenShift Meetup Tokyo #7