Trong bài viết này mình sẽ giới thiệu cách thiết lập CSDL (cơ sở dữ liệu) mongo thông qua docker.

Bài viết gồm các phần

  • Thiết lập máy chủ CSDL không yêu cầu mật khẩu (đơn giản)
  • Thiết lập máy chủ CSDL yêu cầu mật khẩu
  • Cấu hình mật khẩu cho CSDL có sẵn
  • Backup định kỳ CSDL lên AWS s3
  • Cách kết nối với CSDL thông qua mongoose cho người dùng nodejs

Bài viết yêu cầu PC cài sẵn docker. Tham khảo cách cài đặt docker cho các OS tại trang chính thức cho ubuntu, Mac, Windows. Hoặc tham khảo Cách cài đặt docker trên ubuntu

Phần 1: Thiết lập máy chủ CSDL không yêu cầu mật khẩu (đơn giản)

Chạy lệnh này để khởi động hoặc restart máy chủ CSDL hiện tại

docker container stop mongo; docker run --name mongo -p 27017:27017 -v $HOME/mongo:/data/db --rm -d mongo:4.0.4

Giải thích câu lệnh

  • Lệnh đầu tiên docker container stop mongo dùng để stop instance hiện tại nếu đang chạy. Tên mongo là tên được chỉ định bất kì, cụ thể ở trong câu lệnh tiếp theo
  • docker run khới tạo docker instance
  • --name mongo đặt tên cho docker instance để tiện cho câu lệnh stop, tìm instance đang chạy trước đó trong trường hợp muốn restart
  • -p 27017:27017 mở cổng mặc định 27017 để các ứng dụng có thể truy cập CSDL chạy bằng instance này
  • -v $HOME/mongo:/data/db ánh xạ dữ liệu trong instance sang 1 thư mục mặc định trên máy host. Để những lần chạy tiếp theo CSDL không bị mất đi. Thư mục mình chọn ở đây là $HOME/mongo
  • --rm xóa instance sau khi stop. Đây là thiết lập mình rất hay đặt khi khới tạo docker instance, nó giúp tránh những lỗi tạo ra trong qúa trình khởi tạo instance lần trước. Và đảm bảo tính đồng nhất của cấu hình khi chạy ở máy host khác
  • -d: detach, nghĩa là chạy background. Nếu thiết đặt cờ này bạn sẽ không nhìn thấy output nào của câu lệnh cả. Nếu bạn thoát ra khỏi ssh session, máy chủ vẫn tiếp tục chạy
    Trong trường hợp bạn muốn theo dõi output của câu lệnh hoặc các truy vấn, thay cờ -d bằng cờ -it, sau đó dùng tổ hợp phí ctrl-p ctrl-q để chuyển sang background mode nếu muốn. Câu lệnh full trong trường hợp này
docker container stop mongo; docker run --name mongo -p 27017:27017 -v $HOME/mongo:/data/db --rm -it mongo:latest
#ctrl-p ctrl-q to detach
  • -it cờ này bao gồm cờ -i-t nghĩa là khởi tạo terminal ( cờ -t ) trong chế độ tương tác với người dùng (chế độ nhận lệnh từ bàn phím, interative mode, cờ -i)
  • mongo:4.0.4 image chính thức (official) của mongo. Ở đây mình để phiên bản 4.0.4 là phiên bản mới nhất và chạy ổn định (stable) trong thời điểm viết bài này (tháng 12, 2018). Để bạn nào lười có thể copy toàn bộ câu lệnh đó trong trong và sử dụng luôn cho production server.
    Trường hợp có bản cập nhật mới bạn có thể thay số phiên bản bằng các phiên bản mới hơn hoặc sử dụng mongo:latest để tự động lấy phiên bản ổn định mới nhất
docker container stop mongo; docker run --name mongo -p 27017:27017 -v $HOME/mongo:/data/db --rm -d mongo:latest

Phần 2: Thiết lập máy chủ CSDL yêu cầu mật khẩu

