コンテナ上のマイクロサービスの認証強化 ~StrimziとKeycloak~

2021年2月16日(火)
田畑 義之
前回に引き続き、マイクロサービスの認証強化を実現する最先端の機能を紹介します。

第七回は、前回に引き続き、内部向けのAPIであるマイクロサービスの認証を強化する最先端の機能を紹介します。

前回と同様、すべてのサービスをKubernetes(Minikube)上にデプロイする構成とし、堅牢化の対象としてIstioが提供しているBookinfoアプリケーションを取り上げます(図1)。

図1:システム構成

図1:システム構成

使用する各製品のバージョンを表1に示します。

表1:使用する製品とバージョン

製品バージョン
Minikubev1.13.1
  (Kubernetes)v1.19.2
  (Docker)19.03.8
Keycloak11.0.3
Istio1.7.4
Quarkus1.9.2.Final
Strimzi0.20.0

なお、Kubernetes環境(Minikube)はドキュメント等を参考にすでにインストールされ、動作しているものとします。また、前回と前々回の記事でご紹介した、Keycloak、Istio、Quarkusの導入/各種設定はなされているものとします。

StrimziとKeycloak

Strimziとは、Kubernetes上でApache Kafkaのクラスタを管理するオペレータです。Strimziを使用することで、Kubernetes上でのKafkaクラスタの構築/運用を簡素化できます。ここではStrimzi(Kafka)とKeycloakを組み合わせた堅牢化方法をご紹介します(図2)。

図2:StrimziとKeycloakを用いたBookinfoアプリケーションの堅牢化

図2:StrimziとKeycloakを用いたBookinfoアプリケーションの堅牢化

前回までは、暗黙のルールとして、各サービス間のデータのやり取りをREST APIで実現していました。確かにマイクロサービスにおいても、各サービス間のデータのやり取りにはREST APIの使用が主流です。しかし課題もあります(図3)。それはパフォーマンスです。一般的にREST APIによるデータのやり取りは、同期でのやり取りとなるため、それが複数のサービスに伝播すればするほど、パフォーマンスに影響が出てくる可能性があります。

図3:REST APIの課題

図3:REST APIの課題

上記の課題に対し、マイクロサービスでは、Kafkaに代表される非同期APIというものが脚光を浴びています。非同期APIを用いることで、たとえやり取りが複数のサービスに伝播したとしても、パフォーマンスへの影響を小さく抑えることができます(図4)。

図4:非同期API

図4:非同期API

今回は、例としてDetailsサービスのv2をベースに非同期APIによるデータのやり取りを組み込んだDetailsサービスのv3をQuarkusで作成し、そのQuarkusアプリケーションを例にsmallrye-reactive-messaging-kafkaエクステンションとKeycloakを使ったクライアント認証方法を説明します。

StrimziのインストールとKafkaクラスタの作成

まずは、Strimziをインストールしましょう。

リスト1:Strimziをインストール

2$ apt install unzip
3$ unzip strimzi-0.20.0.zip
4$ cd strimzi-0.20.0/
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:設定を書き換え

1env:
2  - name: STRIMZI_NAMESPACE
3    value: my-kafka-project           // この行を追加
4    valueFrom:                        // 以下3行を削除
5      fieldRef:
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クラスタの作成

01apiVersion: kafka.strimzi.io/v1beta1
02kind: Kafka
03metadata:
04  name: my-cluster
05spec:
06  kafka:
07    replicas: 1
08    listeners:
09      plain:
10        port: 9092
11        type: internal
12        tls: false
13      tls:
14        port: 9093
15        type: internal
16        tls: true
17        authentication:
18          type: tls
19      external:
20        port: 9094
21        type: nodeport
22        tls: false
23    storage:
24      type: jbod
25      volumes:
26      - id: 0
27        type: persistent-claim
28        size: 100Mi
29        deleteClaim: false
30    config:
31      offsets.topic.replication.factor: 1
32      transaction.state.log.replication.factor: 1
33      transaction.state.log.min.isr: 1
34  zookeeper:
35    replicas: 1
36    storage:
37      type: persistent-claim
38      size: 100Mi
39      deleteClaim: false
40  entityOperator:
41    topicOperator: {}
42    userOperator: {}

作成したCustomResourceDefinitionを適用します。

