Jaybanuan's Blog

どうせまた調べるハメになることをメモしていくブログ

コンテナで簡易NFSサーバを構築

はじめに

評価用にNFSサーバが必要になったため、コンテナで構築できないか調べてみた。

環境

$ cat /etc/os-release | grep PRETTY_NAME
PRETTY_NAME="Ubuntu 22.04 LTS"

$ uname -srvm
Linux 5.15.0-25-generic #25-Ubuntu SMP Wed Mar 30 15:54:22 UTC 2022 x86_64

$ docker version
Client: Docker Engine - Community
 Version:           20.10.14
(略)
Server: Docker Engine - Community
 Engine:
  Version:          20.10.14
(略)

利用したコンテナイメージ

DockerHubで公開されている以下を利用。

更新が2019年と古いが、NFSは枯れた技術なので数年前のものでも機能的には問題ないはず。

NFSサーバの構築

(1) ディレクトリの準備

まず、今回の調査の作業用として、ホスト側に以下のディレクトリを準備した。

/
`-- nfs
    `-- nfs-root

ホスト側の/nfs/nfs-rootを、コンテナ側の/exportにバインドマウントし、これをNFSで共有する。 ホスト側の/nfsは、後述するdocker-compose.ymlの配置場所にしている。

ディレクトリの作成のために、以下のコマンドを実行する。

$ sudo mkdir -p /nfs/nfs-root

また、後ほどNFSマウントができたかどうかを確認するために、/nfs/nfs-rootの中にファイルを一つ作成しておく。

$ sudo echo "hello, world!" > /nfs/nfs-root/greeting.txt

(2) docker-compose.ymlの作成

以下の内容でdocker-compose.ymlを作成し、/nfsに配置しておく。 一部ドキュメントどおりでは動かなかった部分があり手を加えているが、詳細は後述する。

version: "3.8"
services:
  "nfs-server":
    image: erichough/nfs-server
    privileged: true
#    cap_add:
#      - SYS_ADMIN
#      - SYS_MODULE
    ports:
      - "2049:2049"
    environment:
      NFS_EXPORT_0: "/export *(rw,sync,all_squash,no_subtree_check,fsid=0)"
    volumes:
      - /nfs/nfs-root:/export
      - /lib/modules:/lib/modules:ro

(3) NFSサーバの起動

以下のコマンドを実行して、NFSサーバのコンテナを起動する。

$ cd /nfs
$ docker-compose up -d

動作確認

(1) ホストからマウント

適当なディレクト/nfs-testを作成して、マウントを試してみる。

$ sudo apt update
$ sudo apt install nfs-common
$ sudo mkdir /nfs-test
$ sudo mount -v -t nfs4 [ホストマシンのIP]:/ /nfs-test

マウントに成功すると、以下のようにgreeting.txtを読み込むことができる。

$ cat /nfs-test/greeting.txt
hello, world!

(2) コンテナからマウント

まずはUbuntuのコンテナを起動する。 ネットワークはDocker Composeが作成したnfs_defaultを利用する。

$ docker run -it --privileged --net nfs_default ubuntu:latest /bin/bash

コンテナ起動後、コンテナの中に先ほどと同様に適当なディレクト/nfs-testを作成して、マウントを試してみる。

$ apt update
$ apt install nfs-common
$ mkdir /nfs-test
$ mount -v -t nfs4 nfs-server:/ /nfs-test

マウントに成功すると、以下のようにgreeting.txtを読み込むことができる。

$ cat /nfs-test/greeting.txt
hello, world!

ちなみに、コンテナ起動時にオプション--privilegedを付与しておかないと、以下のようなエラーが出てマウントに失敗する。

$ mount -t nfs4 nfs-server:/ /mnt
mount.nfs4: Operation not permitted

参考

ドキュメントどおりでは動かなかった部分

コンテナイメージerichough/nfs-serverのドキュメントは、ドキュメント内のリンクの都合上、DockerHubよりもGitHubの方を見たほうがよい。 主に次の2つのドキュメントを参照した。

github.com

github.com

ここで、コンテナに付与する権限について--privilegedではなく--cap-addを推奨しているが、ドキュメントどおりにSYS_ADMINSYS_MODULEを付与しても権限が足りずに起動に失敗する。 他に何が必要なのかを調べる時間がないので、--privilegedを利用した。

クライアント側でも--privilegedがないとマウントに失敗するので、NFSLinuxカーネルと密結合しているように思う。

NFSのバージョンについて

