🍣

第4回 クラウド基盤mdxで構築したKubernetesで、サーバレスWebアプリケーションを構築する

2025/03/09に公開

この記事について

こんにちは、東京大学鈴村研究室で、インフラエンジニアとしてお手伝いさせていただいています、福田と申します。

https://sites.google.com/view/toyolab/鈴村研究室概要

今回のこの記事では、立ち上げたKubernetes環境の上で、MatalLBというL3ロードバランサと、Knativeというサーバーレスのアプリケーションを開発・実行・管理するためのライブラリをインストールして、Webサービスをデプロイし、公開するための手順について説明します。

それ以外のコンテンツについては、以下の記事一覧を参照ください。

https://zenn.dev/suzumura_lab/articles/627b5063d6884d

この手順のゴールとしては、KnativeでのサンプルWebアプリケーションをデプロイし、Webブラウザからアクセスできることとなっています。

前提

この記事ではKubernetesクラスタに、MatalLBやKnativeをインストールするところから始めるため、mdxの仮想マシンの構築方法や、Kubernetesクラスタ自体の構築方法については説明しません。
mdxの仮想マシンの構築方法やKubernetesクラスタ自体の構築方法を知りたい場合は、上述の記事一覧を参照ください。

アーキテクチャ全体像

今回構築するサーバレスWebアプリケーションの全体像は以下の通りです。
MetalLBはL3ロードバランサの役割を果たし、KnativeはL7ロードバランサの役割を果たしています。

MetalLBについて

MetalLB は、Kubernetes クラスターにL3 ロードバランシングを提供する、オンプレミス環境向けのLoadBalancer実装となっています。

https://metallb.io/

通常、AWS, GCP, Azureなどのクラウドプロバイダーでは、LoadBalancerサービスを作成すると自動的に外部のロードバランサーが割り当てられます。しかし、オンプレミス環境では、Kubernetesに組み込みのLoadBalancerがないため、MetalLB を使うことでKubernetesのLoadBalancerサービスをオンプレミス環境でも利用できるようになります。

MetalLBのインストール

公式のインストール手順に従って、インストールを実施していきます。


まずは、いつもの通り、以下のコマンドで踏み台サーバにログインします。
ssh-agentを設定しておくと、踏み台サーバから、各Workerインスタンスに秘密鍵などを指定することなくsshアクセスできるようになるので、便利です。
また、port6443をport forwardして、Lensが接続できるようにしておきます。
この辺りの手順は前回の記事を参照ください。

eval `ssh-agent`
ssh-add ~/.ssh/mdx_access_key
ssh -L 6443:(master nodeのPrivate IPアドレス):6443 -A mdxuser@(踏み台サーバのGlobal IPアドレス)

踏み台サーバで以下のコマンドを実行し、MetalLBをインストールします。

kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.9/config/manifests/metallb-native.yaml

IPアドレスプールの設定

次に、L3ロードバランサとして、割り当て可能なIPアドレスプールの設定をしていきます。
割り当て可能なIPアドレスですが、これは、worker nodeに割り当てられているプライベートIPアドレスを指定できます。
worker nodeに割り当てられているプライベートIPアドレスは、mdxのユーザーポータルの画面などから確認できます。

ここでは、例えば、worker nodeに割り当てられているプライベートIPアドレスについて、192.168.0.2だと仮定して説明を進めます。

このプライベートIPアドレスを使って、IPアドレスプールの設定を設定するには、以下のようにyamlファイルを作成します。

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: first-pool # ここは自由な名前を設定してOK
  namespace: metallb-system
spec:
  addresses:
  - 192.168.0.2/32  # ここにWorker nodeのIPアドレスを指定する

上記のyamlファイルについて、以下のコマンドでKubernetesに適用します。

kubectl apply -f (IPアドレスプール定義のyamlファイルのpath)

また、実際に上記のIPアドレスプールが割り当てられるようにするためには、Layer2 Advertisementという設定が必要になるので、以下のyamlファイルを作成し、こちらもKubernetesに適用します。

apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: l2-advertisement # 好きな名前を設定して良い
  namespace: metallb-system
spec:
  ipAddressPools:
  - first-pool # この名前をIPアドレスプールの名前に合わせる

上記のyamlファイルについて、以下のコマンドでKubernetesに適用します。

kubectl apply -f (L2Advertisementのyamlファイルのpath)

これでMetalLBの設定は完了です。
次に、KNativeの設定に移ります。

Knativeについて

