コンテナ上のマイクロサービスの認証強化 ~StrimziとKeycloak~
第七回は、前回に引き続き、内部向けのAPIであるマイクロサービスの認証を強化する最先端の機能を紹介します。
前回と同様、すべてのサービスをKubernetes(Minikube)上にデプロイする構成とし、堅牢化の対象としてIstioが提供しているBookinfoアプリケーションを取り上げます(図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)。
前回までは、暗黙のルールとして、各サービス間のデータのやり取りをREST APIで実現していました。確かにマイクロサービスにおいても、各サービス間のデータのやり取りにはREST APIの使用が主流です。しかし課題もあります(図3)。それはパフォーマンスです。一般的にREST APIによるデータのやり取りは、同期でのやり取りとなるため、それが複数のサービスに伝播すればするほど、パフォーマンスに影響が出てくる可能性があります。
上記の課題に対し、マイクロサービスでは、Kafkaに代表される非同期APIというものが脚光を浴びています。非同期APIを用いることで、たとえやり取りが複数のサービスに伝播したとしても、パフォーマンスへの影響を小さく抑えることができます(図4)。
今回は、例としてDetailsサービスのv2をベースに非同期APIによるデータのやり取りを組み込んだDetailsサービスのv3をQuarkusで作成し、そのQuarkusアプリケーションを例にsmallrye-reactive-messaging-kafkaエクステンションとKeycloakを使ったクライアント認証方法を説明します。
StrimziのインストールとKafkaクラスタの作成
まずは、Strimziをインストールしましょう。
$ wget https://github.com/strimzi/strimzi-kafka-operator/releases/download/0.20.0/strimzi-0.20.0.zip $ apt install unzip $ unzip strimzi-0.20.0.zip $ cd strimzi-0.20.0/ $ kubectl create namespace kafka $ sed -i 's/namespace: .*/namespace: kafka/' install/cluster-operator/*RoleBinding*.yaml $ kubectl create namespace my-kafka-project
一部(install/cluster-operator/060-Deployment-strimzi-cluster-operator.yaml)設定を書き換えます。
env: - name: STRIMZI_NAMESPACE value: my-kafka-project // この行を追加 valueFrom: // 以下3行を削除 fieldRef: fieldPath: metadata.namespace // ここまで削除
Kubernetesにデプロイします。
$ kubectl apply -f install/cluster-operator/ -n kafka $ kubectl apply -f install/cluster-operator/020-RoleBinding-strimzi-cluster-operator.yaml -n my-kafka-project $ kubectl apply -f install/cluster-operator/032-RoleBinding-strimzi-cluster-operator-topic-operator-delegation.yaml -n my-kafka-project $ kubectl apply -f install/cluster-operator/031-RoleBinding-strimzi-cluster-operator-entity-operator-delegation.yaml -n my-kafka-project
次に、Kafkaクラスタを作成します。以下のようなCustomResourceDefinition(kafka.yaml)を作成します。
apiVersion: kafka.strimzi.io/v1beta1 kind: Kafka metadata: name: my-cluster spec: kafka: replicas: 1 listeners: plain: port: 9092 type: internal tls: false tls: port: 9093 type: internal tls: true authentication: type: tls external: port: 9094 type: nodeport tls: false storage: type: jbod volumes: - id: 0 type: persistent-claim size: 100Mi deleteClaim: false config: offsets.topic.replication.factor: 1 transaction.state.log.replication.factor: 1 transaction.state.log.min.isr: 1 zookeeper: replicas: 1 storage: type: persistent-claim size: 100Mi deleteClaim: false entityOperator: topicOperator: {} userOperator: {}
作成したCustomResourceDefinitionを適用します。
$ kubectl create -n my-kafka-project -f kafka.yaml
以上で、Kafkaクラスタが作成されました。作成したKafkaクラスタのIPアドレスとポートを確認します。
$ echo $(minikube ip) 172.30.10.93 $ echo $(kubectl get services/my-cluster-kafka-external-bootstrap -o go-template='{{(index .spec.ports 0).nodePort}}' -n my-kafka-project) 31105
Quarkusアプリケーションの実装
前回実装したDetailsサービスのv2をベースに、Detailsサービスのv3を実装していきます(図5)。/details/0に対するGETリクエストに対して、本の詳細情報をレスポンスする部分はv2と共通ですが、/details/0に対するGETリクエストが走るたびに、メッセージをPublishするProducer部分と、そのメッセージをSubscribeするConsumer部分をv3で新しく実装します。一般的には、Producer部分とConsumer部分は別々のサービスに実装することが多いですが、ここでは簡単のため、両者をDetailsサービスv3に実装します。
まずは、既存のプロジェクトにsmallrye-reactive-messaging-kafkaのエクステンションを追加します。
$ cd security-keycloak-authorization-quickstart/ $ ./mvnw quarkus:add-extension -Dextensions="smallrye-reactive-messaging-kafka"
Producer部分を、security-keycloak-authorization-quickstart/src/main/java/org/acme/security/keycloak/authorization/BooksResource.javaに追加で実装します。
… import org.eclipse.microprofile.reactive.messaging.Channel; import org.eclipse.microprofile.reactive.messaging.Emitter; … import io.smallrye.reactive.messaging.annotations.Broadcast; @Path("/") public class BooksResource { … @Inject @Broadcast @Channel("my-channel-out") Emitter<String> myData; … @GET @Path("/details/0") @NoCache public Response details() { myData.send(identity.getPrincipal().getName()); … } }
Consumer部分を、security-keycloak-authorization-quickstart/src/main/java/org/acme/security/keycloak/authorization/SampleConsumer.javaに実装します。
package org.acme.security.keycloak.authorization; import javax.enterprise.context.ApplicationScoped; import org.eclipse.microprofile.reactive.messaging.Incoming; @ApplicationScoped public class SampleConsumer { @Incoming("my-channel-in") public void consume(String myData) { System.out.println("Hello " + myData + " !"); } }
application.properties(security-keycloak-authorization-quickstart/src/main/ resources/application.properties)に設定を加えます。
… quarkus.kubernetes.labels.version=v3 … # Configure the SmallRye Kafka connector kafka.bootstrap.servers=172.30.10.93:31105 # Configure the Kafka sink (we write to it) mp.messaging.outgoing.my-channel-out.connector=smallrye-kafka mp.messaging.outgoing.my-channel-out.topic=book-info mp.messaging.outgoing.my-channel-out.value.serializer=org.apache.kafka.common.serialization.StringSerializer # Configure the Kafka source (we read from it) mp.messaging.incoming.my-channel-in.connector=smallrye-kafka mp.messaging.incoming.my-channel-in.topic=book-info mp.messaging.incoming.my-channel-in.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
作成したQuarkusアプリケーションをビルドし、Kubernetesにデプロイします。
$ ./mvnw clean package -Dquarkus.kubernetes.deploy=true
DestinationRule(destination-rule-all-mtls-v2.yaml)を書き換えて、Detailsサービスのv3にルーティングするようにします。
… --- apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: details spec: host: details trafficPolicy: tls: mode: ISTIO_MUTUAL subsets: - name: v3 labels: version: v3 ---
変更したDestinationRuleを適用します。
$ kubectl apply -f destination-rule-all-mtls-v2.yaml
以上で、Producer部分とConsumer部分の実装は完了です。admin_userを用いて発行したアクセストークンを付与してBookinfoアプリケーションのProductページにアクセスすると、Productページが問題なく表示されることを確認できます(図6)。
また、ProducerとConsumerによって問題なくメッセージが送受信されたことも確認できます。
$ kubectl logs details-xxx -c details … Hello admin_user !
クライアント認証の設定
Kafkaでは、OAuth 2.0のClient Credentials Grantやトークンイントロスペクションをベースとしたクライアント認証を実現することができます(図7)。
Kafkaクライアント(ProducerやConsumer)は、OAuth 2.0 Client Credentials Grantで発行されたアクセストークンをKafkaクラスタに送ります。Kafkaクラスタは、受け取ったアクセストークンをトークンイントロスペクションで検証し、アクセストークンが有効であれば、Kafkaクライアントとの間にKafkaクライアントセッションを張ります。以降、そのKafkaクライアントセッションを用いて、メッセージをやり取り(Publish/Subscribe)します。実際に設定していきます。まずはClient Credentials Grantでトークンを発行したり、トークンイントロスペクションしたりできるように、Keycloakのリソースを作成します(表 2)。
リソース | 設定項目名 | 設定値 | 備考 |
---|---|---|---|
クライアント | 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)を変更します。
… spec: kafka: … listeners: … external: port: 9094 type: nodeport tls: false authentication: type: oauth clientId: kafka-broker clientSecret: secretName: my-cluster-oauth key: clientSecret validIssuerUri: http://172.30.10.93:31385/auth/realms/bookinfo introspectionEndpointUri: http://172.30.10.93:31385/auth/realms/bookinfo/protocol/openid-connect/token/introspect userNameClaim: preferred_username …
また、Kafkaクラスタ用クライアントのクライアントシークレットを格納するために、Secret(kafka-broker-client-secret.yaml)を作成します。
apiVersion: v1 kind: Secret metadata: name: my-cluster-oauth type: Opaque data: clientSecret: <Kafkaクラスタ用クライアントのクライアントシークレットをBase64エンコードしたもの>
作成したSecretと変更したKafkaクラスタの設定を適用します。
$ kubectl apply -n my-kafka-project -f kafka-broker-client-secret.yaml $ kubectl apply -n my-kafka-project -f kafka.yaml
以上で、Kafkaクラスタ側のクライアント認証設定は完了です。この状態で、admin_userを用いて発行したアクセストークンを付与してBookinfoアプリケーションのProductページにアクセスしてみましょう。Productページは問題なく表示されますが、Consumerからのメッセージは出力されず、接続エラーが出力されることを確認できます。
$ kubectl logs details-xxx -c details … 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を追加します。
… <!-- https://mvnrepository.com/artifact/io.strimzi/kafka-oauth-client --> <dependency> <groupId>io.strimzi</groupId> <artifactId>kafka-oauth-client</artifactId> <version>0.6.1</version> </dependency> …
次に、application.properties(security-keycloak-authorization-quickstart/src/main/resources/application.properties)に設定を加えます。
# Configure the Kafka sink (we write to it) mp.messaging.outgoing.my-channel-out.connector=smallrye-kafka mp.messaging.outgoing.my-channel-out.topic=book-info mp.messaging.outgoing.my-channel-out.value.serializer=org.apache.kafka.common.serialization.StringSerializer mp.messaging.outgoing.my-channel-out.security.protocol=SASL_PLAINTEXT mp.messaging.outgoing.my-channel-out.sasl.mechanism=OAUTHBEARER mp.messaging.outgoing.my-channel-out.sasl.login.callback.handler.class=io.strimzi.kafka.oauth.client.JaasClientOauthLoginCallbackHandler mp.messaging.outgoing.my-channel-out.sasl.jaas.config=org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required \ oauth.client.id="kafka-producer" \ oauth.client.secret="<Producer用クライアントのクライアントシークレット>" \ oauth.token.endpoint.uri="http://172.30.10.93:31385/auth/realms/bookinfo/protocol/openid-connect/token"; # Configure the Kafka source (we read from it) mp.messaging.incoming.my-channel-in.connector=smallrye-kafka mp.messaging.incoming.my-channel-in.topic=book-info mp.messaging.incoming.my-channel-in.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer mp.messaging.incoming.my-channel-in.security.protocol=SASL_PLAINTEXT mp.messaging.incoming.my-channel-in.sasl.mechanism=OAUTHBEARER mp.messaging.incoming.my-channel-in.sasl.login.callback.handler.class=io.strimzi.kafka.oauth.client.JaasClientOauthLoginCallbackHandler mp.messaging.incoming.my-channel-in.sasl.jaas.config=org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required \ oauth.client.id="kafka-consumer" \ oauth.client.secret="<Consumer用クライアントのクライアントシークレット>" \ oauth.token.endpoint.uri="http://172.30.10.93:31385/auth/realms/bookinfo/protocol/openid-connect/token";
作成したQuarkusアプリケーションをビルドし、Kubernetesにデプロイします。
$ cd security-keycloak-authorization-quickstart/ $ ./mvnw clean package -Dquarkus.kubernetes.deploy=true
以上で、ProducerとConsumerのクライアント認証の設定は完了です。admin_userを用いて発行したアクセストークンを付与してBookinfoアプリケーションのProductページにアクセスしてみましょう。Productページは問題なく表示され、Consumerからメッセージが出力されることを確認できます。
$ kubectl logs details-xxx -c details … Hello admin_user !
おわりに
本連載では、Keycloakで実現するAPIセキュリティと題し、Keycloakのインストール方法からハードニングの実装方法、内部向けのAPIであるマイクロサービスの認証を強化する最先端の機能をご紹介しました。Keycloakでは、2020年12月現在、FAPIに対する対応を行うFAPI-SIGや、次のKeycloakを考えるKeycloak.Xという活動が活発に推進されています。着実にパワーアップしているKeycloakを、この機会に使ってみてはいかがでしょうか。
連載バックナンバー
Think ITメルマガ会員登録受付中
全文検索エンジンによるおすすめ記事
- コンテナ上のマイクロサービスの認証強化 ~IstioとKeycloak~
- コンテナ上のマイクロサービスの認証強化 ~QuarkusとKeycloak~
- Kafka on KubernetesのStrimziConから新機能を解説するセッションを紹介
- クラウドネイティブな環境でKeycloakによるシングルサインオンを実現
- Kafka+Spark Streaming+Elasticserachによるシステム構築と検証の進め方
- Keycloakを用いたハードニングの実装方法
- BookinfoデモでIstioを体感する
- Keycloakのインストールと構築例
- Kafka on Kubernetesを実現するStrimziに特化したカンファレンスStrimziCon 2024からキーノートを紹介
- FAPIとKeycloakの概要