NFS v3はRPCやロックなどの様々なサービス(デーモン)を動的に組み合わせて実現しているため、設定が複雑で単にコンテナのポートをホスト側で公開するだけでは動かない。 そのため、コンテナでのNFS v3サーバの構築はハードルが高い。

一方、NFS v4はその辺りが改善されていて、ポート2049にアクセスできればNFSの利用が可能であり、コンテナでのNFS v4サーバの構築は比較的ハードルが低い。 とはいえ、--privileged等を利用した権限付与が必要であり、コンテナを運用する際のセキュリティポリシー次第では、NFS v4は利用できない可能性がある。

複数のGitのユーザを使い分ける

はじめに

複数のGitサーバ/サービスを利用する場合、Gitのユーザの切り替えが必要になる。 適切に切り替えを行わないと、誤ったユーザでコミットしてしまって、履歴を汚してしまう。 できるだけこの失敗を防ぐために、やっていることのメモを残しておく。

ユーザをグローバルに設定しない

あえてグローバルにユーザを設定せず、リポジトリごとにローカルにユーザを設定する。 これで、「デフォルト(=グローバルな設定)のユーザでコミットしてしまった」というミスは防げる。

示すまでもないが、各リポジトリ内で以下を設定することになる。

$ git config --local user.name "jaybanuan"
$ git config --local user.email ”jaybanuan@example.com”

Bashのプロンプトにユーザ名を表示する

カレントディレクトリのリポジトリにどのユーザが設定されているかを、Bashのプロンプトに表示しておく。 これで、「想定とは違うユーザでコミットしてしまった」というミスを減らすことができる。

やりたいことを具体的に説明すると、通常は以下のようなプロンプトだが、

jaybanuan@devpc:~/src $ 

以下のようにgit cloneしたリポジトリに移動すると、

jaybanuan@devpc:~/src $ git clone https://host/path/to/myapp.git
jaybanuan@devpc:~/src $ cd myapp

以下のように、末尾にgitのユーザ名@ブランチ名が付加されたプロンプトに変える。

jaybanuan@devpc:~/src/myapp (jaybanuan@main)$

ユーザ名が設定されていない場合は以下のようにNO-USER-NAMEと表示し、git configでユーザ名を設定すると上記と同様にユーザ名が表示されるようになる。

jaybanuan@devpc:~/src/myapp (NO-USER-NAME@main)$
jaybanuan@devpc:~/src/myapp (NO-USER-NAME@main)$ git config --local user.name jaybanuan
jaybanuan@devpc:~/src/myapp (jaybanuan@main)$

これを実現するために、以下のスクリプトを"~/.bashrc"に付け加えておく。

get_git_info_for_ps1() {
    local GIT_INFO=$(__git_ps1 "%s")
    if [ -n "$GIT_INFO" ]; then
        local GIT_USER_NAME=$(git config --get user.name)
        if [ -z "$GIT_USER_NAME" ]; then
            USERNAME="NO-USER-NAME"            
        fi
        
        echo " ($USERNAME@$GIT_INFO)"
    fi
}

PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\#033[00m\]\[\033[36m\]$(get_git_info_for_ps1)\[\033[00m\]\$ '

複数のユーザを管理できるCredential Helperを使う

gitのパスワード管理にはCredential Helperを利用すると入力の手間が省けて楽である。 しかしながら、Credential Helperの設計思想的には、ある1人のユーザが複数のGitサーバを使い分けるという一対多の関係を想定していると思われる。 現実にはプロジェクトやポジションによってユーザを使い分けることがあり、その場合は多対多の関係になる。

サードパーティ製(GitHub製)のCredential Helperにgit-credential-manager-coreというものがあり、名前空間という独自機能をもっている。 この名前空間を利用してユーザごとに管理領域を分けることで、多対多の管理を実現することができる。

github.com

具体的には、git-credential-manager-coreをインストールした後、cloneしたgitのリポジトリに入って以下のようなコマンドを実行すれば良い。

$ git config --local credential.credentialStore secretservice
$ git config --local credential.namespace jaybanuan

参考

リポジトリをcloneするたびに適切にgit configを実行するのが面倒だったので、スクリプト化した。

github.com

スクリプトはgitのサブコマンドの仕様に沿ったファイル名にしてあるので、パスの通っているディレクトリに配置しておけば、以下のように1コマンドで設定ができる。

$ git user jaybanuan

Minikubeを構築して、ローカルのコンテナイメージをデプロイする

はじめに

コンテナの開発環境を構築する際のメモ。 Minikubeを構築して、ローカルのコンテナイメージをデプロイするまでの手順を残しておく。

