【アプリ開発 １】【Node.js express Docker】 Dockerを用いてNode.js Express MongoDB(mongoose)の環境を構築する【2020年12月】

※当方駆け出しエンジニアのため、間違っていることも多々あると思いますので、ご了承ください。また、間違いに気付いた方はご一報いただけると幸いです。

##Node.js Express MongoDBを用いたアプリ開発
受託開発でNode.js Express MongoDBを用いたアプリ開発を行うことになりました。
開発の中で得られた知見を、支障のない範囲で記録していきたいと思います。
アプリの内容はさておき、チームで開発するため、まずはDockerを用いて開発環境を構築を行います。

###構築する環境

- docker-composeを用いて、アプリコンテナ、mongodbコンテナを作成する。
- アプリコンテナとmongodbコンテナの接続テスト(mongoose)を用いて、テストモデルにデータ保存。
- mongo compassからデータの挿入を確認
- mongodbに認証を設ける。作成するユーザーの権限は"root","read","owner"
- データベースのパスワード等は環境変数で管理(gitignoreにてgitの管理から外す。)

※注意点
<アプリ名> <パスワード>となっているところは、各自で適当なものを記入してください。
そのまま記載するとエラーとなります。
今回は、パスワードは全て共通としています。


###初期インストールするパッケージ一覧

```
 "bcrypt"
 "body-parser"
 "connect-flash"
 "cookie-parser"
 "debug"
 "ejs"
 "express"
 "express-ejs-layouts"
 "express-generator"
 "express-session"
 "express-validator"
 "http-errors"
 "http-status-codes"
 "method-override"
 "mongoose"
 "morgan"
 "nodemon"
 "passport"
 "passport-local-mongoose"
```

#下準備
###最初に用意するファイル群

```
.
├── .env
├── .gitignore
├── data
│   └── db  //空ディレクトリ
├── docker-compose.yml
├── docker_app
│   └── Dockerfile
├── secret_file
│   ├── db.env
│   └── db_init
│         └──mongo_init_user.js
└── src
    ├── controllers
    │     └── initTestsController.js
    ├── models
    │     └── init_test.js
    └── package.json

```

```.env.

MONGO_INITDB_ROOT_USERNAME=root
MONGO_INITDB_ROOT_PASSWORD=<パスワード>
MONGO_INITDB_DATABASE=<DB名>
```
```.gitignore.

node_modules/
data/
.env
secret_file/
```
```docker-compose.yml
version: '3'
services:
  app:
    build: ./docker_app
    container_name: <アプリ名>_app_cnt
    ports:
      - "8080:3000"
    restart: always
    working_dir: /app
    tty: true
    volumes:
      - /etc/passwd:/etc/passwd:ro
      - /etc/group:/etc/group:ro
      - ./src:/app
    env_file:
      - ./secret_file/db.env
    command: bash
    networks:
      - <アプリ名>-network
    depends_on:
      - mongo
  mongo:
    image: mongo:latest
    container_name: <アプリ名>_db_cnt
    ports:
      - "27018:27017"
    restart: always
    environment:
      MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME}
      MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD}
      MONGO_INITDB_DATABASE: ${MONGO_INITDB_DATABASE}
    volumes:
      - ./data/db:/data/db
      - ./secret_file/db_init/:/docker-entrypoint-initdb.d
    env_file:
      - ./secret_file/db.env
    command:
      - mongod
    networks:
      - <アプリ名>-network
networks:
  <アプリ名>-network:
    external: true
```

```Dockerfile.

FROM node:12
WORKDIR /app
RUN npm install
```

```db.env
DB_USER=owner
DB_PASS=<パスワード>
DB_NAME=<アプリ名>_db
```

```mongo_init_user.js
let users = [
  {
    user: "read",
    pwd: "<パスワード>",
    roles: [
      {
        role: "read",
        db: "<アプリ名>_db"
      }
    ]
  },
  {
    user: "owner",
    pwd: "<パスワード>",
    roles: [
      {
        role: "dbOwner",
        db: "<アプリ名>_db"
      }
    ]
  },
  {
    user: "readWriteUser",
    pwd: "<パスワード>",
    roles: [
      {
        role: "readWrite",
        db: "<アプリ名>_db"
      }
    ]
  }
];

for (let i = 0, length = users.length; i < length; ++i) {
  db.createUser(users[i]);
}
```

```initTestsController.js
"use strict";

const InitTest = require('../models/init_test');

const test = () => {
  let initTest = new InitTest({
    name: "Taro",
    age: 20
  })
  initTest.save((error, data) => {
    if (error) {
      console.log(error);
    }
    console.log(data);
  })
}

module.exports = { test };
```

```init.test.js
"use strict";

const mongoose = require("mongoose");

const initTestSchema = new mongoose.Schema({
  name: String,
  age: Number
});

module.exports = mongoose.model("InitTest", initTestSchema);
```

```package.json
{}
```

#環境作成

###コンテナ作成

```ホスト.
docker-compose build
```

