#chiroito ’s blog

Java を中心とした趣味の技術について

OpenShift 4.21で証明書を自動化

OpenShift を素の状態で使い始めると、API サーバや Ingress には OpenShift 内部の仕組みで発行された証明書が使われます。これでもクラスタ内部では問題ありませんが、外部クライアントから素直に信頼される証明書に置き換えたい場面は多くあります。この記事では、OpenShift 4.21.8 に cert-manager を導入し、Cloudflare の DNS01 challenge を使って Let’s Encrypt から証明書を発行し、それを API 用と Ingress 用に適用する流れをまとめます。

今回の構成では Cloudflare を DNS に使います。cert-manager の Cloudflare 連携では API Token を使う方法が案内されており、Let’s Encrypt のワイルドカード証明書も DNS01 で扱う前提です。

OpenShift 上で使う cert-manager は、Red Hat が提供する cert-manager Operator for Red Hat OpenShift を使います。この Operator は OpenShift に最初から入っているわけではないため、Web コンソールからインストールします。また、コミュニティ版の cert-manager Operator を併用しないようにする必要があります。

前提

今回の前提は次のとおりです。

  • DNS は Cloudflare
    • OpenShiftのドメインは o.chiroito.dev
  • OpenShift 4.21.8

YAMLの中にあるドメイン名やトークン、メールアドレスは適宜変更してください。

cert-manager Operator の導入

最初に OpenShift の Web コンソールから cert-manager Operator for Red Hat OpenShift をインストールします。手順としては、Web コンソールに cluster-admin 権限でログインし、Operator のカタログから該当の Operator を選んでインストールします。今回の構成では複数 namespace で Certificate を扱うため、インストールモードは AllNamespaces を選んでおくのが無難です。

ここはこの記事の本題ではないので詳細な画面操作までは追いませんが、少なくとも次の状態になっていれば先へ進めます。

  • cert-manager Operator がインストール済み
  • cert-manager namespace に関連 Pod が起動している

Cloudflare の API Token を作成する

Cloudflare 側では cert-manager が DNS レコードを書き換えられるように API Token を作成します。 Cloudflare の画面では、Profile から API Tokens に進み、Create Token を選択します。Edit zone DNS のテンプレートを使ってもよいですし、Create Custom Token から自分で設定しても構いません。必要なのは次の 2 権限と 1 つの Zone Resources 設定です。

権限

  • Zone - DNS - Edit
  • Zone - Zone - Read

Zone Resources

  • Include - All Zones

作成後に表示されるトークンはその場で控えておきます。

Cloudflare の認証情報を Secret として作成する

次に Cloudflare の API Token を Secret として登録します。ここで使う Secret は、後で ClusterIssuer の apiTokenSecretRef から参照されます。

apiVersion: v1
kind: Secret
metadata:
  name: cloudflare-api-token-secret
  namespace : cert-manager
type: Opaque
stringData:
  api-token: <API Token>

Let’s Encrypt 用の ClusterIssuer を作成する

続いて ACME の発行元を定義します。今回の YAML では、ステージング用の example-issuer と本番用の prod-issuer を分けています。privateKeySecretRef は ACME アカウント鍵を保存するための Secret 名で、cert-manager が自動生成して管理します。

まずは Let’s Encrypt のステージング環境です。

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: example-issuer
spec:
  acme:
    email: <メールアドレス>
    privateKeySecretRef:
      name: example-issuer-account-key
    server: 'https://acme-staging-v02.api.letsencrypt.org/directory'
    solvers:
      - dns01:
          cloudflare:
            apiTokenSecretRef:
              key: api-token
              name: cloudflare-api-token-secret

次に本番環境です。

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: prod-issuer
spec:
  acme:
    email: <メールアドレス>
    privateKeySecretRef:
      name: prod-issuer-account-key
    server: 'https://acme-v02.api.letsencrypt.org/directory'
    solvers:
      - dns01:
          cloudflare:
            apiTokenSecretRef:
              key: api-token
              name: cloudflare-api-token-secret

