18
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Rookと仲間たち、クラウドネイティブなストレージのAdvent Calendar 2020

Day 3

Rook/Cephのブロックストレージとオブジェクトストレージを使ってGROWI WikiをKubernetesで動かす

Last updated at Posted at 2020-12-02

本記事は「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を使って

  1. Rook/Cephのデプロイ
  2. GROWIのDBであるMongoDBでRook/Cephのブロックストレージを使う
  3. 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つ。

Elasticsearch込みの構成については別途やってみる予定。

書きました。
Skaffoldを使ってGROWI on K8s用のElasticsearchとHackMDをビルド&デプロイ - Qiita

Rook/Cephのデプロイ

つい先日v1.5がリリースされましたが、記事作成時点の最新バージョンはv1.5.1です。
Quickstart -> Ceph StorageのTL;DRの通りでよいです。

ポイントは以下の通り。

  1. 全ワーカーノードにOSにマウントされていないディスクを接続
  2. crds.yamlを使ってカスタムリソース作成
  3. common.yamlを使ってロールやアカウント類を作成
  4. operator.yamlを使ってOperatorを作成
  5. cluster.yamlを使ってCephクラスター作成

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

Rook Docs / Block Storage

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の画像アップロード用に使用します。

Rook Docs / Object Storage

オブジェクトストアの作成

まずは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.yamlstorageclass-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.yamlobject-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のデプロイ

いよいよオブジェクトストレージを使用するアプリケーション部分です。

再掲ですが、使用するイメージは以下の通り。

また、作成したマニフェストファイルは、GitHubに置いています。

MongoDB

ストレージ使うのでStatefulSetで定義します。
何気に私がStatefulSetをゼロから書くのが今回が初めてだったりします。(余談)

Docker ComposeではVolumes設定が2か所で行われているので、それぞれのパスに対してvolumeClaimTemplatesの設定を行います。

docker-compose.yml
  mongo:
    image: mongo:4.4
    restart: unless-stopped
    volumes:
      - mongo_configdb:/data/configdb
      - mongo_db:/data/db
growi-on-k8s.yaml
      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 にアクセスします。

image.png

無事に起動・接続できました。

以下はGROWIの使い方になりますが、初期アクセス時には管理ユーザーを作成するので、好きなUserIDを入力してアカウント作成・ログインします。
ログインすると初期設定があり、Wiki名・ドメイン名(今回はIPアドレスしかないので、IPアドレスを入力)などを入力します。

ちなみに初期状態だと、Wikiの編集画面へ画像ファイルをD&Dしようとしても、「File uploading is disabled」と表示されている通りアップロードはできません。

2020-12-02_09h41_58.png

S3バケット設定

S3バケット設定は、Wiki管理メニューの「アプリ設定」の「ファイルアップロード設定」の以下の場所。

image.png

この各設定欄に、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すると

2020-12-02_09h41_05.png

この通り、アップロードできるようになります。

2020-11-27_22h42_52.png

ceph df

Rook Docs / Rook Toolbox

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

関連・参考情報

夏ごろはうまくいかなかったけど改めて試すとすんなり動いてやったー!という感じでしたが、去年のアドカレのうつぼさんの記事を見返してみると、やっていることはほぼほぼ同じでした(;'∀')

それから、Rookのオブジェクトストレージ周りに動きがあったようで、遠くない未来には扱い方が変わるかもしれないようです。

18
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?