谷本 心 in せろ部屋

はてなダイアリーから引っ越してきました

Dapr Advent Calendar 21日目 - Daprの開発環境

こんにちは Dapr Advent Calendar 21日目です。今回からは実践編として、実際に開発する上で役立つノウハウやTipsを紹介したいと思います。

Daprの開発環境

ここまでDaprの機能を説明してきて、なんだかよさげには見えるけど、実際に効率良く開発できるのかについてはまだ疑問があるかも知れません。今回は開発の効率に直結する「開発環境」や「デバッグ」について話したいと思います。

DaprのIDE Support

DaprのIDE対応について、公式ドキュメントにはVisual Studio CodeIntelliJについて記載されています。

docs.dapr.io

Visual Studio CodeIntelliJのどっちを使うと良いですかって聞かれれば、そりゃJava開発なんだから今はIntelliJでしょう、としか答えられないのですが、Visual Studio Code対応がどのようになっているのかを見ておきます。

Visual Studio Code + Dapr extensionの所感

Visual Studio Codeの拡張としてDapr extensionが提供されている。このextensionでできることは次の通りです。

  • componentファイルのscaffold作成(pubsub、statestore、zipkinの設定ファイルの作成)
  • dapr起動コマンドのscaffold作成
  • 起動中のDaprアプリケーション一覧の表示
  • 起動中のDaprアプリケーションに対するinvokeとpublish

設定ファイルのscaffoldは ~/.dapr/components と同じファイルを生成するだけなので大したことはありません。デモ用ですかね。

dapr起動コマンドのscaffold作成は、Visual Studio Codeのlaunch.jsondapr run でアプリケーションを起動するconfigurationを追加するというものです。dapr run コマンドを手打ちしなくて済むようになります。

Daprアプリケーション一覧は、次のように表示されます。

f:id:cero-t:20211221005228p:plain:w300
Dapr extensionで見るアプリケーション一覧

起動中のアプリケーション一覧が表示されます。dapr list コマンドで取得できるものと同様ですね。ちなみにk8s上にデプロイしたDaprアプリケーションは見えませんでした。

これらのアプリケーションを選択し、invokeやpublishを実行することができます。invokeする時にわざわざcurlコマンドを叩いたり、ちょっと長めのURLを打ったりしなくて済みます。

Dapr extensionが提供する機能はこのくらいなので、そんなに高機能というわけでも生産性がすごく上がるというわけでもないですが、Daprアプリケーションの簡易UIとして使うのも悪くないという印象でした。

IntelliJによるデバッグ

続いて、IntelliJによるDaprアプリケーションのデバッグ方法についても説明しておきます。

対象のソースコードGitHubにあるHello Worldアプリケーションを利用します。

https://github.com/cero-t/dapr-advent-2021/

ここにある「hello」モジュールを利用します。

Daprの単独起動

まずはDaprを単独で起動します。公式ドキュメントではIntelliJのExternal Toolsを利用していましたが、わざわざ使う必要もないので次のコマンドでDaprを起動します。

dapr run --app-id hello-app --app-port 8080 --dapr-http-port 18080

これでDaprが単独で起動します。まだアプリケーションは起動していません。

IntelliJ側でアプリケーションを起動

続いて、IntelliJhello モジュールの HelloApplicationデバッグ起動します。HelloApplication を右クリックし、Debug 'HelloApplition' を選択するなどで起動できます。

f:id:cero-t:20211221010340p:plain
IntelliJデバッグ起動

アプリケーションを起動したら HelloControllerreturn Map.of("message", "Hello, world!");ブレークポイントを張っておいてください。

アプリケーションへのアクセス

続いて、次のコマンドでアプリケーションにアクセスします。

curl localhost:18080/v1.0/invoke/hello-app/method/hello

このコマンドを実行すると、IntelliJ側で指定したブレークポイントで止まっているはずです。

つまり、Daprを単体起動し、IntelliJからDaprで指定した app-port のアプリケーションを起動すればDapr経由でアプリケーションにアクセスできる状態になるのです。この方法で起動すれば、IntelliJ側でデバッグができるということです。仕組みが分かってしまえば簡単な話ですね!

いつどのツールを使って開発するのか

これまで半年くらい業務でDaprを使って開発し、実運用も始めているのですが、正味の話こんな風にデバッグをできることは、いま初めて知りました。

なぜこれまで知らなかったというと、ローカル環境で開発する時には(担当範囲にもよるのですが)Daprを使うことはあまりないためです。その辺りについて説明したいと思います。

Daprアプリケーションの開発の実際

例として、運用するシステムが Amazon EKS + Dapr + RabbitMQ + PostgreSQL + Spring Boot という構成だったとします。

その際、開発フェーズごとに使うツールは次のようになります。

  • 開発時
  • Dapr経由での動作検証時
    • Java + Docker + Dapr
  • k8sでの動作検証時
    • Java + Docker + Dapr + Minikube + kubectl
  • Amazon EKSでの動作検証時
    • Java + Docker + kubectl

この辺りを順番に説明していきます。

開発時 (Java + Docker)

アプリケーションを開発する際には、Spring Bootで開発するためにJavaJDK)を利用し、PostgreSQLを利用するためにDockerを利用します。ここでDaprは使いません。

これまでのAdvent Calendarでも少し説明していましたが、ローカル環境で開発する際にはアプリケーション同士を直接呼び出しています。

Invoke APIを使わず直接呼び出し

たとえばInvoke APIを使ったアプリケーションのコードは次のようになります。

@Value("${baseUrl}")
private String baseUrl;

@GetMapping("/invokeHello")
public Map<String, ?> invokeHello() {
    Map<?, ?> result = restTemplate.getForObject(baseUrl + "/hello", Map.class);
    return Map.of("baseUrl", baseUrl, "remoteMessage", result);
}

ここで baseUrl としてローカル環境で開発する際には http://localhost:8080 を指定します。そうすれば、ポート8080番で起動している別のアプリケーションにアクセスできますし、あくまでもSpring Bootだけの世界に閉じて開発ができます。

そしてDaprを利用する際には http://localhost:${DAPR_HTTP_PORT}/v1.0/invoke/hello-app/method というInvoke APIのURLに差し替えるのです。

このようにすればローカルではDaprなし、検証や運用はDaprあり、と切り替えることができます。

Pub/sub APIを使わず直接呼び出し

HTTPで呼び出す同期処理はそれで良いとして、非同期処理はどうなるでしょうか。

たとえばPub/sub APIを使ったアプリケーションのコードは次のようになります。

@Value("${pubsubUrl}")
private String pubsubUrl;

@PostMapping("/publish")
public void publish(@RequestBody MyMessage message) {
    restTemplate.postForObject(pubsubUrl, message, Void.class);
}

そしてメッセージを受け取るsubscribe側は次のように実装します。

@PostMapping("/subscribe")
public void subscribe(@RequestBody CloudEvent<MyMessage> cloudEventMessage) {
    doSubscribe(cloudEventMessage.data);
}

@PostMapping("/doSubscribe")
public void doSubscribe(@RequestBody MyMessage message) {
    System.out.println(message);
}

上がDaprから呼ばれる際にCloudEventのエンベロープがついたクラスを引数にしたメソッドです、下がその中のメッセージを使って実処理を行うメソッドです。

このような構成にして、ローカルの開発時には pubsubUrllocalhost:8084/doSubscribe などを指定して直接同期呼び出しをするようにします。そうすればDaprのことを気にせず、Spring Bootの世界に閉じてビジネスロジックに注力して開発ができます。しかも呼び出しが同期となるため、JUnitなどの自動テストの際に非同期処理が終わることを1秒待つ、みたいなことをしなくても、処理が終わればすぐにassertに進むことができます。

そしてDapr利用時には pubsubUrlhttp://localhost:${DAPR_HTTP_PORT}/v1.0/publish/pubsub/my-message などのPub/sub APIのURLにするのです。このようにすればローカルではDaprなし、検証や運用はDaprあり、と切り替えることができます。

そんな切り替えをして、Daprを使ったときに問題が起きることはないか? と疑問に思うかも知れませんが、もちろん何か問題が起きることはあるかも知れません。ただ基本的にはごく少しのコード修正だけで済むものです。

このような方針にするためにも、Dapr Java SDKは使わず、HTTP通信をするためのクライアントだけを利用しています。

Dapr経由での動作検証時 (Java + Docker + Dapr)

Daprのpub/sub機能が正しく動くかどうか試したい場合や、Daprの分散トレーシングで渡される traceparent ヘッダを利用した処理を行いたい場合には、上の構成に加えてDaprをインストールして動作検証を行います。

やや極端ですが、この時点ではDaprのAPIさえ利用すれば利用するミドルウェアは何でも構いません。たとえば、実際にRabbitMQを用いたpub/sub機能を使って開発をしていたメンバにヒアリングした所、開発時にはDaprが勝手にインストールするRedisを使っていたと話していました。開発時にRedisを使い、検証時からRabbitMQを使っていても、設定ファイル以外は変える必要がなく、何の問題もなくアプリケーションは動作していました。

もちろんRabbitMQ固有の機能、たとえばDead Letter Exchange (DLX)などを用いた処理の開発や検証を行うのであればDocker上にRabbitMQをデプロイして検証する必要がありますが、あくまでもpub/subの主機能の検証を行いたいだけであればRedisを使っても構わないのです。

別にそのような方針を推奨するわけでも何でもないですが、Daprがミドルウェアを抽象化するメリットがこういう所で活きてきます。

k8sでの動作検証時 (Java + Docker + Dapr + Minikube + kubectl)

アプリケーションの開発が終わり、Amazon EKSなどにデプロイする前には、k8sでの検証が必要となります。その場合にはMinikubeが必要となります。インフラ構築を担当する2〜3名ほどだけがローカルにMinikubeをインストールして検証をしていました。もちろんkubectlなどの関連ツールも使います。

逆に言えば、インフラを触らないアプリケーション開発者はMinikubeのインストールすらしていません。k8sに難しいイメージを持っていて、「k8sを使わなければならない」というだけで抵抗があるエンジニアにとっては、k8sを使わずに開発できるという方針にしたほうがハッピーでしょう。

Amazon EKSでの動作検証時 (Java + Docker + kubectl)

Minikube上での検証が終われば、また必要なツールは減ります。イメージを作成してEKSにデプロイするだけですから、イメージのビルドのためのJavaとDocker、またデプロイするためのkubectlが必要となるだけです。DaprやMinikubeは必要ありません。

ただしEKS上で問題が起きた際にログなどを確認できるよう、開発者全員がkubectlを利用できるようにすべきです。ログやメトリクスをCloudWatchやDataDogに集約していれば、開発者がkubectlを利用する機会は減るでしょう。

別にDevとOpsを分離することが正しいのだと主張するつもりはないですが、僕は「それぞれのフェーズにおいて注力すべき部分に注力する」というプロセスを大事にしており、それを実現できるという点で、Daprをとても気に入っています。

まとめ

  • Visual Studio CodeのDapr extensionを使えば、起動中のDaprアプリケーション一覧を表示したりinvoke/publishなどを容易に行えます
  • Daprを単体起動すれば、自分の好きなIDEを使ってアプリケーションをデバッグ起動し、Daprを使った処理のデバッグができます
  • 開発のフェーズごとに、注力すべきことに注力するという構成を取りやすいです
  • 開発時にはDaprもMinikubeもなしでビジネスロジックの開発に注力することができます
  • 検証時以降はそれぞれに必要なツールを少しずつ増やして環境を構築するという方針が良いでしょう

こんな風にしてDaprのアプリケーション開発を進めていました。もちろんこれから変わる所もあるでしょうし、今後も方法論を磨いていきたいと思います。

それでは、また明日!

Dapr Advent Calendar 20日目 - DaprをAmazon EC2で使う

こんにちは Dapr Advent Calendar 20日目です。錦鯉、M-1優勝おめでとうございます! うるさい漫才は好きではないんですが、錦鯉は、なんでしょうか、キャラのせいなんですかね、わりと好きなんです。優勝したペアが涙を流すことはあっても、審査員までもらい泣きしているのはなかなか見ないなと思いましたね!

DNSにConsulを使ってEC2でDaprを運用する

さて、以前 Dapr Advent Calendar 12日目 - Daprをk8s以外の分散環境で使う で、Amazon EC2のようにIPマルチキャストが使えない環境では、Daprを動かすためにConsulが必要ということを書きました。それについて、@kencharosさんからコメントを貰いました。

Consulはdevモードであれば立てるのがそんなに難しくないようなので、今回はこれで試してみましょう。

EKSで動かした翌日にEC2で動かした話を書くなんて、なんか技術スタックが巻き戻ってる気もするのですが、k8sを使うことに抵抗がある人も多いでしょうから、参考にしてもらえると嬉しいです。

f:id:cero-t:20211220101711p:plain
今回作るアプリケーション

EC2にインスタンスを作成

まずはAmazon EC2インスタンスを3つ作ります。1台はConsulサーバ、残り2台はアプリケーションをデプロイするサーバとして利用します。

EC2の使い方や手順などについては割愛しますが、Amazon Linux 2のt2.microインスタンスを3つ作りました。

EC2にConsul環境を構築

まずはEC2インスタンスの1台にConsulの環境を構築しましょう。

次のコマンドを実行してConsulのCLIツールをインストールします。