ClusterIssuer を使っているのは、openshift-configopenshift-ingress の両方から同じ発行元を参照したいためです。cert-manager の Certificate は、issuerRef.kindClusterIssuer にすると cluster-scoped な発行元を参照できます。

最初はステージング側で疎通確認をし、問題がなければ prod-issuer を使います。

Certificate を作成する

OpenShift の API 用証明書と Ingress 用証明書をそれぞれ Certificate として定義します。spec.secretName は cert-manager が自動作成して管理する Secret 名です。Certificate が属する namespace にその Secret が作られるため、API 用は openshift-config、Ingress 用は openshift-ingress に作ります。

まずは API 用です。

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: api-tls-cert
  namespace: openshift-config
spec:
  commonName: api.o.chiroito.dev
  dnsNames:
    - api.o.chiroito.dev
  isCA: false
  issuerRef:
    kind: ClusterIssuer
    name: prod-issuer
  secretName: api-secret

次に Ingress 用です。

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: ingress-tls-cert
  namespace: openshift-ingress
spec:
  commonName: apps.o.chiroito.dev
  dnsNames:
    - apps.o.chiroito.dev
    - '*.apps.o.chiroito.dev'
  isCA: false
  issuerRef:
    kind: ClusterIssuer
    name: prod-issuer
  secretName: ingress-secret

Ingress 側で *.apps.o.chiroito.dev を入れているのは、OpenShift の Route が通常 アプリ名-namespace.apps... のようなホスト名で公開されるためです。

Certificate を作成したあとは、まず cert-manager 側で発行が成功しているかを確認します。

oc get certificate -A
oc describe certificate api-tls-cert -n openshift-config
oc describe certificate ingress-tls-cert -n openshift-ingress

Secret が生成されていることも確認します。

oc get secret api-secret -n openshift-config
oc get secret ingress-secret -n openshift-ingress

OpenShift 側に Secret をひも付ける

証明書を発行しただけでは、まだ OpenShift の API サーバや Ingress はそれを使いません。最後に、cert-manager が作成した Secret を OpenShift 側の設定に参照させます。API サーバの named certificate は APIServer/clusterspec.servingCerts.namedCertificates で設定し、Ingress の既定証明書は IngressController/defaultspec.defaultCertificate.name で設定します。

API サーバ側は次のとおりです。

apiVersion: config.openshift.io/v1
kind: APIServer
metadata:
  name: cluster
spec:
  audit:
    profile: Default
  servingCerts:
    namedCertificates:
      - names:
          - api.o.chiroito.dev
        servingCertificate:
          name: api-secret

kube-apiserver のロールアウト状況を確認します。

oc get clusteroperators kube-apiserver
oc get apiserver cluster -o yaml

Ingress 側は次の設定です。

apiVersion: operator.openshift.io/v1
kind: IngressController
metadata:
  name: default
  namespace: openshift-ingress-operator
spec:
  defaultCertificate:
    name: ingress-secret
(略)

IngressController の defaultCertificateopenshift-ingress namespace にある Secret を参照します。Route 側で個別の証明書を指定していない場合、この既定証明書が使われます。OpenShift は既定では内部生成のワイルドカード証明書を使いますが、ここを差し替えることで外部から信頼される証明書に置き換えられます。

defaultCertificate の参照が反映されているかを確認します。

oc get ingresscontroller/default -n openshift-ingress-operator -o yaml

最後にクライアント側から証明書を確認します。API であれば https://api.o.chiroito.dev:6443、Ingress であればWebコンソールにブラウザで証明書チェーンを確認すると分かりやすいです。

参考資料

OpenShiftでServiceMonitorを作ったのにPrometheusにメトリクスが出ないときの確認手順

OpenShift で監視を構築していると、ServiceMonitor を作成したはずなのに、Prometheus に対象のメトリクスがまったく出てこないことがあります。

こういうとき、最初に確認したくなるのは Service や Endpoint、あるいは /metrics の中身そのものですが、その前に見ておくと切り分けがかなり早くなるポイントがあります。
それが、Prometheus がその ServiceMonitor を認識しているかどうかです。

この記事では、Prometheus の api/v1/targets を使って、ServiceMonitor がどこまで認識されているのかを確認し、設定ミスとスクレイプ失敗を切り分けたときの流れをまとめます。

