Vue.js 開発環境を手作りしてみた手順(自分メモ)

Vue.js の開発環境構築は、もっぱら vue-cli で "vue init webpack hogehoge" という方法が一般的ですが、最近 React から Vue に乗り換えた私は、「とりあえず最初は、開発環境を手作りしたい」とうか、そうしないとしっくりこないので、手探りながら一から構築してみた時のメモです 。あくまでも自分用です。

f:id:ngzm:20171004234025p:plain

概要

おおよその構成

Vue.js を利用したクライアントアプリのビルドに "webpack"、ES2015 のトランスパイラは "babel"、Vue コンポーネントファイルのビルドに "vue-loader" を導入する。その他 CSS のビルドや、静的なファイルコピー、アプリ実行用サーバも仕込む。あと、Eslint も設定してみた。

大まかな流れ

  1. npm init で土台作り
  2. Vue.js インストール
  3. webpack 入れる
  4. babel 導入
  5. css-loader と style-loader 導入
  6. copy-webpack-plugin 導入
  7. webpack-dev-server で動かす
  8. vue-loader 導入
  9. eslint でコードチェック

Vue.js アプリ開発環境構築 基礎編

基礎編は、vueコンポーネントファイルを用いない Vue.js アプリがビルドできる環境を構築する。

1) npm init で土台作り

最初に node をインストール。nodebrew を使用すると吉。
続いて、npm init で開発環境のベースを作る。

$ npm init

いろいろ対話するが、アプリ名とか適宜入力すればOK。

2) Vue.js インストール

作成したアプリのディレクトリに移動した上で、普通に Vue.js をインストール。

$  npm install --save vue

3) webpack を入れる

$ npm install --save-dev webpack

4) Babel を導入

# webpack から babel を利用する場合の基本モジュール
$ npm install --save-dev babel-loader babel-core

# ES2015 をトランスパイルするためのもの
$ npm install --save-dev babel-preset-es2015

babel の設定ファイル ".babel.rc" を作成

$ echo '{ "presets": ["react", "es2015"] }' > .babelrc
$ cat .babelrc

{ "presets": ["react", "es2015"] }

ここで、babel で js をトランスパイルするための webpack 設定をおこなう。

$ vim webpack.config.js

ここまでの webpack.config.js の中身は次のとおり。

var path = require('path');

module.exports = {
  entry: './src/app.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'public')
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader'
      }
    ]
  }
};

ここで試しにサンプルアプリをビルドさせてみると、Vue.js のTemplateがコンパイルできないとかの以下のエラーがでた。

Vue warn: You are using the runtime-only build of Vue where the template option is not available. Either pre-compile the templates into render functions, or use the compiler-included build.

調査したところ、vue.js のビルドは、コンパイラ付のものを import する必要があるとのこと。解決するには、webpack.config.js に ビルドの alias を追加すること。*1

ということで、webpack.config.js に下記の設定を追加。これで動作OK。

  resolve: {
    alias: {
      'vue$': 'vue/dist/vue.js'
    }
  },

5) css-loader と style-loader 導入

$ npm install --save css-loader style-loader

webpack.config.js に下記の loader を追加。

module: {
  loaders: [
    ...
      // ここから
      {
        test: /\.css$/,
        exclude: /node_modules/,
        loader: ['style-loader', 'css-loader'],
      },
      // ここまで
    ],
  },

6) copy-webpack-plugin 導入

$ npm install -save-dev copy-webpack-plugin

webpack.config.js に copy-webpack-plugin の設定を追加。

  plugins: [ 
    new CopyWebpackPlugin([{
      from: path.resolve(__dirname, 'src', 'index.html'),
    }]),
  ],

7) webpack-dev-server で動かす

$ npm install -save-dev webpack-dev-server

またも webpack.config.js に 設定を追加。

  devServer: {
    contentBase: 'public',
    port: 3000,
    host: 'localhost',
    historyApiFallback: true,
  },

"npm start" でアプリを動かせるように、package.json に次の設定を追加。

  "scripts": {
    "start": "webpack-dev-server", <--- 追加
    "build": "webpack",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

ここまでのまとめ

ここまでで webpack.config.js は次のとおりとなりました。

const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'public')
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        include: /src/,
        loader: 'babel-loader'
      },
      {
        test: /\.css$/,
        exclude: /node_modules/,
        loader: ['style-loader', 'css-loader'],
      },
    ],
  },
  plugins: [
    new CopyWebpackPlugin([{
      from: path.resolve(__dirname, 'src', 'index.html'),
    }]),
  ],
  resolve: {
    alias: {
      'vue$': 'vue/dist/vue.js'
    },
  },
  devServer: {
    contentBase: 'public',
    port: 3000,
    host: 'localhost',
    historyApiFallback: true,
  },
};