curl -LO https://releases.hashicorp.com/consul/1.11.1/consul_1.11.1_linux_amd64.zip
unzip consul_1.11.1_linux_amd64.zip
sudo mv consul /usr/local/bin/

続いて、Consulをサーバとして起動します。

consul agent -dev -client=0.0.0.0

開発用に1インスタンスで起動するだけなので -dev オプションをつけ、別のインスタンスからアクセスできるよう -client=0.0.0.0 オプションをつけました。

これでConsulサーバの構築は完了です。とても簡単でしたね。

アプリケーションのDapr実行環境を構築

続いて、残り2台のインスタンスにアプリケーションを実行するための環境を構築しましょう。

まずはJava 11 (corretto)をインストールします。

sudo yum install -y java-11-amazon-corretto-headless

続いて、Dapr CLIをインストールして初期化します。

wget -q https://raw.githubusercontent.com/dapr/cli/master/install/install.sh -O - | /bin/bash
dapr init --slim

Daprは --slim オプションを付けて、Dockerや設定ファイルを作らないスタンドアロンモードで初期化しました。

これでアプリケーションの実行環境が構築できました。

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

ソースコード

続いてアプリケーションの準備です。サンプルアプリケーションのソースコードGitHubに置いてあります。

https://github.com/cero-t/dapr-advent-2021

このうち「hello」と「invoke」を使います。何度も使っているアプリケーションなので、ソースコードの説明は省略します。

名前解決にConsulを使う設定ファイルを作成

Daprが連携するために使う名前解決にConsulを利用できるよう設定ファイルを作成します。ComponentではなくConfigurationとして、次のように設定します。

consul-config.yaml

apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
  name: consul-config
spec:
  nameResolution:
    component: "consul"
    configuration:
      selfRegister: true
      client:
        address: "172.*.*.*:8500"

spec.nameResolution に名前解決の設定をします。

今回はConsulを利用するため componentconsul を指定しました。

そして、起動時にアプリケーションが自動登録されるよう configuration.selfRegistertrue とします。

また configuration.client.address にConsulサーバのプライベートIPアドレスを指定します。後ほど、アプリケーションを実行するインスタンス上でこの値を書き換えます。

この設定ファイルを使うことで、Daprが名前解決としてConsulを利用できるようになります。

ソースコードのダウンロードと書き換え

それでは、アプリケーションを実行する2台のインスタンスでそれぞれアプリケーションのソースコードをダウンロードしして解凍します。

curl -LO https://github.com/cero-t/dapr-advent-2021/archive/refs/heads/main.zip
unzip main.zip

続いて、Consulサーバのアドレスを書き換えましょう。

vi dapr-advent-2021-main/consul-config.yaml

一番最後の行にある 172.*.*.*:8500 の部分に、Consulサーバとして起動したインスタンスのプライベートIPを指定してください。この作業も2台のインスタンスでそれぞれ行います。

アプリケーションの起動

それではアプリケーションを起動しましょう。

1台目のインスタンスでhelloアプリケーションを起動します。

cd dapr-advent-2021-main/hello
dapr run --app-id hello-app --app-port 8080 --dapr-http-port 18080 --config ../consul-config.yaml ../mvnw spring-boot:run

--config ../consul-config.yaml オプションをつけて、設定ファイルが有効化されるようにしました。

2台目のインスタンスも同様にしてinvokeアプリケーションを起動します。

cd dapr-advent-2021-main/invoke
dapr run --app-id invoke-app --app-port 8081 --dapr-http-port 18081 --config ../consul-config.yaml -- ../mvnw spring-boot:run -Dspring-boot.run.profiles=dapr

オプションは上と同様です。また -Dspring-boot.run.profiles=dapr をつけて application-dapr.yaml の設定が有効になるようにしています。

これでアプリケーションの起動が完了しました。

アプリケーションにアクセス

そして、別のコンソールから任意のインスタンス上で次のコマンドでアクセスします。

curl (invokeアプリケーションを起動したインスタンスのプライベートIPアドレス):8081/invokeHello

次のような結果が表示されます。

{"baseUrl":"http://localhost:18081/v1.0/invoke/hello-app/method","remoteMessage":{"message":"Hello, world!"}}

問題なくinvokeアプリケーションからhelloアプリケーションにアクセスできましたね!

AWSではマルチキャストDNSが使えないため、名前解決の設定なしでは通信することができませんでした、Consulを利用することで無事にDapr上で動くアプリケーション同士が連携して通信することができました。

f:id:cero-t:20211220002142p:plain
Consulを使った名前解決

EC2かEKSか

Daprのことを話題にすると、HTTP通信だけですべてが解決できるとか、別のサーバなどなしで開発ができるところをメリットと思ってもらえるのですが、「ただ、k8sはちょっと・・・」というところで尻込みされることが何度となくありました。minikubeやEKSを使ってみて、そこまで難しいとは感じませんでしたが、とは言えやはりこれまで使ってきた技術スタックとは異なるものですし、Daprという新しい技術に加えてk8sまで使うとなると、抵抗があるというのも理解できます。

今回はConsulを使い、EC2アプリケーション上で動くDaprアプリケーション同士が通信できることを確認しました。この方法を使えばk8sを使わなくともDaprアプリケーションを運用することができます。もちろんConsulのサーバを立てて運用する必要がありますが、その辺りはSpring CloudのEurekaなども大きな差はないでしょう。欲を言えば、AWSがマネージドConsulサーバサービスを提供してくれると嬉しいところです。

いずれにせよk8sだけでなく、EC2のような仮想サーバに直接デプロイするとか、ECSのようなコンテナサービスにデプロイするとかが気軽にできるようになれば、またDaprを利用する人も増えるんじゃないかなと思います。名前解決の簡単さはDaprの強みとなるので、今後のアップデートでより洗練されることを期待しています。

まとめ

  • Consulの開発モードを使ってEC2上にConsulサーバを立てました
  • Daprの設定でConsulサーバを名前解決に使うよう設定しました
  • Consulを使えば問題なくEC2でも名前解決ができ、通信することができました

これを12日目のエントリーの時点で書けていれば、もっと良かったのですけどね🙄

ちょっと行き当たりばったりになってしまうところも、毎日ブログを書いてる醍醐味ひとつということで。

それでは、また!

Dapr Advent Calendar 19日目 - DaprをAmazon EKSで使う

こんにちは Dapr Advent Calendar 19日目です。ついに最後の1週間に突入しました。3日目くらいの時に「ちょっとこれ無理かな」とか思ってたんですけど、人間、やってみれば意外といけるもんですね!

DaprをAmazon EKSで運用する

さて、これまでk8s + Daprのアプリケーションをローカルのminikube上で動かしてきましたが、今回はAmazon EKS上で動かしてみます。Daprはマイクロソフトが中心に開発しているOSSなので、Azureのほうがサポートされているミドルウェアが多かったりドキュメントが多かったりするのですが、AWSでも特に問題なく運用できています。

f:id:cero-t:20211219103727p:plain
今回動かすアプリケーション

環境設定

事前準備

まず前提として、次のことは完了済みとします。

  • AWSアカウントの作成
  • AWS CLIのインストール
  • aws configureが済んでいて、awsコマンドが実行できる(~/.aws/credentialsに必要な情報を記載済み)

AWSを日常的に利用している人は、この辺りは既に終わっていると思いますので、説明は割愛します。

また、これまでのAdvent Calendarで利用してきた次のツールもインストール済みとします。

  • Dapr CLI
  • Docker
  • kubectl

もしまだインストールしていなければ、インストールしておいてください。

eksctlのインストール

上に記載したツール群に加えて、eksctlというCLIツールをインストールしてください。これはAmazon EKSをコマンドラインで操作するもので、ちょうどminikubeのCLIに近いものです。

https://docs.aws.amazon.com/eks/latest/userguide/eksctl.html

これで準備は完了です。

EKS環境の構築

EKSクラスタの作成

まず、eksctlコマンドを使ってEKSクラスタを作成します。

eksctl create cluster \
  --name=dapr-test \
  --version 1.21 \
  --region ap-northeast-1 \
  --zones ap-northeast-1a,ap-northeast-1c

僕のアカウントでは --zones オプションをつけずに実行したら「ap-northeast-1bにキャパシティがないよ」というエラーが起きたので、AZとして -1a-1c を指定するようにしました。

なお、クラスタの作成に失敗すると基本的にはロールバックされるのですが、状況次第ではCloudFormationが削除されないことがあるので、残っているようなら手で削除してください。

ちなみに作成したEKSクラスタを放置すると(m5.largeのEC2インスタンス2つも合わせて)月2万円くらい掛かるので、不要になればすぐにEKSクラスタを削除して想定外の課金にならないよう気をつけてくださいね。

kubectlからEKSクラスタにアクセスする

eksctlでk8sクラスタを作成すると、kubectlの向き先が作成したEKSクラスタになっているはずです。念のため確認しておきましょう。

kubectl config current-context

次のように表示されるはずです。

(AWSアカウント名)@dapr-test.ap-northeast-1.eksctl.io

もしminikubeなど別のクラスタを参照している場合は、kubectl config get-contexts でEKSのクラスタ名を確認したうえで

kubectl config use-context (AWSアカウント名)@dapr-test.ap-northeast-1.eksctl.io

などでkubectlの向き先がEKSにするようにしてください。

EKSクラスタを他の人(アカウント)が作成した場合など、手元のコンテキスト一覧にEKSクラスタがない場合は、次のコマンドでコンテキストを取得してください。

aws eks --region ap-northeast-1 update-kubeconfig --name dapr-test

これでkubectlからEKSにアクセスできるようになるはずです。

クラスタ情報を確認しておきましょう。

kubectl cluster-info

次のように表示されます。

Kubernetes control plane is running at https://(アカウントID).gr7.ap-northeast-1.eks.amazonaws.com
CoreDNS is running at https://(アカウントID).gr7.ap-northeast-1.eks.amazonaws.com/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

これでkubectlからEKSにアクセスしていることを確認できました。

Dapr環境の構築

それではEKS上にDapr環境を構築しましょう。

dapr init -k

オプションの -kk8sを対象とします。kubectlが向いているk8s上にDaprの環境が構築されます。

初期化が終わったら、podの一覧を見てみましょう。

kubectl get pods -A

次のように表示されるはずです。

NAMESPACE     NAME                                     READY   STATUS    RESTARTS   AGE
dapr-system   dapr-dashboard-57b4db56fc-vxmcs          1/1     Running   0          89s
dapr-system   dapr-operator-5b4b68b5c5-cgh28           1/1     Running   0          89s
dapr-system   dapr-placement-server-0                  1/1     Running   0          89s
dapr-system   dapr-sentry-c6b746cdf-pxgdx              1/1     Running   0          89s
dapr-system   dapr-sidecar-injector-6f749dbf87-qcg7r   1/1     Running   0          89s
kube-system   aws-node-fp8m2                           1/1     Running   0          15m
kube-system   aws-node-rp5qq                           1/1     Running   0          15m
kube-system   coredns-76f4967988-7psgm                 1/1     Running   0          24m
kube-system   coredns-76f4967988-fjlrb                 1/1     Running   0          24m
kube-system   kube-proxy-xk8wm                         1/1     Running   0          15m
kube-system   kube-proxy-ztkr8                         1/1     Running   0          15m

Daprやk8s関係のpodが起動していることが分かります。

これでEKS上でDaprを動かす準備ができました。

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

続いて、Hello Worldアプリケーションと、それを呼び出すInvokeアプリケーションをEKS上で動かしてみましょう。ソースコードGitHubにあります。

https://github.com/cero-t/dapr-advent-2021

この中にある「hello」と「invoke」を利用します。

作成したアプリケーションのおさらい

helloアプリケーション

helloアプリケーションの HelloController をおさらいしておきましょう。

(hello) HelloController.java

@RestController
public class HelloController {
    @GetMapping("/hello")
    public Map<String, String> hello() {
        return Map.of("message", "Hello, world!");
    }
}

/hello というエンドポイントでメッセージを返すだけのアプリケーションです。

invokeアプリケーション

invokeアプリケーションの InvokeController もおさらいしておきましょう。

(invoke) InvokeController.java

@RestController
public class InvokeController {
    private RestTemplate restTemplate;

    @Value("${baseUrl}")
    private String baseUrl;

    public InvokeController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @GetMapping("/invokeHello")
    public Map<String, ?> invokeHello() {
        Map<?, ?> result = restTemplate.getForObject(baseUrl + "/hello", Map.class);
        return Map.of("baseUrl", baseUrl, "remoteMessage", result);
    }
}

/invokeHello というエンドポイントで、上の /hello を呼ぶ処理を行うアプリケーションです。

baseurlapplication.yamlapplication-dapr.yaml に定義されており、Springのプロファイルが dapr の時は次の設定が利用されます。

(invoke) application-dapr.yaml

baseUrl=http://localhost:${DAPR_HTTP_PORT}/v1.0/invoke/hello-app/method

DaprのInvoke APIを利用したURLです。これを使ってhelloアプリケーションを呼び出します。

アプリケーションのイメージ作成

イメージの置き場所となるECRの作成

続いて、アプリケーションのイメージを作成したいのですが、もちろんEKSからローカルPCにあるイメージレジストリを参照できるわけではありません。EKSを使う場合には、Docker HubやAmazon ECRなどを使うことになります。ここではEKSと距離の近いECRにイメージを置くことにしましょう。

