Help us understand the problem. What is going on with this article?

docker-composeでMongoDBのReplicaSetを構築する

背景

ScalaでMongoDBへMulti-document transaction接続するの続き. MongoDBへMulti-document transaction接続するためにはReplicaSetが必要になる. ReplicaSetを構築してみたけどMongoDBよく分からん.,, 試行錯誤して技術的に解決できない部分も含めてある程度腑に落ちたので整理してみた. Transactions

環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.5
BuildVersion:   18F132

$ docker --version
Docker version 19.03.1, build 74b1e89

$ docker-compose --version
docker-compose version 1.24.1, build 4667896b

ReplicaSet構成

最小構成のPrimary, Secondary, Arbiterとする. Replication

ReplicaSetのコンテナを定義する

Primary, Secondary, Arbiterをdocker-composeにて定義する. ディレクトリ構成, 各パラメータの意図は下記の通り.

$ tree
.
├── docker-compose.yml
└── volumes
    └── mongodb
        ├── docker-entrypoint-initdb.d
        ├── etc
        │   └── mongod-keyfile # Permission: 600
        └── root
            ├── 000_init_replicaSet.js
            ├── 001_init_database.js
            └── 002_init_user.js
  • Primaryコンテナ
    • 初期設定用のrootアカウントを定義する
    • 初期化スクリプトをVolume共有する
  • 各コンテナ
    • Multi-document transactionを使用したいのでMongoDBのVersionは4.xを指定する
    • HostOSのPort用に各MongoDBコンテナのPortを切り分ける
    • 認証鍵をVolume共有する
docker-compose.yml
version: '3'
services:

  # https://hub.docker.com/_/mongo
  mongodb-primary:
    image: mongo:4.2.0-bionic
    container_name: mongodb-primary
    hostname: mongodb-primary
    command: >
      mongod
      --port 27017
      --replSet replset
      --auth --keyFile /etc/mongod-keyfile
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: password
    volumes:
      - ./volumes/mongodb/root:/root:ro # Initialize script
      - ./volumes/mongodb/etc/mongod-keyfile:/etc/mongod-keyfile:ro # Permission: 600
    ports:
      - 27017:27017
    networks:
      - replset
    depends_on:
      - mongodb-secondary
      - mongodb-arbiter
    restart: on-failure

  # https://hub.docker.com/_/mongo
  mongodb-secondary:
    image: mongo:4.2.0-bionic
    container_name: mongodb-secondary
    hostname: mongodb-secondary
    command: >
      mongod
      --port 27018
      --replSet replset
      --auth --keyFile /etc/mongod-keyfile
    volumes:
      - ./volumes/mongodb/etc/mongod-keyfile:/etc/mongod-keyfile:ro # Permission: 600
    ports:
      - 27018:27018
    networks:
      - replset
    depends_on:
      - mongodb-arbiter
    restart: on-failure

  # https://hub.docker.com/_/mongo
  mongodb-arbiter:
    image: mongo:4.2.0-bionic
    container_name: mongodb-arbiter
    hostname: mongodb-arbiter
    command: >
      mongod
      --port 27019
      --replSet replset
      --auth --keyFile /etc/mongod-keyfile
    volumes:
      - ./volumes/mongodb/etc/mongod-keyfile:/etc/mongod-keyfile:ro # Permission: 600
    ports:
      - 27019:27019
    networks:
      - replset
    restart: on-failure

networks:
  replset:
    ipam:
      config:
      - subnet: 192.168.1.0/24 # Any

初期化スクリプトを作成する

下記の技術的課題により各コンテナ起動後に初期化スクリプトにてReplicaSetを構築するアプローチを取る.

ReplicaSet初期化スクリプト

000_init_replSet.js
rs.initiate(    {   _id : "replset"
                ,   members:    [   {   _id: 0,   host: "mongodb-primary:27017"    }
                                ,   {   _id: 1,   host: "mongodb-secondary:27018"  }
                                ,   {   _id: 2,   host: "mongodb-arbiter:27019",   arbiterOnly: true }
                                ]
                }
);

データベース初期化スクリプト

001_init_database.js
var testdb = db.getSiblingDB('test');
testdb.createCollection('test');

JavaScript内でMongoDB shellのuse <database>が使用できないためdb.getSiblingDB(<database>)を使用する.

ユーザー初期化スクリプト

002_init_user.js
var testdb = db.getSiblingDB('test');
testdb.createUser(   {   user:   'test'
                     ,   pwd:    'password'
                     ,   roles:  [   {   role:   'root'
                                     ,   db:     'admin'
                                     }
                                 ,   {   role:   'dbOwner'
                                     ,   db:     'test'
                                     }
                                 ]
                     }
);
testdb.getUsers();