まず確認したいこと

今回のポイントは、次の 2 段階を分けて考えることです。

  1. Prometheus は ServiceMonitor を見つけられているか
  2. 見つけたあと、実際に /metrics を正常にスクレイプできているか

この 2 つは似ているようで別の問題です。
ServiceMonitor が正しく認識されていなくてもメトリクスは出ませんし、ServiceMonitor が認識されていても、スクレイプ先の形式が不正ならやはりメトリクスは出ません。

その切り分けに便利なのが api/v1/targets です。

Prometheus の api/v1/targets を確認する

Prometheus Pod の中から API を叩くと、Prometheus が見つけた監視対象を JSON で確認できます。

oc exec -c prometheus <prometheus-pod-name> -- \
  curl -s 'http://localhost:9090/api/v1/targets' | jq

この API では、Prometheus が「見つけようとしたもの」と「実際に監視対象として扱っているもの」の両方を確認できます。

見るべき項目

特に重要なのは次の項目です。

項目 意味 何がわかるか
activeTargets 現在スクレイプ対象になっているもの 正常に認識され、監視対象に入っているか
droppedTargets 発見したが除外されたもの 見つけてはいるが設定上の理由で捨てられていないか
labels 最終的に付与されるラベル jobnamespace などが意図通りか
health ターゲットの状態 up / down / unknown を確認できる
lastError 直近のエラー内容 スクレイプ失敗の理由を直接確認できる

ここで重要なのは、Prometheus に出てこない = まったく認識されていない、とは限らないという点です。 droppedTargets に入っていれば、Prometheus は発見まではできていることになります。

今回は droppedTargets に入っていた

今回、自分で登録した ServiceMonitor は activeTargets ではなく droppedTargets に入っていました。

この状態を見ると、少なくとも次のことがわかります。

  • ServiceMonitor に由来する候補は Prometheus に見つかっている
  • ただし最終的には監視対象として採用されていない
  • つまり問題は「未発見」ではなく「設定による除外」の可能性が高い

この段階で、ネットワークやアプリケーション側の /metrics を疑うより先に、ServiceMonitor の設定を見直すべきだと判断できます。

原因は ServiceMonitor のラベル不足だった

設定を見直したところ、今回は ServiceMonitor に付けるべきラベルが不足していました。

metadata:
  labels:
    k8s-app: hello-app-monitor

このラベルを付与し直すと、ServiceMonitor は正しく認識されるようになりました。

つまり、最初の問題は /metrics の中身そのものではなく、Prometheus がその ServiceMonitor を適切に拾える形になっていなかったことでした。

認識されたあとに、別の問題が見えた

ラベル修正後は activeTargets に対象が現れるようになりました。 ここで次に確認すべきなのが healthlastError です。

今回のターゲットは activeTargets に入ったものの、状態は down になっており、lastError には次のような内容が出ていました。

non-compliant scrape target sending blank Content-Type and no fallback_scrape_protocol specified for target

この状態は、ServiceMonitor の認識自体は成功している一方で、スクレイプ対象のレスポンス形式に問題があることを意味しています。

つまり、問題は次のように段階的に切り分けられました。

第1段階

ServiceMonitor が Prometheus に正しく認識されていなかった → ラベル不足が原因

第2段階

ServiceMonitor は認識されたが、スクレイプは失敗していた → /metrics の形式が Prometheus 互換ではなかった

今回のケースでは MicroProfile Metrics を読ませていた

最終的には、Prometheus が読みにいっていた /metrics が、Prometheus にとって期待どおりの形式ではありませんでした。

今回は誤って MicroProfile Metrics を読ませていたため、Prometheus 側でエラーになっていました。

この時点で見えていたのは、次のような事実です。

  • ServiceMonitor の認識は成功している
  • ターゲットも activeTargets に入っている
  • しかし healthdown
  • lastError にレスポンス形式の問題が明示されている

ここまで来ると、確認対象は Kubernetes リソースの関連付けではなく、実際に /metrics で返している内容に絞れます。