次のコマンドでECRにリポジトリを作成します。リポジトリはイメージごとに作成する必要があるため helloinvoke という名前のリポジトリをそれぞれ作成します。

export AWS_REGION=ap-northeast-1
aws ecr create-repository --repository-name hello --region ap-northeast-1
aws ecr create-repository --repository-name invoke --region ap-northeast-1

これでリポジトリの作成は完了です。

イメージの作成

続いてイメージを作成します。上で作ったECRのリポジトリにpushできるよう、イメージ名をECRリポジトリURIに合わせる必要があります。

まずはhelloアプリケーションのイメージ作成です。次のようなコマンドになります。

cd (GitHubのディレクトリパス)/dapr-advent-2021

export HELLO_IMAGE=$(aws ecr describe-repositories --repository-names hello --query 'repositories[0].repositoryUri' --output text --region ap-northeast-1)

./mvnw spring-boot:build-image -pl hello -Dspring-boot.build-image.imageName=${HELLO_IMAGE}:1.0.0

(オプションが多くて長くなるため、改行を挟んでいます)

awsコマンドを用いてECRリポジトリのURLを取得し、それを環境変数 HELLO_IMAGE として保持します。リポジトリのURLは (アカウントID).dkr.ecr.ap-northeast-1.amazonaws.com/(リポジトリ名) となっています。

そして、Mavenspring-boot:build-image コマンドでイメージを作成するのですが、ここで -Dspring-boot.build-image.imageName オプションを使い、上で取得したECRリポジトリのURLをイメージ名として指定しています。

イメージの作成に成功したら、invokeアプリケーションも同様にイメージを作成します。

export INVOKE_IMAGE=$(aws ecr describe-repositories --repository-names invoke --query 'repositories[0].repositoryUri' --output text --region ap-northeast-1)

./mvnw spring-boot:build-image -pl invoke -Dspring-boot.build-image.imageName=${INVOKE_IMAGE}:1.0.0

作成が完了したら、念のため確認しておきましょう。

docker images | grep hello
(アカウントID).dkr.ecr.ap-northeast-1.amazonaws.com/hello   1.0.0          d62af7e6faa8   41 years ago    261MB
hello                                                     1.0.0          73b3e3b9795a   41 years ago    261MB
docker images | grep invoke
(アカウントID).dkr.ecr.ap-northeast-1.amazonaws.com/invoke   1.0.0          d38a74e45a8f   41 years ago    261MB
invoke                                                     1.0.0          bf464e49d0e6   41 years ago    261MB

ECRのURIがついたイメージができているはずです。もし以前に作ったhelloやinvokeのイメージがあれば、一緒に表示されます。

イメージのプッシュ

それでは作成したイメージをECRにプッシュしましょう。ECRにログインしてからでないとプッシュできないので、次のコマンドでログインをします。

export ACCOUNT_ID=$(aws sts get-caller-identity --output text --query Account)

aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin https://${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com

アカウントIDやログインパスワードをawsコマンドで取得して、それを使ってログインしています。

これでログインに成功したら、イメージをプッシュします。

docker push ${HELLO_IMAGE}:1.0.0
docker push ${INVOKE_IMAGE}:1.0.0

イメージを作成した際に使用した環境変数をここでも使っています。

これでECRにイメージがプッシュされました。

アプリケーションをEKSにデプロイ

それでは、ECRにプッシュしたイメージを使って、EKS上にアプリケーションをデプロイしてみましょう。

helloアプリケーションのマニフェストファイルを作成

helloアプリケーションをデプロイするためのマニフェストファイルを次のように作成します。

eks/hello-app.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-app
  labels:
    app: hello
spec:
  replicas: 1
  selector:
    matchLabels:
      app: hello
  template:
    metadata:
      labels:
        app: hello
      annotations:
        dapr.io/enabled: "true"
        dapr.io/app-id: "hello-app"
        dapr.io/app-port: "8080"
    spec:
      containers:
      - name: hello
        image: ***.dkr.ecr.ap-northeast-1.amazonaws.com/hello:1.0.0
        ports:
        - containerPort: 8080
        imagePullPolicy: IfNotPresent

以前に作ったものと同様です。イメージ名にECRリポジトリURIを指定する必要があるため *** の部分を自分のアカウントIDに置き換えてください。また、helloアプリケーションには外部からアクセスしないため、Serviceは作成しません。

invokeアプリケーションのマニフェストファイルを作成

続いて、invokeアプリケーション用のマニフェストファイルを作成します。

eks/invoke-app.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: invoke-app
  labels:
    app: invoke
spec:
  replicas: 1
  selector:
    matchLabels:
      app: invoke
  template:
    metadata:
      labels:
        app: invoke
      annotations:
        dapr.io/enabled: "true"
        dapr.io/app-id: "invoke-app"
        dapr.io/app-port: "8081"
    spec:
      containers:
      - name: invoke
        image: ***.dkr.ecr.ap-northeast-1.amazonaws.com/invoke:1.0.0
        ports:
        - containerPort: 8081
        imagePullPolicy: IfNotPresent
        env:
        - name: spring.profiles.active
          value: dapr

---
kind: Service
apiVersion: v1
metadata:
  name: invoke-svc
  labels:
    app: invoke
spec:
  selector:
    app: invoke
  ports:
  - protocol: TCP
    port: 8081
    targetPort: 8081
  type: LoadBalancer

これも以前に作ったものと同様です。イメージ名の *** の部分は自分のアカウントIDに置き換えてください。

環境変数spring.profiles.activedapr を指定して、application-dapr.yaml の設定が有効になるようにしています。

また、こちらは外部からアクセスするため、Serviceを作成しています。

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

それではアプリケーションをデプロイしましょう。デプロイ方法はminikubeでもEKSでも変わりません。

cd (GitHubのディレクトリパス)/dapr-advent-2021
kubectl apply -f eks/hello-app.yaml
kubectl apply -f eks/invoke-app.yaml

コマンドに成功したらしばらく待って、podの一覧を見てみましょう。

kubectl get pods
NAME                          READY   STATUS    RESTARTS   AGE
hello-app-6f97c6ff57-dqqhd    2/2     Running   0          30s
invoke-app-674c8bfb75-zpdrc   2/2     Running   0          33s

hello-appとinvoke-appが起動していればOKです。

アプリケーションへのアクセス

さて、続いてアプリケーションへのアクセスを行います。

Amazon EKSではk8s上に spec.typeLoadBalancer であるServiceを作成すると、自動的にCLB(Classic Load Balancer)が作成され、ポートが外部に公開されます。作成された時点で世界中からアクセスできてしまうため、注意してください。

AWSのコンソールから確認するなり、次のコマンドで確認するなりして、CLBのアドレスを確認します。

aws elb describe-load-balancers --region ap-northeast-1

アドレスの確認ができたら、curlコマンドでアクセスします。

curl (CLB名).ap-northeast-1.elb.amazonaws.com:8081/invokeHello

次の結果が表示できるはずです。

{
  "baseUrl": "http://localhost:3500/v1.0/invoke/hello-app/method",
  "remoteMessage": {
    "message": "Hello, world!"
  }
}

このAdvent Calendarをずっと読んでる人には見慣れたメッセージが表示されました。invokeアプリケーションからDaprを経由してhelloアプリケーションのメッセージを取得することができています。

k8sやeksctlコマンドを使っていると、どこでクラスタ動いているのかをあまり強く意識せず使えてしまえるため忘れがちになりますが、CLBのアドレスでアクセスできている以上、Amazon EKS上で動いているのは確かなのです。意外と簡単でしょ。

f:id:cero-t:20211219103748p:plain
EKS上で動くアプリケーション

後片付け

無事にEKS上で動作確認ができたので、忘れないうちに環境を削除しておきましょう。そのまま残していてはセキュリティ的にも課金的にも危険です。

まずはデプロイしたアプリケーションを削除します。Serviceを削除する際に、一緒にCLBも削除されます。

kubectl delete -f eks

CLBの削除に時間が掛かるため、いつもより応答が遅くなる可能性があります。

続いて、ECRの削除を行います。

aws ecr delete-repository --repository-name hello --region ap-northeast-1 --force
aws ecr delete-repository --repository-name invoke --region ap-northeast-1 --force

最後に、EKSクラスタ自体を削除します。

eksctl delete cluster --name dapr-test --region ap-northeast-1

これで削除は完了です。

AWS上でどのように運用するか

今回はAmazon EKS上にhelloアプリケーションとinvokeアプリケーションのみをデプロイしましたが、もちろん他のアプリケーションをデプロイすることも可能です。

また、これまで使ってきたRabbitMQやPostgreSQLなどは、k8s上にデプロイするのではなく、マネージドサービスのAmazon MQ for RabbitMQやAmazon RDS for PostgreSQL、あるいはAmazon Aurora PostgreSQLなどを利用することも可能です。

それぞれのマネージドサービスでインスタンスを作成し、Daprの接続先がそれぞれのインスタンスになるよう指定し、適切にセキュリティグループを設定すればアクセスができます。その辺りはAWSの説明になりすぎますし、説明すると長くなるため今回は割愛しました。

ところで、今回はk8sのLoadBalancerを利用し、AWSのCLBが自動的に作成されることを確認しました。ただ、たとえばAPI GatewayAPIを管理する場合には、対象としてCLBを指定できないため、ALBが必要となります。実際に僕が運用しているシステムでは、CLBではなくALBを利用しています。

ALBを使う場合には、アプリケーションのServiceの type として LoadBalancer ではなく NodePort を指定し、それとは別に Ingress を作成してALBと接続する設定を行います。その辺りの使い方については、クラスメソッドのブログなどが詳しいのでそちらを参考にしてください。

dev.classmethod.jp

いずれにせよ、目的に合わせて柔軟に環境を構築できることは間違いありません。

まとめ

  • eksctlコマンドを使って、Amazon EKSのクラスタを作成しました
  • kubectlコマンドを使って、EKS上にアプリケーションのデプロイを行いました
  • typeLoadBalancer のServiceを作成すると、CLBが自動的に作成され、インターネット経由でアクセスすることができます
  • ALBを利用する場合は typeNodePort のServiceを作成し、Ingress を利用してALBと接続します
  • EKSを試す場合には、使い終わったら削除することを忘れないようにしてください

ところで今回もソースコードには全く手を入れることがありませんでした。このAdvent Calendarを書き始めたときに、最初からk8sの環境で動かすことを想定してサンプルのソースコードを作成したわけではなく、あくまでDaprを使うことだけを想定していたのですが、k8sでも問題なく動いています。

Daprを使うことで環境から切り離され、ローカルPCで直接動かしても、minikubeで動かしても、あるいはAmazon EKSで運用しても、それぞれの設定ファイルを作成するだけで、ソースコードは変更しなくて済んだのです。

これは間違いなく、Daprのメリットだと言えるでしょうね。

それでは、また明日!

Dapr Advent Calendar 18日目 - Dapr + k8sとZipkinで分散トレーシング

こんにちは Dapr Advent Calendar 18日目です。気づけばあと2週間で年明けじゃないですか。歳を取ると1年が早くなるって言いますけども、それとは別に、この2年はやっぱり早かったですよねぇ。

k8sとDaprで分散トレーシング

さて、今回はk8s + Dapr上のアプリケーションの分散トレーシングを行います。主にDaprのパイプライン設定ファイルをk8sに適用するところや、それをアプリケーション側で有効にする辺りが、今回のポイントとなります。

f:id:cero-t:20211218132418p:plain
今回作るアプリケーション

アプリケーションの作成

動かすアプリケーションは Dapr Advent Calendar 8日目 - DaprとZipkinで分散トレーシング で作成したものです。そちらを先に読んでからこのエントリーを読んでください。

ソースコードgithubに置いてあります。

https://github.com/cero-t/dapr-advent-2021

また Dapr Advent Calendar 13日目 - Dapr + k8sでHello World で説明したツールなどがセットアップ済みであることを前提で説明を書いています。

作成したソースコードのおさらい

以前のエントリーで作成したアプリケーションのソースコードをおさらいします。

TracingController.java(抜粋)

public class TracingController {
    private RestTemplate restTemplate;

    @Value("http://localhost:${DAPR_HTTP_PORT}/v1.0/invoke/hello-app/method")
    private String helloUrl;

    @Value("http://localhost:${DAPR_HTTP_PORT}/v1.0/invoke/publish-app/method")
    private String publishUrl;

    public TracingController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @GetMapping("/invokeHello2")
    public Map<String, ?> invokeHello(@RequestHeader("traceparent") String traceparent) {
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.set("traceparent", traceparent);
        HttpEntity<?> request = new HttpEntity<>(httpHeaders);

        Map<?, ?> result = restTemplate.exchange(helloUrl + "/hello", HttpMethod.GET, request, Map.class).getBody();
        return Map.of("baseUrl", helloUrl, "remoteMessage", result);
    }

