PR

コンテナ上のマイクロサービスの認証強化 ~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をインストール

$ 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)設定を書き換えます。

リスト2:設定を書き換え

    env:
      - name: STRIMZI_NAMESPACE
        value: my-kafka-project           // この行を追加
        valueFrom:                        // 以下3行を削除
          fieldRef:
            fieldPath: metadata.namespace // ここまで削除

Kubernetesにデプロイします。

リスト3: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)を作成します。

リスト4:Kafkaクラスタの作成

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を適用します。

リスト5:CustomResourceDefinitionの適用

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

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

リスト6:Kafakaクラスタの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に実装します。

図5:Detailsサービスv3のシーケンス

図5:Detailsサービスv3のシーケンス

まずは、既存のプロジェクトにsmallrye-reactive-messaging-kafkaのエクステンションを追加します。

リスト7:smallrye-reactive-messaging-kafakのエクステンションを追加

$ 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に追加で実装します。

リスト8:Producer部分を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に実装します。

リスト9:Consumer部分を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)に設定を加えます。

リスト10: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にデプロイします。

リスト11:Quarkusアプリケーションのビルド、デプロイ

$ ./mvnw clean package -Dquarkus.kubernetes.deploy=true

DestinationRule(destination-rule-all-mtls-v2.yaml)を書き換えて、Detailsサービスのv3にルーティングするようにします。

リスト12:DestinationRuleの書き換え

…
---
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を適用します。

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

$ 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:メッセージ送受信されていることを確認

$ kubectl logs details-xxx -c details
…
Hello 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クラスタの設定を変更

…
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)を作成します。

リスト16:Secretを作成

apiVersion: v1
kind: Secret
metadata:
  name: my-cluster-oauth
type: Opaque
data:
  clientSecret: <Kafkaクラスタ用クライアントのクライアントシークレットをBase64エンコードしたもの>

作成したSecretと変更したKafkaクラスタの設定を適用します。

リスト17: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からのメッセージは出力されず、接続エラーが出力されることを確認できます。

リスト18:動作の確認

$ 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を追加します。

リスト19: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)に設定を加えます。

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

# 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にデプロイします。

リスト21:Quarkusアプリケーションのビルド、デプロイ

$ cd security-keycloak-authorization-quickstart/
$ ./mvnw clean package -Dquarkus.kubernetes.deploy=true

以上で、ProducerとConsumerのクライアント認証の設定は完了です。admin_userを用いて発行したアクセストークンを付与してBookinfoアプリケーションのProductページにアクセスしてみましょう。Productページは問題なく表示され、Consumerからメッセージが出力されることを確認できます。

リスト22:動作を確認

$ kubectl logs details-xxx -c details
…
Hello 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のWebサイトにログインすることでさまざまな限定特典を入手できるようになります。

Think IT会員サービスの概要とメリットをチェック

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