リスト5:CustomResourceDefinitionの適用

1$ kubectl create -n my-kafka-project -f kafka.yaml

以上で、Kafkaクラスタが作成されました。作成したKafkaクラスタのIPアドレスとポートを確認します。

リスト6:KafakaクラスタのIPアドレスとポートを確認する

1$ echo $(minikube ip)
2172.30.10.93
3$ echo $(kubectl get services/my-cluster-kafka-external-bootstrap -o go-template='{{(index .spec.ports 0).nodePort}}' -n my-kafka-project)
431105

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のシーケンス

図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に追加で実装

01
02import org.eclipse.microprofile.reactive.messaging.Channel;
03import org.eclipse.microprofile.reactive.messaging.Emitter;
04
05import io.smallrye.reactive.messaging.annotations.Broadcast;
06 
07@Path("/")
08public class BooksResource {
09
10    @Inject
11    @Broadcast
12    @Channel("my-channel-out")
13    Emitter<String> myData;
14
15    @GET
16    @Path("/details/0")
17    @NoCache
18    public Response details() {
19        myData.send(identity.getPrincipal().getName());
20        
21    }
22 
23}

Consumer部分を、security-keycloak-authorization-quickstart/src/main/java/org/acme/security/keycloak/authorization/SampleConsumer.javaに実装します。

リスト9:Consumer部分をSampleConsumer.javaに実装

01package org.acme.security.keycloak.authorization;
02 
03import javax.enterprise.context.ApplicationScoped;
04 
05import org.eclipse.microprofile.reactive.messaging.Incoming;
06 
07@ApplicationScoped
08public class SampleConsumer {
09    @Incoming("my-channel-in")
10    public void consume(String myData) {
11        System.out.println("Hello " + myData + " !");
12    }
13}

application.properties(security-keycloak-authorization-quickstart/src/main/ resources/application.properties)に設定を加えます。

リスト10:application.propertiesに設定追加

01
02quarkus.kubernetes.labels.version=v3
03
04# Configure the SmallRye Kafka connector
05kafka.bootstrap.servers=172.30.10.93:31105
06 
07# Configure the Kafka sink (we write to it)
08mp.messaging.outgoing.my-channel-out.connector=smallrye-kafka
09mp.messaging.outgoing.my-channel-out.topic=book-info
10mp.messaging.outgoing.my-channel-out.value.serializer=org.apache.kafka.common.serialization.StringSerializer
11 
12# Configure the Kafka source (we read from it)
13mp.messaging.incoming.my-channel-in.connector=smallrye-kafka
14mp.messaging.incoming.my-channel-in.topic=book-info
15mp.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の書き換え

01
02---
03apiVersion: networking.istio.io/v1alpha3
04kind: DestinationRule
05metadata:
06  name: details
07spec:
08  host: details
09  trafficPolicy:
10    tls:
11      mode: ISTIO_MUTUAL
12  subsets:
13  - name: v3
14    labels:
15      version: v3
16---

変更したDestinationRuleを適用します。

リスト13:変更したDestinationRuleの適用

1$ kubectl apply -f destination-rule-all-mtls-v2.yaml

以上で、Producer部分とConsumer部分の実装は完了です。admin_userを用いて発行したアクセストークンを付与してBookinfoアプリケーションのProductページにアクセスすると、Productページが問題なく表示されることを確認できます(図6)。

図6:BookinfoアプリケーションのProductページ

図6:BookinfoアプリケーションのProductページ

また、ProducerとConsumerによって問題なくメッセージが送受信されたことも確認できます。

リスト14:メッセージ送受信されていることを確認

1$ kubectl logs details-xxx -c details
2
3Hello admin_user !

クライアント認証の設定

Kafkaでは、OAuth 2.0のClient Credentials Grantやトークンイントロスペクションをベースとしたクライアント認証を実現することができます(図7)。

図7:Kafkaのクライアント認証の流れ

図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 IDkafka-brokerKafkaクラスタ用クライアント。
 Access Typebearer-only
クライアントClient IDkafka-producerProducer用クライアント。
 Access Typeconfidential
 Standard Flow EnabledOFF
 Direct Access Grants EnabledOFF
 Service Accounts EnabledON