    @PostMapping("/invokePublish")
    public void invokePublish(@RequestBody Object message, @RequestHeader("traceparent") String traceparent) {
        System.out.println(traceparent);

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.set("traceparent", traceparent);
        HttpEntity<?> request = new HttpEntity<>(message, httpHeaders);

        restTemplate.exchange(publishUrl + "/publish2", HttpMethod.POST, request, Void.class);
    }

Hello Worldを呼ぶ /invokeHello2 と、メッセージのpublishを呼ぶ /invokePublish というエンドポイントを提供しています。分散トレーシングに用いる traceparent というHTTPヘッダを伝播させるために、少しだけ実装が複雑になっています。

アプリケーションのイメージ作成

続いて、このアプリケーションのイメージを作成しましょう。

イメージ作成先がminikubeのイメージレジストリになるよう次のコマンドを実行します。

eval $(minikube docker-env)

そしてイメージ作成のコマンドを実行します。

cd (GitHubのディレクトリパス)/dapr-advent-2021/tracing
../mvnw spring-boot:build-image

これでイメージが作成できました。

また、hello publish subscribe のアプリケーションも必要となるため、それぞれイメージを作成します。

cd (GitHubのディレクトリパス)/dapr-advent-2021
./mvnw spring-boot:build-image -pl hello,publish,subscribe

これでイメージ側の準備は完了です。

マニフェストファイルの作成

次に、このアプリケーションをデプロイするためのマニフェストファイルを作成します。

k8s/tracing-app.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: tracing-app
  labels:
    app: tracing
spec:
  replicas: 1
  selector:
    matchLabels:
      app: tracing
  template:
    metadata:
      labels:
        app: tracing
      annotations:
        dapr.io/enabled: "true"
        dapr.io/app-id: "tracing-app"
        dapr.io/app-port: "8087"
    spec:
      containers:
      - name: tracing
        image: tracing:1.0.0
        ports:
        - containerPort: 3500
        imagePullPolicy: IfNotPresent

---
kind: Service
apiVersion: v1
metadata:
  name: tracing-svc
  labels:
    app: tracing
spec:
  selector:
    app: tracing
  ports:
  - protocol: TCP
    port: 3500
    targetPort: 3500
  type: LoadBalancer

以前作成した hello.yaml とほとんど同じ構成ですが、今回はアプリケーションのポートを外部公開するのではなく、Daprのポートを外部公開するため3500番ポートを公開する設定にしています。Daprを経由しなければ最初の traceparent を取得できないため、このような構成にしています。

アプリケーションのポートを公開すべきか、Daprのポートを公開すべきかは、あとで論じたいと思います。

Zipkinのマニフェストファイルの作成

続いて、分散トレーシングの収集とUIを提供するZipkinをデプロイするためのマニフェストファイルを作成します。

k8s/zipkin.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: zipkin-deploy
  labels:
    app: zipkin
spec:
  replicas: 1
  selector:
    matchLabels:
      app: zipkin
  template:
    metadata:
      labels:
        app: zipkin
    spec:
      containers:
      - name: zipkin
        image: openzipkin/zipkin
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 9411

---
kind: Service
apiVersion: v1
metadata:
  name: zipkin-svc
  labels:
    app: zipkin
spec:
  type: LoadBalancer
  selector:
    app: zipkin
  ports:
  - protocol: TCP
    port: 9411
    targetPort: 9411

イメージを使ってZipkinをデプロイしています。内容は以前作成したPostgreSQL用のものとほとんど同じです。外部から9411番ポートにアクセスできるようServiceも作成しています。

Daprのパイプライン設定ファイルでZipkinへのトレースを指定する

続いて、Daprのパイプライン設定ファイルを作成して、Zipkinにトレーシング情報を送るようにします。

k8s/zipkin.yaml

apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
  name: tracing
spec:
  tracing:
    samplingRate: "1"
    zipkin:
      endpointAddress: http://zipkin-svc:9411/api/v2/spans

これは Dapr Advent Calendar 8日目 - DaprとZipkinで分散トレーシング で説明した ~/.dapr/config.yaml とほとんど同じ内容で、接続先だけが localhost ではなく zipkin-svc を参照するように修正しています。

samplingRate には 1 を設定して、すべてのトレーシング情報をZipkinに送るようにしています。

またこの設定には metadata.nametracing という名前をつけています。

アプリケーション側で分散トレーシングを有効にする

さて、上でHTTPのパイプライン設定ファイルを作成しましたが、このパイプライン設定がすべてのアプリケーションで有効になる、というわけではありません。Daprでは設定ファイルをk8sにapplyすると、わりと自動で全体的に有効化されることが多かったのですが、パイプラインのConfigurationに関しては勝手に全体で有効化されません。全体に有効化すると弊害が大きいというか、アプリケーションによってパイプライン設定が異なるのが当たり前ですからね。

そのため、アプリケーション側がこのパイプライン設定を使うように設定する必要があります。

アプリケーション側のマニフェストファイルを次のように修正します。

k8s/tracing-app.yaml(抜粋)

      annotations:
        dapr.io/enabled: "true"
        dapr.io/app-id: "tracing-app"
        dapr.io/app-port: "8087"

これを次のように変更します。

      annotations:
        dapr.io/enabled: "true"
        dapr.io/app-id: "tracing-app"
        dapr.io/app-port: "8087"
        dapr.io/config: "tracing"

dapr.io/configtracing を指定しています。この tracing というのは上のパイプライン設定ファイルの metadata.name で指定した名前です。これで、アプリケーション側がパイプライン設定を利用するようになり、分散トレーシングが有効になるわけです。

同じように k8s/hello-app.yaml k8s/publish-app.yaml k8s/subscribe-app.yaml にも dapr.io/config: "tracing" を追加してください。

これでマニフェストファイルの作成は完了です。

アプリケーションをデプロイしてアクセスする

イメージをk8sにデプロイ

それでは作成したイメージをk8sにデプロイします。

cd (GitHubのディレクトリパス)/dapr-advent-2021
kubectl apply -f k8s/zipkin.yaml
kubectl apply -f k8s/tracing.yaml
kubectl apply -f k8s/tracing-app.yaml
kubectl apply -f k8s/hello-app.yaml
kubectl apply -f k8s/publish-app.yaml
kubectl apply -f k8s/subscribe-app.yaml

まずZipkinをデプロイし、HTTPパイプライン設定ファイルをデプロイし、アプリケーションのデプロイという順番に行っています。

RabbitMQなどをデプロイしていない場合には、そちらもデプロイする必要があります。面倒なので次のコマンドでまとめてやってしまったほうが良いでしょう。

kubectl apply -f k8s

これでk8sフォルダにあるすべてのマニフェストファイルに対して kubectl apply が行われます。

k8s上のアプリケーションにアクセスする

それでは、起動したアプリケーションにアクセスしてみましょう。

ポートフォワードの設定

まずはローカルPCからminikube内のpodにアクセスできるよう、ポートフォワードの設定を行います。

次のコマンドを実行します。

kubectl port-forward service/zipkin-svc 9411:9411

これでローカルPCの9411番ポート経由でZipkinにアクセスできるようになりました。

また、別のコンソールで次のコマンドを実行します。

kubectl port-forward service/tracing-app 3500:3500

これでローカルPCの3500番ポート経由で、tracing-appのDaprにアクセスできるようになりました。

Hello Worldを呼び出す処理にアクセスする

それでは、別のコンソールからアクセスしてみましょう。

curl localhost:3500/invokeHello2 -H "dapr-app-id:tracing-app"

せっかくなのでDapr 1.4から使えるようになった方法でアクセスしてみました。これは curl localhost:3500/v1.0/invoke/tracing-app/method/invokeHello2 と同じ意味になります。

次のような結果が表示されます。

{
  "baseUrl": "http://localhost:3500/v1.0/invoke/hello-app/method",
  "remoteMessage": {
    "message": "Hello, world!"
  }
}

正しくメッセージが返ってきました。

Zipkin側でトレース情報を確認してみましょう。

f:id:cero-t:20211218134028p:plain
Zipkinのトレース一覧

f:id:cero-t:20211218134058p:plain
helloを呼んだトレースの詳細

トレーシング情報は、上から順番に

  • Dapr → tracing-app (/invokeHello2)
  • tracing-app → Dapr (Invoke APIでhello-appの /hello)
  • Dapr → hello-app (/hello)

となっています。Daprを通過する部分で分散トレーシングの情報が取得されていることが分かります。

Pub/subを呼び出す処理にアクセスする

続いて、Pub/subを行うエンドポイントにもアクセスしてみましょう。

curl -XPOST "localhost:3500/invokePublish" -H "dapr-app-id:tracing-app" -H "Content-type:application/json" -d '{
  "name": "Shin Tanimoto",
  "twitter": "@cero_t"
}'

特にエラーなどが起きなければメッセージングは成功しているはずです。

Zipkin側でトレース情報を確認してみましょう。

f:id:cero-t:20211218134116p:plain
Zipkinのトレース一覧

f:id:cero-t:20211218134131p:plain
publish/subscribeを呼んだトレースの詳細

トレーシング情報は、上から順番に

  • Dapr → tracing-app (/invokePublish)
  • tracing-app → Dapr (Invoke APIでpublish-appの /publish2)
  • Dapr → publish-app (/publish2)
  • publish-app → Dapr (Pub/sub APIでrabbitmq-pubsubのmy-message)
  • (Dapr -> RabbitMQ -> Dapr)
  • Dapr → subscribe-app (my-messageの受信)

となっており、やはりDaprを通過する部分で分散トレーシングの情報が取得されています。

RabbitMQにエンキュー、デキューするところ自体は分散トレーシングの対象外になっています。本当はここも表示されれば嬉しいんですけどね。

いずれにせよ、k8s上でも問題なく分散トレーシングができることが分かりました。

f:id:cero-t:20211218134311p:plain
Dapr + k8s上で動く分散トレーシングと対象のアプリケーション

外部からアプリケーションにアクセスすべきか、Daprにアクセスすべきか

今回はアプリケーションのポートではなく、Daprのポートに外部からアクセスする形としました。実際に運用する際、アプリケーション側を外部に公開すべきか、Dapr側を公開すべきか、どちらが良いのか改めて考えてみましょう。

基本的にはアプリケーション側のポートを外部公開するべきだと思いますし、実際に僕はそのように運用しています。

ただ、Dapr側を公開することで、次のようなメリット/デメリットがあります。

  • メリット
    • 外部からアクセスする際に、OAuthやJWTトークン検証などの認可機能を利用することができる
    • Dapr内部の通信だけでなく、外部からアクセスされた最初のリクエストを分散トレーシングに含めることができる
      • (逆に言えば、アプリケーションのポートに直接アクセスさせた場合は、最初のリクエストが分散トレーシングの範疇外となる)
    • アプリケーションの個別のポート番号を気にする必要はなく、Daprの3500番ポートにアクセスしさえすれば良い
  • デメリット
    • Daprが管理するすべてのアプリケーションやステートストア、メッセージング、シークレットストアにアクセスされてしまう危険性がある

デメリットが大きすぎるため、原則としてアプリケーション側のポートを外部公開すべきなのです。

ただ今回の例のように -H "dapr-app-id:app-name" を指定することで、任意のアプリケーションのDaprにのみアクセスさせることは可能です。k8sの前段にあるロードバランサなどでヘッダを固定し、仮に元のリクエストに dapr-app-id が含まれていてもそれを確実に無視するような仕組みが利用できるのであれば、daprのポートを安全に外部公開することも可能でしょう。そうすれば、認可や分散トレーシングなどの機能をうまく活用することができます。

実は僕もいま実運用しているシステムで、ちょうど同じ課題に直面しており、外部からのリクエストを分散トレーシングに含めるために、Daprのポートを外部公開するか、それともアプリケーション側のポートを外部公開して、アプリケーション側で traceparent を発行するのか、あるいはk8sまでのどこかで traceparent を発行するのかを迷っているところです。

僕自身は「迷ったときは安全側に倒したほうが良い」と考えているので、アプリケーション側のポートを公開し、一方で多少は雑に扱っても良い traceparent はクライアント側で発行するとか、k8sの前段のどこかで発行する、くらいが良いのではないかと考えています。

この辺りは様々なポリシーがあるでしょうから、Daprの識者たちともディスカッションしてみたいですね。

まとめ

  • Dapr + k8sの環境でも問題なく分散トレーシングができました
  • アプリケーション側の metadata.annotations で利用するHTTPパイプラインを指定する必要があります
  • 外部公開するのは、アプリケーション側のポートか、Dapr側のポートか、慎重に検討する必要があります

だいぶDaprとk8sの運用に慣れてきて、構成も考えられるようになってきましたね。

これでいったんDapr + k8sの機能を紹介するのはおしまいにして、明日はAWS上にデプロイする方法を説明したいと思います。

それでは!

Dapr Advent Calendar 17日目 - Dapr + k8sの環境が壊れたので作り直し

こんにちは Dapr Advent Calendar 17日目です。今日はもともと分散トレーシングの話を書くつもりだったのですが、その前に環境でトラブってしまったので、その辺りの経緯や対策などを書きたいと思います。

TL;DR

「私はminikubeをメモリ割り当て2GBで動かそうとしていました」

  • Daprの環境が壊れた時は、dapr unintall -k minikube stop Dockerの再起動 minikube start dapr init -k という手順で復旧できる
  • minikubeはデフォルトで2CPU、2GBのメモリで環境を作るが、Javaアプリケーションを5〜6つくらい作成するだけでメモリが足りなくなる
  • minikube start--cpus--memory オプションで、割り当てるリソースを指定ができる
    • ただし既に作成したminikubeはリソース割り当てを変更できないため、一度 minikube delete してから再作成する必要がある
  • docker container stats でDockerのリソース状況を確認できる
  • eval $(minikube docker-env)docker container stats でminikubeのリソース状況を確認できる