ReplicaSetの認証鍵を作成する

$ openssl rand -base64 756 > mongod-keyfile
$ chmod 600 mongod-keyfile

--auth --keyfile <path-to-keyfile>オプションを指定せずにReplicaSetを構築すると認証エラーが発生したので公式に倣って認証鍵を設定する. Update Replica Set to Keyfile Authentication

hostsを定義する

$ sudo vim /private/etc/hosts
+127.0.0.1 mongodb-primary
+127.0.0.1 mognodb-secondary
+127.0.0.1 mongodb-arbiter

HostOS側にClientがいる環境だったので名前解決のために /private/etc/hosts に追加定義する. ClientがDockerNetwork内に共存するならDockerDNSで名前解決できるので /private/etc/hosts への追加定義は必要ない. コンテナの DNS を設定 Dokcer-docs-ja

MongoDBのReplicaSetを立ち上げる

初期化スクリプト, 認証鍵をディレクトリ構成の配置に格納する.

コンテナを起動する.

$ docker-compose up -d

Primaryコンテナで初期化スクリプトを実行する.

$ docker-compose exec mongodb-primary mongo admin -u root -p password /root/000_init_replSet.js
$ docker-compose exec mongodb-primary mongo admin -u root -p password /root/001_init_database.js
$ docker-compose exec mongodb-primary mongo admin -u root -p password /root/002_init_user.js

docker-compose downコマンドでdocker-composeをもとに構築された全てを削除できる.

MongoDBのReplicaSetに接続する

MongoDB Scala Driverに下記URIを指定して接続できることを確認できた. MongoDB Scala Driver

test.infrastructure.mongodb.MongoDBConnector.scala
package test.infrastructure.mongodb

import com.typesafe.config.ConfigFactory
import org.mongodb.scala.MongoClient