Tương tự như trên nhưng thêm cấu hình user và password

 docker container stop mongo; docker run -e MONGO_INITDB_ROOT_USERNAME=carstay -e MONGO_INITDB_ROOT_PASSWORD=carstay-password --name mongo -p 27017:27017 -v $HOME/mongo:/data/db --rm -d mongo:4.0.4 --auth

Chú ý:

  • Lệnh này chỉ có tác dụng khi khới tạo máy chủ lần đầu. Tức thư mục $HOME/mongo trên máy host trống. Nếu bạn đã có CSDL hoạt động sẵn, tức thư mục `$HOME/mongo` đã được anh xạ trước đó, tham khảo phần 3
  • Lệnh này có in cả user và password ở trong câu lệnh nên sẽ không an toàn do người khác có thể truy cập lịch sử bash và xem được mật khẩu. Có 2 giải pháp. Hoặc là đặt thông tin mật khẩu trong file, tham khảo tại đây, mình sẽ không giới thiệu trong bài này. Hoặc thêm dấu cách ở đầu câu lệnh, câu lệnh này sẽ không được lưu lại vào lịch sử bash.
  • User được tạo trong lệnh này sẽ có quyền cao nhất với CSDL. Để tạo user với quyền giới hạn. Tham khảo phần 3

Chú giải lệnh

  • -e MONGO_INITDB_ROOT_USERNAME=carstay -e MONGO_INITDB_ROOT_PASSWORD=carstay-password dùng để đặt tên user và password cho CSDL. Thay carstaycarstay-password  bằng thông tin phù hợp bạn muốn
  • --auth: cờ này thực chất không cần thiết vì nếu cờ -e MONGO_INITDB_ROOT_USERNAME=carstay -e MONGO_INITDB_ROOT_PASSWORD=carstay-password được thiết đặt, instance sẽ tự động thêm cờ --auth. Tuy nhiên, mình khuyến khích đặt cờ --auth để chắc chắn CSDL đã được bật chế độ bảo mật

Phần 3: Cấu hình mật khẩu cho CSDL có sẵn

3.1 Thiết lập thông tin bảo mật

Gỉả sử CSDL hiện tại chưa được thiết lập mật khẩu hoặc chưa được khởi tạo. Chạy lệnh ở phần 1 để khởi động CSDL.

Truy cập vào CSDL để thiết lập mật khẩu

docker exec -it mongo sh -c 'exec mongo'
  • exec : truy cập vào docker instance đang chạy và thực hiện lệnh
  • -it: khởi tạo terminal có thể thao tác với người dùng (qua bàn phím, interative mode + tty)
  • mongo: tên của instnace mà được chỉ định ở phần 1 (tên bất kỳ bạn đặt ở phần 1). Ở đây mình đặt trùng với tên của docker image
  • sh -c 'exec mongo' chạy lệnh này trong docker instance

Sau khi chạy lệnh này bạn sẽ truy cập được vào môi trường của mongo

Mongo hiện cảnh báo CSDL chưa được bảo mật

Sẽ có 1 vài Warning(s) xuất hiện vì bạn chưa thiết lập mật khẩu cho CSDL. Sau khi kết thúc phần này, nếu suôn sẻ sẽ không có warning nào nữa.

Chạy các lệnh sau trong môi trường mongo

use admin
db.createUser({
  user: 'carstay',
  pwd: '6wryfFadhGB57xhlTY63Gook',
  roles: [{
    role: 'readWrite',
    db: 'carstay'
  }]
})

Trong VD trên tài khoản tên carstay với mật khẩu 6wryfFadhGB57xhlTY63Gook được tạo và cấp quyền đọc và ghi readWrite đối với CSDL có tên là carstay. Thiết lập này nhằm tránh tạo user có quyền truy cập qúa lớn.

Tham số cho roles ở trong câu lệnh trên có thể thay đổi để tạo user với các quyền tương ứng. Tham khảo danh sách các quyền ở đây. Ví dụ quyền root dùng để tạo tài khoản root, có thể được tạo bằng cách thiết lập roles: ['root']

3.2 Xác nhận cấu hình thiết lập có hoạt động tốt