問題の経緯

問題が起きる前

数日前ですが、このAdvent Calendarの連載を書くために、ローカル環境のminikubeに色々とアプリケーションをデプロイしたり何だりしていたら、急にkubectlコマンドの反応が極端に遅くなったことがありました。その時はMacのCPU使用率が200%になっていて、2CPUを使いきって遅くなっていたため、「podが無限ループでもしたかなぁ?」などと思い、とりあえず kubectl delete -f k8s で順番にpodを削除しようとしました。

ただpodの削除も途中で止まったので minikube stop コマンドでk8sごと停止させ、改めて minikube start コマンドで起動して、残ったpodなどを削除などしてから作業を続けていました。

その後は特に問題なくminikubeを使うことができたので、まぁそんなこともあるのかなと思い、あまり気にしていませんでした。

なぜかpodを4つ以上起動できない!

昨晩、またブログを書くためにminikube上にアプリケーションをデプロイしていたら、3つか4つ以上のアプリケーションをデプロイしたところで、なぜか「アプリケーションのpodのうち1つがランダムにERRORになる」という問題が起きました。kubectl describe pod で詳細を見ると、Liveness ProbeとReadiness Probeが500エラーを返しているようです。

何かDapr関連のポートが被って通信エラーが起きているのか、サンプルのアプリケーションに問題があるのか、複数のServiceを起動しているのが問題なのか、などいくつか仮説を立て、とりあえず問題切り分けのためにHello WorldアプリケーションをServiceなしで5つほど起動してみたところ、1つは必ず同じエラーが起きるようになっていました。

Daprをダウングレードしてみようとしたものの

いま仕事でAmazon EKS上でDaprを使ってもっとたくさんのアプリケーションを運用しているので、ポートが被って通信エラーが起きるというのも、ちょっと考えにくい話です。ただ、いま運用はDapr 1.4系を使っているのですが、このブログはDapr 1.5を使って書いているので、1.5の問題かも知れないと思い、一度バージョンを落とそうと思いました。

dapr uninstall -k
dapr install -k --runtime-version 1.4.4

これでバージョンがどうなったかなと思い、dapr status -k コマンドを実行したところ、何かおかしい。

  NAME                   NAMESPACE    HEALTHY  STATUS                       REPLICAS  VERSION  AGE  CREATED              
  dapr-placement-server  dapr-system  False    Waiting (ContainerCreating)  1         1.5.1    5d   2021-12-11 21:08.30  
  dapr-sidecar-injector  dapr-system  False    Waiting (ContainerCreating)  2         1.5.1    5d   2021-12-11 21:08.30  
  dapr-operator          dapr-system  False    Terminated                   2         1.5.1    5d   2021-12-11 21:08.30  
  dapr-dashboard         dapr-system  False    Terminated                   2         0.9.0    5d   2021-12-11 21:08.30  
  dapr-sentry            dapr-system  False    Waiting (ContainerCreating)  2         1.4.4    7s   2021-12-16 23:15.34  

1.4.4のコンテナを作れていないだけでなく、1.5.1のコンテナもまだ残っている、、というか作成途中で止まっていて、まともに動いていません。どうやらminikube上のDapr環境が壊れているようです。おそらく、冒頭に書いたCPU使用率が上がった時に、環境の一部が壊れてしまったのでしょう。

環境の再構築

これはもうDaprのバージョンの問題ではなく、Dapr環境が壊れているせいだと確信したので、環境を再構築することにしました。

dapr uninstall -k
minikube stop

この後、Docker Desktopを再起動して、また環境を構築しました。

minikube start
dapr init -k

そしてまたHello Worldアプリケーションを5つほどデプロイすると、今度は5つともきちんと起動できたので、問題解決。無事にトラブルシューティングが完了しました。さすが俺!

またCPU使用率が200%になる問題が発生

環境が治ったので、とりあえず全アプリケーションをデプロイすることにしました。

kubectl apply -f k8s

そして起動するのを待っていると、、、またkubectlの反応がなくなり、CPU使用率が200%になる問題が起きました。えぇぇ、なんで。。。

Javaエンジニアの経験と勘から、これはリソースが足りていなくてメモリ不足になってフルGCでも起きているのかなと思い、とりあえずDockerのリソース状況を確認することにしました。

docker container stats
CONTAINER ID   NAME       CPU %     MEM USAGE / LIMIT    MEM %     NET I/O          BLOCK I/O       PIDS
cbe04e02f2d1   minikube   202.36%   1.894GiB / 1.94GiB   97.60%    691kB / 2.11MB   102GB / 168MB   1100

いやー、もう完全に足りてないですね。メモリの割り当て2GBを完全に使いきっています。そりゃ中のアプリケーションもフルGCを頻発するでしょうよ。そもそもメモリ2GBでminikubeを運用しようとするなんて無茶、誰がしたんだよ。。。まぁ僕なんですが。

そんなわけで、環境をすべて作り直すことにしました。

環境の再構築

そんなわけで、ここから環境の再構築手順を説明します。このブログを読んで手を動かしていた人は、この手順に従って環境を作り直してください。

minikubeの削除

minikubeはstartする際の引数でCPU数やメモリ割り当て量を変えることできるのですが、既に作成してあるminikubeに対して変更することはできないため、まずminikubeそのものを削除する必要があります。

次のコマンドで削除します。

minikube stop
minikube delete

これでDaprのコンテナや、作成したイメージなどもすべて削除されます。

minikubeとDapr環境の再構築

続いて環境の再構築です。次のコマンドで構築します。

minikube start --cpus=4 --memory=4096

環境が構築できたら、念のためリソース状況を確認します。

docker container stats

メモリのLIMITが4GiBになっていることを確認してください。

続いて、Dapr環境の構築です。

dapr init -k

環境が構築できたら、念のためpodが正常に動いていることを確認します。

dapr status -k

5つのpodのHEALTYがTRUEになっていて、STATUSがRunningになっていればOKです。

イメージの再作成

これまでのブログで作ってきたアプリケーションのイメージは、minikubeをdeleteした際に一緒に削除されてしまったので、すべて作り直す必要があります。

eval $(minikube docker-env)
cd (GitHubのディレクトリパス)/dapr-advent-2021
./mvnw spring-boot:build-image

これですべてのアプリケーションのイメージが再作成されます。

ちなみに最後に親モジュールのイメージも作ろうとしてmavenビルドがエラーになりますが、その時点で子モジュールのイメージはすべて作成されているためヨシとします。

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

イメージの作成が完了したら、アプリケーションをまとめてデプロイしてしまいましょう。

kubectl apply -f k8s

デプロイする順番など気にせず、一気にデプロイしても特に問題などは起きません。

念のため kubectl get pods コマンドで、起動に成功していることを確認しておきましょう。

NAME                                READY   STATUS    RESTARTS   AGE
hello-app-fb644cd5d-j2z7b           2/2     Running   0          1m
invoke-app-745b5f4bf6-2hf6l         2/2     Running   0          1m
postgresql-deploy-658df4946-lm6gp   1/1     Running   0          1m
publish-app-5b4c66c9-mhj67          2/2     Running   0          1m
rabbitmq-5d474484f5-rhjzd           1/1     Running   0          1m
state-app-849f6d6944-2mtr5          2/2     Running   0          1m
subscribe-app-cdc58446-n82px        2/2     Running   0          1m

問題ありませんね。

minikubeのリソース確認

最後に、minikubeのリソースを確認しておきましょう。

新しいコンソールを開いて、docker container stats コマンドを実行します。

CONTAINER ID   NAME       CPU %     MEM USAGE / LIMIT   MEM %     NET I/O          BLOCK I/O        PIDS
079bf48016ec   minikube   53.21%    2.339GiB / 4GiB     58.48%    1.56GB / 250MB   106MB / 9.87GB   1062

メモリは4GiB中、2.3GiBほど使っているようです。そりゃ2GBしか割り当ててなければ、メモリが足りなくて止まっちゃいますよね、、、

なお、minikube内部のpodごとのリソース状況は、次のコマンドで確認できます。

eval $(minikube docker-env)
docker container stats

次のような結果が出るはずです。