Knativeは、Kubernetes上でサーバーレスアプリケーションを実行するためのフレームワークです。

https://knative.dev/docs/

KnativeはKubernetesクラスタ上にデプロイされ、Knative ServingとKnative Eventingという機能があります。

  • Knative Serving

    • HTTPリクエストに基づいてWebアプリケーションを自動スケールする
    • 負荷がない場合は自動でゼロスケール(完全にPodを停止)することができる
    • IstioやKourierなどのL7ロードバランサーと統合しトラフィック管理を行う
  • Knative Eventing

    • クラウドイベントを処理するための仕組みを提供する
    • 各種イベントソース(Google Cloud Pub/Sub、AWS SQSなど)からのイベントをトリガーとしてサーバーレスアプリを起動できる

この記事では、サーバレスWebアプリケーションの構築を目指すため、Knative Servingのみを対象とし、Knative Eventingについては対象としません。

Knative Servingのインストール

以下の公式の手順に従って、インストールを行って行きます。
https://knative.dev/docs/install/yaml-install/serving/install-serving-with-yaml/

まずは、いつもの通り、以下のコマンドで踏み台サーバにログインします。
ssh-agentを設定しておくと、踏み台サーバから、各Workerインスタンスに秘密鍵などを指定することなくsshアクセスできるようになるので、便利です。

eval `ssh-agent`
ssh-add
ssh -A (踏み台サーバのIPアドレス)

踏み台サーバで以下のコマンドを実行し、以下のKnative Servingのコアライブラリについてインストールを行って行きます。

kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.17.0/serving-crds.yaml
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.17.0/serving-core.yaml

Kourierのインストール

次に、L7ロードバランサーであるKourierのインストールを行って行きます。

kubectl apply -f https://github.com/knative/net-kourier/releases/download/knative-v1.17.0/kourier.yaml

KnativeのL7ロードバランサとして、Kourierが使われるようにconfigmapの編集を行います。

kubectl patch configmap/config-network \
  --namespace knative-serving \
  --type merge \
  --patch '{"data":{"ingress-class":"kourier.ingress.networking.knative.dev"}}'

以下のコマンドを実行し、EXTERNAL-IPとして、MetalLBで設定したIPアドレスプールから、IPアドレスが割り当てられていることを確認します。

kubectl --namespace kourier-system get service kourier

こちらが、出力結果の例で、EXTERNAL-IPとして192.168.0.2が割り当てられていることが確認できます。

NAME      TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)                      AGE
kourier   LoadBalancer   10.xxx.xxx.xxx   192.168.0.2    80:32118/TCP,443:30143/TCP   19d

KnativeへのグローバルIPアドレスの割り当て

立ち上がったKnativeに対して、インターネット経由でアクセスできるように、グローバルIPアドレスを割り当てます。
グローバルIPアドレスの割り当てはmdxのユーザーポータル上でネットワークメニューのDNATの設定画面で行います。
転送先プライベートIPアドレスについては、knourierに割り当てられたEXTERNAL-IPの値を設定します。

また、ACLの設定も行って行きます。
ACLの設定については、HTTP通信で用いるport:80と、HTTPS通信で用いるport:443に対して、アクセス許可設定を行います。
Srcアドレスについては、全てのIPアドレスを許可したい場合は、0.0.0.0を設定し、特定のグローバルIPアドレスからだけアクセスを許可したい場合は、そのグローバルIPアドレスの値を設定します。
Dstアドレスについては、knourierに割り当てられたEXTERNAL-IPの値を設定します。

ドメイン取得と、DNSレコード作成

外部からドメイン名でアクセスできるように、適切なドメイン取得会社から、ドメインを購入するなどします。
私の場合は、今回の実験のため、Cloudflareにて適当な安いドメインを購入しました。
https://www.cloudflare.com/ja-jp/

本手順では、ドメインの例として、example.comを使用します。
実際には、example.comは取得出来ないドメインですが、手順の分かりやすさのため、仮にこのドメイン名を取得出来たとして説明を行います。

次に、DNSのAレコードとして、*.defalut.example.comを設定し、そのレコードの値として、先ほどmdxで割り当てたグローバルIPアドレスを設定します。

なお、サブドメインのdefaultはKnativeでWebアプリケーションをデプロイする時のNamespaceを指定しますが、別のNamespaceでWebアプリケーションをデプロイしたい場合は、そのNamespaceを指定してください。

詳しくは以下の公式手順を参照ください。
https://knative.dev/docs/serving/using-a-custom-domain/#publish-your-domain