環境

$ cat /etc/os-release | grep PRETTY_NAME
PRETTY_NAME="Ubuntu 20.04.4 LTS"

$ uname -srvm
Linux 5.13.0-39-generic #44~20.04.1-Ubuntu SMP Thu Mar 24 16:43:35 UTC 2022 x86_64

$ docker version
Client: Docker Engine - Community
 Version:           20.10.14
(略)
Server: Docker Engine - Community
 Engine:
  Version:          20.10.14
(略)

手順

(1) Minikubeのインストール

インストール方法はいくつかあるが、今回はDebian packageを利用する。 以下のコマンドを実行。

$ curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube_latest_amd64.deb
$ sudo dpkg -i minikube_latest_amd64.deb

詳細はこちら。

minikube.sigs.k8s.io

(2) kubectlのインストール

インストール方法はいくつかあるが、今回はsnapを利用する。 以下のコマンドを実行。

$ sudo snap install kubectl --classic

詳細はこちら。

kubernetes.io

(3) Minikubeの起動

以下のコマンドを実行。

$ minikube start
😄  Ubuntu 20.04 上の minikube v1.25.2
✨  docker ドライバーが自動的に選択されました。他の選択肢: kvm2, ssh, none
👍  minikube クラスター中のコントロールプレーンの minikube ノードを起動しています
🚜  ベースイメージを取得しています...
💾  Kubernetes v1.23.3 のダウンロードの準備をしています
    > gcr.io/k8s-minikube/kicbase: 379.06 MiB / 379.06 MiB  100.00% 169.59 KiB 
    > preloaded-images-k8s-v17-v1...: 505.68 MiB / 505.68 MiB  100.00% 220.72 K
🔥  docker container (CPUs=2, Memory=3900MB) を作成しています...
🐳  Docker 20.10.12 で Kubernetes v1.23.3 を準備しています...
    ▪ kubelet.housekeeping-interval=5m
    ▪ 証明書と鍵を作成しています...
    ▪ コントロールプレーンを起動しています...
    ▪ RBAC のルールを設定中です...
🔎  Kubernetes コンポーネントを検証しています...
    ▪ gcr.io/k8s-minikube/storage-provisioner:v5 イメージを使用しています
🌟  有効なアドオン: storage-provisioner, default-storageclass
🏄  完了しました! kubectl が「"minikube"」クラスタと「"default"」ネームスペースを使用するよう構成されました

(4) ダッシュボードで動作確認

以下のコマンドを実行。

$ minikube dashboard
🔌  ダッシュボードを有効化しています...
    ▪ kubernetesui/metrics-scraper:v1.0.7 イメージを使用しています
    ▪ kubernetesui/dashboard:v2.3.1 イメージを使用しています
🤔  ダッシュボードの状態を検証しています...
🚀  プロキシーを起動しています...
🤔  プロキシーの状態を検証しています...
🎉  デフォルトブラウザーで http://127.0.0.1:35159/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/ を開いています...
既存のブラウザ セッションで開いています。

デフォルトのブラウザが自動的に起動されて、以下のようにダッシュボードが表示される。 まだ何もデプロイしていないので「表示するものがありません」とメッセージが出ている。

f:id:redj:20220406115027p:plain

(5) テスト用のWebサーバのイメージを作成

まず、作成するコンテナイメージをMinikubeが認識できるようにしなければならない。 その方法はいくつかあるが、今回は「docker-env command」という方法を採用する。 以下のコマンドを実行する。

$ eval $(minikube docker-env)

詳細はこちら。

minikube.sigs.k8s.io

次に、以下の内容でDockerfileを作成する。 Ningxのドキュメントルートに、hello, world! を表示するだけのHTMLを配置している。

FROM nginx:1.21
RUN echo "<!DOCTYPE html><html><body>hello, world!</body></html>" > /usr/share/nginx/html/index.html

そして、以下のコマンドを実行して、コンテナイメージを作成する。

$ docker build --tag test-web:1.0.0 .

(6) Minikubeにデプロイ

以下の内容でmanifest.ymlを作成する。

---
apiVersion: v1
kind: Service
metadata:
  name: test-web-service
spec:
  type: NodePort
  selector:
    app: test-web
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-web-deployment
  labels:
    app: test-web
spec:
  replicas: 1
  selector:
    matchLabels:
      app: test-web
  template:
    metadata:
      labels:
        app: test-web
    spec:
      containers:
      - name: test-web
        image: test-web:1.0.0
        ports:
        - containerPort: 80

このマニフェストファイルをMinikubeに適用する。