CONTAINER ID   NAME                                                                                                                  CPU %     MEM USAGE / LIMIT     MEM %     NET I/O           BLOCK I/O         PIDS
bb2650777c7a   k8s_daprd_state-app-849f6d6944-2mtr5_default_fa0cc253-ddab-4026-9328-fc3a29d2151e_0                                   0.14%     15.75MiB / 7.773GiB   0.20%     1.59MB / 1.51MB   0B / 0B           11
e51e7d17a744   k8s_daprd_publish-app-5b4c66c9-mhj67_default_9f1de14f-0498-4f12-ab17-931771d7f0b3_0                                   0.21%     15.68MiB / 7.773GiB   0.20%     1.59MB / 1.51MB   0B / 0B           12
b9c2c68edd1b   k8s_daprd_hello-app-fb644cd5d-j2z7b_default_114f279b-313b-4763-b779-3a1128986b9e_0                                    0.09%     15.74MiB / 7.773GiB   0.20%     1.59MB / 1.5MB    8.19kB / 0B       12
3d7590c40e30   k8s_daprd_invoke-app-745b5f4bf6-2hf6l_default_852d6fd1-582d-44ea-b06f-cbaa95a7af2b_0                                  0.08%     15.69MiB / 7.773GiB   0.20%     1.59MB / 1.51MB   0B / 0B           12
52769d846edb   k8s_subscribe_subscribe-app-cdc58446-n82px_default_b0d3ebfa-8a4f-4dec-ad6e-4296c9dfaa74_0                             0.12%     172.4MiB / 7.773GiB   2.17%     1.59MB / 1.52MB   0B / 1.29MB       32
5daf791beb83   k8s_state_state-app-849f6d6944-2mtr5_default_fa0cc253-ddab-4026-9328-fc3a29d2151e_0                                   0.14%     164.7MiB / 7.773GiB   2.07%     1.59MB / 1.51MB   16.4kB / 1.32MB   32
9c162e211125   k8s_publish_publish-app-5b4c66c9-mhj67_default_9f1de14f-0498-4f12-ab17-931771d7f0b3_0                                 0.12%     180.6MiB / 7.773GiB   2.27%     1.59MB / 1.51MB   8.19kB / 1.3MB    32
4652afb3deef   k8s_postgresql_postgresql-deploy-658df4946-lm6gp_default_f080e2f5-f9ec-41f3-a8b3-752b59445d8b_0                       0.00%     52.89MiB / 7.773GiB   0.66%     12.5kB / 12kB     32.7MB / 55MB     12
39ba4e76702f   k8s_POD_subscribe-app-cdc58446-n82px_default_b0d3ebfa-8a4f-4dec-ad6e-4296c9dfaa74_0                                   0.00%     212KiB / 7.773GiB     0.00%     1.59MB / 1.52MB   0B / 0B           1
6b06c40a04b6   k8s_POD_state-app-849f6d6944-2mtr5_default_fa0cc253-ddab-4026-9328-fc3a29d2151e_0                                     0.00%     208KiB / 7.773GiB     0.00%     1.59MB / 1.51MB   0B / 0B           1
2f3b07bc26e6   k8s_rabbitmq_rabbitmq-5d474484f5-rhjzd_default_f39f8fcd-47af-420a-aebd-d4dbf4385d8d_0                                 0.25%     126.4MiB / 7.773GiB   1.59%     2.5kB / 354B      33.8MB / 819kB    33
b2bc5b2bcd5c   k8s_POD_publish-app-5b4c66c9-mhj67_default_9f1de14f-0498-4f12-ab17-931771d7f0b3_0                                     0.00%     208KiB / 7.773GiB     0.00%     1.59MB / 1.51MB   0B / 0B           1
78778711254e   k8s_POD_rabbitmq-5d474484f5-rhjzd_default_f39f8fcd-47af-420a-aebd-d4dbf4385d8d_0                                      0.00%     208KiB / 7.773GiB     0.00%     2.5kB / 354B      0B / 0B           1
90754eca8f01   k8s_hello_hello-app-fb644cd5d-j2z7b_default_114f279b-313b-4763-b779-3a1128986b9e_0                                    0.20%     194.5MiB / 7.773GiB   2.44%     1.59MB / 1.5MB    0B / 1.56MB       32
0ccbd01ea780   k8s_POD_postgresql-deploy-658df4946-lm6gp_default_f080e2f5-f9ec-41f3-a8b3-752b59445d8b_0                              0.00%     204KiB / 7.773GiB     0.00%     12.5kB / 12kB     0B / 0B           1
986d6f27779d   k8s_invoke_invoke-app-745b5f4bf6-2hf6l_default_852d6fd1-582d-44ea-b06f-cbaa95a7af2b_0                                 0.11%     179.7MiB / 7.773GiB   2.26%     1.59MB / 1.51MB   143kB / 1.64MB    32
0361dd7cf5bb   k8s_POD_hello-app-fb644cd5d-j2z7b_default_114f279b-313b-4763-b779-3a1128986b9e_0                                      0.00%     208KiB / 7.773GiB     0.00%     1.59MB / 1.5MB    0B / 0B           1
1f374ad3ef7c   k8s_POD_invoke-app-745b5f4bf6-2hf6l_default_852d6fd1-582d-44ea-b06f-cbaa95a7af2b_0                                    0.00%     204KiB / 7.773GiB     0.00%     1.59MB / 1.51MB   0B / 0B           1
6d1ef557c15a   k8s_dapr-placement-server_dapr-placement-server-0_dapr-system_677cb5fd-ae57-41eb-918c-58d42e6c7652_0                  0.33%     7.207MiB / 7.773GiB   0.09%     6.06MB / 6.09MB   0B / 0B           11
23acf6a45fe2   k8s_dapr-operator_dapr-operator-5b4b68b5c5-mptxp_dapr-system_c9d399ef-c82c-4781-9ae4-624a6933abc5_0                   0.17%     18.25MiB / 7.773GiB   0.23%     27.1MB / 14.1MB   0B / 0B           13
9ff28f354a81   k8s_dapr-sidecar-injector_dapr-sidecar-injector-6f749dbf87-9vn96_dapr-system_0ab8e63d-8073-411c-9c87-d9abd03083b7_0   0.11%     8.184MiB / 7.773GiB   0.10%     1.55MB / 1.37MB   0B / 0B           12
116fb67e12ec   k8s_dapr-sentry_dapr-sentry-c6b746cdf-hp2n9_dapr-system_e30b08ca-9060-4853-8c26-a37351e41c26_0                        0.06%     7.883MiB / 7.773GiB   0.10%     1.53MB / 1.39MB   0B / 0B           14
9ef9efefb9c2   k8s_dapr-dashboard_dapr-dashboard-57b4db56fc-pr62l_dapr-system_4ba3a32e-afe9-4c7e-a8c2-ba2ac4745da9_0                 0.00%     5.734MiB / 7.773GiB   0.07%     2.89kB / 0B       0B / 0B           8
9bc3f4edc295   k8s_POD_dapr-placement-server-0_dapr-system_677cb5fd-ae57-41eb-918c-58d42e6c7652_0                                    0.00%     200KiB / 7.773GiB     0.00%     6.06MB / 6.09MB   0B / 0B           1
db607d3df442   k8s_POD_dapr-sidecar-injector-6f749dbf87-9vn96_dapr-system_0ab8e63d-8073-411c-9c87-d9abd03083b7_0                     0.00%     204KiB / 7.773GiB     0.00%     1.55MB / 1.37MB   0B / 0B           1
2f2426091427   k8s_POD_dapr-operator-5b4b68b5c5-mptxp_dapr-system_c9d399ef-c82c-4781-9ae4-624a6933abc5_0                             0.00%     208KiB / 7.773GiB     0.00%     27.1MB / 14.1MB   0B / 0B           1
351e4b9d4e76   k8s_POD_dapr-sentry-c6b746cdf-hp2n9_dapr-system_e30b08ca-9060-4853-8c26-a37351e41c26_0                                0.00%     204KiB / 7.773GiB     0.00%     1.53MB / 1.39MB   0B / 0B           1
292b25653234   k8s_POD_dapr-dashboard-57b4db56fc-pr62l_dapr-system_4ba3a32e-afe9-4c7e-a8c2-ba2ac4745da9_0                            0.00%     204KiB / 7.773GiB     0.00%     2.89kB / 0B       0B / 0B           1
da405d04f18f   k8s_storage-provisioner_storage-provisioner_kube-system_3526d87b-7f1f-4e57-a5c4-6a9f054e7ec2_1                        0.05%     9.297MiB / 7.773GiB   0.12%     0B / 0B           0B / 0B           8
4d7fe3d17eb4   k8s_coredns_coredns-558bd4d5db-t4gxq_kube-system_ec6ebffa-abce-45b9-9978-b3349311791f_0                               0.36%     11.11MiB / 170MiB     6.53%     2.23MB / 719kB    0B / 0B           12
7e4d83af4783   k8s_kube-proxy_kube-proxy-tmzwt_kube-system_29295269-e286-48b4-84e0-eb2583132419_0                                    0.02%     12.56MiB / 7.773GiB   0.16%     0B / 0B           0B / 16.4kB       10
52cab24b1823   k8s_POD_coredns-558bd4d5db-t4gxq_kube-system_ec6ebffa-abce-45b9-9978-b3349311791f_0                                   0.00%     204KiB / 7.773GiB     0.00%     2.23MB / 719kB    0B / 0B           1
1ec5d319864e   k8s_POD_kube-proxy-tmzwt_kube-system_29295269-e286-48b4-84e0-eb2583132419_0                                           0.00%     280KiB / 7.773GiB     0.00%     0B / 0B           0B / 0B           1
23e447125876   k8s_POD_storage-provisioner_kube-system_3526d87b-7f1f-4e57-a5c4-6a9f054e7ec2_0                                        0.00%     148KiB / 7.773GiB     0.00%     0B / 0B           0B / 0B           1
d0b6da913375   k8s_etcd_etcd-minikube_kube-system_be5cbc7ffcadbd4ffc776526843ee514_0                                                 1.65%     39.15MiB / 7.773GiB   0.49%     0B / 0B           0B / 235MB        19
094635d3e682   k8s_kube-apiserver_kube-apiserver-minikube_kube-system_cefbe66f503bf010430ec3521cf31be8_0                             3.95%     314.8MiB / 7.773GiB   3.96%     0B / 0B           0B / 0B           12
4c8cac060a66   k8s_kube-scheduler_kube-scheduler-minikube_kube-system_a2acd1bccd50fd7790183537181f658e_0                             0.20%     16.75MiB / 7.773GiB   0.21%     0B / 0B           0B / 0B           11
a638324aa7b6   k8s_kube-controller-manager_kube-controller-manager-minikube_kube-system_a5754fbaabd2854e0e0cdce8400679ea_0           1.61%     48.86MiB / 7.773GiB   0.61%     0B / 0B           0B / 0B           10
a21fdf45e9ab   k8s_POD_etcd-minikube_kube-system_be5cbc7ffcadbd4ffc776526843ee514_0                                                  0.00%     144KiB / 7.773GiB     0.00%     0B / 0B           0B / 0B           1
4983ebfc71a4   k8s_POD_kube-scheduler-minikube_kube-system_a2acd1bccd50fd7790183537181f658e_0                                        0.00%     144KiB / 7.773GiB     0.00%     0B / 0B           0B / 0B           1
ffa6824f6939   k8s_POD_kube-controller-manager-minikube_kube-system_a5754fbaabd2854e0e0cdce8400679ea_0                               0.00%     140KiB / 7.773GiB     0.00%     0B / 0B           0B / 0B           1
6e78a62d73d0   k8s_POD_kube-apiserver-minikube_kube-system_cefbe66f503bf010430ec3521cf31be8_0                                        0.00%     140KiB / 7.773GiB     0.00%     0B / 0B           0B / 0B           1
19977ecb1ab2   k8s_daprd_subscribe-app-cdc58446-n82px_default_b0d3ebfa-8a4f-4dec-ad6e-4296c9dfaa74_0                                 0.21%     15.47MiB / 7.773GiB   0.19%     1.59MB / 1.52MB   0B / 0B           12

何やよく分かりませんけど、わりとたくさんpodが動いてるんですね(今さら)

JVM系のpodは、たとえばこんな感じです。

90754eca8f01   k8s_hello_hello-app-fb644cd5d-j2z7b_default_114f279b-313b-4763-b779-3a1128986b9e_0                                    0.20%     194.5MiB / 7.773GiB   2.44%     1.59MB / 1.5MB    0B / 1.56MB       32

アプリケーション1つで200MiBほどメモリを使うのですから、そりゃ5つも6つも立ち上げれば2GBでは足りませんね。

そんなわけで、無事に環境を作り直すことができました。

ドキュメントはちゃんと読もうね

ちなみにDaprのminikube環境構築のドキュメントを見ると、、、

docs.dapr.io

minikube startのコマンドは、このようになっています。

minikube start --cpus=4 --memory=4096

ドキュメントで、メモリ4GiBにしろって書いてあるんですから、それをきちんと守れって話ですよね・・・。

いや、別にオプションなしで実行しても問題ないんだったら、それに越したことはないのかなって、、、(言い訳)

まとめ

ということで今回のまとめです。

  • Daprの環境が壊れた時は、dapr unintall -k minikube stop Dockerの再起動 minikube start dapr init -k という手順で復旧できる
  • minikubeはデフォルトで2CPU、2GBのメモリで環境を作るが、Javaアプリケーションを5〜6つくらい作成するだけでメモリが足りなくなる
  • minikube start--cpus--memory オプションで、割り当てるリソースを指定ができる
    • ただし既に作成したminikubeはリソース割り当てを変更できないため、一度 minikube delete してから再作成する必要がある
  • docker container stats でDockerのリソース状況を確認できる
  • eval $(minikube docker-env)docker container stats でminikubeのリソース状況を確認できる

いやぁ、こういうことを経験して、使いこなしレベルって上がっていくもんですよね!

それでは、また明日!

Dapr Advent Calendar 16日目 - Dapr + k8sでステート保存とシークレットストア

こんにちは Dapr Advent Calendar 16日目です。最近ちょくちょく周りの人からDaprに興味があるとか、Daprのメリットに魅力を感じるという話を耳にするようになりました。とは言えまだ「業務で使う」ところまで行く人はなかなかいないのですが、来年にはv1.0リリースから1周年を迎えますし、ぼちぼち使う人も出てくるんじゃないかなと思っています。

k8s + Dapr + PostgreSQLでステート保存。あとシークレットストア

今回はk8s + Dapr上のアプリケーションからPostgreSQLへの読み書きを行います。ただ、単純に読み書きするだけなら、過去に作ったアプリケーションを使い、k8s上にデプロイするためのマニフェストファイルを作って kubectl apply するだけで済んでしまうので、あまり面白くありません。せっかくなのでシークレットストアも一緒に使い、アカウント情報などを秘匿する形にしてみましょう。

f:id:cero-t:20211216103045p:plain
今回作るアプリケーション

アプリケーションの作成

今回動かすアプリケーションは Dapr Advent Calendar 4日目 - Daprでデータストアにアクセス で作成したものです。そちらを先に読んでからこのエントリーを読んでください。

シークレットストアについては Dapr Advent Calendar 7日目 - Daprでシークレットストアを使う でも解説しているため、そちらも参考にしてください。

ソースコードgithubに置いてあります。

https://github.com/cero-t/dapr-advent-2021

また Dapr Advent Calendar 13日目 - Dapr + k8sでHello World で説明したツールなどがセットアップ済みであることを前提で説明を書いています。

作成したソースコードのおさらい

以前のエントリーで作成したアプリケーションのソースコードをおさらいします。データの読み書きを行うためのコントローラークラスです。

StateController.java

@RestController
public class StateController {
    private RestTemplate restTemplate;

    @Value("http://localhost:${DAPR_HTTP_PORT}/v1.0/state/statestore")
    private String stateUrl;

    public StateController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @PostMapping("/write")
    public void write(@RequestBody Object message) {
        restTemplate.postForObject(stateUrl, message, Void.class);
    }

    @GetMapping("/read/{key}")
    public Object read(@PathVariable String key) {
        return restTemplate.getForObject(stateUrl + "/" + key, Object.class);
    }
}

ステートを保存する /write と、それを読み込む /read というエンドポイントを提供しています。いずれもデータストア名に statestore を指定して、DaprのStatemangent APIを呼び出しているだけです。

このアプリケーションは8082番ポートで起動するように設定してあります。

アプリケーションのイメージ作成

続いて、このアプリケーションのイメージを作成しましょう。

イメージ作成先がminikubeのイメージレジストリになるよう次のコマンドを実行します。

eval $(minikube docker-env)

そしてイメージ作成のコマンドを実行します。

cd (GitHubのディレクトリパス)/dapr-advent-2021/state
../mvnw spring-boot:build-image

これでイメージが作成できました。

マニフェストファイルの作成

次に、このアプリケーションをデプロイするためのマニフェストファイルを作成します。

k8s/state-app.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: state-app
  labels:
    app: state
spec:
  replicas: 1
  selector:
    matchLabels:
      app: state
  template:
    metadata:
      labels:
        app: state
      annotations:
        dapr.io/enabled: "true"
        dapr.io/app-id: "state-app"
        dapr.io/app-port: "8082"
    spec:
      containers:
      - name: state
        image: state:1.0.0
        ports:
        - containerPort: 8082
        imagePullPolicy: IfNotPresent

---
kind: Service
apiVersion: v1
metadata:
  name: state-svc
  labels:
    app: state
spec:
  selector:
    app: state
  ports:
  - protocol: TCP
    port: 8082
    targetPort: 8082
  type: LoadBalancer

以前作成した hello.yaml と同じ構成ですので、説明は割愛します。

PostgreSQLマニフェストファイルの作成

続いて、データストアとしてPostgreSQLを起動するためのマニフェストファイルを作成します。

パスワードが平文で書かれたマニフェストを作成する

PostgreSQLを起動するためのマニフェストファイルは次のような内容になります。パスワードは平文で指定しています。

k8s/postgresql.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgresql-deploy
  labels:
    app: postgresql
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgresql
  template:
    metadata:
      labels:
        app: postgresql
    spec:
      containers:
      - name: postgresql
        image: postgres:11-alpine
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 5432
        env:
        - name: POSTGRES_PASSWORD
          value: secretpassword

---
kind: Service
apiVersion: v1
metadata:
  name: postgresql-svc
  labels:
    app: postgresql