切り分けの順番として有効だったこと

今回の確認で特に有効だったのは、いきなり原因をひとつに決め打ちせず、Prometheus の見え方を順番に確認したことです。

おすすめの順番は次の通りです。

1. api/v1/targets を見る

まずは Prometheus が対象をどう見ているかを確認します。

2. activeTargets と droppedTargets を分けて考える

  • どちらにもいないなら、発見条件を疑う
  • droppedTargets にいるなら、設定による除外を疑う
  • activeTargets にいるなら、次はスクレイプ結果を見る

3. health と lastError を確認する

ここで通信失敗なのか、認証エラーなのか、フォーマット不正なのかが見えてきます。

まとめ

ServiceMonitor を作ったのに Prometheus にメトリクスが出ないときは、まず api/v1/targets を見ると状況をかなり整理できます。

今回の流れでは、問題は次の 2 つに分かれていました。

  • 最初は ServiceMonitor のラベル不足により、Prometheus に正しく採用されていなかった
  • その修正後は、今度は /metrics の形式が Prometheus に適合せずスクレイプに失敗していた

このように、ServiceMonitor が認識されているかと、認識されたうえで正常にスクレイプできているかは別問題として切り分けるのが重要です。

Prometheus 側の見え方を先に確認しておくと、Kubernetes の設定ミスなのか、メトリクスの内容の問題なのかを早い段階で判断しやすくなります。

同じように「ServiceMonitor を作ったのに何も見えない」という状況に遭遇したときは、まず api/v1/targets を確認してみるとよいと思います。

JDK Mission Control からリモートの JBoss EAP に接続する

JBoss EAP を運用していると、JDK Mission Control(JMC)からリモート接続して状態を確認したくなることがあります。 ただし、JBoss EAP の管理インタフェース経由で JMX 接続する場合は、通常の JMX URL ではなく remote+http という独自プロトコルを使うため、そのままでは JMC から接続できません。

この記事では、JDK Mission Control からリモートの JBoss EAP に接続するための最小限の準備と、実際の接続方法を簡単にまとめます。

要約

先に要点だけまとめると、必要なのは次の内容です。

JBoss EAP を展開したディレクトリに含まれている bin/client/jboss-cli-client.jar を使います。 この JAR には、JBoss EAP の remote+http プロトコルを扱うために必要なクラスが含まれています。

JDK Mission Control を展開したディレクトリにある jmc.ini の末尾に、次の 1 行を追加します。

-Xbootclasspath/a:./jboss-cli-client.jar

この例では jboss-cli-client.jarjmc.ini と同じ場所に置いていますが、JAR の配置場所は任意です。 別の場所に置く場合は、その場所に合わせてパスを書き換えれば問題ありません。

JMX サービス URL には、たとえば次のような形式を使います。

service:jmx:remote+http://localhost:9990

これは、localhost 上で動作している JBoss EAP の管理ポート 9990 に接続する例です。 実際にリモート接続する場合は、localhost の部分を対象サーバのホスト名または IP アドレスに置き換えます。

環境によっては認証も必要です。 Galleon で JBoss EAP を構築している場合は、管理ユーザとして ADMIN_USERNAMEADMIN_PASSWORD の環境変数を利用できます。

なぜ追加の JAR が必要なのか

JMC は標準の JMX 接続には対応していますが、JBoss EAP の remote+http は一般的な JMX プロトコルではありません。 そのため、JMC 側に JBoss EAP 用のクライアントライブラリを読み込ませる必要があります。

そこで使うのが、JBoss EAP に含まれている jboss-cli-client.jar です。 この JAR を JMC の起動時に読み込ませることで、service:jmx:remote+http://... という URL を解釈できるようになります。

手順 1: jboss-cli-client.jar を用意する

まず、JBoss EAP を展開したディレクトリから次の JAR を探します。

bin/client/jboss-cli-client.jar

これを JMC 側から参照できる場所に置きます。 手軽なのは、jmc.ini と同じディレクトリにコピーしてしまう方法です。

もちろん、別のディレクトリに置いても構いません。 その場合は、後で jmc.ini に書くパスを正しく合わせてください。