下記の様な警告がでるが問題ない。

```
npm WARN saveError ENOENT: no such file or directory, open '/app/package.json'
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN enoent ENOENT: no such file or directory, open '/app/package.json'
npm WARN app No description
npm WARN app No repository field.
npm WARN app No README data
npm WARN app No license field.
```

```ホスト.
#コンテナを一時的に起動コンテナ内に入る（--rmで停止後削除する。コンテナ起動後、bashに入る）
docker-compose run --rm app /bin/bash
```

```appコンテナ内.
# express-generatorでアプリケーションのひな形を生成
npx express-generator --view=ejs
```
下記の用に即abortingで拒絶されたら再度実行

```
destination is not empty, continue? [y/N]
aborting
```
二回目は質問で待ってくれるので y でエンター

```
destination is not empty, continue? [y/N] y
```
下記の様な各種ファイルが作成される。

```
   create : public/
   create : public/javascripts/
   create : public/images/
   create : public/stylesheets/
   create : public/stylesheets/style.css
   create : routes/
   create : routes/index.js
   create : routes/users.js
   create : views/
   create : views/error.ejs
   create : views/index.ejs
   create : app.js
   create : package.json
   create : bin/
   create : bin/www
```

package.jsonに下記の内容を上書き

```package.json
{
  "name": "uniq_app",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "nodemon ./bin/www"
  },
  "dependencies": {
    "bcrypt": "^5.0.0",
    "body-parser": "^1.19.0",
    "connect-flash": "^0.1.1",
    "cookie-parser": "~1.4.4",
    "debug": "~2.6.9",
    "ejs": "^3.1.5",
    "express": "~4.16.1",
    "express-ejs-layouts": "^2.5.0",
    "express-generator": "^4.16.1",
    "express-session": "^1.17.1",
    "express-validator": "^6.7.0",
    "http-errors": "~1.6.3",
    "http-status-codes": "^2.1.4",
    "method-override": "^3.0.0",
    "mongoose": "^5.11.9",
    "morgan": "~1.9.1",
    "nodemon": "^2.0.6",
    "passport": "^0.4.1",
    "passport-local-mongoose": "^6.0.1"
  }
}
```

app.jsに下記の内容を上書き

```app.js
const createError = require('http-errors');
const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const mongoose = require("mongoose");


const indexRouter = require('./routes/index');
const usersRouter = require('./routes/users');

const initTestController = require('./controllers/initTestsController');

const app = express();

mongoose.connect(
  `mongodb://${process.env.DB_USER}:${process.env.DB_PASS}@mongo:27017/<アプリ名>_db`,
  { userNewParser: true }
);

mongoose.set("useCreateIndex", true);

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.get('/initTest', initTestController.test);/* 初期テストルーティング */
app.use('/', indexRouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function (req, res, next) {
  next(createError(404));
});

// error handler
app.use(function (err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;
```

パッケージのインストール

```appコンテナ内.
npm install
```

一度コンテナを抜ける

```appコンテナ内.
#コンテナを抜ける（この仮コンテナは削除される)
exit
```
コンテナを再起動

```ホスト.
docker-compose up
```

下記クリックで接続確認
http://localhost:8080/

###mongooseとの接続確認
下記クリックでデータが作成されるか確認
http://localhost:8080/initTest

下記の様なログが出力されれば成功(docker-compose upしたターミナルで)

```
uniq_app_cnt |   _id: 5fec5a6213a2fd002d89acca,
uniq_app_cnt |   name: 'initTestUser',
uniq_app_cnt |   age: 20,
uniq_app_cnt |   __v: 0
uniq_app_cnt | }
uniq_app_cnt | 【ログ】--接続成功--【ログ】
```
###mongodbの権限周り確認

ターミナルを別タブで開き、dbコンテナに入る

```
docker exec -it <アプリ名>_db_cnt bash
```

```dbコンテナ内
mongo
//mongodbに接続

show dbs
//何も表示されない。(認証でロックされていることを確認)

use admin
//adminデータベースへ接続

db.auth("root", "<パスワード>")

//1 と返れば認証成功

db.system.users.find().pretty()
//作成されたユーザー確認
```
以下の様な、データベースに対する各権限者が作成されていたらオッケー。

```js
{
	"_id" : "admin.root",
	"roles" : [
		{
			"role" : "root",
			"db" : "admin"
		}
	]
}
{
	"_id" : "uniq_db.read",
	"roles" : [
		{
			"role" : "read",
			"db" : "<データベース名>_db"
		}
	]
}
{
	"_id" : "uniq_db.owner",
	"roles" : [
		{
			"role" : "dbOwner",
			"db" : "<データベース名>_db"
		}
	]
}
{
	"_id" : "uniq_db.readWriteUser",
	"roles" : [
		{
			"role" : "readWrite",
			"db" : "<データベース名>_db"
		}
	]
}
```

#mongo compassからの接続確認。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/3e2df96e-bfe6-5883-f3ee-c70e42623978.png)