Kubernetes 上に MongoDB レプリカセット を 構築する 方法

0 件のコメント

今回は「Kubernetes 上 に MongoDB の レプリカセット を 構築する 方法」についてまとめます。

概要

MongoDB 3台構成 の レプリカセット を構築します。 また、レプリカセット構築にあたっては「キーファイル(--keyFile)」「認証(--auth)」「実行ユーザー変更(runAsUser)」も含めた内容になっています。 最後の StatefulSet がいろいろな情報を含んでしまって濁ってしまっていますが…3種類を含んでいることを踏まえて読んでいただければ理解しやすいと思います。

前提

今回の検証は以下のような環境で実施しています。

  • Kubernetes v1.14.2
  • Docker v18.09.6

PersistentVolume

今回は3台構成なのでデータ保存先も3か所用意します。 環境の都合で保存先は同じ NFS になっています。

  1. マニュフェスト作成

    mongo-pv.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: pv0001
      labels:
        type: nfs
    spec:
      capacity:
        storage: 3Gi
      storageClassName: slow
      accessModes: ["ReadWriteMany"]
      persistentVolumeReclaimPolicy: Retain
      nfs:
        server: 10.51.1.103
        path: /var/share/pv0001
     
    ---
    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: pv0002
      labels:
        type: nfs
    spec:
      capacity:
        storage: 3Gi
      storageClassName: slow
      accessModes: ["ReadWriteMany"]
      persistentVolumeReclaimPolicy: Retain
      nfs:
        server: 10.51.1.103
        path: /var/share/pv0002
     
    ---
    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: pv0003
      labels:
        type: nfs
    spec:
      capacity:
        storage: 3Gi
      storageClassName: slow
      accessModes: ["ReadWriteMany"]
      persistentVolumeReclaimPolicy: Retain
      nfs:
        server: 10.51.1.103
        path: /var/share/pv0003
  2. 適用

    1
    kubectl apply -f mongo-pv.yml
  3. 確認

    1
    kubectl get pv

Service

Podネットワーク中でサービス名を使った検索をするわけではないので、ClusterIP は不要になります。 必要なのはPodの名前からIPアドレスを引けるようにするため、ヘッドレスサービス を作成します。

  1. マニュフェスト作成

    mongo-svc.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    apiVersion: v1
    kind: Service
    metadata:
      name: mongo-svc
      labels:
        app: mongodb
    spec:
      ports:
        - port: 27017
          targetPort: 27017
      clusterIP: None               # Headless Service
      selector:
        app: mongodb                # match "StatefulSet spec.template.metadata.labels"

    HeadlessService は clusterIP に None を指定することで実現できます。

  2. 適用

    1
    kubectl apply -f mongo-svc.yml
  3. 確認

    1
    kubectl get svc

Secret

「ユーザー名、パスワード、キーファイル」を準備します。

  1. キーファイル作成

    1
    openssl rand -base64 768 > keyfile

    MongoDBのキーファイルは「6~1024文字」なのですが…1024文字で生成すると少しデータが膨らむようでうまく動きませんでした。 そのため文字数は少な目で生成します。

  2. Secretリソース作成

    1
    2
    3
    4
    kubectl create secret generic mongo-secret \
      --from-literal=root_username=admin \
      --from-literal=root_password=Passw0rd \
      --from-file=./keyfile
  3. 確認

    1
    kubectl get secrets

StatefulSet