Vue.js アプリ開発環境構築 vue ファイルビルド編

ここからは、さらに vueコンポーネントファイルを用いた Vue.js アプリもビルドできるように環境設定を追加する。

8) vue-loader 導入

vueコンポーネントファイル をビルドさせるには、 vue-loader が必要ということで、早速導入。

$ npm install -save-dev vue-loader

さらに vue-template-compiler がないとbuildでエラーとなるのでインストールする

$ npm install --save-dev vue-template-compiler

vue-loader の設定を webpack.config.js に追加。

  module: {
    loaders: [
      // ここから追加
      {
        test: /\.vue$/,
        exclude: /node_modules/,
        loader: 'vue-loader',
        options: {
          loaders: {
          }
        }
      },
      // ここまで

9) eslint でコードチェック

Vue.js を利用したアプリでも eslint でリントチェックできるようにしてみる。 最初は、eslint の基本設定。今回は対話で設定するケース。

$ eslint --init
? How would you like to configure ESLint? Answer questions about your style
? Are you using ECMAScript 6 features? Yes
? Are you using ES6 modules? Yes
? Where will your code run? Browser
? Do you use CommonJS? No
? Do you use JSX? No
? What style of indentation do you use? Spaces
? What quotes do you use for strings? Single
? What line endings do you use? Unix
? Do you require semicolons? Yes
? What format do you want your config file to be in? JavaScript
Successfully created .eslintrc.js file in /Users/foo/bar/vue-sample/

これで ".eslintrc.js" が生成されるはず。ただしこのままでは、vue template がきちんとチェックできないので、eslint-for-vue を導入する。

$ npm install --save-dev eslint-plugin-vue@beta

.eslintrc.js に設定を追加。今回はこのような設定内容となりました。

module.exports = {
    "env": {
        "browser": true,
        "commonjs": true,
        "es6": true
    },
    "extends": [
      "eslint:recommended",
      "plugin:vue/recommended" <--- eslint-for-vue の設定追加
    ],
    "parserOptions": {
        "sourceType": "module"
    },
    "rules": {
        "indent": [
            "error",
            2
        ],
        "linebreak-style": [
            "error",
            "unix"
        ],
        "quotes": [
            "error",
            "single"
        ],
        "semi": [
            "error",
            "always"
        ],
        "vue/valid-v-if": "error" <--- eslint-for-vue の設定追加
    }
};

まとめ

今回作成した webpack.config.js はこちら

const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'public')
  },
  module: {
    loaders: [
      {
        test: /\.vue$/,
        include: /src/,
        loader: 'vue-loader',
        options: {
          loaders: {
          }
        }
      },
      {
        test: /\.js$/,
        include: /src/,
        loader: 'babel-loader'
      },
      {
        test: /\.css$/,
        exclude: /node_modules/,
        loader: ['style-loader', 'css-loader'],
      },
    ],
  },
  plugins: [
    new CopyWebpackPlugin([{
      from: path.resolve(__dirname, 'src', 'index.html'),
    }]),
  ],
  resolve: {
    alias: {
      'vue$': 'vue/dist/vue.js'
    },
  },
  devServer: {
    contentBase: 'public',
    port: 3000,
    host: 'localhost',
    historyApiFallback: true,
  },
};

package.json はこうなりました。

{
  "name": "vue-tutorial01",
  "version": "1.0.0",
  "description": "Vue.js tutorial project",
  "author": "hoge hoge fuga fuga",
  "private": true,
  "main": "index.js",
  "license": "ISC",
  "scripts": {
    "start": "webpack-dev-server",
    "build": "webpack",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "dependencies": {
    "eslint": "^4.7.1",
    "vue": "^2.4.4"
  },
  "devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-preset-es2015": "^6.24.1",
    "copy-webpack-plugin": "^4.0.1",
    "css-loader": "^0.28.7",
    "eslint-config-standard": "^10.2.1",
    "eslint-plugin-import": "^2.7.0",
    "eslint-plugin-node": "^5.1.1",
    "eslint-plugin-promise": "^3.5.0",
    "eslint-plugin-standard": "^3.0.1",
    "eslint-plugin-vue": "^3.13.0",
    "file-loader": "^0.11.2",
    "style-loader": "^0.18.2",
    "vue-loader": "^13.0.5",
    "vue-template-compiler": "^2.4.4",
    "webpack": "^3.6.0",
    "webpack-dev-server": "^2.8.2"
  }
}

所感

