本記事は「Rookと仲間たち、クラウドネイティブなストレージの Advent Calendar 2020」の3日目の記事となります。よろしくお願いします。
前置き
こばさんにそそのかされて、夏ごろに仕事でRook/Cephのオブジェクトストレージを使ったアプリデプロイの機会があったのですが、当時は構築がうまくいかないまま別の作業にさくっと再アサインされて中途半端なままだったので、アドベントカレンダーへのエントリをきっかけに改めてRook/Cephのオブジェクトストレージを試してみました。
GROWIというWikiツールがあるのですがご存じでしょうか。
Markdownで記述でき、画像は(アップロード先S3などの設定を行えば)D&Dやクリップボードから簡単に貼り付けられ、割とサクサク軽快に動作するOSSのWikiツールです。
QiitaやVS CodeのMarkdown Previewの、左側で編集・右側にリアルタイムプレビューの2画面構成に慣れている方は、とくに使いやすいと思います。
デモサイトもあります。
GitHubのページでは使ってる様子をアニメーションで見れるのでわかりやすいです。
SaaSとして提供されているのですが、実はDockerイメージも公開されており、Docker Composeを使用することで、手元の環境で簡単にデプロイすることができます。(https-portalも付属しているので、Let's Encryptを使ったHTTPS公開も簡単にできます)
本記事はこのGROWIを使って
- Rook/Cephのデプロイ
- GROWIのDBであるMongoDBでRook/Cephのブロックストレージを使う
- GROWIで画像アップロードのためのAmazon S3のかわりにRook/Cephのオブジェクトストレージを使う
ということをやって、ストレージ含めてKubernetes上で完結するGROWI Wikiを動かしてみようという内容になります。
環境
以下の通りです。
- Kubernetes v1.19.4
- kubeadmを使って素のCentOS 7.9上に構築
- いわゆるオンプレKubernetes
- コントロールプレーン1台・ワーカー2台構成
- MetalLB v0.9.3 (最新はv0.9.5)
- Rook v1.5.1
- GROWI 4.2
MetalLBについては本記事では説明しません。以下参照。
[Kubernetes] オンプレK8sでもtype:LoadBalancer Serviceが使えるようになるMetalLBを入れてみた - zaki work log
GROWIで使用するイメージ
Kubernetes用のマニフェスト類は用意されていないので、docker-compose.ymlの内容ベースにポーティングしていきます。
現バージョンで使用しているイメージは以下の通り。
アプリケーション | イメージ |
---|---|
app (GROWI本体) | 自前でビルド |
mongo (MongoDB) | mongo:4.4 |
elasticsearch (Elasticsearch) | 自前でビルド |
自前でビルドしているDockerfileの内容については以下の通り。
Dockerfile | 概要 |
---|---|
app |
weseek/growi:4 をベースにdockerizeをインストール |
elasticsearch |
docker.elastic.co/elasticsearch/elasticsearch:6.8.10 をベースに日本語検索系のプラグイン追加 |
dockerize
のインストールについては、MongoDBとElasticsearchの起動を待つために使用しているので、Kubernetes環境ではlivenessProbeで代用でもよさそうなので不要。
また、Elasticsearchは実はオプションなので、今回は省略。
よって、使うイメージは以下の2つ。
- GROWI本体
- MongoDB
Elasticsearch込みの構成については別途やってみる予定。
↓
書きました。
Skaffoldを使ってGROWI on K8s用のElasticsearchとHackMDをビルド&デプロイ - Qiita
Rook/Cephのデプロイ
つい先日v1.5がリリースされましたが、記事作成時点の最新バージョンはv1.5.1です。
Quickstart -> Ceph StorageのTL;DRの通りでよいです。
ポイントは以下の通り。
- 全ワーカーノードにOSにマウントされていないディスクを接続
-
crds.yaml
を使ってカスタムリソース作成 -
common.yaml
を使ってロールやアカウント類を作成 -
operator.yaml
を使ってOperatorを作成 -
cluster.yaml
を使ってCephクラスター作成- ※ 検証環境など小規模であれば代わりに
cluster-test.yaml
を使用
- ※ 検証環境など小規模であれば代わりに
Rook Operatorをデプロイする
前述ポイント2~4のマニフェストに対してkubectl create -f
でリソースを作成します。
[zaki@cloud-dev ceph]$ kubectl get pod -n rook-ceph
NAME READY STATUS RESTARTS AGE
rook-ceph-operator-78554769bd-9dw5f 1/1 Running 0 23s
ちなみにこのRook Operatorのデプロイは、Helmを使ってデプロイもできます。
Cephクラスタを作成する
前述ポイント5のリソース作成。今回はcluster-test.yaml
を使用します。
[zaki@cloud-dev ceph]$ kc get pod -n rook-ceph
NAME READY STATUS RESTARTS AGE
csi-cephfsplugin-6msxk 0/3 ContainerCreating 0 45s
csi-cephfsplugin-provisioner-7dc78747bf-5plfv 0/6 ContainerCreating 0 45s
csi-cephfsplugin-provisioner-7dc78747bf-w7ml5 6/6 Running 0 45s
csi-cephfsplugin-vdxwx 3/3 Running 0 45s
csi-rbdplugin-fffpb 3/3 Running 0 46s
csi-rbdplugin-provisioner-54d48757b4-l2pxr 0/6 ContainerCreating 0 46s
csi-rbdplugin-provisioner-54d48757b4-wcx5c 6/6 Running 0 46s
csi-rbdplugin-r7dt7 0/3 ContainerCreating 0 46s
rook-ceph-mgr-a-679f49c584-qfnqp 1/1 Running 0 87s
rook-ceph-mon-a-6776558969-9cxnm 1/1 Running 0 97s
rook-ceph-operator-78554769bd-9dw5f 1/1 Running 0 4m44s
rook-ceph-osd-0-675958d76b-bzt6f 1/1 Running 0 68s
rook-ceph-osd-1-f88668f5d-fb4dm 1/1 Running 0 57s
rook-ceph-osd-prepare-k8s-worker01.esxi.jp-z.jp-g56gz 0/1 Completed 0 85s
rook-ceph-osd-prepare-k8s-worker02.esxi.jp-z.jp-dffhx 0/1 Completed 0 85s
これは別ターミナルでkubectl get pod -n rook-ceph -w
しておくと楽しい。
Cephクラスタが作成されたらCephClusterリソースをget
するとヘルス状態を確認できます。HEALTH_OK
であればOK
[zaki@cloud-dev ceph]$ kubectl -n rook-ceph get cephcluster
NAME DATADIRHOSTPATH MONCOUNT AGE PHASE MESSAGE HEALTH
my-cluster /var/lib/rook 1 2m57s Ready Cluster created successfully HEALTH_OK
Rook OperatorをHelmでインストールした場合でも、Cephクラスタの作成はチャートが用意されてないためマニフェストからリソース生成します。
pvを使用するためのStorageClassを作成
今回はMongoDBでpvを使用します。
pvをRook/Cephから動的にプロビジョニングするためにStorageClassを作成します。
StorageClassのマニフェストはcsi/rbd/storageclass.yaml
にあるのでそれを使います。
が、今回は検証環境なので、csi/rbd/storageclass-test.yaml
を使用。
[zaki@cloud-dev ceph]$ kubectl create -f csi/rbd/storageclass-test.yaml
cephblockpool.ceph.rook.io/replicapool created
storageclass.storage.k8s.io/rook-ceph-block created
[zaki@cloud-dev ceph]$ kubectl get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
rook-ceph-block rook-ceph.rbd.csi.ceph.com Delete Immediate true 5s
オブジェクトストレージ
前置きが長くなりましたが、本記事のメインです。
今回はGROWIの画像アップロード用に使用します。
オブジェクトストアの作成
まずはCephObjectStoreリソースを作成します。
object.yaml
が用意されていますが、こちらも検証用にobject-test.yaml
をデプロイします。
[zaki@cloud-dev ceph]$ kubectl apply -f object-test.yaml
cephobjectstore.ceph.rook.io/my-store created
[zaki@cloud-dev ceph]$ kubectl get cephobjectstore -n rook-ceph
NAME AGE
my-store 29s
少し経つと、CephのRGWサービスのpodとserviceが作成されます。
これがS3 API互換のオブジェクトストレージのデータストアになります。(という表現で合ってると思うけど…たぶん。。)
[zaki@cloud-dev ceph]$ kubectl get pod,svc -n rook-ceph -l ceph_daemon_type=rgw
NAME READY STATUS RESTARTS AGE
pod/rook-ceph-rgw-my-store-a-d7857c998-8sp6g 1/1 Running 0 116s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/rook-ceph-rgw-my-store ClusterIP 10.97.160.63 <none> 80/TCP 2m39s
バケットの作成
Amazon S3でもバケットを作ってオブジェクトストレージを利用しますが、Rook/Cephでも同じようにバケットを作成します。
この「バケットの作成」は、Rook/Ceph環境では「StorageClassの作成」によって行われます。
storageclass-bucket-delete.yaml
とstorageclass-bucket-retain.yaml
が用意されているので、今回はdeleteの方を使ってみます。
[zaki@cloud-dev ceph]$ kubectl apply -f storageclass-bucket-delete.yaml
storageclass.storage.k8s.io/rook-ceph-delete-bucket created
[zaki@cloud-dev ceph]$ kubectl get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
rook-ceph-block rook-ceph.rbd.csi.ceph.com Delete Immediate true 28m
rook-ceph-delete-bucket rook-ceph.ceph.rook.io/bucket Delete Immediate false 5s
これでストレージ側の準備ができました。
…と言いたいとこですが、このバケットに対するClaimも作成します。
PersistentVolumeに対するPersistentVolumeClaimと同じようなものです。
ObjectBucketClaimの作成
PersistentVolumeに対するPersistentVolumeClaimに相当する、オブジェクトストア用StorageClassに要求を出すためのObjectBucketClaimを作成します。
ObjectBucketClaimを作成することで、AWS S3と同様にRook/Ceph上のバケットにアクセスするためのエンドポイントやアクセスキーが提供されます。
object-bucket-claim-delete.yaml
とobject-bucket-claim-retain.yaml
が用意されています。今回はバケット作成時にdeleteを使用しているので、ここでもdeleteを使います。
なお、このリソースはPersistentVolumeClaimと同様に、バケットを使用するアプリケーションのネームスペースに作成します。
なので、この後のGROWIをデプロイするためのネームスペースをここで作っておきます。
[zaki@cloud-dev ceph]$ kubectl create namespace growi-on-k8s
namespace/growi-on-k8s created
ObjectBucketClaimを作成すると、ConfigMapとSecretリソースも作成されます。
[zaki@cloud-dev ceph]$ kubectl apply -f object-bucket-claim-delete.yaml -n growi-on-k8s
objectbucketclaim.objectbucket.io/ceph-delete-bucket created
[zaki@cloud-dev ceph]$ kubectl get objectbucket -n growi-on-k8s
NAME AGE
obc-growi-on-k8s-ceph-delete-bucket 3s
[zaki@cloud-dev ceph]$ kubectl get cm,secret -n growi-on-k8s
NAME DATA AGE
configmap/ceph-delete-bucket 5 15s
configmap/istio-ca-root-cert 1 3m28s
NAME TYPE DATA AGE
secret/ceph-delete-bucket Opaque 2 15s
secret/default-token-mp6kq kubernetes.io/service-account-token 3 3m28s
[zaki@cloud-dev ceph]$ kc get cm -n growi-on-k8s ceph-delete-bucket -o yaml
apiVersion: v1
data:
BUCKET_HOST: rook-ceph-rgw-my-store.rook-ceph.svc
BUCKET_NAME: ceph-bkt-4e095ccf-dbb0-41b6-babc-e646d441d548
BUCKET_PORT: "80"
BUCKET_REGION: us-east-1
BUCKET_SUBREGION: ""
kind: ConfigMap
metadata:
[zaki@cloud-dev ceph]$ kc get secrets -n growi-on-k8s ceph-delete-bucket -o yaml
apiVersion: v1
data:
AWS_ACCESS_KEY_ID: NThMME1OU1UxNzJUQjdOQjU0N0k=
AWS_SECRET_ACCESS_KEY: MlFneDdmOHNTNFFqU0E5bmZvdG5EWTVjRklQVmhjNlVaa2VwRHlVSQ==
kind: Secret
metadata:
S3を使ったことがある方であれば、このConfigMapとSecretの内容で何をどう使えばよいかわかると思いますが、アプリケーションでS3の設定を行う箇所でこれらの値を設定すればOKです。
※ Secretの値は、Base64エンコードされているので、base64 -d
などでデコードして利用すること
GROWIのデプロイ
いよいよオブジェクトストレージを使用するアプリケーション部分です。
再掲ですが、使用するイメージは以下の通り。
- GROWI本体
- MongoDB
また、作成したマニフェストファイルは、GitHubに置いています。
MongoDB
ストレージ使うのでStatefulSetで定義します。
何気に私がStatefulSetをゼロから書くのが今回が初めてだったりします。(余談)
Docker ComposeではVolumes設定が2か所で行われているので、それぞれのパスに対してvolumeClaimTemplates
の設定を行います。
mongo:
image: mongo:4.4
restart: unless-stopped
volumes:
- mongo_configdb:/data/configdb
- mongo_db:/data/db
containers:
- name: growi-mongo
image: mongo:4.4
volumeMounts:
- name: mongo-config
mountPath: /data/configdb
- name: mongo-db
mountPath: /data/db
volumeClaimTemplates:
- metadata:
name: mongo-config
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: rook-ceph-block
resources:
requests:
storage: 256Mi
- metadata:
name: mongo-db
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: rook-ceph-block
resources:
requests:
storage: 4Gi
GROWI本体
Docker Composeの設定をベースに環境変数をDeploymentに設定します。
また、起動チェックのためにLivenessProbeも追加します。
[zaki@cloud-dev growi-on-k8s]$ kubectl apply -f manifests/growi-on-k8s.yaml -n growi-on-k8s
deployment.apps/growi-app created
service/growi-app created
statefulset.apps/growi-mongo created
service/growi-mongo created
[zaki@cloud-dev growi-on-k8s]$ kubectl get pod,svc -n growi-on-k8s
NAME READY STATUS RESTARTS AGE
pod/growi-app-fd6fcdd9d-vmtn2 1/1 Running 1 108s
pod/growi-mongo-0 1/1 Running 0 108s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/growi-app LoadBalancer 10.110.19.195 192.168.0.185 3000:32510/TCP 108s
service/growi-mongo ClusterIP 10.111.134.164 <none> 27017/TCP 108s
[zaki@cloud-dev growi-on-k8s]$ kubectl get pvc -n growi-on-k8s
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
mongo-config-growi-mongo-0 Bound pvc-6b29acb4-bad6-4b01-9135-4302bf1f35d3 256Mi RWO rook-ceph-block 87s
mongo-db-growi-mongo-0 Bound pvc-21ea3319-5487-4b10-83d3-a9c076d30b28 4Gi RWO rook-ceph-block 87s
[zaki@cloud-dev growi-on-k8s]$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-21ea3319-5487-4b10-83d3-a9c076d30b28 4Gi RWO Delete Bound growi-on-k8s/mongo-db-growi-mongo-0 rook-ceph-block 88s
pvc-6b29acb4-bad6-4b01-9135-4302bf1f35d3 256Mi RWO Delete Bound growi-on-k8s/mongo-config-growi-mongo-0 rook-ceph-block 88s
動いてますね。
webアクセスと初期設定
この構成ではMetalLBを使ってtype:LoadBalancer
のServiceをデプロイできるようにしているので、表示されているExternal-IPへブラウザでアクセスします。(ない場合はport-forward
などを使用してください)
上の例でいえば
[zaki@cloud-dev growi-on-k8s]$ kc get svc -n growi-on-k8s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
growi-app LoadBalancer 10.110.19.195 192.168.0.185 3000:32510/TCP 5m13s
growi-mongo ClusterIP 10.111.134.164 <none> 27017/TCP 5m13s
なので、ブラウザで http://192.168.0.185:3000 にアクセスします。
無事に起動・接続できました。
以下はGROWIの使い方になりますが、初期アクセス時には管理ユーザーを作成するので、好きなUserIDを入力してアカウント作成・ログインします。
ログインすると初期設定があり、Wiki名・ドメイン名(今回はIPアドレスしかないので、IPアドレスを入力)などを入力します。
ちなみに初期状態だと、Wikiの編集画面へ画像ファイルをD&Dしようとしても、「File uploading is disabled」と表示されている通りアップロードはできません。
S3バケット設定
S3バケット設定は、Wiki管理メニューの「アプリ設定」の「ファイルアップロード設定」の以下の場所。
この各設定欄に、ObjectBucketClaimによって作成されたConfigMapとSecretの内容を入力して「更新」ボタン押下します。
[zaki@cloud-dev ceph]$ kc get cm -n growi-on-k8s ceph-delete-bucket -o yaml
apiVersion: v1
data:
BUCKET_HOST: rook-ceph-rgw-my-store.rook-ceph.svc
BUCKET_NAME: ceph-bkt-4e095ccf-dbb0-41b6-babc-e646d441d548
BUCKET_PORT: "80"
BUCKET_REGION: us-east-1
BUCKET_SUBREGION: ""
kind: ConfigMap
metadata:
[zaki@cloud-dev ceph]$ kc get secrets -n growi-on-k8s ceph-delete-bucket -o yaml
apiVersion: v1
data:
AWS_ACCESS_KEY_ID: NThMME1OU1UxNzJUQjdOQjU0N0k=
AWS_SECRET_ACCESS_KEY: MlFneDdmOHNTNFFqU0E5bmZvdG5EWTVjRklQVmhjNlVaa2VwRHlVSQ==
kind: Secret
metadata:
設定項目 | 入力値 |
---|---|
リージョン | us-east-1 |
カスタムエンドポイント | http://rook-ceph-rgw-my-store.rook-ceph.svc |
バケット名 | ceph-bkt-4e095ccf-dbb0-41b6-babc-e646d441d548 |
Access Key ID |
AWS_ACCESS_KEY_ID の値をBase64デコード |
Secret access key |
AWS_SECRET_ACCESS_KEY の値をBase64デコード |
アップロード用S3が設定済みであれば、編集画面へ画像ファイルをD&Dすると
この通り、アップロードできるようになります。
ceph df
toolbox.yaml
をデプロイすると、ceph
コマンドが使用できるようになります。
画像を1つアップロードした状態だとこの通り。
[zaki@cloud-dev ceph]$ kc exec -it -n rook-ceph $(kc get pod -n rook-ceph -l app=rook-ceph-tools -o 'jsonpath={.items[].metadata.name}') -- ceph df
--- RAW STORAGE ---
CLASS SIZE AVAIL USED RAW USED %RAW USED
ssd 40 GiB 38 GiB 103 MiB 2.1 GiB 5.25
TOTAL 40 GiB 38 GiB 103 MiB 2.1 GiB 5.25
--- POOLS ---
POOL ID STORED OBJECTS USED %USED MAX AVAIL
device_health_metrics 1 0 B 0 0 B 0 36 GiB
replicapool 3 101 MiB 64 101 MiB 0.27 36 GiB
my-store.rgw.control 4 3.9 KiB 8 3.9 KiB 0 36 GiB
my-store.rgw.meta 5 2.7 KiB 12 40 KiB 0 36 GiB
my-store.rgw.log 6 3.6 KiB 211 136 KiB 0 36 GiB
my-store.rgw.buckets.index 7 190 KiB 22 190 KiB 0 36 GiB
my-store.rgw.buckets.non-ec 8 0 B 0 0 B 0 36 GiB
.rgw.root 9 3.8 KiB 16 60 KiB 0 36 GiB
my-store.rgw.buckets.data 10 74 KiB 1 76 KiB 0 36 GiB
最後の行のmy-store.rgw.buckets.data
に、OBJECTS: 1
となってます。
画像の更に1ファイル追加でアップロードすると
--- RAW STORAGE ---
CLASS SIZE AVAIL USED RAW USED %RAW USED
ssd 40 GiB 38 GiB 104 MiB 2.1 GiB 5.25
TOTAL 40 GiB 38 GiB 104 MiB 2.1 GiB 5.25
--- POOLS ---
POOL ID STORED OBJECTS USED %USED MAX AVAIL
device_health_metrics 1 0 B 0 0 B 0 36 GiB
replicapool 3 102 MiB 64 102 MiB 0.28 36 GiB
my-store.rgw.control 4 3.9 KiB 8 3.9 KiB 0 36 GiB
my-store.rgw.meta 5 2.7 KiB 12 40 KiB 0 36 GiB
my-store.rgw.log 6 3.6 KiB 211 136 KiB 0 36 GiB
my-store.rgw.buckets.index 7 190 KiB 22 190 KiB 0 36 GiB
my-store.rgw.buckets.non-ec 8 0 B 0 0 B 0 36 GiB
.rgw.root 9 3.8 KiB 16 60 KiB 0 36 GiB
my-store.rgw.buckets.data 10 87 KiB 2 92 KiB 0 36 GiB
2
に増えました。
ちなみにこの数値はファイル数とイコールでなく、ファイルサイズが大きいと分割されるそうです。
RGWで置かれるデータはデフォルト4MiBでストライプされるので、14MiBのファイルは4つのオブジェクトにストライプされて置かれてるってことです。
「ストレージオーケストレーター Rook : 第8話 宿命のObject Bucket(Bパート)」より
https://rheb.hatenablog.com/entry/2019/12/05/rook_ep8
関連・参考情報
夏ごろはうまくいかなかったけど改めて試すとすんなり動いてやったー!という感じでしたが、去年のアドカレのうつぼさんの記事を見返してみると、やっていることはほぼほぼ同じでした(;'∀')
- [Kubernetes] オンプレK8sでもRook-Cephを使ってpvのダイナミックプロビジョニングを試してみる - zaki work log
- ストレージオーケストレーター Rook : 第7話 宿命のObject Bucket(Aパート) - 赤帽エンジニアブログ
- ストレージオーケストレーター Rook : 第8話 宿命のObject Bucket(Bパート) - 赤帽エンジニアブログ
- 「Ceph、完全に理解した」って Tweetする為のセッション ー Ceph 101 ー - Speaker Deck
それから、Rookのオブジェクトストレージ周りに動きがあったようで、遠くない未来には扱い方が変わるかもしれないようです。
もともとRookで使っていたPV,PVCのオブジェクトストレージ版ObjectBucket(OB)、ObjectBucketClaim(OBC)を標準化しようとしていろいろあって全然別のインタフェースになった、という経緯があります。つまりRookも数年後にはオブジェクトストレージを扱うインタフェースが変わります
— sat🚪 (@satoru_takeuchi) November 19, 2020