Knativeに入門しました


この記事は OpenSaaS Studio Advent Calendar 2019 の 22 日目の記事です。

初めまして。サイバーエージェント OpenSaaS Studio の@satoshiyamamoto です。コンテンツモデレーションシステム Orion でデータストリーム処理の開発や自社サービスへの導入などを担当しています。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 が作成される

Getting Started

Installing Knativeに従い Kubernetes のクラスタに Knative をインストールします。Configuring DNSの項を参考に 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 に準拠

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 は、必要な数だけ作成できます。

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の通り 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 に基づいてフィルターを指定できます。

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: greetingsource: 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 は、様々なプログラミング言語で Addressable または Collable なオブジェクトを実装することができます。 さらに Trigger のフィルタにより逐次発生するイベントを適切な Consumer に伝搬させることが出来ます。

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

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