spec:
  type: LoadBalancer
  selector:
    app: postgresql
  ports:
  - protocol: TCP
    port: 5432
    targetPort: 5432

イメージを使ってPostgreSQLをデプロイするマニフェストファイルです。

パスワードは POSTGRES_PASSWORDsecretpassword を指定しています。

DB名とユーザ名は省略しているため、いずれも postgres が使われます。

また、このPostgreSQLに他のpodからアクセスできるよう postgresql-svc という名前のサービスを作り、5432番ポートにpod外からアクセスできるようにしています。

さて、上のマニフェストファイルにはパスワードが平文で書かれているため、このパスワードをシークレットストアに持っていきましょう。k8s自身がシークレットストアの機能を持っているため、今回はそれを使います。

base64エンコードされたパスワードを作成する

まずはパスワードをbase64エンコードするため、次のコマンドを実行します。

echo -n "secretpassword" | base64

-n は、echoの表示時に改行を入れないためのオプションです。これを入れないと、secretpassword の後ろに改行が入ったものがbase64エンコードされてしまいます。僕は一度それでヤラカシたことがあるので気をつけてください。

実行結果は、次のようになります。

c2VjcmV0cGFzc3dvcmQ=

念のため、base64デコードして復元できるかを確認しておきましょう。

echo c2VjcmV0cGFzc3dvcmQ= | base64 --decode
secretpassword

元の文字列が復元できました。

シークレットストアのマニフェストファイルを作成する

続いて、シークレットストアのマニフェストファイルを作成します。上でbase64エンコードしたパスワードを利用します。

k8s/postgresql-secret.yaml

apiVersion: v1
kind: Secret
metadata:
  name: postgresql-secret
type: Opaque
data:
  postgres_password: c2VjcmV0cGFzc3dvcmQ=

typeOpaque にすると、シークレットの値としてbase64エンコードされた文字列を指定することができます。type には他にも kubernetes.io/tlskubernetes.io/service-account-token などを指定することができますが、通常はこの Opaque で問題なさそうです。

data には key: value 形式でシークレットのキーとbase64エンコードした値を指定します。

これで、シークレットストアの準備ができました。

ちなみにbase64エンコードしているとは言え、これではパスワードを平文と保存していることと大差ないですよね。よりセキュアにするためには、シークレットストアをマニフェストファイルではなくkubectlコマンドで作成するとか、作成したシークレットの詳細を閲覧できるユーザを絞るなどする必要があります。その辺りはk8sの使いこなしの話になるため、今回は割愛したいと思います。

PostgreSQLのマニュフェストファイルからシークレットストアを参照する

続いて、上で作成したPostgreSQLを起動するマニフェストファイルを修正し、シークレットストアを参照するようにします。次のような内容になります。

k8s/postgresql.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgresql-deploy
  labels:
    app: postgresql
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgresql
  template:
    metadata:
      labels:
        app: postgresql
    spec:
      containers:
      - name: postgresql
        image: postgres:11-alpine
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 5432
        env:
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgresql-secret
              key: postgres_password

---
kind: Service
apiVersion: v1
metadata:
  name: postgresql-svc
  labels:
    app: postgresql
spec:
  type: LoadBalancer
  selector:
    app: postgresql
  ports:
  - protocol: TCP
    port: 5432
    targetPort: 5432

変更箇所は、パスワードの部分だけです。

この部分が、

        - name: POSTGRES_PASSWORD
          value: secretpassword

次のようになります。

        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgresql-secret
              key: postgres_password

パスワードを平文で書くのではなく valueFrom.secretKeyRef としてシークレットストアを参照するようにしました。

name にはシークレットストア名、key にはシークレットのキーを指定します。

これでPostgreSQLのパスワードとしてシークレットストアの値を利用できるようになりました。今のところDaprは全く関係なくて、ただただk8sマニフェストを作成しているだけです。

DaprからPostgreSQLへの接続設定ファイルの作成

続いて、DaprからPostgreSQLにアクセスするための設定ファイルを作成します。

接続文字列が平文で書かれた接続設定ファイルを作成する

DaprからPostgreSQLに接続するための設定ファイルは次のようになります。

k8s/statestore.yaml

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: statestore
spec:
  type: state.postgresql
  version: v1
  metadata:
  - name: connectionString
    value: host=postgresql-svc user=postgres password=secretpassword port=5432 connect_timeout=10 database=postgres

これは Dapr Advent Calendar 4日目 - Daprでデータストアにアクセス で作成した設定ファイルとほとんど同じです。ローカル環境向けに作ったファイルを、ほぼそのままk8s向けに流用できるのがDaprの良いところです。

metadata.name にデータストア名として statestore を指定しています。これはソースコードで指定した値に合わせます。

このファイルにもやはりパスワードが平文で書かれているため、接続文字列の部分をシークレットストアに移動させましょう。

接続文字列をbase64エンコードする

まずは接続文字列をbase64エンコードします。

echo -n "host=postgresql-svc user=postgres password=secretpassword port=5432 connect_timeout=10 database=postgres" | base64
aG9zdD1wb3N0Z3Jlc3FsLXN2YyB1c2VyPXBvc3RncmVzIHBhc3N3b3JkPXNlY3JldHBhc3N3b3JkIHBvcnQ9NTQzMiBjb25uZWN0X3RpbWVvdXQ9MTAgZGF0YWJhc2U9cG9zdGdyZXM=

少し長いですが、base64エンコードされた文字列ができました。

シークレットストアのファイルに追加する

先ほど作ったシークレットストアのファイルに、接続文字列の値を追加します。

k8s/postgresql-secret.yaml

apiVersion: v1
kind: Secret
metadata:
  name: postgresql-secret
type: Opaque
data:
  postgres_password: c2VjcmV0cGFzc3dvcmQ=
  connection: aG9zdD1wb3N0Z3Jlc3FsLXN2YyB1c2VyPXBvc3RncmVzIHBhc3N3b3JkPXNlY3JldHBhc3N3b3JkIHBvcnQ9NTQzMiBjb25uZWN0X3RpbWVvdXQ9MTAgZGF0YWJhc2U9cG9zdGdyZXM=

最後の行に connection というキーで、base64エンコードされた接続文字列を追加しました。

PostgreSQLへの接続設定ファイルからシークレットを参照する

それでは、DaprのPostgreSQL接続設定ファイルからシークレットストアを参照するように修正しましょう。次のようになります。

k8s/statestore.yaml

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: statestore
spec:
  type: state.postgresql
  version: v1
  metadata:
  - name: connectionString
    secretKeyRef:
      name: postgresql-secret
      key: connection

最後の connectionString の部分を修正しました。

修正前

  - name: connectionString
    value: host=postgresql-svc user=postgres password=secretpassword port=5432 connect_timeout=10 database=postgres

修正後

  - name: connectionString
    secretKeyRef:
      name: postgresql-secret
      key: connection

シークレットストアを参照するようにしました。こっちは valueFrom: は挟まなくて良いんですね。ちょっと設定ファイルの文法をよく分かってないのですが、これで良いみたいです。

アプリケーションをデプロイしてアクセスする

イメージをk8sにデプロイ

それでは作成したイメージをk8sにデプロイします。

cd (GitHubのディレクトリパス)/dapr-advent-2021
kubectl apply -f k8s/postgresql-secret.yaml
kubectl apply -f k8s/postgresql.yaml
kubectl apply -f k8s/statestore.yaml
kubectl apply -f k8s/state-app.yaml

まずシークレットストアを作成し、PostgreSQLをデプロイし、そのPostgreSQLにDaprからアクセスするための設定(コンポーネント)をデプロイし、最後にアプリケーションをデプロイする、という順番になっています。

ちなみに複数のファイルを指定してデプロイする際は、次のようにディレクトリを指定してデプロイすることも可能です。

kubectl apply -f k8s

こうすればk8sフォルダにあるすべてのマニフェストファイルに対して kubectl apply が行われます。

また、k8s上にデプロイされたpodやコンポーネントと、手元にあるマニフェストファイルの差分を確認する際には次のコマンドが使えます。

kubectl diff -f k8s

差分を確認してから kubectl apply することで、意図しない設定の更新などを避けることができますね。

まとめて全部削除したい時は、次のようにできます。

kubectl delete -f k8s

これも便利ですね。

k8s上のアプリケーションにアクセスする

それでは、起動したアプリケーションにアクセスしてみましょう。

次のコマンドを実行します。

kubectl port-forward service/state-svc 8082:8082

これでローカルPCの8082番ポート経由でminikubeにアクセスできるようになりました。

それでは、別のコンソールからアクセスしてデータを保存してみましょう。

curl -XPOST "localhost:8082/write" -H "Content-type:application/json" -d '[
  {
    "key": "cero_t",
    "value": {
      "name": "Shin Tanimoto",
      "twitter": "@cero_t"
    }
  },
  {
    "key": "BABYMETAL",
    "value": [
      {
        "name": "SU-METAL",
        "alias": "Suzuka Nakamoto"
      },
      {
        "name": "MOAMETAL",
        "alias": "Moa Kikuchi"
      },
      {
        "name": "YUIMETAL",
        "alias": "Yui Mizuno"
      }
    ]
  }
]'

エラーなく応答が返ってくれば成功です。エラーが発生した場合は kubectl logskubectl describe pods コマンドなどで詳細を確認してみてください。

次のコマンドで、保存したデータを取得します。

curl localhost:8082/read/cero_t

次のようなデータが取得できます。

{
  "name": "Shin Tanimoto",
  "twitter": "@cero_t"
}

問題なくPostgreSQLにデータが保存され、取得できることが分かりました。

f:id:cero-t:20211216120656p:plain
k8sのシークレットストアを参照するアプリケーション

どのシークレットストアを使うべきか

今回はk8sでシークレットストアを作成し、k8sマニフェストファイルと、Daprの接続設定ファイルからそれぞれ利用する形にしました。一方 Dapr Advent Calendar 7日目 - Daprでシークレットストアを使う で利用したDaprのシークレットストア機能については出番がありませんでした。この機能はどういう時に使うべきなのでしょうか。

僕自身の使い分けとしては、正味の話、Daprのシークレットストアを使う機会は今のところありません。ローカル環境で(k8sなしで)開発する時はシークレットストアを使うことがなく、設定ファイルに直接パスワードなどを書いていますし、k8sを利用する検証環境や運用環境では、k8sのシークレットストアを使うためです。

「アプリケーション内から外部サービスを利用する際に必要となるアカウント情報」などは、Daprのシークレットストアに問い合わせて取得する可能性があるのですが、僕自身の経験としては、そのような情報は開発時には application.yaml に書いておき、k8s環境ではk8sのシークレットストアから取得した情報を使って、application.yamlの値を環境変数で上書きするようにしています。そうするとDaprのシークレットストアの出番がなくなるのですよね。

Daprをローカル環境でもk8s環境でもなく、個別のサーバにデプロイして運用する際には、もしかしたらDaprのシークレットストア機能の出番になるかも知れませんね。そのように使ってみることがあれば、またブログなどで紹介したいと思います。

まとめ

  • Daprの設定ファイルからk8sのシークレットストアを参照することができます
  • k8sで運用するシステムにおいては、Dapr側のシークレットストア機能を使うことはないかも知れません

今回の話を要約すると、ずいぶんシンプルになっちゃいましたね。

それでは、また明日!

追記

まきんぐさんから、平文のままシークレットを作ることもできるよと教えてもらいました。知らなかった!

世のサイトの例ではだいたいbase64エンコードされた文字列を使っているようですが、base64にすることで「カジュアルに目に触れてしまった時にパッとは値が分からない」くらいの効果ならあるものの、そんな頼りない効果が不要であれば、平文で作ってしまっても良いでしょうね。

Dapr Advent Calendar 15日目 - Dapr + k8sでメッセージング

こんにちは Dapr Advent Calendar 15日目です。急に冷え込みましたが皆さんお風邪など召されてないでしょうか。

k8s + Dapr + RabbitMQのメッセージング

今回はk8s + Dapr上でRabbitMQを介したメッセージングを行います。

f:id:cero-t:20211215020331p:plain
今回作るアプリケーション

アプリケーションの作成

動かすアプリケーションは Dapr Advent Calendar 5日目 - Daprでメッセージング で作成したものです。そちらを先に読んでからこのエントリーを読んでください。

ソースコードgithubに置いてあります。

https://github.com/cero-t/dapr-advent-2021

また Dapr Advent Calendar 13日目 - Dapr + k8sでHello World で説明したツールなどがセットアップ済みであることを前提で説明を書いています。

作成したソースコードのおさらい

まずは以前のエントリーで作成したアプリケーションのソースコードをおさらいします。

メッセージを送る側(publish)のアプリケーション

メッセージを送る側のアプリケーションは、DaprのPub/sub APIを使ってメッセージを送ります。

(publish) PublishController.java

@RestController
public class PublishController {
    private RestTemplate restTemplate;

    @Value("${pubsubUrl}")
    private String pubsubUrl;

    public PublishController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @PostMapping("/publish")
    public void publish(@RequestBody Object message) {
        restTemplate.postForObject(pubsubUrl, message, Void.class);
    }
}

メッセージを送信する /publish というエンドポイントで、DaprのPub/sub APIを呼び出してエンキューする処理を書いています。

また、設定ファイルは次のようになっています。

(publish) application.properties