Thoát ra khỏi môi trường mongo (tổ hợp phím Ctrl-D) và khởi động lại server CSDL và bật chế độ yêu cầu mật khẩu khi truy cập bằng cách thêm cờ --auth vào cuối

 docker container stop mongo; docker run --name mongo -p 27017:27017 -v $HOME/mongo:/data/db --rm -d mongo:4.0.4 --auth

Truy cập lại vào CSDL bằng tài khoản vừa tạo để xác nhận lại thiết lập hoạt động tốt

docker exec -it mongo sh -c 'exec mongo'
Lúc này mongo sẽ không có cảnh báo Warning nữa

Trong môi trường mongo nhập các lệnh

use carstay
show collections

Khi chưa nhập thông tin bảo mật, CSDL sẽ trả về lỗi Warning: unable to run listCollections, attempting to approximate collection names by parsing connectionStatus

Tiếp tục với các lệnh sau để truy cập bằng thông tin đã tạo ở bước trước

use admin
db.auth('carstay', 'carstay-password')
use carstay
show collections

Danh sách các collections sẽ xuất hiện và không có thông báo lỗi

3.3 Thay đổi thông tin vừa thiết lập

Trong qúa trình thiết lập bạn có thể nhập nhầm mật khẩu, tên user, hoặc muốn thay đổi các thông tin.

Trong trường hợp đó dùng lệnh dropUser để xóa user trong môi trường mongo. Cú pháp như sau

dropUser('carstay')

Trong đó carstay là tên người dùng cần được xóa.

Lưu ý, lệnh này cần được chạy với 1 trong 2 điều kiện.

  • Server mongo được chạy mà không yêu cầu bảo mật. Tức không có cờ ---auth hoặc không có cả 2 biến môi trường -e MONGO_INITDB_ROOT_USERNAME=carstay -e MONGO_INITDB_ROOT_PASSWORD=carstay-password. Với docker, việc này có thể thực hiện dễ dàng bằng cách stop server hiện tại và tạo server mới với các cờ thích hợp.
  • Môi trường mongo được truy cập với 1 user có quyền admin (root user)

Phần 4: Backup định kỳ CSDL lên AWS s3

4.1 Backup

Phần này chủ yếu giới thiệu cách backup định kỳ toàn bộ CSDL của 1 bảng (db) nào đó, backup lên AWS s3, gửi thông báo tới slack channel nếu qúa trình backup xảy ra lỗi

Câu lệnh nhìn chung khá đơn giản, chú yếu là ý tưởng, bạn có thể thay thế việc upload lên AWS s3, bằng cách push lên github repo, copy vào 1 server khác, ...

Để tránh lan man vì baì viết cũng hơi dài, phần thiết lập AWS s3, slack incoming web hook người đọc tự tham khảo. Lưu ý trong thiết lập AWS s3 nên sử dụng tùy chọn versioning để s3 lưu giữ nhiều phiên bản của cùng 1 file (tương tự như git)

Tùy vào kích thước của CSDL và mức độ quan trọng của dữ liệu mà cần thiết lập tần số backup cho phù hợp. Cá nhân mình khuyến khích cho các CSDL nhỏ và quan trọng, thì nên backup khoảng 15 phút/1 lần. Cách cấu hình như sau

Gõ lệnh crontab -e từ terminal. Editor sẽ xuất hiện để nhập nội dung, trỏ xuống cuối cùng của file và thêm vào nội dung sau

*/15 * * * * docker exec mongo sh -c 'exec mongodump -d <db-name> --archive=/mongodump.bson --authenticationDatabase admin --username <username> --password <password>' && docker cp mongo:/mongodump.bson $HOME/mongodump.bson && aws s3 cp $HOME/mongodump.bson s3://<bucket-name> ||  curl -X POST -H 'Content-type: application/json' --data '{"text":"Can not backup db"}' <slack-hook-url>

Trong đó thay thế các biến như <db-name>, <username>, <password>, <bucket-name>, <slack-hook-url> bằng các gía trị cụ thể thích hợp.

File mongdump.bson sẽ được tạo ở thư mục $HOME và upload lên AWS s3.

Trường hợp server CSDL không có bảo mật, bạn có thể loại bỏ cờ --authenticationDatabase admin --username <> --password <>