とても残念なことに MongoDB の公式イメージを素のまま使おうとすると認証回りの初期化処理がうまく動きませんでした。。 仕方ないので、公式イメージのコマンドに対してシェルスクリプトを挟むことで回避するようにしています。 ただ…結果としてマニュフェストが大きくなってしまってます。。 実運用する場合はきちんとDockerイメージ作って運用したほうがよさそうです。

  1. マニュフェスト作成

    mongo-sts.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: mongo-sts
      namespace: default
      labels:
        app: mongodb
    spec:
      selector:
        matchLabels:
          app: mongodb
      serviceName: mongo-svc        # match "Service metadata.name"
      replicas: 3
      template:
        metadata:
          name: mongodb
          namespace: default
          labels:
            app: mongodb
            ver: "0.0.1"
            env: staging
        spec:
          terminationGracePeriodSeconds: 10
          containers:
            - name: mongodb
              image: mongo:4.0
              ports:
                - containerPort: 27017
              command:
                - "/bin/sh"
                - "-c"
              args:
                - |
                  INIT_FLAG_FILE=/data/db/init-completed
                  INIT_LOG_FILE=/data/db/init-mongodb.log
     
                  start_mongod_as_daemon() {
                  echo
                  echo "> start mongod ..."
                  echo
                  mongod \
                    --fork \
                    --logpath ${INIT_LOG_FILE} \
                    --quiet \
                    --bind_ip 127.0.0.1 \
                    --smallfiles;
                  }
     
                  create_user() {
                  echo
                  echo "> create user ..."
                  echo
                  mongo "${MONGO_INITDB_DATABASE}" <<-EOS
                    db.createUser({
                      user: "${MONGO_INITDB_ROOT_USERNAME}",
                      pwd: "${MONGO_INITDB_ROOT_PASSWORD}",
                      roles: [{ role: "root", db: "${MONGO_INITDB_DATABASE}" }]
                    })
                  EOS
                  }
     
                  create_initialized_flag() {
                  echo
                  echo "> create initialized flag file ..."
                  echo
                  cat <<-EOF > "${INIT_FLAG_FILE}"
                  [$(date +%Y-%m-%dT%H:%M:%S.%3N)] Initialize scripts is finished.
                  EOF
                  }
     
                  stop_mongod() {
                  echo
                  echo "> stop mongod ..."
                  echo
                  mongod --shutdown;
                  }
     
                   
                  start_mongod() {
                  mongod \
                    --auth \
                    --clusterAuthMode "keyFile" \
                    --keyFile "/home/mongodb/keyfile" \
                    --replSet "rs0" \
                    --bind_ip_all \
                    --smallfiles;
                  }
                   
                   
                  if [ ! -e ${INIT_FLAG_FILE} ]; then
                  echo
                  echo "--- Initialize MongoDB --------------"
                  echo
                  start_mongod_as_daemon
                  create_user
                  create_initialized_flag
                  fi
     
                  echo
                  echo "--- Start MongoDB -------------------"
                  echo
                  start_mongod
     
              env:
                - name: "MONGO_INITDB_ROOT_USERNAME"
                  valueFrom:
                    secretKeyRef:
                      name: mongo-secret
                      key: root_username
                - name: "MONGO_INITDB_ROOT_PASSWORD"
                  valueFrom:
                    secretKeyRef:
                      name: mongo-secret
                      key: root_password
                - name: "MONGO_INITDB_DATABASE"
                  value: "admin"
              volumeMounts:
                - name: mongo-secret-keyfile
                  mountPath: /etc/mongo/secrets
                  readOnly: true
                - name: volatile-volume
                  mountPath: /home/mongodb
                - name: nfs-storage
                  mountPath: /data/db
              securityContext:
                runAsUser: 999
                runAsGroup: 999
          initContainers:
            - image: centos:7
              name: mongo-init
              volumeMounts:
                - name: mongo-secret-keyfile
                  mountPath: /etc/mongo/secret
                - name: volatile-volume
                  mountPath: /home/mongodb
              command:
                - "/bin/sh"
                - "-c"
              args:
                - |
                  cp /etc/mongo/secret/keyfile /home/mongodb/keyfile
                  chown 999:999 /home/mongodb/keyfile
                  chmod 700 /home/mongodb/keyfile
                  exit
          securityContext:
            fsGroup: 999
          volumes:
            - name: mongo-secret-keyfile
              secret:
                secretName: mongo-secret
                items:
                  - key: keyfile
                    path: keyfile
                    mode: 384
            - name: volatile-volume
              emptyDir: {}
      volumeClaimTemplates:
        - metadata:
            name: nfs-storage
          spec:
            storageClassName: slow
            accessModes:
              - ReadWriteMany
            resources:
              requests:
                storage: 3Gi

    L.12
    serviceName は 作成済みの HeadlessService 名 と同じ名前を指定してあげます。 ここでサービス名が異なっているとサービス利用してPod名を解決できません(=DNSにレコードが登録されない)。

    L.34-102
    DBの初期化で利用するユーザー名、パスワードは 作成済の Secretリソース から取得します。 こうすることで、機微な情報をリポジトリにコミットせずにすみます。 MongoDB公式の初期化処理がいまいちなので…結局このあたりの処理は自前実装しています。

    L.128, L.146, L.154
    keyfile の パーミッションは実行ユーザーのみに限定しないと MongoDB が起動しません。 これを実現するためには、 mode に 10進数 か 8進数 でパーミッションを指定し、fsGroupmongodb (UID=999) を指定します。 本来であればこの設定(modefsGroup)のみでうまく動いてほしいのですが… Secrets と ConfigMap はマウントする際、強制的に「root 所有の 読み取り専用、フルアクセス」になってしまいます。 そのため、 initContainersemptyDir の組み合わせ技で中間処理を挟んで適切な所有者とアクセス権になるよう修正します。

    L.161, 162, 163, 166
    volumeClaimTemplates は作成した PersistentVolume にあわせて指定します。 一致しなければいけないのは storageClassNameaccessModesstorage は PersistentVolume より小さい値であればOKです。

  2. 適用

    1
    kubectl apply -f mongo-sts.yml.yml
  3. 確認

    1
    kubectl get sts,po

初期設定

前述までの実装ができていれば MongoDB サーバー が 3台 立ち上がっているはずです。 まだ単独で動作しているだけの状態なので、レプリカセットとしての初期化を行っていきます。

  1. いずれかのPodに入る

    1
    [root@k8s-master ~]# kubectl exec -it mongo-sts-0 -- sh
  2. MongoDBに接続

    1
    # mongo mongo-sts-0.mongo-svc
  3. adminデータベースにログイン

    1
    2
    > use admin
    > db.auth("admin", "Passw0rd")
  4. レプリカセットを初期化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    > config = {
       _id : "rs0",
       members: [
          { _id: 0, host: "mongo-sts-0.mongo-svc:27017" },
          { _id: 1, host: "mongo-sts-1.mongo-svc:27017" },
          { _id: 2, host: "mongo-sts-2.mongo-svc:27017" },
       ]
    }
    > rs.initiate(config)

動作確認

動作確認は MongoDB クラスタに接続して以下のようなコマンドを実行することで状態確認してみます。

1
> rs.status()

今回は「Kubernetes 上 に MongoDB の レプリカセット を 構築する 方法」についてまとめました。 参考になったでしょうか? 本記事がお役に立っていると嬉しいです!!

最後に… このブログに興味を持っていただけた方は、 ぜひ 「Facebookページ に いいね!」または 「Twitter の フォロー」 お願いします!!