$ kubectl apply -f manifest.yml 

(7) 動作確認

以下のコマンドを実行して、アクセス用のURLを取得する。

$ minikube service test-web-service --url
http://192.168.49.2:32219

WebブラウザでこのURLにアクセスすると、以下のようにhello, world!が表示される。

f:id:redj:20220406115152p:plain

ダッシュボードを確認すると、デプロイメント、ポッド、レプリカセットが作成されていることを確認できる。

f:id:redj:20220406115249p:plain

Keycloak 16 + OAuth2 ProxyをDocker Composeでお試し環境を構築

はじめに

Keycloak 16を使う必要に迫られたので、Docker Composeでお試し環境を作ることにした。 OpenID Connect用のリバースプロキシとしてOAuth2 Proxyがお手軽に使えそうなので、これも試用してみた。

なお、現時点でのKeycloakの最新版はKeycloak 17で、一部非互換がありそう。 特にレルムのインポート/エクスポート等の管理系が大幅に変わっているように見受けられる。

環境

$ cat /etc/os-release | grep PRETTY_NAME
PRETTY_NAME="Ubuntu 20.04.4 LTS"

$ uname -srvm
Linux 5.13.0-35-generic #40~20.04.1-Ubuntu SMP Mon Mar 7 09:18:32 UTC 2022 x86_64

$ docker version
Client: Docker Engine - Community
 Version:           20.10.12
(略)
Server: Docker Engine - Community
 Engine:
  Version:          20.10.12
(略)

$ docker compose version
Docker Compose version v2.2.3

今回の構成

Docker Composeを利用して、Keycloak 16とOAuth2 ProxyとNginxを立ち上げる。 以下が構成の概要図で、図中の水色の箱はDockerのネットワークを示している。

f:id:redj:20220319080639p:plain

ここで、auth-demo:80がブラウザから見た場合のWebアプリになる。 実際のWebアプリはOAuth2 Proxyの後ろに控えているwebapp:80で、今回は素のNginxである。

また、KeycloakについてはDockerのネットワークの内と外で同じ名前とポート番号keycloak:8080でアクセスできるようにしている。 その理由は、認証画面やOpenID ConnectのエンドポイントのURLが内と外で変わると、うまく動作しなかった*1からである。

これらを踏まえて、ホストマシンの/etc/hostsのローカルホストのエントリを以下のように修正している。

127.0.0.1 localhost keycloak auth-demo

もし、他のマシンでWebブラウザを起動するなら、そのマシンの/etc/hostsを適切に編集する必要がある。

docker-compose.ymlの作成

以下の内容のdocker-compose.ymlを作成する。 細かい説明は後回しにして、まずは内容全体を示す。

version: '3.9'

services:
  keycloak:
    image: jboss/keycloak:16.1.1
    ports:
      - 8080:8080
      - 9990:9990
    environment:
      KEYCLOAK_USER: kc
      KEYCLOAK_PASSWORD: kc
      KEYCLOAK_IMPORT:


  webapp:
    image: nginx:1.21.6


  oauth2-proxy:
    image: quay.io/oauth2-proxy/oauth2-proxy:v7.2.1-amd64
    ports:
      - 80:4180
    environment:
      OAUTH2_PROXY_PROVIDER: oidc
      OAUTH2_PROXY_CLIENT_ID: auth-demo
      OAUTH2_PROXY_CLIENT_SECRET:
      OAUTH2_PROXY_REDIRECT_URL: http://auth-demo/oauth2/callback
      OAUTH2_PROXY_OIDC_ISSUER_URL: http://keycloak:8080/auth/realms/demo
      OAUTH2_PROXY_COOKIE_SECRET: "01234567890123456789012345678901"
      OAUTH2_PROXY_COOKIE_SECURE: "false"
      OAUTH2_PROXY_COOKIE_NAME: "auth_demo"
      OAUTH2_PROXY_EMAIL_DOMAINS: "*"
      OAUTH2_PROXY_HTTP_ADDRESS: 0.0.0.0:4180
      OAUTH2_PROXY_UPSTREAMS: http://webapp/
      OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER: "true"

一部の環境変数で値が未指定のものがあるが、その場合はDocker Compose起動時に利用可能な環境変数の値が、そのままコンテナに引き渡される動きになる。 詳細は以下を参照。

docs.docker.com

Makefileの作成

Makefileは必須ではないが、今回はKeycloakのレルムのデータのインポートとエクスポートを行うためのヘルパとして利用する。 こちらも、細かい説明は後回しにして、まずは内容全体を示す。

