第七回は、前回に引き続き、内部向けのAPIであるマイクロサービスの認証を強化する最先端の機能を紹介します。
前回と同様、すべてのサービスをKubernetes(Minikube)上にデプロイする構成とし、堅牢化の対象としてIstioが提供しているBookinfoアプリケーションを取り上げます(図1)。
図1:システム構成
使用する各製品のバージョンを表1に示します。
表1:使用する製品とバージョン
製品 | バージョン |
Minikube | v1.13.1 |
(Kubernetes) | v1.19.2 |
(Docker) | 19.03.8 |
Keycloak | 11.0.3 |
Istio | 1.7.4 |
Quarkus | 1.9.2.Final |
Strimzi | 0.20.0 |
なお、Kubernetes環境(Minikube)はドキュメント等を参考にすでにインストールされ、動作しているものとします。また、前回と前々回の記事でご紹介した、Keycloak、Istio、Quarkusの導入/各種設定はなされているものとします。
StrimziとKeycloak
Strimziとは、Kubernetes上でApache Kafkaのクラスタを管理するオペレータです。Strimziを使用することで、Kubernetes上でのKafkaクラスタの構築/運用を簡素化できます。ここではStrimzi(Kafka)とKeycloakを組み合わせた堅牢化方法をご紹介します(図2)。
図2:StrimziとKeycloakを用いたBookinfoアプリケーションの堅牢化
前回までは、暗黙のルールとして、各サービス間のデータのやり取りをREST APIで実現していました。確かにマイクロサービスにおいても、各サービス間のデータのやり取りにはREST APIの使用が主流です。しかし課題もあります(図3)。それはパフォーマンスです。一般的にREST APIによるデータのやり取りは、同期でのやり取りとなるため、それが複数のサービスに伝播すればするほど、パフォーマンスに影響が出てくる可能性があります。
図3:REST APIの課題
上記の課題に対し、マイクロサービスでは、Kafkaに代表される非同期APIというものが脚光を浴びています。非同期APIを用いることで、たとえやり取りが複数のサービスに伝播したとしても、パフォーマンスへの影響を小さく抑えることができます(図4)。
図4:非同期API
今回は、例としてDetailsサービスのv2をベースに非同期APIによるデータのやり取りを組み込んだDetailsサービスのv3をQuarkusで作成し、そのQuarkusアプリケーションを例にsmallrye-reactive-messaging-kafkaエクステンションとKeycloakを使ったクライアント認証方法を説明します。
StrimziのインストールとKafkaクラスタの作成
まずは、Strimziをインストールしましょう。
リスト1:Strimziをインストール
3 | $ unzip strimzi-0.20.0.zip |
5 | $ kubectl create namespace kafka |
6 | $ sed -i 's/namespace: .*/namespace: kafka/' install/cluster-operator/*RoleBinding*.yaml |
7 | $ kubectl create namespace my-kafka-project |
一部(install/cluster-operator/060-Deployment-strimzi-cluster-operator.yaml)設定を書き換えます。
リスト2:設定を書き換え
2 | - name: STRIMZI_NAMESPACE |
3 | value: my-kafka-project // この行を追加 |
6 | fieldPath: metadata.namespace // ここまで削除 |
Kubernetesにデプロイします。
リスト3:Kubernetesにデプロイ
1 | $ kubectl apply -f install/cluster-operator/ -n kafka |
2 | $ kubectl apply -f install/cluster-operator/020-RoleBinding-strimzi-cluster-operator.yaml -n my-kafka-project |
3 | $ kubectl apply -f install/cluster-operator/032-RoleBinding-strimzi-cluster-operator-topic-operator-delegation.yaml -n my-kafka-project |
4 | $ kubectl apply -f install/cluster-operator/031-RoleBinding-strimzi-cluster-operator-entity-operator-delegation.yaml -n my-kafka-project |
次に、Kafkaクラスタを作成します。以下のようなCustomResourceDefinition(kafka.yaml)を作成します。
リスト4:Kafkaクラスタの作成
01 | apiVersion: kafka.strimzi.io/v1beta1 |
27 | type: persistent-claim |
31 | offsets.topic.replication.factor: 1 |
32 | transaction.state.log.replication.factor: 1 |
33 | transaction.state.log.min.isr: 1 |
37 | type: persistent-claim |
作成したCustomResourceDefinitionを適用します。
リスト5:CustomResourceDefinitionの適用
1 | $ kubectl create -n my-kafka-project -f kafka.yaml |
以上で、Kafkaクラスタが作成されました。作成したKafkaクラスタのIPアドレスとポートを確認します。
リスト6:KafakaクラスタのIPアドレスとポートを確認する
3 | $ echo $(kubectl get services/my-cluster-kafka-external-bootstrap -o go-template='{{(index .spec.ports 0).nodePort}}' -n my-kafka-project) |
Quarkusアプリケーションの実装
前回実装したDetailsサービスのv2をベースに、Detailsサービスのv3を実装していきます(図5)。/details/0に対するGETリクエストに対して、本の詳細情報をレスポンスする部分はv2と共通ですが、/details/0に対するGETリクエストが走るたびに、メッセージをPublishするProducer部分と、そのメッセージをSubscribeするConsumer部分をv3で新しく実装します。一般的には、Producer部分とConsumer部分は別々のサービスに実装することが多いですが、ここでは簡単のため、両者をDetailsサービスv3に実装します。
図5:Detailsサービスv3のシーケンス
まずは、既存のプロジェクトにsmallrye-reactive-messaging-kafkaのエクステンションを追加します。
リスト7:smallrye-reactive-messaging-kafakのエクステンションを追加
1 | $ cd security-keycloak-authorization-quickstart/ |
2 | $ ./mvnw quarkus:add-extension -Dextensions="smallrye-reactive-messaging-kafka" |
Producer部分を、security-keycloak-authorization-quickstart/src/main/java/org/acme/security/keycloak/authorization/BooksResource.javaに追加で実装します。
リスト8:Producer部分をBooksResource.javaに追加で実装
02 | import org.eclipse.microprofile.reactive.messaging.Channel; |
03 | import org.eclipse.microprofile.reactive.messaging.Emitter; |
05 | import io.smallrye.reactive.messaging.annotations.Broadcast; |
08 | public class BooksResource { |
12 | @Channel("my-channel-out") |
13 | Emitter<String> myData; |
18 | public Response details() { |
19 | myData.send(identity.getPrincipal().getName()); |
Consumer部分を、security-keycloak-authorization-quickstart/src/main/java/org/acme/security/keycloak/authorization/SampleConsumer.javaに実装します。
リスト9:Consumer部分をSampleConsumer.javaに実装
01 | package org.acme.security.keycloak.authorization; |
03 | import javax.enterprise.context.ApplicationScoped; |
05 | import org.eclipse.microprofile.reactive.messaging.Incoming; |
08 | public class SampleConsumer { |
09 | @Incoming("my-channel-in") |
10 | public void consume(String myData) { |
11 | System.out.println("Hello " + myData + " !"); |
application.properties(security-keycloak-authorization-quickstart/src/main/ resources/application.properties)に設定を加えます。
リスト10:application.propertiesに設定追加
02 | quarkus.kubernetes.labels.version=v3 |
04 | # Configure the SmallRye Kafka connector |
05 | kafka.bootstrap.servers=172.30.10.93:31105 |
07 | # Configure the Kafka sink (we write to it) |
08 | mp.messaging.outgoing.my-channel-out.connector=smallrye-kafka |
09 | mp.messaging.outgoing.my-channel-out.topic=book-info |
10 | mp.messaging.outgoing.my-channel-out.value.serializer=org.apache.kafka.common.serialization.StringSerializer |
12 | # Configure the Kafka source (we read from it) |
13 | mp.messaging.incoming.my-channel-in.connector=smallrye-kafka |
14 | mp.messaging.incoming.my-channel-in.topic=book-info |
15 | mp.messaging.incoming.my-channel-in.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer |
作成したQuarkusアプリケーションをビルドし、Kubernetesにデプロイします。
リスト11:Quarkusアプリケーションのビルド、デプロイ
1 | $ ./mvnw clean package -Dquarkus.kubernetes.deploy=true |
DestinationRule(destination-rule-all-mtls-v2.yaml)を書き換えて、Detailsサービスのv3にルーティングするようにします。
リスト12:DestinationRuleの書き換え
03 | apiVersion: networking.istio.io/v1alpha3 |
変更したDestinationRuleを適用します。
リスト13:変更したDestinationRuleの適用
1 | $ kubectl apply -f destination-rule-all-mtls-v2.yaml |
以上で、Producer部分とConsumer部分の実装は完了です。admin_userを用いて発行したアクセストークンを付与してBookinfoアプリケーションのProductページにアクセスすると、Productページが問題なく表示されることを確認できます(図6)。
図6:BookinfoアプリケーションのProductページ
また、ProducerとConsumerによって問題なくメッセージが送受信されたことも確認できます。
リスト14:メッセージ送受信されていることを確認
1 | $ kubectl logs details-xxx -c details |
クライアント認証の設定
Kafkaでは、OAuth 2.0のClient Credentials Grantやトークンイントロスペクションをベースとしたクライアント認証を実現することができます(図7)。
図7:Kafkaのクライアント認証の流れ
Kafkaクライアント(ProducerやConsumer)は、OAuth 2.0 Client Credentials Grantで発行されたアクセストークンをKafkaクラスタに送ります。Kafkaクラスタは、受け取ったアクセストークンをトークンイントロスペクションで検証し、アクセストークンが有効であれば、Kafkaクライアントとの間にKafkaクライアントセッションを張ります。以降、そのKafkaクライアントセッションを用いて、メッセージをやり取り(Publish/Subscribe)します。実際に設定していきます。まずはClient Credentials Grantでトークンを発行したり、トークンイントロスペクションしたりできるように、Keycloakのリソースを作成します(表 2)。
表2:作成するKeycloakのリソース
リソース | 設定項目名 | 設定値 | 備考 |
クライアント | Client ID | kafka-broker | Kafkaクラスタ用クライアント。 |
| Access Type | bearer-only | ― |
クライアント | Client ID | kafka-producer | Producer用クライアント。 |
| Access Type | confidential | ― |
| Standard Flow Enabled | OFF | ― |
| Direct Access Grants Enabled | OFF | ― |
| Service Accounts Enabled | ON | ― |
クライアント | Client ID | kafka-consumer | Consumer用クライアント。 |
| Access Type | confidential | ― |
| Standard Flow Enabled | OFF | ― |
| Direct Access Grants Enabled | OFF | ― |
| Service Accounts Enabled | ON | ― |
次に、Kafkaクラスタの設定(kafka.yaml)を変更します。
リスト15:Kafkaクラスタの設定を変更
13 | clientId: kafka-broker |
15 | secretName: my-cluster-oauth |
19 | userNameClaim: preferred_username |
また、Kafkaクラスタ用クライアントのクライアントシークレットを格納するために、Secret(kafka-broker-client-secret.yaml)を作成します。
リスト16:Secretを作成
7 | clientSecret: <Kafkaクラスタ用クライアントのクライアントシークレットをBase64エンコードしたもの> |
作成したSecretと変更したKafkaクラスタの設定を適用します。
リスト17:Secret、Kafkaクラスタの設定を適用
1 | $ kubectl apply -n my-kafka-project -f kafka-broker-client-secret.yaml |
2 | $ kubectl apply -n my-kafka-project -f kafka.yaml |
以上で、Kafkaクラスタ側のクライアント認証設定は完了です。この状態で、admin_userを用いて発行したアクセストークンを付与してBookinfoアプリケーションのProductページにアクセスしてみましょう。Productページは問題なく表示されますが、Consumerからのメッセージは出力されず、接続エラーが出力されることを確認できます。
リスト18:動作の確認
1 | $ kubectl logs details-xxx -c details |
3 | 2020-12-10 08:36:45,003 INFO [org.apa.kaf.cli.FetchSessionHandler] (kafka-coordinator-heartbeat-thread | dfd2724b-17ff-4dbe-948b-30768b4fe02a) [Consumer clientId=consumer-dfd2724b-17ff-4dbe-948b-30768b4fe02a-1, groupId=dfd2724b-17ff-4dbe-948b-30768b4fe02a] Error sending fetch request (sessionId=284737104, epoch=INITIAL) to node 0: {}.: org.apache.kafka.common.errors.DisconnectException |
Detailsサービスに、ProducerとConsumerのクライアント認証の設定を加えていきます。まずは、pom.xml(security-keycloak-authorization-quickstart/pom.xml)の依存関係に、kafka-oauth-clientを追加します。
リスト19:pom.xmlの依存関係にkafka-oauth-clientを追加
4 | <groupId>io.strimzi</groupId> |
5 | <artifactId>kafka-oauth-client</artifactId> |
6 | <version>0.6.1</version> |
次に、application.properties(security-keycloak-authorization-quickstart/src/main/resources/application.properties)に設定を加えます。
リスト20:applicationpropertiesに設定を追加
01 | # Configure the Kafka sink (we write to it) |
02 | mp.messaging.outgoing.my-channel-out.connector=smallrye-kafka |
03 | mp.messaging.outgoing.my-channel-out.topic=book-info |
04 | mp.messaging.outgoing.my-channel-out.value.serializer=org.apache.kafka.common.serialization.StringSerializer |
05 | mp.messaging.outgoing.my-channel-out.security.protocol=SASL_PLAINTEXT |
06 | mp.messaging.outgoing.my-channel-out.sasl.mechanism=OAUTHBEARER |
07 | mp.messaging.outgoing.my-channel-out.sasl.login.callback.handler.class=io.strimzi.kafka.oauth.client.JaasClientOauthLoginCallbackHandler |
08 | mp.messaging.outgoing.my-channel-out.sasl.jaas.config=org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required \ |
09 | oauth.client.id="kafka-producer" \ |
10 | oauth.client.secret="<Producer用クライアントのクライアントシークレット>" \ |
13 | # Configure the Kafka source (we read from it) |
14 | mp.messaging.incoming.my-channel-in.connector=smallrye-kafka |
15 | mp.messaging.incoming.my-channel-in.topic=book-info |
16 | mp.messaging.incoming.my-channel-in.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer |
17 | mp.messaging.incoming.my-channel-in.security.protocol=SASL_PLAINTEXT |
18 | mp.messaging.incoming.my-channel-in.sasl.mechanism=OAUTHBEARER |
19 | mp.messaging.incoming.my-channel-in.sasl.login.callback.handler.class=io.strimzi.kafka.oauth.client.JaasClientOauthLoginCallbackHandler |
20 | mp.messaging.incoming.my-channel-in.sasl.jaas.config=org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required \ |
21 | oauth.client.id="kafka-consumer" \ |
22 | oauth.client.secret="<Consumer用クライアントのクライアントシークレット>" \ |
作成したQuarkusアプリケーションをビルドし、Kubernetesにデプロイします。
リスト21:Quarkusアプリケーションのビルド、デプロイ
1 | $ cd security-keycloak-authorization-quickstart/ |
2 | $ ./mvnw clean package -Dquarkus.kubernetes.deploy=true |
以上で、ProducerとConsumerのクライアント認証の設定は完了です。admin_userを用いて発行したアクセストークンを付与してBookinfoアプリケーションのProductページにアクセスしてみましょう。Productページは問題なく表示され、Consumerからメッセージが出力されることを確認できます。
リスト22:動作を確認
1 | $ kubectl logs details-xxx -c details |
おわりに
本連載では、Keycloakで実現するAPIセキュリティと題し、Keycloakのインストール方法からハードニングの実装方法、内部向けのAPIであるマイクロサービスの認証を強化する最先端の機能をご紹介しました。Keycloakでは、2020年12月現在、FAPIに対する対応を行うFAPI-SIGや、次のKeycloakを考えるKeycloak.Xという活動が活発に推進されています。着実にパワーアップしているKeycloakを、この機会に使ってみてはいかがでしょうか。