KNativeのデフォルトドメインの設定

Knativeでは、Webアプリケーションをデプロイしたとき、デフォルトでは、*.default.svc.cluster.localというドメイン名が割り当てられます。

今回は、ドメインとしてexample.comを用いたいので、それが割り当てられるように変更を行います。

まず、以下のコマンドで、configmapの修正を行います。

kubectl edit configmap config-domain -n knative-serving

そこで、dataの項目に、適用したいドメイン名を、以下の通りに追加し、保存します。

apiVersion: v1
data:
  example.com: ""  # これを追加する
kind: ConfigMap
[...]

シンプルなWebアプリケーションのデプロイ

ここまででKnativeでHTTPアクセスするために必要な準備が整いました。
次に、実際のWebアプリケーションをデプロイし、Webブラウザなどでアクセスしてみます。
そのため、以下のようなyamlファイルを定義します。

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: hello-world
  namespace: default
spec:
  template:
    spec:
      containers:
        - image: gcr.io/knative-samples/helloworld-go
          env:
            - name: TARGET
              value: "Knative User"

imageの部分で指定されているgcr.io/knative-samples/helloworld-goは、Knativeで提供されている、サンプル用のWebアプリケーションのDocker image repositoryです。
実装コードが気になる方は、以下のレポジトリを覗いてみてください。

https://github.com/knative-sample/helloworld-go

さて、話は戻り、このyamlファイルをKubernetesに適用し、サーバレスWebアプリケーションをデプロイします。 そのため、いつもの通り、kubectl applyでこのファイルの適用を行います。

kubectl apply -f (作成したWebアプリケーションのyamlファイルのpath)

Lensなどで、デプロイしたhello-worldアプリが起動するのを確認します。

以下のコマンドを叩き、URLが発行され、Ready Trueとなっていることを確認します。

kubectl get ksvc 

出力結果

NAME              URL                                           LATESTCREATED           LATESTREADY             READY   REASON
hello-world       http://hello-world.default.example.com        hello-world-00001       hello-world-00001       True    

ブラウザでHTTPでアクセス

ブラウザなどで、http://hello-world.default.example.com にアクセスし、結果が表示されることを確認します。
正しくデプロイされていれば、画面上に、Hello Knative User! と表示されるはずです。

cert-managerの設定

無事にHTTPでアクセスできるようになったので、次にHTTPSによる通信の暗号化のための設定を行います。

Kubernetesには、cert-managerという、Kubernetesクラスタ内でTLS証明書を自動管理するためのライブラリがあり、Let's Encryptなどの証明書発行機関と連携し、証明書の発行・更新・管理を自動化できます。

https://cert-manager.io/

今回はこのcert-managerをインストールして、TLS証明書の発行などを行います。

まず、以下のコマンドでcert-managerをインストールします。

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.17.0/cert-manager.yaml

以下のコマンドを実行し、cert-managerがインストールされたことを確認します。

$ kubectl get pods --namespace cert-manager

NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager-5c6866597-zw7kh               1/1     Running   0          2m
cert-manager-cainjector-577f6d9fd7-tr77l   1/1     Running   0          2m
cert-manager-webhook-787858fcdb-nlzsq      1/1     Running   0          2m

次に、Issuerの設定をしていきます。
今回は、Let's Encryptを使って、証明書の発行を行うため、以下のyamlファイルの作成を行います。
Let's Encryptには、検証用のstaging環境と本番用のproduction環境があり、これら2つのIssuerの設定を行います。

その際、privateKeySecretRefのnameには、任意の値を設定できますが、この値は、Let's Encryptで一意である必要があり、このキーを使ってこの証明書を発行することになるので、他の人には推測されにくいランダムな文字列などが理想です。
stg環境とprod環境では、別々のキー名を設定しておくと良いです。

stg用のyaml

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging # 任意の名前を設定できます。
spec:
  acme:
    email: hoge@example.com # 受信可能なメールアドレスを設定します。
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: hogehoge-staging-key-name # ここにはシークレットキーを設定します。他の人に推測されにくいランダムな文字列が理想です。
    solvers:
    - http01:
        ingress:
          ingressClassName: kourier

prod用のyaml

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod # 任意の名前を設定できます。
spec:
  acme:
    email: hoge@example.com # 受信可能なメールアドレスを設定します。
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: hogehoge-prod-key-name # ここにはシークレットキーを設定します。他の人に推測されにくいランダムな文字列が理想です。
    solvers:
    - http01:
        ingress:
          ingressClassName: kourier