##############################################################################
# Variables

REALM_NAME := demo
REALM_FILE_TO_EXPORT := $(REALM_NAME)-realm-$(shell date +%s).json


##############################################################################
# Targets

.PHONY: up-keycloak
up-keycloak:
  if [ "$(REALM_FILE_TO_IMPORT)" ]; then \
      echo "up keycloak with realm file to import"; \
      KEYCLOAK_IMPORT="/$(REALM_NAME)-realm.json" docker compose up --no-start keycloak; \
      docker compose cp "$(REALM_FILE_TO_IMPORT)" "keycloak:/$(REALM_NAME)-realm.json"; \
      docker compose start keycloak; \
  else \
      echo "up keycloak without realm file to import"; \
      docker compose up --detach keycloak; \
  fi


.PHONY: down
down:
   @docker compose down


.PHONY: up
up: up-keycloak
  sleep 30
  docker compose up --detach webapp oauth2-proxy


# see
#   https://hub.docker.com/r/jboss/keycloak/
#   https://www.keycloak.org/docs/16.1/server_admin/#assembly-exporting-importing_server_administration_guide
.PHONY: export-realm
export-realm:
  docker compose exec keycloak /opt/jboss/keycloak/bin/standalone.sh \
      -Djboss.socket.binding.port-offset=100 \
      -Dkeycloak.migration.action=export \
      -Dkeycloak.migration.provider=singleFile \
      -Dkeycloak.migration.realmName=$(REALM_NAME) \
      -Dkeycloak.migration.usersExportStrategy=REALM_FILE \
      -Dkeycloak.migration.file=/tmp/$(REALM_FILE_TO_EXPORT)

    docker compose cp keycloak:/tmp/$(REALM_FILE_TO_EXPORT) $(REALM_FILE_TO_EXPORT)

Keycloakでレルムを設定

(1) Keycloakを立ち上げる

$ make up-keycloak

この時、まだ設定は何も実施していないので、以下の部分のelse側が実行される。 処理内容的には、単にdocker compose upを実行してKeycloakだけを起動している。

up-keycloak:
  if [ "$(REALM_FILE_TO_IMPORT)" ]; then \
      (略)
  else \
      echo "up keycloak without realm file to import"; \
      docker compose up --detach keycloak; \
  fi

(2) WebブラウザでKeycloakにログインする

Webブラウザhttp://keycloak:8080/auth/にアクセスする。

f:id:redj:20220319080657p:plain

表示されたログイン画面で以下を入力して、ボタン「Sign in」を押下する。

項目
Username or email kc
Password kc

ここで入力するユーザ名とパスワードは、docker-compose.ymlの以下の部分で指定している。

services:
  keycloak:
    environment:
      KEYCLOAK_USER: kc
      KEYCLOAK_PASSWORD: kc

(3) レルムを作成する

画面左上の「Master」というレルム名が表示されている部分にマウスカーソルを合わせると、「Add realm」というボタンが現れるので、そのボタンを押下する。

f:id:redj:20220319080704p:plain

以下のようなレルムの作成画面が表示されるので、

f:id:redj:20220319080728p:plain

以下を入力して、ボタン「Create」を押下する。

項目
Name demo

(4) issuerのURLを確認する

画面左のメニューから「Realm Settings」を選択する。 タブ「General」の中にある項目「Endpoints」で、「OpenID Endpoint Configuration」がリンクになっているので、それをクリックする。

f:id:redj:20220319080737p:plain

以下の画面のようにJSON形式の構成情報が表示されるので、その中のキーissuerの値を確認する。

f:id:redj:20220319080744p:plain

この値はOAuth2 Proxyに設定する必要があり、docker-compose.ymlの以下の場所で設定している。

services:
  oauth2-proxy:
    environment:
      OAUTH2_PROXY_OIDC_ISSUER_URL: http://keycloak:8080/auth/realms/demo

(5) クライアントを作成する

画面左のメニューから「Clients」を選択すると、以下のような画面が表示される。

f:id:redj:20220319080758p:plain

この画面のタブ「Lookup」の画面の右上にあるボタン「Create」を押下すると、以下のようなクライアントを追加する画面が表示される。

f:id:redj:20220319083653p:plain

この画面で以下を入力して、ボタン「Save」を押下する。

項目
Clinet ID auth-demo
Client Protocol openid-connect

ここで入力したClient IDはOAuth2 Proxyに設定する必要があり、docker-compose.ymlの以下の場所で設定している。

