Tạo một Nodejs microservice và deploy với docker (Phần 1)
Bài đăng này đã không được cập nhật trong 5 năm
Trong loạt bài này, chúng ta sẽ tạo một Nodejs microservice và deploy nó lên Docker Swarm Cluster Những công cụ mà chúng ta sẽ cài đặt:
- Nodejs version 7.2.0
- MongoDB 4.0
- Docker Trước khi thao khảo bài này bạn nên có:
- Kiến thức cơ bản về NodeJS
- Kiến thức cơ bản về và cách cài đặt Docker
- Kiến thức cơ bản về MongoDB
Đầu tiên, Microservice là gì?
Microservice là một đơn vị độc lập, cùng với nhiều đơn vị khác tạo lên một ứng dụng lớn hơn. Bằng cách chia nhỏ app thành các đơn vị nhỏ hơn, mỗi phần có thể deploy và mở rộng một cách độc lập, có thể viết bởi các team khác nhau, bằng cách ngôn ngữ khác nhau và chúng ta có test những phần riêng biệt. --- Max Stoiber
Lợi ích của Microservice
- Ứng dụng được khởi động nhanh chóng, giúp cho dev làm việc hiệu quả hơn và tăng tốc độ triển khai
- Mỗi dịch vụ có thể deploy độc lập với dịch vụ khác nên dễ dàng deploy các phiên bản dịch vụ mới thường xuyên hơn
- Dễ dàng mờ rộng và có thể nâng cao về hiệu suất
- Loại bỏ trường hợp dùng quá lâu 1 công nghệ. Khi phát triển dịch vụ mới dễ dàng lựa chọn công nghệ mới.
- Microservice thông thường tổ chức tốt hơn, vì mỗi service sẽ thực hiện một công việc cụ thể, và công việc của các cấu phần khác nhau không liên quan đến nhau
- Các dịch vụ tác rời dễ dàng tái cấu trúc và cấu hình lại để phục vụ các mục đích các ứng dụng khác nhau.
Hạn chế của Microservice
- Nhà phát triển gặp phải sự phức tạp của hệ thống phân tán.
- Triển khai phức tạp. Trong sản phẩm, có sự phức tạp hoạt động của việc triển khai và quản lý hệ thống bao gồm nhiều dịch vụ khác nhau.
- Khi xây dựng một kiến trúc microservice mới, bạn có thể phát hiện rất nhiều mối quan tâm xuyên suốt mà bạn không lường trước tại thời điểm thiết kế.
Kiến trúc microservice của chúng ta
Trên là ảnh là kiến trức của hệ thống của chúng ta. Bài viết này sẽ tập trung vào danh mục của các bộ phim. Trong kiến trúc chúng ta sẽ thấy chúng có 3 thiết bị khác nhau sử dụng microservice, the POS (điểm thanh toán), thiết bị di động, và máy tính, với POS và thiết bị di động sẽ sử dụng ứng dụng riêng biệt còn máy tính sẽ sử dụng thông qua web app.
Bắt đầu xây dựng microservice
Đầu tiên chúng ta muốn xem những bộ phim nào hiện có sẵn trong rạp chiếu phim. Sơ đồ sau đây cho chúng ta thấy làm thế nào giao tiếp các phần trong microservice thông qua REST. API của chúng ta trong bài này sẽ có raml chi tiết như sau:
#%RAML 1.0
title: cinema
version: v1
baseUri: /
types:
Movie:
properties:
id: string
title: string
runtime: number
format: string
plot: string
releaseYear: number
releaseMonth: number
releaseDay: number
example:
id: "123"
title: "Assasins Creed"
runtime: 115
format: "IMAX"
plot: "Lorem ipsum dolor sit amet"
releaseYear : 2017
releaseMonth: 1
releaseDay: 6
MoviePremieres:
type: Movie []
resourceTypes:
Collection:
get:
responses:
200:
body:
application/json:
type: <<item>>
/movies:
/premieres:
type: { Collection: {item : MoviePremieres } }
/{id}:
type: { Collection: {item : Movie } }
```markdown
Cấu trúc của Api project sẽ trông như thế này:
```js
- api/ # our apis
- config/ # config for the app
- mock/ # not necessary just for data examples
- repository/ # abstraction over our db
- server/ # server setup code
- package.json # dependencies
- index.js # main entrypoint of the app
Ok bắt đầu thôi. Trong phần đầu tiên sẽ tập trung là repository
. Ở đây chúng ta sẽ làm truy vẫn đến database.
// repository.js
'use strict'
// factory function, that holds an open connection to the db,
// and exposes some functions for accessing the data.
const repository = (db) => {
// since this is the movies-service, we already know
// that we are going to query the `movies` collection
// in all of our functions.
const collection = db.collection('movies')
const getMoviePremiers = () => {
return new Promise((resolve, reject) => {
const movies = []
const currentDay = new Date()
const query = {
releaseYear: {
$gt: currentDay.getFullYear() - 1,
$lte: currentDay.getFullYear()
},
releaseMonth: {
$gte: currentDay.getMonth() + 1,
$lte: currentDay.getMonth() + 2
},
releaseDay: {
$lte: currentDay.getDate()
}
}
const cursor = collection.find(query)
const addMovie = (movie) => {
movies.push(movie)
}
const sendMovies = (err) => {
if (err) {
reject(new Error('An error occured fetching all movies, err:' + err))
}
resolve(movies)
}
cursor.forEach(addMovie, sendMovies)
})
}
const getMovieById = (id) => {
return new Promise((resolve, reject) => {
const projection = { _id: 0, id: 1, title: 1, format: 1 }
const sendMovie = (err, movie) => {
if (err) {
reject(new Error(`An error occured fetching a movie with id: ${id}, err: ${err}`))
}
resolve(movie)
}
// fetch a movie by id -- mongodb syntax
collection.findOne({id: id}, projection, sendMovie)
})
}
// this will close the database connection
const disconnect = () => {
db.close()
}
return Object.create({
getAllMovies,
getMoviePremiers,
getMovieById,
disconnect
})
}
const connect = (connection) => {
return new Promise((resolve, reject) => {
if (!connection) {
reject(new Error('connection db not supplied!'))
}
resolve(repository(connection))
})
}
// this only exports a connected repo
module.exports = Object.assign({}, {connect})
File tiếp theo chú ý là server.js
'use strict'
const express = require('express')
const morgan = require('morgan')
const helmet = require('helmet')
const movieAPI = require('../api/movies')
const start = (options) => {
return new Promise((resolve, reject) => {
// we need to verify if we have a repository added and a server port
if (!options.repo) {
reject(new Error('The server must be started with a connected repository'))
}
if (!options.port) {
reject(new Error('The server must be started with an available port'))
}
// let's init a express app, and add some middlewares
const app = express()
app.use(morgan('dev'))
app.use(helmet())
app.use((err, req, res, next) => {
reject(new Error('Something went wrong!, err:' + err))
res.status(500).send('Something went wrong!')
})
// we add our API's to the express app
movieAPI(app, options)
// finally we start the server, and return the newly created server
const server = app.listen(options.port, () => resolve(server))
})
}
module.exports = Object.assign({}, {start})
Đây là những gì chúng ta làm, khởi tạo express app mới, xác thực một repository và server port object, sau đó chúng ta tích hợp vài middleware cho express app giống morgan cho logging, helmet cho bảo mật, vả hàm xử lý lỗi, đến cuối chúng ta export một hàm start để chạy server.
Tiếp tục với file movies.js
'use strict'
const status = require('http-status')
module.exports = (app, options) => {
const {repo} = options
// here we get all the movies
app.get('/movies', (req, res, next) => {
repo.getAllMovies().then(movies => {
res.status(status.OK).json(movies)
}).catch(next)
})
// here we retrieve only the premieres
app.get('/movies/premieres', (req, res, next) => {
repo.getMoviePremiers().then(movies => {
res.status(status.OK).json(movies)
}).catch(next)
})
// here we get a movie by id
app.get('/movies/:id', (req, res, next) => {
repo.getMovieById(req.params.id).then(movie => {
res.status(status.OK).json(movie)
}).catch(next)
})
}
Chúng ta sẽ tạo router cho API, các hàm định nghĩa sẽ xác định router được gọi, và repo được sử dụng kỹ thuật interface, express router không nhận biết một data object, database queries logic, vân vân, nó chỉ gọi hàm repo để xử lý tất cả vấn đề của database.
Tiếp theo là file test cho file movies.js
/* eslint-env mocha */
const request = require('supertest')
const server = require('../server/server')
describe('Movies API', () => {
let app = null
let testMovies = [{
'id': '3',
'title': 'xXx: Reactivado',
'format': 'IMAX',
'releaseYear': 2017,
'releaseMonth': 1,
'releaseDay': 20
}, {
'id': '4',
'title': 'Resident Evil: Capitulo Final',
'format': 'IMAX',
'releaseYear': 2017,
'releaseMonth': 1,
'releaseDay': 27
}, {
'id': '1',
'title': 'Assasins Creed',
'format': 'IMAX',
'releaseYear': 2017,
'releaseMonth': 1,
'releaseDay': 6
}]
let testRepo = {
getAllMovies () {
return Promise.resolve(testMovies)
},
getMoviePremiers () {
return Promise.resolve(testMovies.filter(movie => movie.releaseYear === 2017))
},
getMovieById (id) {
return Promise.resolve(testMovies.find(movie => movie.id === id))
}
}
beforeEach(() => {
return server.start({
port: 3000,
repo: testRepo
}).then(serv => {
app = serv
})
})
afterEach(() => {
app.close()
app = null
})
it('can return all movies', (done) => {
request(app)
.get('/movies')
.expect((res) => {
res.body.should.containEql({
'id': '1',
'title': 'Assasins Creed',
'format': 'IMAX',
'releaseYear': 2017,
'releaseMonth': 1,
'releaseDay': 6
})
})
.expect(200, done)
})
it('can get movie premiers', (done) => {
request(app)
.get('/movies/premiers')
.expect((res) => {
res.body.should.containEql({
'id': '1',
'title': 'Assasins Creed',
'format': 'IMAX',
'releaseYear': 2017,
'releaseMonth': 1,
'releaseDay': 6
})
})
.expect(200, done)
})
it('returns 200 for an known movie', (done) => {
request(app)
.get('/movies/1')
.expect((res) => {
res.body.should.containEql({
'id': '1',
'title': 'Assasins Creed',
'format': 'IMAX',
'releaseYear': 2017,
'releaseMonth': 1,
'releaseDay': 6
})
})
.expect(200, done)
})
})
/* eslint-env mocha */
const server = require('./server')
describe('Server', () => {
it('should require a port to start', () => {
return server.start({
repo: {}
}).should.be.rejectedWith(/port/)
})
it('should require a repository to start', () => {
return server.start({
port: {}
}).should.be.rejectedWith(/repository/)
})
})
const MongoClient = require('mongodb')
// here we create the url connection string that the driver needs
const getMongoURL = (options) => {
const url = options.servers
.reduce((prev, cur) => prev + `${cur.ip}:${cur.port},`, 'mongodb://')
return `${url.substr(0, url.length - 1)}/${options.db}`
}
// mongoDB function to connect, open and authenticate
const connect = (options, mediator) => {
mediator.once('boot.ready', () => {
MongoClient.connect( getMongoURL(options), {
db: options.dbParameters(),
server: options.serverParameters(),
replset: options.replsetParameters(options.repl)
}, (err, db) => {
if (err) {
mediator.emit('db.error', err)
}
db.admin().authenticate(options.user, options.pass, (err, result) => {
if (err) {
mediator.emit('db.error', err)
}
mediator.emit('db.ready', db)
})
})
})
}
module.exports = Object.assign({}, {connect})
// simple configuration file
// database parameters
const dbSettings = {
db: process.env.DB || 'movies',
user: process.env.DB_USER || 'cristian',
pass: process.env.DB_PASS || 'cristianPassword2017',
repl: process.env.DB_REPLS || 'rs1',
servers: (process.env.DB_SERVERS) ? process.env.DB_SERVERS.split(' ') : [
'192.168.99.100:27017',
'192.168.99.101:27017',
'192.168.99.102:27017'
],
dbParameters: () => ({
w: 'majority',
wtimeout: 10000,
j: true,
readPreference: 'ReadPreference.SECONDARY_PREFERRED',
native_parser: false
}),
serverParameters: () => ({
autoReconnect: true,
poolSize: 10,
socketoptions: {
keepAlive: 300,
connectTimeoutMS: 30000,
socketTimeoutMS: 30000
}
}),
replsetParameters: (replset = 'rs1') => ({
replicaSet: replset,
ha: true,
haInterval: 10000,
poolSize: 10,
socketoptions: {
keepAlive: 300,
connectTimeoutMS: 30000,
socketTimeoutMS: 30000
}
})
}
// server parameters
const serverSettings = {
port: process.env.PORT || 3000
}
module.exports = Object.assign({}, { dbSettings, serverSettings })
'use strict'
// we load all the depencies we need
const {EventEmitter} = require('events')
const server = require('./server/server')
const repository = require('./repository/repository')
const config = require('./config/')
const mediator = new EventEmitter()
// verbose logging when we are starting the server
console.log('--- Movies Service ---')
console.log('Connecting to movies repository...')
// log unhandled execpetions
process.on('uncaughtException', (err) => {
console.error('Unhandled Exception', err)
})
process.on('uncaughtRejection', (err, promise) => {
console.error('Unhandled Rejection', err)
})
// event listener when the repository has been connected
mediator.on('db.ready', (db) => {
let rep
repository.connect(db)
.then(repo => {
console.log('Repository Connected. Starting Server')
rep = repo
return server.start({
port: config.serverSettings.port,
repo
})
})
.then(app => {
console.log(`Server started succesfully, running on port: ${config.serverSettings.port}.`)
app.on('close', () => {
rep.disconnect()
})
})
})
mediator.on('db.error', (err) => {
console.error(err)
})
// we load the connection to the repository
config.db.connect(config.dbSettings, mediator)
// init the repository connection, and the event listener will handle the rest
mediator.emit('boot.ready')
npm install # setup node dependencies
npm test # unit test with mocha
npm start # starts the service
npm run node-debug # run the server in debug mode
npm run chrome-debug # debug the node with chrome
npm run lint # lint the code with standard
# Node v7 as the base image to support ES6
FROM node:7.2.0
# Create a new user to our new container and avoid the root user
RUN useradd --user-group --create-home --shell /bin/false nupp && \
apt-get clean
ENV HOME=/home/nupp
COPY package.json npm-shrinkwrap.json $HOME/app/
COPY src/ $HOME/app/src
RUN chown -R nupp:nupp $HOME/* /usr/local/
WORKDIR $HOME/app
RUN npm cache clean && \
npm install --silent --progress=false --production
RUN chown -R nupp:nupp $HOME/*
USER nupp
EXPOSE 3000
CMD ["npm", "start"]
docker build -t movies-service .
bài viết đến đây là hết rồi, chúng ta sẽ trở lại ở phần 2
link tham khảo https://medium.com/@cramirez92/build-a-nodejs-cinema-microservice-and-deploying-it-with-docker-part-1-7e28e25bfa8b
All rights reserved