server.port=8083
pubsubUrl=http://localhost:${DAPR_HTTP_PORT}/v1.0/publish/pubsub/my-message

ポート8083番で起動します。

また、pubsubUrl は次のファイルで上書きしています。

(publish) application-rabbitmq.properties

pubsubUrl=http://localhost:${DAPR_HTTP_PORT}/v1.0/publish/rabbitmq-pubsub/my-message

Springのプロファイルが rabbitmq の場合は pubsubUrlhttp://localhost:${DAPR_HTTP_PORT}/v1.0/publish/rabbitmq-pubsub/my-message となります。pubsub名が rabbitmq-pubsub で、メッセージトピックが my-message となっています。

メッセージを受ける側(subscribe)のアプリケーション

メッセージを受ける側のアプリケーションは、ただWeb APIのエンドポイントを作成するだけです。

(subscribe) SubscribeController.java

@RestController
public class SubscribeController {
    @PostMapping("/subscribe")
    public void subscribe(@RequestBody String message) {
        System.out.println(message);
    }
}

メッセージを受ける /subscribe というエンドポイントで、受け取ったメッセージを標準出力に出力しています。

このアプリケーションはポート8084番で起動します。

アプリケーションのイメージ作成

それでは、このアプリケーションのイメージを作成しましょう。

イメージ作成先がminikubeのイメージレジストリになるよう次のコマンドを実行します。

eval $(minikube docker-env)

そしてイメージ作成のコマンドを実行します。publishとsubscribeのそれぞれのイメージを作成します。

cd (GitHubのディレクトリパス)/dapr-advent-2021/publish
../mvnw spring-boot:build-image
cd (GitHubのディレクトリパス)/dapr-advent-2021/subscribe
../mvnw spring-boot:build-image

これでイメージが作成できました。

マニフェストファイルの作成

次に、これらのアプリケーションをデプロイするためのマニフェストファイルを作成します。

publishアプリケーションのマニフェストファイル

まずはpublish側のアプリケーションをデプロイするためのマニフェストファイルです。

k8s/publish-app.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: publish-app
  labels:
    app: publish
spec:
  replicas: 1
  selector:
    matchLabels:
      app: publish
  template:
    metadata:
      labels:
        app: publish
      annotations:
        dapr.io/enabled: "true"
        dapr.io/app-id: "publish-app"
        dapr.io/app-port: "8083"
    spec:
      containers:
      - name: publish
        image: publish:1.0.0
        ports:
        - containerPort: 8083
        imagePullPolicy: IfNotPresent
        env:
          - name: spring.profiles.active
            value: rabbitmq

---
kind: Service
apiVersion: v1
metadata:
  name: publish-svc
  labels:
    app: publish
spec:
  selector:
    app: publish
  ports:
  - protocol: TCP
    port: 8083
    targetPort: 8083
  type: LoadBalancer

publish側は以前作成した hello.yaml と同じく、podを作る Deployment と、pod外からpodにアクセスできるようにするための Service という構成です。

また環境変数として、次のような値を指定しています。

        env:
          - name: spring.profiles.active
            value: rabbitmq

Springのプロファイルを rabbitmq にすることで application-rabbitmq.yaml が利用されるようにしています。

subscribeアプリケーションのマニフェストファイル

続いて、subscribe側のアプリケーションをデプロイするためのマニフェストファイルです。

k8s/subscribe-app.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: subscribe-app
  labels:
    app: subscribe
spec:
  replicas: 1
  selector:
    matchLabels:
      app: subscribe
  template:
    metadata:
      labels:
        app: subscribe
      annotations:
        dapr.io/enabled: "true"
        dapr.io/app-id: "subscribe-app"
        dapr.io/app-port: "8084"
    spec:
      containers:
      - name: subscribe
        image: subscribe:1.0.0
        ports:
        - containerPort: 8084
        imagePullPolicy: IfNotPresent

subscribe側はServiceを作成しません。内部通信のみ行い、外部からアクセスする必要がないためです。

RabbitMQのマニフェストファイル

次に、メッセージブローカーであるRabbitMQを起動するためのマニフェストファイルを作成します。

k8s/rabbitmq.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: rabbitmq
  labels:
    app: rabbitmq
spec:
  replicas: 1
  selector:
    matchLabels:
      app: rabbitmq
  template:
    metadata:
      labels:
        app: rabbitmq
    spec:
      containers:
      - name: rabbitmq
        image: rabbitmq:3-management
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 5672
        - containerPort: 15672

---
kind: Service
apiVersion: v1
metadata:
  name: rabbitmq-svc
  labels:
    app: rabbitmq
spec:
  type: LoadBalancer
  selector:
    app: rabbitmq
  ports:
  - name: queue
    protocol: TCP
    port: 5672
    targetPort: 5672
  - name: management
    protocol: TCP
    port: 15672
    targetPort: 15672

DockerHubにあるイメージを使ってRabbitMQをデプロイするマニフェストファイルです。

spec.template.spec.containers.ports に2つのポートを指定しています。

        - containerPort: 5672
        - containerPort: 15672

これは、キューが5672番ポート、管理コンソールが15672番ポートをそれぞれ利用するためです。

またpodの外部から接続できるよう、Serviceとして rabbitmq-svc を作成し、5672番ポートと15672番ポートでアクセスできるようにしています。

DaprからRabbitMQに接続するためのマニフェストファイル

さらに、DaprからRabbitMQにアクセスするための設定ファイルを作成します。

k8s/rabbitmq-pubsub.yaml

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: rabbitmq-pubsub
spec:
  type: pubsub.rabbitmq
  version: v1
  metadata:
  - name: host
    value: amqp://rabbitmq-svc:5672

これは host の値を除き Dapr Advent Calendar 5日目 - Daprでメッセージング で作成した設定ファイルと全く同じです。その際には --components-path 引数で設定ファイルの場所を指定して利用しました。

しかしk8sで運用する際には、このファイルを kubectl apply でそのまま利用する形となります。初めて動かした時は「なんでDaprで作成したファイルをそのままk8sで使えるの!?」って思ったのですが、Daprは設定ファイルの形式をk8sと合わせることで、そのままk8sで使えるようにしている、という理解で良いのでしょうかね。

metadata.name として rabbitmq-pubsub という名前をつけました。また hostamqp://rabbitmq-svc:5672 を指定しています。podからpodへのアクセスはServiceを利用するのですが、ここでホストのアドレスにServiceの名前を指定することで、k8sDNSを利用して名前解決が行われ、目的のpodにアクセスできるようになります。

subscribeするためのマニフェストファイル

そして、subscribeアプリケーションがRabbitMQからメッセージを受け取るための設定ファイルを作成します。

k8s/subscription.yaml

apiVersion: dapr.io/v1alpha1
kind: Subscription
metadata:
  name: subscription
spec:
  pubsubname: rabbitmq-pubsub
  topic: my-message
  route: /subscribe
scopes:
  - subscribe-app

やはりこれも Dapr Advent Calendar 5日目 - Daprでメッセージング で作成した設定ファイルと全く同じです。Componentファイルだけでなく、Subscriptionファイルもそのままk8sにapplyできるのです。

rabbitmq-pubsubコンポーネントを利用し、メッセージトピックである my-message に対してデキューし、メッセージを取得したら subscribe-app/subscribe を呼び出すという設定となっています。

設定に関してもう少し詳しく知りたい場合は Dapr Advent Calendar 5日目 - Daprでメッセージング を読んでください。

アプリケーションをデプロイしてアクセスする

イメージをk8sにデプロイ

それでは作成したイメージをk8sにデプロイします。

cd (GitHubのディレクトリパス)/dapr-advent-2021
kubectl apply -f k8s/rabbitmq.yaml
kubectl apply -f k8s/rabbitmq-pubsub.yaml
kubectl apply -f k8s/subscription.yaml

RabbitMQをデプロイし、そのRabbitMQにDaprからアクセスするためのコンポーネントをデプロイし、RibbitMQコンポーネントに対するsubscribeの設定をする、という順番です。

次のコマンドでRabbitMQが起動したことを確認します。

kubectl get pods

次のように表示されていればRabbitMQの起動は完了です。

NAME                           READY   STATUS        RESTARTS   AGE
rabbitmq-5d474484f5-krnwj      1/1     Running       0          10s

また、pubsubの設定などが本当に反映されているかも気になるところでしょうから、それらも確認しておきます。

kubectl get components
NAME              AGE
rabbitmq-pubsub   1m

問題なくコンポーネントが作成されていますね。

subscriptionも確認しておきましょう。

kubectl get subscriptions
NAME           AGE
subscription   1m

やはり問題なく反映されています。

ちなみに -o yaml オプションをつければ、それぞれの設定をyaml形式で表示することができます。ここでは出力結果は割愛しますが、適用したyamlファイル(+いくつかのフィールドに初期設定値が適用されたもの)が表示されるはずです。

続いて、アプリケーション2種類をデプロイします。

kubectl apply -f k8s/publish-app.yaml
kubectl apply -f k8s/subscribe-app.yaml

これでデプロイは完了です。

Serviceをポートフォワードしてアクセスできるようにする

それでは、起動したアプリケーションにアクセスしてみましょう。

publishアプリケーションにアクセスするため、次のコマンドを実行します。

kubectl port-forward service/publish-svc 8083:8083

これでローカルPCの8083番ポート経由でminikubeにアクセスできるようになりました。

また、RabbitMQの管理コンソールにアクセスできるよう、別のコンソールで次のコマンドを実行します。

kubectl port-forward service/rabbitmq-svc 15672:15672

これでローカルPCの15672番ポート経由でRabitMQの管理コンソールにアクセスできるになりました。

k8s上のアプリケーションにアクセスする

それではデプロイしたアプリケーションにアクセスしてみましょう。

RabbitMQのコンソールを確認

まずはRabbitMQのコンソールからアクセスします。ブラウザで次のURLを開いてください。

http://localhost:15672/

ログインユーザの初期アカウントは guest / guest です。

ExchangeタブやQueueタブを開けば、my-message というExchangeと、subscribe-app-my-message というQueueができていることが分かります。

f:id:cero-t:20211215020650p:plain:w600
Exchangeの一覧

f:id:cero-t:20211215020707p:plain:w800
Queueの一覧

これらは、Dapr Advent Calendar 5日目 - Daprでメッセージング で説明したとおり、subscribe側のアプリケーションがデプロイされた時点で作られます。

メッセージを送る

それでは別のコンソールからアクセスしてみましょう。次のコマンドでメッセージを送ってみます。

curl -XPOST "localhost:8083/publish" -H "Content-type:application/json" -d '{
  "name": "Shin Tanimoto",
  "twitter": "@cero_t"
}'

特にエラーなく応答が返ってくれば成功です。

受け取ったメッセージの確認

受け取ったメッセージをログで確認してみましょう。

ログを確認するために、podの一覧を取得します。

kubectl get pods
NAME                           READY   STATUS        RESTARTS   AGE
publish-app-5b4c66c9-7gl4j     2/2     Running       0          4m37s
rabbitmq-5d474484f5-krnwj      1/1     Running       0          4m50s
subscribe-app-cdc58446-66m22   2/2     Running       0          4m36s

これでsubscribe-appの名称が分かったため、次のコマンドでログを確認します。

kubectl logs subscribe-app-cdc58446-66m22 subscribe

ログの一番下に、次のようなメッセージが表示されているはずです。

{"id":"e1429fce-9875-45b8-b8d1-5a93d452523f","topic":"my-message","pubsubname":"rabbitmq-pubsub","data":{"name":"Shin Tanimoto","twitter":"@cero_t"},"specversion":"1.0","datacontenttype":"application/json","source":"publish-app","type":"com.dapr.event.sent","traceid":"00-01f98d679d9373efa8206d82d3d2c293-c01c8ed071c12a1a-00"}

data の部分で送ったメッセージをきちんと受け取れていることが分かりましたね!

f:id:cero-t:20211215090713p:plain
ちょっと複雑になってきたk8sで動くアプリケーション

ローカル環境向けに開発していたファイルをそのまま使える

今回は、ローカル環境でDaprを動かす際に作成したpublishとsubscribeのアプリケーションをデプロイして、メッセージングを行いました。

前回、前々回のエントリーでもそうでしたが、今回もソースコードは一文字も変更していません。しかも、ComponentやSubscriptionの設定ファイルも、ホスト名部分のみ変えたものの、他は一切変更していません。

このように、動かす環境が変わってもソースコードを変更する必要がなく、またk8sでデプロイするためのマニフェストファイルを作成する必要があるものの、既存の設定ファイルをほぼそのまま使えるというのが、Daprの大きなメリットの1つです。

Daprというランタイム上で動くように開発できれば、Dapr自身がクラウド上で動いていようが、ローカル環境で動いていようが構わないのです。

まとめ

  • ローカル環境向けに作ったメッセージングのアプリケーションを、そのままk8s上で動かすことができました
  • ソースコードは全く変更しませんでした
  • ComponentやServiceの設定ファイルも、ほぼそのままマニフェストファイルとして流用できました

インフラにあまり詳しくないエンジニアがアプリケーションを開発し、インフラに詳しいエンジニアがそのアプリケーションをクラウドk8s上で運用する、なんて体制にすることもできるでしょう。実際、僕はそれに近い体制で運用をしていますし、それで大きくハマることもありませんでした。なかなか魅力的、ですよね。

それでは、また明日!