object MongoDBConnector {
  private val config          = ConfigFactory.load
  private lazy val mongodbUri = config.getString("infrastructure.mongodb.uri")
  val client                  = MongoClient(mongodbUri)
  // .close
}
application.conf
infrastructure {

  mongodb {
    # username: test
    # password: password
    # database: test
    # host: mongodb-primary:27017, mongodb-secondary:27018
    # replicaSet: replset
    uri = "mongodb://test:password@mongodb-primary:27017,mongodb-secondary:27018/?authSource=test&replicaSet=replset"
  }

}
[info] No tests to run for Test / testOnly
01:40:59.028 [pool-7-thread-2-ScalaTest-running-UsecaseSpec] INFO org.mongodb.driver.cluster - Cluster created with settings {hosts=[mongodb-primary:27017, mongodb-secondary:27018], mode=MULTIPLE, requiredClusterType=REPLICA_SET, serverSelectionTimeout='30000 ms', maxWaitQueueSize=500, requiredReplicaSetName='replset'}
01:40:59.033 [pool-7-thread-2-ScalaTest-running-UsecaseSpec] INFO org.mongodb.driver.cluster - Adding discovered server mongodb-primary:27017 to client view of cluster
01:40:59.098 [pool-7-thread-2-ScalaTest-running-UsecaseSpec] INFO org.mongodb.driver.cluster - Adding discovered server mongodb-secondary:27018 to client view of cluster
01:40:59.106 [pool-7-thread-2-ScalaTest-running-UsecaseSpec] DEBUG org.mongodb.driver.cluster - Updating cluster description to  {type=REPLICA_SET, servers=[{address=mongodb-secondary:27018, type=UNKNOWN, state=CONNECTING}, {address=mongodb-primary:27017, type=UNKNOWN, state=CONNECTING}]
01:40:59.157 [pool-7-thread-2-ScalaTest-running-TuneUsecaseSpec] INFO org.mongodb.driver.cluster - No server chosen by com.mongodb.async.client.ClientSessionHelper$1@d54dba6 from cluster description ClusterDescription{type=REPLICA_SET, connectionMode=MULTIPLE, serverDescriptions=[ServerDescription{address=mongodb-secondary:27018, type=UNKNOWN, state=CONNECTING}, ServerDescription{address=mongodb-primary:27017, type=UNKNOWN, state=CONNECTING}]}. Waiting for 30000 ms before timing out
01:40:59.219 [cluster-ClusterId{value='5d558b1b9f1f417c08b4a869', description='null'}-mongodb-secondary:27018] INFO org.mongodb.driver.connection - Opened connection [connectionId{localValue:2, serverValue:17}] to mongodb-secondary:27018
01:40:59.220 [cluster-ClusterId{value='5d558b1b9f1f417c08b4a869', description='null'}-mongodb-primary:27017] INFO org.mongodb.driver.connection - Opened connection [connectionId{localValue:1, serverValue:23}] to mongodb-primary:27017
01:40:59.276 [cluster-ClusterId{value='5d558b1b9f1f417c08b4a869', description='null'}-mongodb-secondary:27018] INFO org.mongodb.driver.cluster - Monitor thread successfully connected to server with description ServerDescription{address=mongodb-secondary:27018, type=REPLICA_SET_SECONDARY, state=CONNECTED, ok=true, version=ServerVersion{versionList=[4, 2, 0]}, minWireVersion=0, maxWireVersion=8, maxDocumentSize=16777216, logicalSessionTimeoutMinutes=30, roundTripTimeNanos=49927070, setName='replset', canonicalAddress=mongodb-secondary:27018, hosts=[mongodb-secondary:27018, mongodb-primary:27017], passives=[], arbiters=[mongodb-arbiter:27019], primary='mongodb-primary:27017', tagSet=TagSet{[]}, electionId=null, setVersion=1, lastWriteDate=Fri Aug 16 01:40:50 JST 2019, lastUpdateTimeNanos=133745025150196}
01:40:59.276 [cluster-ClusterId{value='5d558b1b9f1f417c08b4a869', description='null'}-mongodb-primary:27017] INFO org.mongodb.driver.cluster - Monitor thread successfully connected to server with description ServerDescription{address=mongodb-primary:27017, type=REPLICA_SET_PRIMARY, state=CONNECTED, ok=true, version=ServerVersion{versionList=[4, 2, 0]}, minWireVersion=0, maxWireVersion=8, maxDocumentSize=16777216, logicalSessionTimeoutMinutes=30, roundTripTimeNanos=49535877, setName='replset', canonicalAddress=mongodb-primary:27017, hosts=[mongodb-secondary:27018, mongodb-primary:27017], passives=[], arbiters=[mongodb-arbiter:27019], primary='mongodb-primary:27017', tagSet=TagSet{[]}, electionId=7fffffff0000000000000001, setVersion=1, lastWriteDate=Fri Aug 16 01:40:50 JST 2019, lastUpdateTimeNanos=133745025119925}
01:40:59.279 [cluster-ClusterId{value='5d558b1b9f1f417c08b4a869', description='null'}-mongodb-primary:27017] INFO org.mongodb.driver.cluster - Adding discovered server mongodb-arbiter:27019 to client view of cluster
01:40:59.280 [cluster-ClusterId{value='5d558b1b9f1f417c08b4a869', description='null'}-mongodb-primary:27017] INFO org.mongodb.driver.cluster - Setting max election id to 7fffffff0000000000000001 from replica set primary mongodb-primary:27017
01:40:59.281 [cluster-ClusterId{value='5d558b1b9f1f417c08b4a869', description='null'}-mongodb-primary:27017] INFO org.mongodb.driver.cluster - Setting max set version to 1 from replica set primary mongodb-primary:27017
01:40:59.281 [cluster-ClusterId{value='5d558b1b9f1f417c08b4a869', description='null'}-mongodb-primary:27017] INFO org.mongodb.driver.cluster - Discovered replica set primary mongodb-primary:27017
01:40:59.293 [cluster-ClusterId{value='5d558b1b9f1f417c08b4a869', description='null'}-mongodb-arbiter:27019] INFO org.mongodb.driver.connection - Opened connection [connectionId{localValue:3, serverValue:13}] to mongodb-arbiter:27019
01:40:59.304 [cluster-ClusterId{value='5d558b1b9f1f417c08b4a869', description='null'}-mongodb-arbiter:27019] INFO org.mongodb.driver.cluster - Monitor thread successfully connected to server with description ServerDescription{address=mongodb-arbiter:27019, type=REPLICA_SET_ARBITER, state=CONNECTED, ok=true, version=ServerVersion{versionList=[4, 2, 0]}, minWireVersion=0, maxWireVersion=8, maxDocumentSize=16777216, logicalSessionTimeoutMinutes=30, roundTripTimeNanos=11086151, setName='replset', canonicalAddress=mongodb-arbiter:27019, hosts=[mongodb-secondary:27018, mongodb-primary:27017], passives=[], arbiters=[mongodb-arbiter:27019], primary='mongodb-primary:27017', tagSet=TagSet{[]}, electionId=null, setVersion=1, lastWriteDate=Fri Aug 16 01:40:50 JST 2019, lastUpdateTimeNanos=133745058351796}
01:41:02.724 [anInnocuousThread] INFO org.mongodb.driver.connection - Opened connection [connectionId{localValue:4, serverValue:24}] to mongodb-primary:27017

所感

docker-compose up -dも含めた管理スクリプトを書けば"ポチッとな"と楽ちんになりそうだけど何かピンとこないから保留中. ちゃんちゃん.

参考文献

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away