これらのyamlファイルを、いつものように、kubectl applyコマンドで適用します。

kubectl apply -f (作成したIssuerのyamlファイルのpath)

Issuerが正しく適用されたかについては、以下のコマンドで確認できます。

kubectl get clusterissuer letsencrypt-staging -o yaml

出力結果は以下の通りで、statusの項目を見ると、正しくACME serverに登録されたかどうかを確認できます。

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  annotations:
    [...]
spec:
  [...]
status:
  [...]
  conditions:
  - lastTransitionTime: "2025-02-13T01:54:00Z"
    message: The ACME account was registered with the ACME server
    observedGeneration: 1
    reason: ACMEAccountRegistered
    status: "True"
    type: Ready

ここで、少し話は逸れますが、Let's Encryptのstaging環境とproduction環境の違いですが、以下の通りです。

  • Production(本番環境)
    • 実際にHTTPS通信に使用でき、ブラウザでも認識される正式なSSL/TLS証明書を発行
    • 厳しいレート制限
      • 1週間に取得できる証明書の数などに制限がある
    • ドメイン検証が必須
      • HTTP-01 や DNS-01 などの認証方式でドメイン所有者であることを証明する必要がある
  • Staging(ステージング環境)
    • テスト用の証明書を発行
      • ただし、ドメイン検証がされていないため、ブラウザで「証明書が信頼されていません」という警告が出る
    • レート制限が緩い
      • 何度でも証明書を取得できるため、動作確認やスクリプトのテストに最適
    • 本番と同じACME APIを使用
      • 本番環境と同じプロセスで証明書の発行や更新のテストができる

さて話は戻り、次はここで定義したIssuerについて、Knative servingに設定していきます。
そのため、まずは以下のコマンドで、config-certmanagerの編集モードに入ります。

kubectl edit configmap config-certmanager -n knative-serving

dataのセクションに、issuerRefの項目を追記して保存します。

apiVersion: v1
kind: ConfigMap
metadata:
  name: config-certmanager
  namespace: knative-serving
  labels:
    networking.knative.dev/certificate-provider: cert-manager
data:
  issuerRef: |  # この部分を追記
    kind: ClusterIssuer
    name: letsencrypt-prod # nameにはIssuer名を指定

次に、Knative Servingで、tls通信を有効にするため、以下のコマンドで、config-networkの編集モードに入ります。
ここで、dataのセクションに、external-domain-tls: Enabledを追加して、保存します。

apiVersion: v1
kind: ConfigMap
metadata:
  name: config-network
  namespace: knative-serving
data:
   ...
   external-domain-tls: Enabled  # この項目を追加する。
   ...

Webアプリケーションの再作成とブラウザでHTTPSでアクセス

ここまでの設定で、HTTPS通信に必要な全ての設定が完了しました。

先ほどの手順でデプロイしたシンプルなWebアプリケーションについて、今度はHTTPSでの通信を有効にするため、一旦削除を行い、再度applyを行います。

削除は以下のコマンドで実行します。

kubectl delete -f (作成したWebアプリケーションのyamlファイルのpath)

再度applyを行い、Webアプリケーションをデプロイします。

kubectl apply -f (作成したWebアプリケーションのyamlファイルのpath)

以下のコマンドを実行し、READYがTrueとなり、先ほどデプロイしたhello-worldアプリのURLがhttpsになっていることを確認します。

kubectl get ksvc 

出力結果

NAME              URL                                           LATESTCREATED           LATESTREADY             READY   REASON
hello-world       https://hello-world.default.example.com        hello-world-00001       hello-world-00001       True    

ブラウザで、https://hello-world.default.example.comを入力し、証明書のエラーが出ず、正しく画面に、Hello Knative User! が表示されることを確認します。

今回のまとめと次回について

今回は、立ち上げたKubernetes環境の上で、MatalLBというL3ロードバランサと、Knativeというサーバーレスのアプリケーションを開発・実行・管理するためのライブラリをインストールして、Webサービスをデプロイし、公開するための手順について説明しました。

次の記事では、vLLMというLLMの様々なモデルが使えるライブラリとFastAPIというRest APIのフレームワークを組み合わせて、LLMをWebアプリケーションとしてデプロイし、公開する方法について説明しています。

気になる方は、こちらの記事一覧を参照ください。

https://zenn.dev/suzumura_lab/articles/627b5063d6884d

東京大学鈴村研究室について

https://sites.google.com/view/toyolab/鈴村研究室概要

Discussion