services:
  oauth2-proxy:
    environment:
      OAUTH2_PROXY_CLIENT_ID: auth-demo

(6) クライアントの設定を行う

クライアントを作成すると画面左のメニューの「Clients」のタブ「Settings」が選択された状態になっている。 画面は以下のようになっており、

f:id:redj:20220319083658p:plain

f:id:redj:20220319083703p:plain

f:id:redj:20220319083708p:plain

以下を入力して、ボタン「Save」を押下する。

項目
Access Type confidential
Valid Redirect URIs http://auth-demo/*

(7) クライアントシークレットを確認する

前の手順のクライアントの設定で「Access Type」を「confidential」にしたことにより、タブ「Credentials」が増えているので、これを選択する。 画面は以下のようになっている。

f:id:redj:20220319083712p:plain

ここで、項目「Secret」の値を控えておく。 これはクライアントアプリケーション、すなわちコンテナoauth-proxyがKeycloakにアクセスする際に必要になるシークレットであり、docker-compose.ymlの以下の場所で設定している。

services:
  oauth2-proxy:
    environment:
      OAUTH2_PROXY_CLIENT_SECRET:

「設定している」と書きつつも値を書いていないが、これはクライアントシークレットが機微情報であるため、docker-compose.ymlに直書きはせずに、Docker Composeの起動時に環境変数で指定するようにしているため。 具体的な指定方法は後述する。

(8) ユーザを追加する

画面左のメニューから「Users」を選択すると、以下のような画面になる。

f:id:redj:20220319083717p:plain

この画面のタブ「Lookup」の中にあるボタン「Add user」を押下すると、以下のようなユーザを追加する画面が表示される。

f:id:redj:20220319083727p:plain

この画面で以下を入力して、ボタン「Save」を押下する。

項目
Username demo-user
Email demo-user@example.com
Email Verified ON

次に、追加したユーザのパスワードを設定する。 タブ「Credentials」を選択すると以下のような画面が表示される。

f:id:redj:20220319083734p:plain

この画面で以下を入力して、ボタン「Set Password」を押下する。

項目
Password (任意のパスワード)
Password Confirmation (任意のパスワード)
Temporary OFF

(9) レルムのデータをエクスポートする

ここまでで設定したレルムを別環境でも復元できるように、JSONファイル形式でエクスポートしておく。 以下のコマンドを実行する。

$ make export-realm

上記コマンドについて、Makefileの当該部分を以下に抜粋する。

.PHONY: export-realm
export-realm:
  docker compose exec keycloak /opt/jboss/keycloak/bin/standalone.sh \
      -Djboss.socket.binding.port-offset=100 \
      -Dkeycloak.migration.action=export \
      -Dkeycloak.migration.provider=singleFile \
      -Dkeycloak.migration.realmName=$(REALM_NAME) \
      -Dkeycloak.migration.usersExportStrategy=REALM_FILE \
      -Dkeycloak.migration.file=/tmp/$(REALM_FILE_TO_EXPORT)

    docker compose cp keycloak:/tmp/$(REALM_FILE_TO_EXPORT) $(REALM_FILE_TO_EXPORT)

Keycloakのコンテナに入って特別なオプションをつけてKeycloakを立ち上げると、レルムのデータをJSON形式でエクスポートできる。 データをエクスポートするためにKeycloakのインスタンスがもう一つ立ち上がるのはどうかと思うが、想像するに管理系の機能を後付けで入れたために、こうなってしまったのだろうと思う*2。 Keycloak 16のインポート/エクスポートに関するマニュアルはこちら。

www.keycloak.org

上記のとおりにmakeを実行すると、コンソールにKeycloakの起動時のログが出力される。

(略)
22:27:54,767 INFO  [org.jboss.as.server] (ServerService Thread Pool -- 42) WFLYSRV0010: Deployed "keycloak-server.war" (runtime-name : "keycloak-server.war")
22:27:54,820 INFO  [org.jboss.as.server] (Controller Boot Thread) WFLYSRV0212: Resuming server
22:27:54,823 INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: Keycloak 16.1.1 (WildFly Core 18.0.4.Final) started in 13501ms - Started 573 of 851 services (576 services are lazy, passive or on-demand)
22:27:54,825 INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0060: Http management interface listening on http://127.0.0.1:10090/management
22:27:54,825 INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0051: Admin console listening on http://127.0.0.1:10090

最後のWFLYSRV0051: Admin console listening on http://127.0.0.1:10090で起動完了と思えばよい。 ここで[CTRL] + [C]でエクスポート用のKeycloakを停止すると、Makefileの中のdocker compose cpに処理が進みホスト側にJSONファイルがコピーされる*3