なんというか、"vue init webpack-simple" で開発環境を構築した場合とほぼ同じような感じになってしまった。しかし、今回は自分の理解のために頑張ったということで良かろう。

次回からは、素直に "vue init webpack-simple" で環境構築することにしよう。

以上。

*1:本家の次の情報を参考にしました

インストール — Vue.js

Docker で node.js を動かすときは PID 1 にしてはいけない

これは、node.js on Docker の構成で 2〜3日ハマってしまった時の話です。忘れないように記録しておきます。なお、将来は改善・改良されているかもしれませんのでご注意ください。

何が起こったのか

node.js の Docker コンテナを、"docker stop" でコンテナを止めようとしても正常に停止せず、10秒くらい経過した後に強制終了してしまうという症状が発生しました。いつも等しくそうなるので、状態とかタイミングとかそういった要因ではなく、そもそも根本的に何かがおかしいと考えられます。

1. node on Docker の構成

Docker コンテナ上で node.js が動いているだけの極めてシンプルな構成でこの問題が発生しました。

f:id:ngzm:20170822140126p:plain

node.js で動くアプリは、"Hello World" を出すだけの超簡単な hello.js です。こんな感じです。

const http = require('http');

http.createServer((req, res) => {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
}).listen(3000);

hello.js アプリを Docker に仕込むための Docker ファイルは以下の通りです。ベースとなる Docker イメージは、Docker Hub で node.js が提供する node:8.4 (現在のlatest) を利用しました。

FROM node:latest
ADD ./hello.js /usr/src/app/
EXPOSE 3000
CMD ["node", "/usr/src/app/hello.js"]

普通にビルドして、生成したイメージからコンテナを作って起動します。たぶん … 無事に動作するはずです。

$ docker build -t nodetest .
$ docker run --name hello_node -P 3000:3000 nodetest

2. 問題を発生させる

動いている node.js アプリのコンテナを docker stop で停止しますと、今回の問題が発生します。

$ docker stop hello_node

恐らく、10秒ほど経過した後にコンテナが停止しプロンプトが戻ってくると思いますが、この停止に10秒かかることが問題です。

3. docker stop で何が起こっているか?

“docker stop” コマンドを実行すると、Docker はそのコンテナのルートプロセス(すなわち、PID=1 のプロセス)に SIGTERM シグナルを投げます。

このシグナルを受信したプロセスは、該当するシグナルハンドリング処理を実行します。多くのプログラムでは、SIGTEM を受けた場合、自身を安全に停止するシグナルハンドリング処理が実装されています。

f:id:ngzm:20170822140136p:plain

SIGTERM を投げた後、Docker は コンテナ(ルートプロセス)が終了するまで デフォルト10秒間待ちます。そして、10秒経過しても終了しない場合は、コンテナのルートプロセスに対して SIGKILL を投げます。SIGKILL は強制終了を指示するものなので、これを受信したプロセスは、直ちにABORTします。

f:id:ngzm:20170822140148p:plain

この時、プロセスの終了に必要な処理は全てスキップされますので ABORT 後は正常な状態であることが保証されません。したがって、次回起動時にうまく立ち上がらなくなる等、思わぬ障害のリスクがあります。

今回の node.js のコンテナも、10秒後に停止しています。これはすなわち SIGKILL で強制終了していることになり、このままではやばい感じです。

原因調査

A. 物理サーバ環境と Docker 環境での違いを検証

ここで、そもそも Docker に起因する問題なのか? それとも node.js の問題なのか?を切り分けたいと思います。

ということで、仮想化しない物理サーバ環境と Docker 環境の両方で node.js の hello アプリを動かし、そのプロセスに対してそれぞれ SIGTERM を送信してみます。

(1)物理サーバ環境で検証

#### Terminal 1 で node アプリを起動
$ node hello.js

#### Terminal 2 で node に SIGTERM を送信
$ ps -ef | grep node
ubuntu 8526 4061 3 05:21 pts/0 00:00:00 node hello.js

$ kill -TERM 8526
#### 成功、node hello.js は、直ちに問題なく終了した

うまくいきました。
物理サーバ環境において、SIGTERM を受信した node.js プロセスは、直ちに自分自身を終了させています。これにより、node.js は SIGTERM を受信した際のシグナルハンドリングがきちんと実装されていることが分かります。

(2) Docker コンテナ環境で検証

#### nodeアプリが入った Docker コンテナを起動
$ docker run --name hello_node -d nodetest

$ docker ps
CONTAINER ID IMAGE    COMMAND                CREATED         STATUS        PORTS     NAMES
ef2b7ab09468 nodetest "node /usr/src/app..." 50 seconds ago  Up 49 seconds 3000/tcp hello_node