手順 2: jmc.ini に設定を追加する

次に、JDK Mission Control を展開したディレクトリにある jmc.ini を開きます。 その一番最後に、次の 1 行を追加します。

-Xbootclasspath/a:./jboss-cli-client.jar

この設定は、JMC 起動時に jboss-cli-client.jar を読み込ませるためのものです。

もし jboss-cli-client.jar を別の場所に置いた場合は、たとえば次のように絶対パスや相対パスで指定してください。

-Xbootclasspath/a:/path/to/jboss-cli-client.jar

大事なのは、JAR の置き場所そのものではなく、jmc.ini に書いたパスが正しいことです。

手順 3: JMC から JBoss EAP に接続する

JMC を起動したら、新しい接続先として JBoss EAP の JMX サービス URL を指定します。 たとえばローカル環境であれば、次の URL を使えます。

service:jmx:remote+http://localhost:9990

この URL の意味は次のとおりです。

service:jmx: は JMX サービス URL の接頭辞です。 remote+http は JBoss EAP 独自の接続プロトコルです。 localhost:9990 は JBoss EAP の管理インタフェースの接続先です。

リモートサーバへ接続する場合は、次のように書き換えます。

service:jmx:remote+http://your-hostname:9990

認証が有効になっている場合は、管理ユーザ名とパスワードもあわせて指定します。 Galleon で構築した環境では、ADMIN_USERNAMEADMIN_PASSWORD を使って管理ユーザを設定していることがあります。

うまく接続できないときの確認ポイント

もっとも分かりやすい典型例は、jboss-cli-client.jar が正しく読み込まれていないケースです。 この場合、JMC は remote+http を理解できず、次のような例外が発生します。

org.openjdk.jmc.rjmx.common.ConnectionException caused by java.net.MalformedURLException: Unsupported protocol: remote+http
    at org.openjdk.jmc.rjmx.common.internal.RJMXConnection.connect(RJMXConnection.java:364)
    at org.openjdk.jmc.rjmx.internal.ServerHandle.doConnect(ServerHandle.java:121)
    at org.openjdk.jmc.rjmx.internal.ServerHandle.connect(ServerHandle.java:111)
    at org.openjdk.jmc.console.ui.editor.internal.ConsoleEditor$ConnectJob.run(ConsoleEditor.java:99)
    at org.eclipse.core.internal.jobs.Worker.run(Worker.java:63)
Caused by: java.net.MalformedURLException: Unsupported protocol: remote+http
    at java.management/javax.management.remote.JMXConnectorFactory.newJMXConnector(JMXConnectorFactory.java:366)
    at org.openjdk.jmc.rjmx.common.internal.RJMXConnection.connectJmxConnector(RJMXConnection.java:591)
    at org.openjdk.jmc.rjmx.common.internal.RJMXConnection.establishConnection(RJMXConnection.java:572)
    at org.openjdk.jmc.rjmx.common.internal.RJMXConnection.connect(RJMXConnection.java:357)
    ... 4 more

この例外が出た場合は、まず次の点を確認すると切り分けしやすいです。

jmc.ini-Xbootclasspath/a:... の行が追加されているか。 その行が jmc.ini の末尾に正しく書かれているか。 指定した jboss-cli-client.jar のパスが正しいか。 JMC を設定変更後に再起動しているか。

ここが通っていれば、少なくとも remote+http を理解できないという段階の問題は解消できます。

まとめ

JDK Mission Control から JBoss EAP に接続するには、JBoss EAP 付属の jboss-cli-client.jar を JMC に読み込ませる必要があります。 設定先は jmc.ini で、-Xbootclasspath/a:... を追加します。

接続先 URL には、JBoss EAP 独自の remote+http プロトコルを使った service:jmx:remote+http://ホスト名:9990 形式を指定します。 認証が必要な環境では、管理ユーザの資格情報も必要です。 Galleon 構築環境では ADMIN_USERNAMEADMIN_PASSWORD を使っていることがあります。

JMC 側で Unsupported protocol: remote+http が出る場合は、まず jboss-cli-client.jar の読み込み設定を疑うのが近道です。