エクスポートされるJSONファイルは、Makefile内で以下のように定義しているので、

REALM_FILE_TO_EXPORT := $(REALM_NAME)-realm-$(shell date +%s).json

具体的にはdemo-realm-1647642460.jsonといったようなファイルがホスト側に生成される。

(10) Keycloakのコンテナの破棄

ここまでで設定してきたKeycloakのコンテナは、一旦破棄する。 動作確認では、エクスポートされたレルム(JSONファイル)をロードすることで初期データをセットアップする。

$ make down

Makefileでは、以下のように単にdocker compose downを実行している。

.PHONY: down
down:
   @docker compose down

動作確認

(1) デモアプリを起動する

クライアントシークレットはdocker-compose.ymlの中で値を指定していない。 そのため、コンテナにクライアントシークレットを引き渡せるように、ターミナルで環境変数を設定しておく。

$ export OAUTH2_PROXY_CLIENT_SECRET=[クライアントシークレット]

次に、インポートするレルムのJSONファイルを環境変数で指定しておく。 このJSONファイルはmake export-realmを実行して作成されたファイルであり、具体的には以下のような感じで指定する。

$ export REALM_FILE_TO_IMPORT=./demo-realm-1647642460.json

そして、以下のコマンドを実行してKeycloak、OAuth2 Proxy、Nginxを起動する。

$ make up

この上記コマンドについて、Makefileの当該部分を抜粋しながら説明する。 まずはターゲットupについてだが、Makefileでは以下のようになっている。

.PHONY: up
up: up-keycloak
  sleep 30
  docker compose up --detach webapp oauth2-proxy

ターゲットupの実行の前に、ターゲットup-keycloakが実行される。 そしてKeycloakの起動完了の待ち合わせのために30秒スリープした後、OAuth2 ProxyとNginxを立ち上げている。 OAuth2 Proxyは起動処理の過程でKeycloakにアクセスしているため、起動完了の待ち合わせがないとOAuth2 Proxyは起動に失敗する*4

そして、ターゲットup-keycloakは以下のようになっており、

.PHONY: up-keycloak
up-keycloak:
  if [ "$(REALM_FILE_TO_IMPORT)" ]; then \
      echo "up keycloak with realm file to import"; \
      KEYCLOAK_IMPORT="/$(REALM_NAME)-realm.json" docker compose up --no-start keycloak; \
      docker compose cp "$(REALM_FILE_TO_IMPORT)" "keycloak:/$(REALM_NAME)-realm.json"; \
      docker compose start keycloak; \
  else \
      (略)
  fi

今回は変数(環境変数)REALM_FILE_TO_IMPORTが定義されているためthenの方が実行される。 ここでは、コンテナの作成(docker compose up --no-start)、コンテナ内にレルムのJSONファイルをコピー(docker compose cp)、コンテナの起動(docker compose start)を実施している。

インポートするレルムのJSONファイルを引き渡す方法としては、ボリュームを利用するやり方もあるが、これだとdocker-compose.ymlがベタに環境依存になるので避けた。

(2) デモアプリにWebブラウザでアクセスする

Webブラウザhttp://auth-demo/にアクセスすると、以下のような画面が表示される。

f:id:redj:20220319083742p:plain

この画面でボタン「Sign in with OpenID Connect」を押下すると、以下のログイン画面が表示される。

f:id:redj:20220319083746p:plain

この画面で以下を入力して、ボタン「Sign in」を押下する。

項目
Username or email demo-user
Password (demo-userのパスワード)

ログインに成功すると、以下のようなNginxのデフォルトのページが表示される。

f:id:redj:20220319083750p:plain

これにより、OAuth2 ProxyをリバースプロキシとしてKeycloakと連携し、KeyCloakによる認証を通過した後、OAuth2 Proxy経由でバックエンドのWebアプリケーション(Nginx)にアクセスできることを確認できた。

参考

Keycloakとは

Red HatのSSO (Single Sign On)のサーバソフトウェア。 今回はSSOというよりは、OpenID Connectに基づく認証サーバとして利用している。

www.keycloak.org

OAuth2 Proxyとは

アプリケーションサーバ等の前段にリバースプロキシとして立って、OAuth2/OpenID Connectのための処理を請け負ってくれるサーバソフトウェア。 アプリケーション内にOAuth2/OpenID Connectの処理を組み込まなくてもよいので、レガシーなサーバに簡易的に認証/認可の機能を付加する場合に便利。

oauth2-proxy.github.io