#### 起動したコンテナにアタッチ
$ docker exec -it hello_node /bin/bash

#### ---- hello_node コンテナ内 ----
# ps -ef
UID  PID PPID C  STIME TTY   TIME     CMD
root 1   0    1  01:53 pts/0 00:00:00 node /usr/src/app/hello.js
root 16  0    0  01:54 pts/1 00:00:00 /bin/bash

# kill 1
### しなない!!

ダメです!!
幾ら SIGTERM を投げても反応しません。Docker コンテナの内の node は、何かの理由で SIGTERM を受信できないか、もしくはシグナルを無視しているように見えます。

B. プロセスID 1 に注目してさらに調査

Linux についてご存知の方は、Dockerコンテナ内の node.js プロセスIDが “1” であることに、限りない怪しさを覚えるかもしれません。

f:id:ngzm:20170822140159p:plain

普通の Linux 環境において、プロセスID = 1 は init プロセスです。これは カーネルから起動されますが、(特に Linux においては)プロセスD=1 に対してシグナルを送ることはいろいろ制限されています(see ”man 2 kill" on Linux)。

ということで、今度は、特に、プロセスID 1 に何か特殊な理由が無いか?ということを留意しながら、さらに node.js のソースコードを確認したり、同様の問題がなかったか?という調査を行いました。


・・
・・・

見つけました

github.com

上記ページの「Handling Kernel Signals」というところに、下記の記載があります。

Node.js was not designed to run as PID 1 which leads to unexpected behaviour when running inside of Docker. For example, a Node.js process running as PID 1 will not respond to SIGTERM (CTRL-C) and similar signals. As of Docker 1.13, you can use the –init flag to wrap your Node.js process with a lightweight init system that properly handles running as PID 1.

docker run -it –init node

You can also include Tini directly in your Dockerfile, ensuring your process is always started with an init wrapper.

まじか。。そもそもnode.js は PID 1 で動くように設計されていないとか、なので Docker コンテナで動かすときは、SIGTERM とか Ctrl+C とか効かないとか、まさに、今回の問題について言及されています。

もう一つ、"lightweight init system" というツールがあって、Docker で動かすときは、それを PID 1 として動作させ、node はその子プロセス にすればいいじゃん的なことも書いてあります。

f:id:ngzm:20170822140208p:plain

というか、"Tini" という便利なものがあったのか!! しかも、Docker 1.13 からは “–init” オプションでこれが自動的にラップするように組み込まれているということです。知らなかった。

GitHub - krallin/tini: A tiny but valid `init` for containers

対処内容

問題の原因が分かったので、もう簡単に対策ができます。ということで、早速 Docker 本体にも組み込まれている “Tini” を活用します。

結局、今回は、以下のように Dockerfile に Tiny の設定を追加するだけで、万事 OK となりました。

FROM node:latest

# Add Tini
ENV TINI_VERSION v0.15.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
ENTRYPOINT ["/tini", "--"]

# MyApp
ADD ./test3.js /usr/src/app/
EXPOSE 3000
CMD ["node", "/usr/src/app/hello.js"]

まとめ

  • node は PID1 で動作するようにデザインされていないので、 docker にいれるときは、直接 Docker の管理プロセス(つまりPID1)にするのはやめよう!

  • Docker で PID 1 で Linux の init みたいに制御してくれる Tiny という便利なツールがあるので、それを活用するといいかも

そもそも、node やその他 サーバプログラムをDocker が直接起動するプロセス(つまり PID 1 となるプロセス)とするのは、いろいろ問題があるなぁと思います。

以前、Unicorn が SIGTERM ではなく SIGQUIT を期待しており、Docker stop で異常終了してしまう問題にも遭遇しています。

なので、個別に作成したスーパバイザ Shell でラップするとか、今回ご紹介した Tiny をかませるとか、その方が安全ではないか?と、最近はそう思っています。

なお、PID 1 とする Tiny のような lightweight init プログラムは、例えば、自分自身が SIGTERM を受信したときに、そのシグナルを、子プロセスまできちんと伝播させたり、子プロセスがゾンビにならないよう wait したり、そういった init の作法に則ったものでなければなりません。その辺は十分ご注意ください。

おまけ

Docker で使える lightweight init たち

ここでは、Docker コンテナで PID 1 のプロセスとして使える Tiny をご紹介しました。その後調べたところ、この他にも、幾つか似たようなものがありましたので、まとめとして、以下にリンクしておきます。

Tiny

本記事でご紹介した lightweight init 、前途した通りDocker にも組み込まれており “docker run” の “–init” オプションで自動的にラップできます。

github.com

dumb-init

“Yelp” で使用している lightweight init

github.com

my_init

“baseimage-docker” という本来の Linux に近いプロセス環境を Docker で提供している。そのイメージで使用している my_init、こちらはマルチプロセスを管理できるようです。

github.com

inits_on_docker/init_node.sh

ちなみに、私も Shell で簡易に作成してみました。node.js 専用ですが、せっかくだし、ご紹介させてください。(つっこみ歓迎)

inits_on_docker/init_node.sh at master · ngzm/inits_on_docker · GitHub

#!/bin/bash
#
# init_node.sh
# - init process for aplications using node.js.
# 
# Usage
# - add your Dockerfile as follows.
#   ------------
#   ADD ./init_node.sh /usr/local/bin
#   RUN chmod +x /usr/local/bin/init_node.sh
#   CMD ["init_node.sh", [path_to_your_app]]
#   ------------
#

echo "start init_node.sh"

# set application path
path_to_your_app=${1:-''}
if [ -z ${path_to_your_app} ]; then
  echo "require first argment for path_to_your_app"
  echo "quit this container"
  exit 1
fi

if [ ! -e ${path_to_your_app} ]; then
  echo "${path_to_your_app} is not exists"
  echo "quit this container"
  exit 2
fi

# Application PID initialize
your_app_pid=0

# SIGINT handler
int_handler() {
  echo "int_handler called"
  if [ ${your_app_pid} -ne 0 ]; then
    kill -INT ${your_app_pid}
  fi
}

# SIGTERM handler
term_handler() {
  echo "term_handler called"
  if [ ${your_app_pid} -ne 0 ]; then
    kill -TERM ${your_app_pid}
  fi
}

# trap SIGINT - usually caught by Ctrl+c
trap 'int_handler' INT

# trap SIGTERM - sent when 'docker stop'
trap 'term_handler' TERM

# run application
echo "run ${path_to_your_app}"

node ${path_to_your_app} &
your_app_pid="${!}"

# wait untill the application (child process) will be killed
wait ${your_app_pid}
your_app_pid=0

echo "finish init_node.sh"

補足とか

何もこんなに苦労しなくても最初から “npm run” で起動するようにしておけば問題なく起動・終了ができそうです!という情報もあって、一応動作させてみたら、よさげに見えました。確かにこれだと、node の PID は 1 ではなくなるのでいい感じです。

ただし、本当にこれで、子プロセス、孫プロセスまで管理できているのか?すなわち、ゾンビにならずに、また強制終了せずに健全に起動、終了しているのか?といったところまでは裏付けができていません。なので継続して調べていきたいと思います。

誰か知っている人が居たらぜひ教えてください。

最後に苦情です

docker-node/README のページ、本家のトップページで思いっきり PID 1 で起動するインストラクションしてるじゃん

docker-node/README.md at master · nodejs/docker-node · GitHub

あと、こんなシビアな問題があるなら、もっと目立つところで情報公開してください

ubuntu ファイアーウォール :Docker が UFW 管理外のポートを開けてしまう問題

UFW とは ファイアーウォールを設定・管理するツールで、Ubuntuでは標準的に利用します。このツールを使用すれば、iptables を使うよりはるかに簡単にファイアーウォールを構築できます。
UFW - Community Help Wiki

Docker はご存知ですね。
Docker - Build, Ship, and Run Any App, Anywhere

遭遇した問題

今回 UFW と docker の両方を使用するサーバで、UFW で許可していないポート番号がいつのまにか空いてしまった問題に遭遇しました。 次のような感じです。

  • ubuntu xenial サーバに対し、UFW で 22番と443番だけアクセス可能、その他は遮断というファイアーウォールをセットアップ
  • 同サーバに Docker 入れて、nginx コンテナ起動、この時 “-p 80:80” オプションを付与
  • 外部からこのサーバにアクセスしたら、UFW で開けていないはずの 80番ポートのページが見れてしまった
  • UFW の設定を再確認してみたが、やっぱし 80番ポートは Allow してない

なんだかやばい感じです。
ちな、UFW の設定内容は以下の通りです。

$ sudo ufw status verbose

Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip

To         Action      From
--         ------      ----
443        ALLOW IN    Anywhere
22         LIMIT IN    Anywhere
443 (v6)   ALLOW IN    Anywhere (v6)
22 (v6)    LIMIT IN    Anywhere (v6)

問題の原因

Docker の問題

調査したところ、ひとつの原因は … Docker でした。docker run の -p や -P オプションでコンテナを起動すると、Docker は、その指定されたポート番号について、ファイアーウォールに穴を開ける処理をします。

UFWの問題

では、どうして UFW で確認できなかったのでしょうか? それは … 直接 iptables のルール一覧を確認すれば分かるかもしれません。

$ sudo iptables -L

Chain INPUT (policy DROP)
target     prot opt source               destination
ufw-before-logging-input  all  --  anywhere             anywhere
ufw-before-input  all  --  anywhere             anywhere
ufw-after-input  all  --  anywhere             anywhere
ufw-after-logging-input  all  --  anywhere             anywhere
ufw-reject-input  all  --  anywhere             anywhere
ufw-track-input  all  --  anywhere             anywhere

Chain FORWARD (policy DROP)
target     prot opt source               destination
DOCKER-USER  all  --  anywhere             anywhere
DOCKER-ISOLATION  all  --  anywhere             anywhere

... 省略 ...

Chain DOCKER (2 references)
target     prot opt source               destination
ACCEPT     tcp  --  anywhere             172.18.0.4           tcp dpt:https
ACCEPT     tcp  --  anywhere             172.18.0.4           tcp dpt:http

Chain DOCKER-ISOLATION (1 references)
target     prot opt source               destination
DROP       all  --  anywhere             anywhere
DROP       all  --  anywhere             anywhere
RETURN     all  --  anywhere             anywhere

Chain DOCKER-USER (1 references)
target     prot opt source               destination
RETURN     all  --  anywhere             anywhere

Chain ufw-after-forward (1 references)
target     prot opt source               destination

... 省略 ...

Chain ufw-user-input (1 references)
target     prot opt source               destination
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:https
ACCEPT     udp  --  anywhere             anywhere             udp dpt:https

... 省略 ...

UFW が設定したルールには、"ufw-[hogehoge]“ という名前が付いています。一方、"DOCKER” で始まるチェーンも見ることができますが、こちらは明らかに Docker によって追加されたルールですね。

どうやら UFW は 自分が設定したルール、すなわち “ufw-[hogehoge]” についてのみ管理対象としているようです。従って、Docker など他のプログラムや、手作業で追加したルールについては関与していません。これこそがもう一つの原因であり、運用する上で大変注意が必要である事項であります。

ちなみに、Docker が勝手にポートを開けるのを抑える方法

ubuntu の場合だと、”/etc/default/docker” の ”DOCKER_OPTS" オプションに、"–iptables=false" を追加すればいいようです。

DOCKER_OPTS="--dns 8.8.8.8 --dns 8.8.4.4 --iptables=false" 

なお、オプションを有効にするため Docker の再起動を忘れないようにしてください。

まとめ

-p や -P オプション(also EXPOSE)をつけて “docker run” すると Docker が 該当するポートを許可する設定を iptables に追加する。

UFW は自分で設定したルール以外は関与しない。このため、UFW で確認できる設定内容がファイアーウォールの全てではないことに注意すること。

ファイアーウォールについて、最終的には iptables コマンドでも確認した方が良いと思われます。

参考

Let's Encrypt で HTTPS な nginx 構築

おおまかな流れ

nginx を HTTPS 対応するおまかな流れは以下の通りです。

  • Let's Encript が提供するツール "Certbot" のインストール
  • 上記ツールを利用して SSL/TSL 証明書を取得
  • 取得した証明書を nginx に設定して HTTPS に対応

ちなみに、Let's Encript のサイトはこちらです。

letsencrypt.org

また、日本語ポータルサイトもあるので、適宜参考にすること。

letsencrypt.jp

Certbot のインストール

基本的には、Certbot 本家サイトに従えばインストールできます。

certbot.eff.org

自分のサイトは、OS:Ubuntu 16.04(Xenial) 、Webサーバ:nginx である。この場合、インストール手順は下記のとおり(本家サイトの説明通り)。

$ sudo apt-get update
$ sudo apt-get install software-properties-common
$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt-get update
$ sudo apt-get install python-certbot-nginx 

証明書取得

ここからは、Certbot 本家のサイトに書いてあるのとちょっと違う。なぜなら、nginxサーバが Dockerコンテナの入っていたりゴニュゴニュしているためです。*1

今回は、3つのサーバに対応した証明書を発行してもらいます。具体的には、"-d" オプションで複数のサーバ名を指定すればOK。これについては、以下のサイトが参考になります。

tnamao.hatenablog.com

また、"--standalone" オプションを指定。これにより、Certbot クライアントに内蔵されている簡易的なウェブサーバが機能が一時的に使われるため、自分サイトの nginx は一旦停止しておく必要があります。

ともあれ、実行したコマンドのログを貼り付けておきます。

### 証明書の取得 ###

$ sudo certbot certonly --standalone -d my-domain.jp -d www.my-domain.jp -d app.my-domain.jp

### メールアドレスの指定(初回のみらしい) ###

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Enter email address (used for urgent renewal and security notices) (Enter 'c' to
cancel):mysccount@my-domain.jp

### なんか同意する ###

-------------------------------------------------------------------------------
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf. You must agree
in order to register with the ACME server at
https://acme-v01.api.letsencrypt.org/directory
-------------------------------------------------------------------------------
(A)gree/(C)ancel: A

-------------------------------------------------------------------------------
Would you be willing to share your email address with the Electronic Frontier
Foundation, a founding partner of the Let's Encrypt project and the non-profit
organization that develops Certbot? We'd like to send you email about EFF and
our work to encrypt the web, protect its users and defend digital rights.
-------------------------------------------------------------------------------
(Y)es/(N)o: N

### 証明書が発行される!! ###

Obtaining a new certificate
Performing the following challenges:
tls-sni-01 challenge for my-domain.jp
tls-sni-01 challenge for www.my-domain.jp
tls-sni-01 challenge for app.my-domain.jp
Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at
   /etc/letsencrypt/live/my-domain.jp/fullchain.pem. Your cert
   will expire on 2017-11-11. To obtain a new or tweaked version of
   this certificate in the future, simply run certbot again. To
   non-interactively renew *all* of your certificates, run "certbot
   renew"
 - Your account credentials have been saved in your Certbot
   configuration directory at /etc/letsencrypt. You should make a
   secure backup of this folder now. This configuration directory will
   also contain certificates and private keys obtained by Certbot so
   making regular backups of this folder is ideal.
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

ここまで無事に成功したら、"/etc/letsencript/live/[domein name]/" の下に証明書ファイルが生成されているはず。 ちなみに、証明書はいくつかのファイルがあり、それぞれシンボリックリンクされている。

# cd /etc/letsencrypt/live/my-domain.jp

# ls -l
lrwxrwxrwx 1 root root   44 Aug 13 12:57 cert.pem -> ../../archive/my-domain.jp/cert1.pem
lrwxrwxrwx 1 root root   45 Aug 13 12:57 chain.pem -> ../../archive/my-domain.jp/chain1.pem
lrwxrwxrwx 1 root root   49 Aug 13 12:57 fullchain.pem -> ../../archive/my-domain.jp/fullchain1.pem
lrwxrwxrwx 1 root root   47 Aug 13 12:57 privkey.pem -> ../../archive/my-domain.jp/privkey1.pem
-rw-r--r-- 1 root root  543 Aug 13 12:57 README

nginx 設定

証明書を入手できたので、これを nginx に仕込みます。
具体的には、次の2つを設定すれば OK です。

  • listen のポートを 443 にする
  • "ssl_certificate" と ”ssl_certificate_key" に証明書ファイルを指定する

ということで、以下のような感じになりました。

server {
    listen  443 ssl;
    server_name  my-domain.jp www.my-domain.jp;
    ssl_certificate         /etc/letsencrypt/live/my-domain.jp/fullchain.pem;
    ssl_certificate_key     /etc/letsencrypt/live/my-domain.jp/privkey.pem;

    access_log  /var/log/nginx/default.ssl.access.log  main;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    error_page  404              /404.html;
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

以上です。さらに、80番ポートでアクセスされた場合は、443ポートにリダイレクトするような設定を追加するとバッチリかと思います。

今後の運用

Let's Encript 証明書の有効期限は 90日間なので、今後は、90日ごとに更新手続きを行う必要があります。こちらについても、"Certbot" を使って簡単に実行できますので、そのころまたレポートを追加したいと思います。

2017年10月29日 追記: 証明書を更新しました

この記事を書いた時から3ヶ月くらい経ったので、Let's encript 証明書を更新しました。更新手順はざっくり以下の通りです。

  1. Nginx などwebサーバを止める
  2. Let's encript 証明書を更新
  3. Nginx などwebサーバを再起動

ここで、"2. Let's encript 証明書を更新" は次のコマンドを投入するだけでOKです。

$ sudo certbot renew

### コマンドを投入するといろいろ処理メッセージが出ます
Saving debug log to /var/log/letsencrypt/letsencrypt.log

-------------------------------------------------------------------------------
Processing /etc/letsencrypt/renewal/my-domain.jp.conf
-------------------------------------------------------------------------------
Cert is due for renewal, auto-renewing...
Renewing an existing certificate
Performing the following challenges:
tls-sni-01 challenge for my-domain.jp
tls-sni-01 challenge for www.my-domain.jp
tls-sni-01 challenge for app.my-domain.jp
Waiting for verification...
Cleaning up challenges

-------------------------------------------------------------------------------
new certificate deployed without reload, fullchain is
/etc/letsencrypt/live/my-domain.jp/fullchain.pem
-------------------------------------------------------------------------------

Congratulations, all renewals succeeded. The following certs have been renewed:
  /etc/letsencrypt/live/my-domain.jp/fullchain.pem (success)

以上で完了です。とてもシンプルです。
次回からは、自動的に更新処理が実行されるよう cron に登録するといいですね。

*1:ノーマルな nginx サーバであれば、本家 Certbot のサイトに書いてあるように、"sudo certbot --nginx" もしくは "sudo certbot --nginx certonly" とやれば、自サイトの nginx 設定を見ながらいい感じに証明書を発行してくれるようです。

node express + react 開発環境作成手順(自分メモ)

サーバアプリは “node” + “express"、クライアントアプリには "React” を使用した場合の開発環境を構築した時の手順を記載します。あくまでも自分用メモです。

概要

大体の構成

  • サーバ:
    ”node" + “express"。

  • クライアント:
    “React"。クライアントのビルドに "webpack"。JSトランスパイラは "babel"。その他 CSS のビルドに "style-loader"、また、ビルドしたReact JSファイルを読み込む index.htmlを生成してくれるプラグインには "html-webpack-plugin” を利用する。

大まかな流れ

  1. node + express-generator をインストール
  2. express 開発環境を作成
  3. react インストール
  4. webpack 導入
  5. babel 導入
  6. style-loader 導入
  7. html-webpack-plugin 導入

サーバアプリ開発環境の構築

1) node + express-generator をインストール

最初に node をインストール。nodebrew を使用すると吉。
続いて、express-generator をグローバルにインストール。

$ npm install express-generator -g

2) express 開発環境を構築