4.2 Omake (khuyến mại)

Định dừng viết rồi nhưng mà lại lòi ra thêm cái nữa

Gỉa sử CSDL bạn đang điều hành là khá lớn, hoặc không cần tần suất backup qúa lớn, hoặc muốn tiết kiệm chi phí lưu trữ file backup (hơi hiếm vì hard disk giờ khá bèo)

Tóm lại, cần backup 1 ngày 1 lần. Trong trường hợp này, nên backup vào giờ mà có ít truy cập để hạn chế lỗi và tải cho server. Khoảng 3A.M là -> 5A.M là khoảng thời gian hợp lý. Khi đó nội dung của câu lệnh trong crontab nên được viết như sau

0 18 * * * perl -e 'sleep int(rand(7200))' && <other command>

Mình sẽ giải thích từ phải qua trái

  • <other command> là lệnh cần được thực thi, cụ thể ở đây là docker exec ...
  • perl -e 'sleep int(rand(7200))' dùng để sleep randomly, tức tạm dừng câu lệnh trong khoảng bất kỳ từ 0 giây đến 7200 giây (tức 2h)
  • 0 18 * * * thực thi lệnh vào 18 giờ 0 phút mỗi ngày. Oh, tại sao là 18 mà không phải là 3A.M. Vì trong trường hợp này múi giờ của server không khớp với múi giờ của người dùng (tức múi giờ bạn mong muốn câu lệnh được thực). Trong trường hợp này mình đang muốn chạy lệnh định kỳ vào 3A.M +9GMT, tức là 6P.M ETC
    Để kiểm tra múi giờ của server, có thể sử dụng lệnh date
    Để thay đổi múi giờ của server dùng lệnh
ln -sf /usr/share/zoneinfo/America/Los_Angeles /etc/localtime

Trong đó, các múi giờ có sẵn trong máy có thể được tìm thấy ở thư mục  /usr/share/zoneinfo/

4.3 Cách khôi phục

Gỉả sữ bạn đã có sẵn file mongodump.bson ở thư mục $HOME

Copy file backup vào docker instance docker cp $HOME/mongodump.bson mongo:/mongodump.bson

Lưu ý: các câu lệnh tiếp theo sẽ xóa toàn bộ CSDL hiện có của bảng (db) đích. Chỉ 1 thao tác nhầm bạn có thể xóa toàn bộ CSDL của bất kỳ bảng nào

Tuyệt đối khuyến khích bạn nên backup toàn bộ data hiện tại, nhờ có docker, việc này có thể thực hiện đơn giản bằng cách backup thư mục dữ liệu tại $HOME/mongo. VD: sudo cp -Rf $HOME/mongo $HOME/mongo-bak

Nếu bảng (db) cũ cùng tên với bảng mới. Dùng lệnh

docker exec mongo sh -c 'exec mongorestore --drop --nsInclude "<db name>.*" --archive=/mongodump.bson'

Thay <db name> bằng tên bảng phù hợp

Nếu bảng cũ khác tên với bảng mới. Dùng lệnh

docker exec mongo sh -c 'exec mongorestore --drop --nsFrom "<source db>.*" --nsTo "<target db>.*" --archive=/mongodump.bson'

Thay thế <source db>, <target db> bằng tên bảng backup và bảng khổi phục thích hợp.


Phần 5: Cách kết nối với CSDL thông qua mongoose cho người dùng nodejs

Lưu ý khi dùng với gói mongoose, ngoài option user, và pass, cần thêm option auth: {authdb: 'admin'} để không bị lỗi

import mongoose from 'mongoose'
import chalk from 'chalk'

// mongoose.set('useNewUrlParser', true)
(async () => {
  try {
    await mongoose.connect(process.env.MONGODB_URI, {
      autoReconnect: true,
      family: 4,
      user: process.env.MONGODB_USER,
      pass: process.env.MONGODB_PASS,
      auth: {
        authdb: 'admin'
      }
    })
  } catch (err) {
    logger.error(err)
    logger.log('%s MongoDB connection error. Please make sure MongoDB is running.', chalk.red('✗'))
    process.exit()
  }
})()