*1:これはKeycloakの設定次第かもしれないが、調べきれなかった。

*2:Keycloak 17から基盤がWildflyからQuarkusに変わったのでこの方法は使えない。管理系のコマンドが整備されているように見えるので、改善されているかもしれない

*3:[CTRL] + [C]でファイルがコピーされるとか不格好過ぎるが、Keycloak 17では改善しているかもしれないし、そもそもデモなのでプロダクションレベルの品質は不要だしで、このままでいいやという結論になった

*4:スリープによる待ち合わせは超簡易的な方法なので、プロダクション環境であればhttpを利用したアライブチェック等にすべき。

Ubuntu 20.04LTSでウィンドウのスクリーンショットをとったら周辺に透明な領域があって邪魔だった

はじめに

ウィンドウのスクリーンショットを取ると、何故かウィンドウの周辺に透明な領域(drop shadow ?)がある。 邪魔なので消そうと思った。

f:id:redj:20220319013204p:plain

環境

$ cat /etc/os-release | grep PRETTY_NAME
PRETTY_NAME="Ubuntu 20.04.4 LTS"

やったこと

ウィンドウのエフェクトが関係していそうだが、色々調べても解決策が見当たらず。 仕方がないので、ウィンドウ部分を抜き出すシェルスクリプトを作成した。

#!/bin/bash

WIN_WIDTH=${WIN_WIDTH:-1280}
WIN_HEIGHT=${WIN_HEIGHT:-800}

if [ -v WIN_ID ]; then
    wmctrl -i -r ${WIN_ID} -e 0,100,100,$((WIN_WIDTH+44)),$((WIN_HEIGHT+44))
    wmctrl -i -a ${WIN_ID}
    gnome-screenshot --window --clipboard --delay 1 --file tmp.png; rm tmp.png
    xclip -selection clipboard -t image/png -o | convert - -crop ${WIN_WIDTH}x${WIN_HEIGHT}+22+19 "screenshot-$(date +%s)".png
else
    echo "export WIN_ID=..."
    wmctrl -l
fi

wmctrlでウィンドウを指定する際には、IDで指定するようにしている。 これで、以下のようにウィンドウの周辺の透明な領域がないスクリーンショットを取得できた。

f:id:redj:20220319013216p:plain

補足

gnome-screenshotでとったスクリーンショットクリップボードに転送したかったが、うまくいかず。 以下の情報より、ダミーの画像ファイルtmp.pngを作成することで、クリップボードへの転送ができた。

askubuntu.com

以前に「--clipboardと--fileを同時に使えない」というバグがあって修正されたらしいが、これがきっかけで別のバグを作ったのではないかと思う。。。

bugs.launchpad.net

Flaskアプリのデバッグではloggerを使う

Flaskでアプリを作成している時に、デバッグ情報をprintで出力しようとしたが、出力されなかった。 調べてみると、出力がバッファリングされるようなので、以下のような感じでloggerを使うのがよいとのこと。

from flask import Flask
import logging

app = Flask(__name__)
app.logger.setLevel(logging.INFO)

以下を参考にした。

hawksnowlog.blogspot.com

PythonにおけるMixin

PythonにおけるMixinについて、以下のブログの説明が分かりやすかった。

www.thedigitalcatonline.com

Mixinの説明を以下に引用する。

Python doesn't provide support for mixins with any dedicated language feature, so we use multiple inheritance to implement them. This clearly requires great discipline from the programmer, as it violates one of the main assumptions for mixins: their orthogonality to the inheritance tree. In Python, so-called mixins are classes that live in the normal inheritance tree, but they are kept small to avoid creating hierarchies that are too complicated for the programmer to grasp. In particular, mixins shouldn't have common ancestors other than object with the other parent classes.

一部補足して意訳すると、以下のようになる。

PythonはMixinに対応するための専用の言語機能を提供していないため、Mixinを実装するには多重継承を代用することになります。ところが、何の制約もなく自由に多重継承を利用すると、Mixinの根幹をなす前提である継承ツリーの直交性を乱すことになるため、プログラマによる優れた規律(コーディングルール)が明らかに必要になります。Pythonにおいては、Mixinと呼ばれるものは通常の継承ツリーに現れるクラスですが、プログラマが把握不能に陥るような複雑な階層にならないように、そのクラスは小さく保つ必要があります。特に、Mixinは(埋め込み先のクラスに対して)object以外の他の親クラスを共通の祖先に持つべきではありません。

要するに「PythonではMixinは言語機能じゃなくてコーディングルール」ということのよう。