express-generator でサーバアプリ開発環境の雛形を作成。
今回は、HTMLテンプレートエンジンに “pug” を指定。(実際は使用しないかもしれないけど一応あると便利かも)

$  express [app-name] --view=pug

これで、[app-name] ディレクトリ配下に、node + express 開発環境の雛形が生成され、最低限のサーバアプリ開発環境ができた。あとは、自分の必要に応じて、".gitignore" などを作ったり、EsLint のセットアップをすること。

クライアントアプリ開発環境を追加する

3) reacインストール

$ npm install --save react react-dom

4) webpack 導入

$ npm install --save-dev webpack

5) Babel を導入

# webpack から babel を利用する場合の基本モジュール
$ npm install --save-dev babel-loader babel-core

# React をとトランスパイルするモジュール
$ npm install --save-dev babel-cli babel-preset-react

# Es2015 をトランスパイルするためのもの
$ npm install --save-dev babel-preset-es2015

babel の設定ファイル “.babel.rc” を作成

$ echo '{ "presets": ["react", "es2015"] }' > .babelrc
$ cat .babelrc

{ "presets": ["react", "es2015"] }

ここで、babel で js をトランスパイルするための webpack 設定をおこなう。

$ vim webpack.config.js

ここまでの webpack.config.js の中身は次のとおり。

var path = require('path');

module.exports = {
  entry: './client/src/app.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'public')
  },
  module: {
    loaders: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: 'babel-loader'
      }
    ]
  }
};

6) style-loader 導入

$ npm install --save css-loader

webpack.config.js に下記の loader を追加。

module: {
      {
        test: /\.css$/,
        exclude: /node_modules/,
        loader: ['style-loader', 'css-loader'],
      },
    ],
  },

7) html-webpack-plugin 導入

$ npm install -save-dev html-webpack-plugin

index.html の元となるテンプレ “client/template/index.ejs” を作成。

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <div id='app'></div>
  </body>
</html>

あとは、webpack.config.js に html-webpack-plugin の設定を追加する。

  plugins: [
    new HtmlWebpackPlugin({
      title: 'ngzm uploader',
      template: './client/template/index.ejs',
    }),
  ],

まとめ

最終的な webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './client/src/App.jsx',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'public'),
  },
  devtool: 'inline-source-map',
  module: {
    loaders: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
      },
      {
        test: /\.css$/,
        exclude: /node_modules/,
        loader: ['style-loader', 'css-loader'],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'ngzm uploader',
      template: './client/template/index.ejs',
    }),
  ],
  resolve: {
    extensions: ['.js', '.jsx'],
  },
};

なお今回は、"webpack-dev-server" は利用していませんが、これも便利そうなので次回は導入したいと思います。

参考

以上。