クライアントClient IDkafka-consumerConsumer用クライアント。
 Access Typeconfidential
 Standard Flow EnabledOFF
 Direct Access Grants EnabledOFF
 Service Accounts EnabledON

次に、Kafkaクラスタの設定(kafka.yaml)を変更します。

リスト15:Kafkaクラスタの設定を変更

01
02spec:
03  kafka:
04    
05    listeners:
06      
07      external:
08        port: 9094
09        type: nodeport
10        tls: false
11        authentication:
12          type: oauth
13          clientId: kafka-broker
14          clientSecret:
15            secretName: my-cluster-oauth
16            key: clientSecret
17          validIssuerUri: http://172.30.10.93:31385/auth/realms/bookinfo
19          userNameClaim: preferred_username
20

また、Kafkaクラスタ用クライアントのクライアントシークレットを格納するために、Secret(kafka-broker-client-secret.yaml)を作成します。

リスト16:Secretを作成

1apiVersion: v1
2kind: Secret
3metadata:
4  name: my-cluster-oauth
5type: Opaque
6data:
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
2
32020-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を追加

1
3        <dependency>
4            <groupId>io.strimzi</groupId>
5            <artifactId>kafka-oauth-client</artifactId>
6            <version>0.6.1</version>
7        </dependency>
8

次に、application.properties(security-keycloak-authorization-quickstart/src/main/resources/application.properties)に設定を加えます。

リスト20:applicationpropertiesに設定を追加

01# Configure the Kafka sink (we write to it)
02mp.messaging.outgoing.my-channel-out.connector=smallrye-kafka
03mp.messaging.outgoing.my-channel-out.topic=book-info
04mp.messaging.outgoing.my-channel-out.value.serializer=org.apache.kafka.common.serialization.StringSerializer
05mp.messaging.outgoing.my-channel-out.security.protocol=SASL_PLAINTEXT
06mp.messaging.outgoing.my-channel-out.sasl.mechanism=OAUTHBEARER
07mp.messaging.outgoing.my-channel-out.sasl.login.callback.handler.class=io.strimzi.kafka.oauth.client.JaasClientOauthLoginCallbackHandler
08mp.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用クライアントのクライアントシークレット>" \
12 
13# Configure the Kafka source (we read from it)
14mp.messaging.incoming.my-channel-in.connector=smallrye-kafka
15mp.messaging.incoming.my-channel-in.topic=book-info
16mp.messaging.incoming.my-channel-in.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
17mp.messaging.incoming.my-channel-in.security.protocol=SASL_PLAINTEXT
18mp.messaging.incoming.my-channel-in.sasl.mechanism=OAUTHBEARER
19mp.messaging.incoming.my-channel-in.sasl.login.callback.handler.class=io.strimzi.kafka.oauth.client.JaasClientOauthLoginCallbackHandler
20mp.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
2
3Hello admin_user !

おわりに

本連載では、Keycloakで実現するAPIセキュリティと題し、Keycloakのインストール方法からハードニングの実装方法、内部向けのAPIであるマイクロサービスの認証を強化する最先端の機能をご紹介しました。Keycloakでは、2020年12月現在、FAPIに対する対応を行うFAPI-SIGや、次のKeycloakを考えるKeycloak.Xという活動が活発に推進されています。着実にパワーアップしているKeycloakを、この機会に使ってみてはいかがでしょうか。

株式会社 日立製作所
OSSソリューションセンタにて、API管理や認証周りのOSSの開発/サポート/普及活動に従事。3scaleおよびkeycloakコミュニティのコントリビュータであり、多数のコードをコミットしている。

連載バックナンバー

運用・管理技術解説
第7回

コンテナ上のマイクロサービスの認証強化 ~StrimziとKeycloak~

2021/2/16
前回に引き続き、マイクロサービスの認証強化を実現する最先端の機能を紹介します。
運用・管理技術解説
第6回

コンテナ上のマイクロサービスの認証強化 ~QuarkusとKeycloak~

2021/1/19
連載6回目となる今回は、マイクロサービスの認証強化を実現する最先端の機能を紹介します。
セキュリティ技術解説
第5回

コンテナ上のマイクロサービスの認証強化 ~IstioとKeycloak~

2020/12/15
連載5回目となる今回は、Istioを用いたマイクロサービスのシステムをKeycloakを用いて認証強化する手順を紹介します。

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています