20190327のdockerに関する記事は10件です。

起動中のrailsコンテナに入るシェルスクリプトのメソッド

起動中のrailsコンテナに入るシェルスクリプトのメソッド

zshrc.
function dbash() {
    id=$(docker ps -q --filter "name=web")
    command docker exec -it $id bash
}

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

失敗:CoreOS Container LinuxをZFS上でインストール:corezfs

dockerのストレージをZFSにすると、管理がしやすいと思うので、ZFSを導入。

このスクリプトで簡単にインストールができる。
https://github.com/varasys/corezfs

容量に十分に空きがあることを確認し、下記のコードを実行。

wget https://raw.githubusercontent.com/varasys/corezfs/master/corezfs
sudo ./corezfs install

必要なファイルのコンパイルを行ったり、結構時間がかかります。

途中でエラーが出たので諦めた。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

失敗:Docker on Fedora Atomic hostで特定コンテナの容量制限を柔軟に行ってみた

今までのデータは消えるでしょう。結果として失敗。

環境

Kernel Version: 4.20.15-200.fc29.x86_64
Operating System: Fedora 29.20190318.0 (Atomic Host)
[root@localhost core]# docker info
Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 0
Server Version: 1.13.1
Storage Driver: overlay2
 Backing Filesystem: xfs
 Supports d_type: true

インストール直後でのテスト

[root@localhost core]# docker run --storage-opt size=5g -t -i ubuntu /bin/bash
Unable to find image 'ubuntu:latest' locally
Trying to pull repository docker.io/library/ubuntu ... 
sha256:017eef0b616011647b269b5c65826e2e2ebddbe5d1f8c1e56b3599fb14fabec8: Pulling from docker.io/library/ubuntu
898c46f3b1a1: Pull complete 
63366dfa0a50: Pull complete 
041d4cd74a92: Pull complete 
6e1bee0f8701: Pull complete 
Digest: sha256:017eef0b616011647b269b5c65826e2e2ebddbe5d1f8c1e56b3599fb14fabec8
Status: Downloaded newer image for docker.io/ubuntu:latest
/usr/bin/docker-current: Error response from daemon: --storage-opt is supported only for overlay over xfs with 'pquota' mount option.
See '/usr/bin/docker-current run --help'.
sudo vi /etc/default/grub

GRUB_CMDLINE_LINUX= に「 rootflags=uquota,gquota,pquota 」を追記してください。

sudo grub2-mkconfig -o /boot/grub2/grub.cfg
sudo reboot

で適用できたが、だめ。

2

vi /etc/sysconfig/docker-storage-setup

STORAGE_DRIVER=overlay とする

service docker stop
atomic storage reset
service docker start

だめ

別ディスクを作成、fstabでpquotaを。

fdisk /dev/sdc
n Enter... w
mkfs -t xfs /dev/sdc1
vi /etc/fstab
/dev/sdc1 /var/lib/docker xfs defaults,pquota 0 0
service docker stop
#エラー防止
umount -a 
atomic storage reset
service docker start

だめでした

まとめ

XFSでquotaは難しそう。

Ref

https://www.server-memo.net/centos-settings/system/quota_xfs.html
https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux_atomic_host/7/html/managing_containers/managing_storage_with_docker_formatted_containers
https://qiita.com/a-killer-bee/items/564d51034c125d192df3

See Also

https://qiita.com/haniokasai/items/2b7a1889e4930b7682c9

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【動いてないです】WSL@1809 でDockerを動かすためのメモ

Win10@1809 + Ubuntu18.04ではDockerが動かないらしいですね

  • いろいろ試してみましたがdockerdまでは動きました
  • docker runで死ぬ
  • containerd.ioを操作してみたけど死ぬ

dockerdを動かす

$ sudo vim /etc/default/docker
DOCKER_OPTS="--bridge=none --iptables=false --tls=false"

# sudo cgroupfs-mount # 自動でやってくれてる
$ sudo /etc/init.d/docker start
$ sudo docker version
~ 省略 ~ 
$ sudo docker info
~ 省略 ~ 
$ sudo docker run hello-world
docker: Error response from daemon: OCI runtime create failed: container_linux.go:344: starting container process caused "process_linux.go:297: getting the final child's pid from pipe caused \"EOF\"": unknown.
ERRO[0000] error waiting for container: context canceled
/var/log/docker.io
time="2019-03-27T23:15:30.964731800+09:00" level=warning msg="Seccomp is not enabled in your kernel, running container without default profile."
time="2019-03-27T23:15:31.043117200+09:00" level=info msg="shim containerd-shim started" address="/containerd-shim/moby/6e5818fab85b97c64699a5e2ef1fa1aa263db787ff4404e1ee0b3febbb122a34/shim.sock" debug=false pid=477
time="2019-03-27T23:15:31.174600900+09:00" level=info msg="shim reaped" id=6e5818fab85b97c64699a5e2ef1fa1aa263db787ff4404e1ee0b3febbb122a34
time="2019-03-27T23:15:31.190734000+09:00" level=error msg="stream copy error: reading from a closed fifo"
time="2019-03-27T23:15:31.190792100+09:00" level=error msg="stream copy error: reading from a closed fifo"
time="2019-03-27T23:15:31.222245400+09:00" level=error msg="6e5818fab85b97c64699a5e2ef1fa1aa263db787ff4404e1ee0b3febbb122a34 cleanup: failed to delete container from containerd: no such container"
time="2019-03-27T23:15:31.222357100+09:00" level=error msg="Handler for POST /v1.39/containers/6e5818fab85b97c64699a5e2ef1fa1aa263db787ff4404e1ee0b3febbb122a34/start returned error: OCI runtime create failed: container_linux.go:344: starting container process caused \"process_linux.go:297: getting the final child's pid from pipe caused \\\"EOF\\\"\": unknown"

docker version

$ sudo docker version
Client:
 Version:           18.09.3
 API version:       1.39
 Go version:        go1.10.8
 Git commit:        774a1f4
 Built:             Thu Feb 28 06:53:11 2019
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          18.09.3
  API version:      1.39 (minimum version 1.12)
  Go version:       go1.10.8
  Git commit:       774a1f4
  Built:            Thu Feb 28 05:59:55 2019
  OS/Arch:          linux/amd64
  Experimental:     false

docker info

$ sudo docker info
Containers: 1
 Running: 0
 Paused: 0
 Stopped: 1
Images: 1
Server Version: 18.09.3
Storage Driver: overlay2
 Backing Filesystem: <unknown>
 Supports d_type: true
 Native Overlay Diff: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
 Volume: local
 Network: bridge host macvlan null overlay
 Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
Swarm: inactive
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: N/A
runc version: 6635b4f0c6af3810594d2770f662f34ddc15b40d
init version: fec3683
Kernel Version: 4.4.0-17763-Microsoft
Operating System: Ubuntu 18.04.2 LTS
OSType: linux
Architecture: x86_64
CPUs: 8
Total Memory: 15.97GiB
Name: DESKTOP-GM6MUIU
ID: UUTN:SO5Q:FMA4:P33X:TFXM:WVFZ:PF7P:VDFW:5MT6:5V6L:OTEH:O2SN
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): false
Registry: https://index.docker.io/v1/
Labels:
Experimental: false
Insecure Registries:
 127.0.0.0/8
Live Restore Enabled: false
Product License: Community Engine

WARNING: No memory limit support
WARNING: No swap limit support
WARNING: No kernel memory limit support
WARNING: No oom kill disable support
WARNING: No cpu cfs quota support
WARNING: No cpu cfs period support
WARNING: No cpu shares support
WARNING: No cpuset support

docker runで死んだ

$ sudo docker run hello-world
docker: Error response from daemon: OCI runtime create failed: container_linux.go:344: starting container process caused "process_linux.go:297: getting the final child's pid from pipe caused \"EOF\"": unknown.
ERRO[0000] error waiting for container: context canceled

ctr で死んだ

♯ 別窓 & rootで実行
# containerd

# ctr image pull docker.io/library/redis:latest
docker.io/library/redis:latest:                                                   resolved       |++++++++++++++++++++++++++++++++++++++|
index-sha256:e09c533359fca9039ac943fec0dc3a212897582e66859853c808351b2eb47e09:    done           |++++++++++++++++++++++++++++++++++++++|
manifest-sha256:478392fbb4f132b36b8ead3435087553ee7e6f0395b3d64987ae195268d13493: exists         |++++++++++++++++++++++++++++++++++++++|
layer-sha256:cfbdd870cf756e661412595b81f9b819efc6c16a4c8f19858091d873e72588ed:    done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:cde8019a4b43a4c7fb70ef830196d12a8ba0d031add8e93c3853685b60146393:    done           |++++++++++++++++++++++++++++++++++++++|
config-sha256:a55fbf438dfd878424c402e365ef3d80c634f07d0f5832193880ee1b95626e4e:   done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:39c8f5ba1240221350872c51b65a0bd1944a5dcc735fddbe132ff565afcba704:    done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:c6fe0dfbb7e3e8e6828b7e292526ac875639aaeeb98ad2f4bb89b9cedaa1645d:    done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:97a473b37fb2921176dcdeb10cecd0171a8b2ef20ea51fcbf330a8ccd9c7efb3:    done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:27833a3ba0a545deda33bb01eaf95a14d05d43bf30bce9267d92d17f069fe897:    done           |++++++++++++++++++++++++++++++++++++++|
elapsed: 3.9 s                                                                    total:  2.3 Ki (607.0 B/s)            
unpacking linux/amd64 sha256:e09c533359fca9039ac943fec0dc3a212897582e66859853c808351b2eb47e09...
done
♯ 死ぬ時と死なない時がある

# ctr run -d docker.io/library/redis:latest redis1
ctr: OCI runtime create failed: container_linux.go:344: starting container process caused "process_linux.go:297: getting the final child's pid from pipe caused \"EOF\"": unknown
# ctr  plugins ls
TYPE                            ID                    PLATFORMS      STATUS
io.containerd.content.v1        content               -              ok
io.containerd.snapshotter.v1    btrfs                 linux/amd64    error
io.containerd.snapshotter.v1    aufs                  linux/amd64    error
io.containerd.snapshotter.v1    native                linux/amd64    ok
io.containerd.snapshotter.v1    overlayfs             linux/amd64    ok
io.containerd.snapshotter.v1    zfs                   linux/amd64    error
io.containerd.metadata.v1       bolt                  -              ok
io.containerd.differ.v1         walking               linux/amd64    ok
io.containerd.gc.v1             scheduler             -              ok
io.containerd.service.v1        containers-service    -              ok
io.containerd.service.v1        content-service       -              ok
io.containerd.service.v1        diff-service          -              ok
io.containerd.service.v1        images-service        -              ok
io.containerd.service.v1        leases-service        -              ok
io.containerd.service.v1        namespaces-service    -              ok
io.containerd.service.v1        snapshots-service     -              ok
io.containerd.runtime.v1        linux                 linux/amd64    ok
io.containerd.runtime.v2        task                  linux/amd64    ok
io.containerd.monitor.v1        cgroups               linux/amd64    ok
io.containerd.service.v1        tasks-service         -              ok
io.containerd.internal.v1       restart               -              ok
io.containerd.grpc.v1           containers            -              ok
io.containerd.grpc.v1           content               -              ok
io.containerd.grpc.v1           diff                  -              ok
io.containerd.grpc.v1           events                -              ok
io.containerd.grpc.v1           healthcheck           -              ok
io.containerd.grpc.v1           images                -              ok
io.containerd.grpc.v1           leases                -              ok
io.containerd.grpc.v1           namespaces            -              ok
io.containerd.internal.v1       opt                   -              ok
io.containerd.grpc.v1           snapshots             -              ok
io.containerd.grpc.v1           tasks                 -              ok
io.containerd.grpc.v1           version               -              ok

関係ありそうなところ

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DockerでSpringBoot+Mongoなアプリケーションを立ち上げる

目標

SpringBoot+MongoDBなアプリケーションをDockerで起動するサンプルを作る。

構築

docker-compose.yml

docker-compose.yml
version: '3.1'

services:

  mongo:
    image: mongo:latest
    volumes:
      - ./mongodb/data/db:/data/db
      - ./docker-entrypoint-initdb.d/:/docker-entrypoint-initdb.d/
    command: ["mongod", "--smallfiles", "--logpath=/dev/null"]

  example-application:
    build: ./
    image: iwanagat85:example-application
    volumes:
      - ./logs:/app/logs
    ports:
      - 8080:8080
    depends_on:
      - mongo
    restart: always
    links:
      - mongo

Dockerfile

  • MongoDBの立ち上がりまでアプリの起動を待つためにDocker公式に書いてあったスクリプトを参考に。
  • Gradleで ./build/libs にスクリプトをコピーする方法も考えたけど、RUNで出力するほうが性に合った。
Dockerfile
FROM openjdk:8-jdk-alpine

WORKDIR /app

COPY ./build/libs /app

RUN : "make wait.sh" && { \
  echo "#!/bin/sh"; \
  echo "# wait.sh"; \
  echo ""; \
  echo "set -e"; \
  echo "HOST_NAME=\${1}"; \
  echo "host=\${HOST_NAME%:*}"; \
  echo "port=\${HOST_NAME#*:}"; \
  echo "shift"; \
  echo ""; \
  echo "cmd=\"\$@\""; \
  echo ""; \
  echo "while !(nc -z \${host} \${port}) ; do"; \
  echo "    echo \"Waiting \${host}:\${port} to initialize...\""; \
  echo "    sleep 2"; \
  echo "done"; \
  echo ""; \
  echo ">&2 echo \"Executing command: \${cmd}\""; \
  echo "exec \${cmd}"; \
} | tee /app/wait.sh


CMD ["sh", "wait.sh", "mongo:27017", "java", "-jar", "spring-boot-docker-1.0.0-SNAPSHOT.jar"]

build.gradle

  • Gradleでイメージの作成を行う。
  • 最初はDocker内でclone→buildしてみたけど、イメージのサイズが信じられないくらい膨れ上がったので止め。
  • --no-cache--force-rm を入れてもいい。
task execDockerCompose(type: Exec) {
    commandLine 'docker-compose', 'build', '--force'
}

task buildDockerImages(group: 'build', type: GradleBuild) {
    tasks = ['clean', 'build', 'bootJar', 'execDockerCompose']
}

ディレクトリ構成

.
├── .dockerignore
├── .gitignore
├── Dockerfile
├── build.gradle
├── docker-compose.yml
├── docker-entrypoint-initdb.d
│   └── init.js
├── gradle
├── gradle.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── src
    ├── main
    └── test

実行

$ ./gradlew buildDockerImages
$ docker-compose up

その他

  • 開発中はmongohosts127.0.0.1として登録して、mongoだけ立ち上げていた。
  • もっと他に良い方法がありそう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

dockerfile

vi Dockerfile

# ベースとなるイメージを指定する
FROM ruby:2.5

# コンテナ上のワーキングディレクトリを指定する
WORKDIR /usr/src/

RUN useradd -m batch
# ディレクトリやファイルをコピーする
# 左側がホストのディレクトリ、右側がコンテナ上のディレクトリ
COPY ./sample.rb /usr/src/sample.rb
USER batch
COPY --chown=batch:batch ./test /usr/src/test

# "docker build"時に実行される処理
RUN echo "building..."

# "docker run"実行時に実行される処理
#CMD ruby sample.rb

TakahashinoMacBook-Air:ReactSample-master docker build -t sample-image .

TakahashinoMacBook-Air:ReactSample-master $ docker run -it sample-image "/bin/bash"
batch@ab0a0fe33254:/usr/src$ ls -al
total 16
drwxr-xr-x 1 root root 4096 Mar 27 12:49 .
drwxr-xr-x 1 root root 4096 Mar 26 12:00 ..
-rw-r--r-- 1 root root 13 Mar 27 12:43 sample.rb
drwxr-xr-x 2 batch batch 4096 Mar 27 12:49 test

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

KubernetesとNode.jsでマイクロサービスを作成する 2/8

第2章 Tweetサービス

本章ではNode.jsを利用してTweetの作成/取得を行うTweetサービスを作成します。
Node.jsを利用したREST APIサービスの作成における(著者的)ベストプラクティスな内容を記載しているため、ボリュームはけっこう大きめとなっております。

なお、Kubernetesだけ触りたいという方は以下の完成済みのリポジトリをforkすることで、この章実装をスキップすることができます。

reireias/microservice-sample-tweet

システム構成

Tweetサービスのシステム構成は以下のようにします。

要素 採用技術
言語 Node.js
フレームワーク express
DB MongoDB
DB用ライブラリ mongoose
テストフレームワーク ava

REST API

まずはTweetサービスが必要なAPIについて設計していきます。

Tweetサービスに関係ありそうな操作は以下になります。

  • ツイートする
  • ユーザーのタイムラインを取得する
    • タイムラインにはユーザー自身のツイートと、ユーザーがフォローしているツイートが表示される

ツイートに関しては、拡張性を考えてCRUD操作全てを実装しておきましょう。

タイムラインに関してはTweetサービス単体ではユーザーのフォロー関係はわかりません。
なので、POSTのbodyに取得対象となるユーザーのID配列を入れて取得する方式とします。

まとめると、Tweetサービスでは以下の表のようなREST APIを作成します。

method path description
GET /tweets ツイート一覧取得
POST /tweets ツイート作成
GET /tweets/{id} ツイート取得
DELETE /tweets/{id} ツイート削除
POST /timeline ユーザーのタイムライン取得

DBスキーマ

続いてDBに保存するデータについて設計を行います。

Tweetサービスで永続化すべきデータは(今の所)ツイートのみです。

Tweetデータが持つ情報としては以下になりそうです。

  • 投稿者
  • 投稿日時
  • ツイート内容

これをMongoDBのスキーマに落とし込むと、次のように定義するのが妥当でしょう。

tweet document
{
  _id:       ObjectId,
  userId:    ObjectId,
  content:   String,
  createdAt: Date
}

ObjectId型はMongoDBが生成するID型を表しています。
投稿者に関しては詳細な情報はUserサービスから取得すると思うので、ここではユーザーのIDのみ保持する設計としています。

実装

では、Tweetサービスを実装していきましょう。

リポジトリ作成

GitHubにリポジトリを作成しましょう。
名前はmicroservice-sample-tweetとします。
プライベートリポジトリでもパブリックリポジトリでも、どちらでも問題ありません。

作成後、git cloneでリポジトリをローカルにcloneします。

git clone https://github.com/<username>/microservice-sample-tweet.git

また、.gitignoreファイルを以下の内容で作成しておきましょう。

gitignore
node_modules
yarn-error.log

プロジェクトの初期化

cloneしたリポジトリへ移動します。

cd microservice-sample-tweet

yarn initでプロジェクトを初期化します。

yarn init
# 対話形式でプロジェクトの設定を行う
# 基本的には任意の値で問題ないが、entry pointにはapp.jsを指定すること
yarn init v1.13.0
question name (microservice-sample-tweet): tweet
question version (1.0.0): 
question description: Tweet service.
question entry point (index.js): app.js
question repository url (https://github.com/reireias/microservice-sample-tweet): 
question author (reireias <reireias@gmail.com>): 
question license (MIT): 
question private: 
success Saved package.json

スタイルチェックとコードフォーマッタ

次にコードの品質を担保するために、eslintprettierを追加します。

eslintはコードがコーディングルールに違反していないかをチェックするスタイルチェックツールです。
prettierはコードをルールに基づき、フォーマット(整形)するツールです。
これらを導入することで、チーム開発でも統一されたスタイルで実装できますし、レビュー時の無駄な指摘も減らすことができます。(個人的には必ず導入すべきだと思っています)

以下のコマンドでnpmパッケージをプロジェクトへ追加します。

yarn add -D eslint eslint-config-standard eslint-plugin-standard eslint-plugin-promise eslint-plugin-import eslint-plugin-node prettier eslint-config-prettier eslint-plugin-prettier

-DオプションはdevDependencies(開発時の依存)への追加のオプションです。
production環境用のビルドには含めないnpmパッケージはこちらに追加します。

eslintの設定ファイル.eslintrc.jsを以下のように作成します。

.eslintrc.js
module.exports = {
  root: true,
  env: {
    browser: true,
    node: true
  },
  parserOptions: {
    ecmaVersion: 2018
  },
  extends: [
    'standard',
    'plugin:prettier/recommended'
  ],
  plugins: [
    'prettier'
  ],
  // add your custom rules here
  rules: {}
}

prettierの設定ファイル.prettierrcを以下のように作成します。

.prettierrc
{
  "semi": false,
  "singleQuote": true
}

最後にpackage.jsonに設定を追加し、yarnから実行できるようにしましょう。
場所はどこでもよいのですが、普段私はlicenseの下に記述しています。

package.json
...
  "license": "MIT",
  "scripts": {
    "lint": "eslint --ext .js --ignore-path .gitignore ."
  },
  "devDependencies": {
...

jsファイルを適当に作成し、lintコマンドを実施してみましょう。
Doneと表示されれば問題ありません。

touch app.js
yarn lint

次にprettierの動作を確認してみます。
app.jsの内容を下記のように変更します。

app.js
const a = "hoge"
const b = 1+2

ターミナルからprettierを実行します。

yarn run prettier --write app.js

すると、下記のようにapp.jsの中身が変更されているはずです。

app.js
const a = 'hoge'
const b = 1 + 2

eslintprettierもいちいちターミナルから実行するのは面倒ですよね?
なので、VimやVisual Studio Codeといったエディタのプラグイン等を利用し、エディタから実行or自動実行されるように設定することを推奨します。
(私はVimでALEというプラグインを利用してコードが変更される度に非同期実行しています)

コミット時に自動でyarn lintを実行する

前節でeslintprettierを追加しましたが、すべての開発者がこれらのツールを必ず実行し、コードをクリーンに保ってくれるとは限りません。実行せずにスタイルに違反したコードをコミットしてしまう開発者も存在します。(私は何度もそういった現場に遭遇してきました。)

こういった問題を避けるためにはどうすればよいでしょうか?

主に2つの解決策があります。

  • Gitフックを利用する
  • CIで継続的にチェックする

両方実施するのが望ましいのですが、まずはGitフックを利用した自動実行を導入してみましょう。

Gitはcommitpush等のgitコマンドの実行前/実行後に任意のスクリプトを実行する機能です。
詳しい説明は公式ドキュメントを参照してください。

リポジトリ内の.git/hooksディレクトリ以下にスクリプトを設置すれば設定できるのですが、Node.jsの場合はhuskyというnpmモジュールを利用してこれらをpackage.jsonで管理することが可能です。

以下のコマンドでプロジェクトにhuskyを追加します。

yarn add -D husky

package.jsongit commit前にyarn lintを実施する設定を追加します。
scriptsの下に下記の設定を追記しましょう。

package.json
...
  "scripts": {
    "lint": "eslint --ext .js --ignore-path .gitignore ."
  },
  "husky": {
    "hooks": {
      "pre-commit": "yarn lint"
    }
  },
...

現在、app.jsprettierでフォーマットしましたが、変数の未使用によりeslintのエラーがでる状態です。(yarn lintを実施すればエラーがでます)
この状態でコードをコミットしてみましょう。

git add -A
git commit
# コミットメッセージを記述するエディタは開かず、以下のように実行結果が出力されます。
husky > pre-commit (node v11.9.0)
yarn run v1.13.0
$ eslint --ext .js --ignore-path .gitignore .

/home/takumi/dev/src/github.com/reireias/microservice-sample-tweet/app.js
  1:7  error  'a' is assigned a value but never used  no-unused-vars
  2:7  error  'b' is assigned a value but never used  no-unused-vars

✖ 2 problems (2 errors, 0 warnings)

error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
husky > pre-commit hook failed (add --no-verify to bypass)

これでスタイルチェックをコミット時に強制できるようになりました。
(しかし、huskyをインストールしていない場合や、.git/hooks以下のスクリプトを削除してしまえば回避は可能なので、CIでもチェックする必要があると思います)

最後にapp.jsを空にし、ここまでの実装はコミットしておきましょう。

expressでのREST API実装

次にREST APIを実装していきます。

npmモジュールexpressmorganをプロジェクトに追加します。

yarn add express morgan body-parser

expressはNode.jsのweb serverフレームワーク、morganexpress用のアクセスログ出力ツールになります。
body-parserexpressのリクエストボディでJSONを利用できるようにするモジュールです。

続いて、controllersディレクトリとcontrollers/v1ディレクトリを作成します。
なお、ディレクトリ構成に関しては、Best practices for Express app structureを参考にしています。

mkdir -p controllers/v1

/v1ディレクトリを作成した理由としては、APIはhttp://localhost/v1/tweetsのようなパスにすることで、将来の破壊的変更時に/v2パスで新規APIを提供できるようにするためです。

GET /v1/tweetsにダミー応答を返す実装をしてみましょう。

app.js
const express = require('express')
const morgan = require('morgan')
const bodyParser = require('body-parser')
const app = express()
app.use(morgan('short'))

// server
app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json())
app.use(require('./controllers'))

app.listen(process.env.PORT || 3000, () => {})
controllers/index.js
const express = require('express')
const tweets = require('./v1/tweets.js')

const router = express.Router()

router.use('/v1/tweets', tweets)

module.exports = router
controllers/v1/tweets.js
const express = require('express')
const router = express.Router()

router.get('/', (req, res, next) => {
  res.status(200).json({ message: 'hello' })
})

module.exports = router

起動用のスクリプトをpackage.jsonに定義します。

package.json
...
  "scripts": {
    "start": "node app.js",
    "lint": "eslint --ext .js --ignore-path .gitignore ."
  },
...

では、app.jsを実行し、URLにアクセスしてみましょう。

# サーバー起動
yarn start
# 別ターミナルで実行
curl http://localhost:3000/v1/tweets
# {"message":"hello"} が表示される

nodemonによるホットリロード

Node.jsでの開発では、開発スピードを向上させるためにホットリロード機能を利用します。
今回はnodemonを導入し、ホットリロードを実現します。

yarn add -D nodemon

package.jsonのスクリプトにdevを追加します。

package.json
...
  "scripts": {
    "start": "node app.js",
    "dev": "NODE_ENV=development nodemon ./app.js",
    "lint": "eslint --ext .js --ignore-path .gitignore ."
  },
...

yarn devでサーバーを起動した後、controllers/v1/tweets.js内のhellohogeに書き換えて見ましょう。
restartのログが出力され、アクセスすると実際に変更された値が返ってくるはずです。

yarn dev
yarn run v1.13.0
$ NODE_ENV=development nodemon ./app.js
[nodemon] 1.18.10
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node ./app.js`
# hogeに変更した後、restartされる
[nodemon] restarting due to changes...
[nodemon] starting `node ./app.js`
# curlでアクセスすると、返ってくる値が変わっている

最後に実装予定のREST APIのメソッドすべてを作成しておきましょう。
DBへのRead/Writeといった中身のロジックは次の節で実装しますので、ダミーの実装とします。

controllers/index.jsにタイムライン用のコントローラーを追加します。

controllers/index.js
const express = require('express')
const tweets = require('./v1/tweets.js')
const timeline = require('./v1/timeline.js')

const router = express.Router()

router.use('/v1/tweets', tweets)
router.use('/v1/timeline', timeline)

module.exports = router

controllers/v1/tweets.jsには/tweetsへのCRUD操作を一通り追加します。

controllers/v1/tweets.js
const express = require('express')
const router = express.Router()

router.get('/', (req, res, next) => {
  res.status(200).json({ message: 'hello' })
})

router.get('/:id', (req, res, next) => {
  res.status(200).json({ message: 'hello' })
})

router.post('/', (req, res, next) => {
  res.status(200).json({ message: 'hello' })
})

router.delete('/:id', (req, res, next) => {
  res.status(200).json({ message: 'hello' })
})

module.exports = router

controllers/v1/timeline.jsを以下の内容で作成します。

controllers/v1/timeline.js
const express = require('express')
const router = express.Router()

router.post('/', (req, res, next) => {
  res.status(200).json({ message: 'hello' })
})

module.exports = router

実装ができたらcurlコマンドで各パス、各メソッドにアクセスして、レスポンスが返ってくることを確認してみましょう。

MongoDBへの保存

それではDBへのRead/Write部分を実装していきましょう。

まずはDB関連のnpmモジュールを追加します。

yarn add mongodb mongoose

モデルを実装するディレクトリを作成します。

mkdir models

Tweetモデルを次のように実装します。

models/tweet.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema

const Tweet = new Schema(
  {
    userId: { type: Schema.Types.ObjectId, required: true },
    content: {
      type: String,
      required: true,
      minlength: 1,
      maxlength: 140
    },
    createdAt: { type: Date, default: Date.now }
  },
  {
    versionKey: false
  }
)

exports.Tweet = mongoose.model('Tweet', Tweet)

app.jsにMongoDBへのコネクションの作成を追加します。

app.js
const express = require('express')
const morgan = require('morgan')
const bodyParser = require('body-parser')
const mongoose = require('mongoose')
const app = express()
app.use(morgan('short'))

// database
const dbUrl = process.env.MONGODB_URL || 'mongodb://localhost:27017/tweet'
const options = { useNewUrlParser: true }
if (process.env.MONGODB_ADMIN_NAME) {
  options.user = process.env.MONGODB_ADMIN_NAME
  options.pass = process.env.MONGODB_ADMIN_PASS
  options.auth = { authSource: 'admin' }
}
mongoose.connect(dbUrl, options)

// server
app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json())
app.use(require('./controllers'))

app.listen(process.env.PORT || 3000, () => {})

controllers/v1/tweets.jsの実装をDBを利用するように変更します。

controllers/v1/tweets.js
const express = require('express')
const model = require('../../models/tweet.js')
const Tweet = model.Tweet
const router = express.Router()

router.get('/', (req, res, next) => {
  ;(async () => {
    const tweets = await Tweet.find({}, null, {
      sort: { createdAt: -1 }
    }).exec()
    res.status(200).json(tweets)
  })().catch(next)
})

router.get('/:id', (req, res, next) => {
  ;(async () => {
    try {
      const tweet = await Tweet.findById(req.params.id).exec()
      if (tweet) {
        res.status(200).json(tweet)
      } else {
        res.status(404).json({ error: 'NotFound' })
      }
    } catch (err) {
      console.error(err)
      res.status(400).json({ error: 'BadRequest' })
    }
  })().catch(next)
})

router.post('/', (req, res, next) => {
  ;(async () => {
    try {
      const record = new Tweet({
        userId: req.body.userId,
        content: req.body.content
      })
      const savedRecord = await record.save()
      res.status(200).json(savedRecord)
    } catch (err) {
      console.error(err)
      res.status(400).json({ error: 'BadRequest' })
    }
  })().catch(next)
})

router.delete('/:id', (req, res, next) => {
  ;(async () => {
    try {
      const removedRecord = await Tweet.findByIdAndDelete(req.params.id).exec()
      if (removedRecord) {
        res.status(200).json({})
      } else {
        res.status(404).json({ error: 'NotFound' })
      }
    } catch (err) {
      console.error(err)
      res.status(400).json({ error: 'BadRequest' })
    }
  })().catch(next)
})

module.exports = router

controllers/v1/timeline.jsも同様に実装します。

controllers/v1/timeline
const express = require('express')
const model = require('../../models/tweet.js')
const Tweet = model.Tweet
const router = express.Router()

router.post('/', (req, res, next) => {
  ;(async () => {
    const userIds = req.body
    const tweets = await Tweet.find({ userId: { $in: userIds } }, null, {
      sort: { createdAt: -1 }
    }).exec()
    res.status(200).json(tweets)
  })().catch(next)
})

module.exports = router

さて、yarn devでサーバーを起動し、実装通り動くか試したいところですが、接続先のMongoDBを用意する必要があります。
ローカルにインストールしてもいいですし、Dockerを利用して用意してもいいでしょう。

下記はDockerを利用してMongoDBコンテナを実行する例になります。

docker run -p 27017:27017 --name mongodb -d mongo

app.jsに実装したように、環境変数MONGODB_URLが空の場合はlocalhost:27017に接続するようになっています。
なので、上記コマンドでMongoDBを起動した場合はyarn devで用意したMongoDBコンテナに接続できます。

では、サーバーを起動してみましょう。

yarn dev

curlコマンドを使って動作を確認してみます。

# tweet一覧取得(空配列が返る)
curl http://localhost:3000/v1/tweets
# []

# tweet作成
curl -X POST -H "Content-Type: application/json" http://localhost:3000/v1/tweets -d '{"userId": "000000000000000000000000", "content": "hello world."}'
# {"_id":"5c84d6e0d48d0a346cad3bd9","userId":"000000000000000000000000","content":"hello world.","createdAt":"2019-03-10T09:20:32.956Z"}

# 別のユーザーでtweet作成
curl -X POST -H "Content-Type: application/json" http://localhost:3000/v1/tweets -d '{"userId": "000000000000000000000001", "content": "Fizz Buzz!"}'

# 2人のuserIdを指定してtimelineを取得
# (二人目のユーザーをフォローしている一人目のユーザーのタイムラインを想定)
curl -X POST -H "Content-Type: application/json" http://localhost:3000/v1/timeline -d '["000000000000000000000000", "000000000000000000000001"]' 
# 2つのツイートが返ってくる
# [{"_id":"5c84d75ad48d0a346cad3bda","userId":"000000000000000000000001","content":"Fizz Buzz!","createdAt":"2019-03-10T09:22:34.714Z"},{"_id":"5c84d6e0d48d0a346cad3bd9","userId":"000000000000000000000000","content":"hello world.","createdAt":"2019-03-10T09:20:32.956Z"}]

avaによるユニットテスト

コードを修正するたびにcurlを用いて動作確認を行うのは現代に生きるエンジニアのやることではありません。
ユニットテストを導入しましょう。

javascriptではavamochajest等、いくつか有名なユニットテストフレームワークが存在します。
今回は最もシンプルなavaを利用してユニットテストを記述していきます。

まずはプロジェクトにnpmパッケージを追加しましょう。
今回のユニットテストで利用するsupertestmongodb-memory-serverも一緒に追加します。

yarn add -D ava supertest@^3.4.2 mongodb-memory-server

簡単にテストコマンドを実行できるようにpackage.jsontestコマンドとwatchコマンドを定義しましょう。

package.json
...
  "scripts": {
    "start": "node app.js",
    "dev": "NODE_ENV=development nodemon ./app.js",
    "lint": "eslint --ext .js --ignore-path .gitignore .",
    "test": "ava",
    "watch": "ava --watch"
  },
...

ユニットテストファイル用のディレクトリを作成します。

mkdir test

今回のユニットテストではコントローラの界面でテストを書いていきます。
以下の実装では、supertestを利用してコントローラに簡単にリクエストを送れるようにしています。
また、ユニットテスト時にローカルのDBにはなるべく依存したくないため、mongodb-memory-serverというオンメモリで動作するMongoDBを利用します。

それぞれのコントローラについてユニットテストを実装していきます。

test/tweets.js
const test = require('ava')
const supertest = require('supertest')
const mongoose = require('mongoose')
const express = require('express')
const bodyParser = require('body-parser')
const { MongoMemoryServer } = require('mongodb-memory-server')

console.error = () => {}
const router = require('../controllers/v1/tweets.js')
const model = require('../models/tweet.js')
const Tweet = model.Tweet

const mongod = new MongoMemoryServer()
const app = express()
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use('/tweets', router)

const user1Id = new mongoose.Types.ObjectId()
const user2Id = new mongoose.Types.ObjectId()

test.before(async () => {
  const uri = await mongod.getConnectionString()
  mongoose.connect(uri, { useNewUrlParser: true })
})

test.beforeEach(async t => {
  let tweets = []
  tweets.push(
    await new Tweet({
      userId: user1Id,
      content: 'aaa'
    }).save()
  )
  tweets.push(
    await new Tweet({
      userId: user1Id,
      content: 'bbb'
    }).save()
  )
  tweets.push(
    await new Tweet({
      userId: user2Id,
      content: 'ccc'
    }).save()
  )
  tweets.push(
    await new Tweet({
      userId: user2Id,
      content: 'ddd'
    }).save()
  )
  t.context.tweets = tweets
})

test.afterEach.always(async () => {
  await Tweet.deleteMany().exec()
})

// GET /tweets
test.serial('get tweets', async t => {
  const res = await supertest(app).get('/tweets')
  t.is(res.status, 200)
  t.is(res.body.length, 4)
  t.is(res.body[0]._id, t.context.tweets[3]._id.toString())
})

// GET /tweets/:id
test.serial('get tweet', async t => {
  const target = t.context.tweets[0]
  const res = await supertest(app).get(`/tweets/${target._id}`)
  t.is(res.status, 200)
  t.is(res.body._id, target._id.toString())
  t.is(res.body.userId, target.userId.toString())
  t.is(res.body.content, target.content)
})

test.serial('get tweet not found', async t => {
  const res = await supertest(app).get(
    `/tweets/${new mongoose.Types.ObjectId()}`
  )
  t.is(res.status, 404)
  t.deepEqual(res.body, { error: 'NotFound' })
})

test.serial('get tweet id is invalid', async t => {
  const res = await supertest(app).get('/tweets/invalid')
  t.is(res.status, 400)
  t.deepEqual(res.body, { error: 'BadRequest' })
})

// POST /tweets
test.serial('create tweet', async t => {
  const content = 'xxx'
  const res = await supertest(app)
    .post('/tweets')
    .send({ userId: user1Id.toString(), content: content })
  t.is(res.status, 200)
  t.true('_id' in res.body)
  t.is(res.body.content, content)
})

test.serial('create tweet no userId', async t => {
  const content = 'xxx'
  const res = await supertest(app)
    .post('/tweets')
    .send({ content: content })
  t.is(res.status, 400)
  t.deepEqual(res.body, { error: 'BadRequest' })
})

test.serial('create tweet no content', async t => {
  const res = await supertest(app)
    .post('/tweets')
    .send({ userId: user1Id.toString() })
  t.is(res.status, 400)
  t.deepEqual(res.body, { error: 'BadRequest' })
})

test.serial('create tweet content is empty', async t => {
  const res = await supertest(app)
    .post('/tweets')
    .send({ userId: user1Id.toString(), content: '' })
  t.is(res.status, 400)
  t.deepEqual(res.body, { error: 'BadRequest' })
})

test.serial('create tweet content is too long', async t => {
  const res = await supertest(app)
    .post('/tweets')
    .send({ userId: user1Id.toString(), content: 'a' * 141 })
  t.is(res.status, 400)
  t.deepEqual(res.body, { error: 'BadRequest' })
})

// DELETE /tweets/:id
test.serial('delete tweet', async t => {
  const res = await supertest(app).delete(`/tweets/${t.context.tweets[0]._id}`)
  t.is(res.status, 200)
  const actual = await Tweet.find()
  t.is(actual.length, 3)
})

test.serial('delete tweet not found', async t => {
  const res = await supertest(app).delete(
    `/tweets/${new mongoose.Types.ObjectId()}`
  )
  t.is(res.status, 404)
  t.deepEqual(res.body, { error: 'NotFound' })
})

test.serial('delete tweet id is invalid', async t => {
  const res = await supertest(app).delete('/tweets/invalid')
  t.is(res.status, 400)
  t.deepEqual(res.body, { error: 'BadRequest' })
})
test/timeline.js
const test = require('ava')
const supertest = require('supertest')
const mongoose = require('mongoose')
const express = require('express')
const bodyParser = require('body-parser')
const { MongoMemoryServer } = require('mongodb-memory-server')

console.error = () => {}
const router = require('../controllers/v1/timeline.js')
const model = require('../models/tweet.js')
const Tweet = model.Tweet

const mongod = new MongoMemoryServer()
const app = express()
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use('/timeline', router)

const user1Id = new mongoose.Types.ObjectId()
const user2Id = new mongoose.Types.ObjectId()
const user3Id = new mongoose.Types.ObjectId()

test.before(async () => {
  const uri = await mongod.getConnectionString()
  mongoose.connect(uri, { useNewUrlParser: true })
})

test.beforeEach(async t => {
  let tweets = []
  tweets.push(await new Tweet({ userId: user1Id, content: 'aaa' }).save())
  tweets.push(await new Tweet({ userId: user1Id, content: 'bbb' }).save())
  tweets.push(await new Tweet({ userId: user2Id, content: 'ccc' }).save())
  tweets.push(await new Tweet({ userId: user2Id, content: 'ddd' }).save())
  tweets.push(await new Tweet({ userId: user3Id, content: 'eee' }).save())
  t.context.tweets = tweets
})

test.afterEach.always(async () => {
  await Tweet.deleteMany().exec()
})

// POST /timeline
test.serial('get timeline', async t => {
  const res = await supertest(app)
    .post('/timeline')
    .send([user1Id.toString(), user2Id.toString()])
  t.is(res.status, 200)
  t.is(res.body.length, 4)
})

テストが書けたらyarn testで全テストを実行してみましょう。
また、yarn watchを実行すると、ファイルの変更を検知して自動でテストを実行してくれます。テスト駆動開発の場合に重宝します。

ダミーデータ作成用スクリプト

ユニットテストで正しく実装されているかは確認できるようになりました。
しかし、全体の動作を確認したい場合等、サーバーを立ち上げてcurl等で確認したいシーンは存在します。
その時に毎回POSTでデータを作成するのは面倒なので、ダミーデータを作成するスクリプトを作っておきましょう。
後で複数サービスを立ち上げたテスト環境を作成する際にも、テスト用データの作成に重宝します。

スクリプト用のディレクトリを作成します。

mkdir scripts

DB中のレコードを消去し、ダミーデータを登録するスクリプトを実装していきます。

scripts/initialize.js
const mongoose = require('mongoose')
const Tweet = require('../models/tweet.js').Tweet

const dbUrl = process.env.MONGODB_URL || 'mongodb://localhost:27017/tweet'
const options = { useNewUrlParser: true, useCreateIndex: true }
if (process.env.MONGODB_ADMIN_NAME) {
  options.user = process.env.MONGODB_ADMIN_NAME
  options.pass = process.env.MONGODB_ADMIN_PASS
  options.auth = { authSource: 'admin' }
}

const ObjectId = mongoose.Types.ObjectId
const user1Id = new ObjectId('000000000000000000000000')
const user2Id = new ObjectId('000000000000000000000001')
const tweets = [
  {
    userId: user1Id,
    content: 'Hello World',
    createdAt: '2019-01-01T12:00:00.000Z'
  },
  {
    userId: user1Id,
    content: 'Fizz Buzz',
    createdAt: '2019-01-01T13:00:00.000Z'
  },
  {
    userId: user2Id,
    content: '古池や\n蛙飛びこむ\n水の音',
    createdAt: '2019-01-01T12:01:00.000Z'
  },
  {
    userId: user2Id,
    content: '夏草や\n兵どもが\n夢の跡',
    createdAt: '2019-01-01T12:02:00.000Z'
  }
]

const initialize = async () => {
  mongoose.connect(dbUrl, options)

  await Tweet.deleteMany().exec()

  await Tweet.insertMany(tweets)
  mongoose.disconnect()
}

initialize()
  .then(() => {
    // eslint-disable-next-line no-console
    console.log('finish.')
  })
  .catch(error => {
    console.error(error)
  })

では、スクリプトを実行してみましょう。

node scripts/initialize.js

yarn devでサーバーを起動し、curlコマンドでTweet一覧を取得してみましょう。
ダミーデータが返ってくるはずです。

Swaggerの導入

マイクロサービスを複数のチームで開発する上で、各サービス間のインターフェース定義を統一された方法で記述することが望ましいでしょう。
インターフェース定義が曖昧であったり、メンテナンスされなかったり、最新のインターフェース定義の取得が困難だったりすると、プロジェクトはまず間違いなく炎上します。(経験談)
今回は、REST APIのインターフェース定義のデファクトスタンダードであるSwaggerを利用します。

swaggerの利用方法としては、ボトムアップ型(コードやコメントからswagger specファイルを作成)とトップダウン型(swagger specファイルからコードを生成)の2種類があります。
トップダウン型は一部の言語でないと自動生成されたコードの管理が煩雑になる傾向があるため、私はボトムアップ型を採用することが多いです。

では、実際にTweetサービスにSwaggerを導入し、Swagger Specファイルを出力できるようにしていきます。

プロジェクトにswagger-jsdocを追加します。
swagger-jsdocを利用することで、javascript中のコメントに記述されたswagger定義からSwagger Specファイルを生成できるようになります。

yarn add swagger-jsdoc

swagger関連のファイルを配置するディレクトリを作成します。

mkdir swagger

各APIの定義はcontrollerのそれぞれのメソッドのコメントとして記述することになるのですが、全体で共通する設定はswagger/swaggerDef.jsに記述します。
swaggerのバージョンは最新の3.0を利用します。

swagger/swaggerDef.js
const pkg = require('../package.json')

module.exports = {
  openapi: '3.0.0',
  info: {
    title: pkg.name,
    version: pkg.version,
    description: pkg.description
  },
  servers: [
    {
      url: '/v1'
    }
  ]
}

swagger/components.ymlを以下のように記述します。

swagger/components.yml
---
components:
  schemas:
    Tweet:
      required:
        - userId
        - content
      properties:
        _id:
          type: string
          example: '999999999999999999999999'
        userId:
          type: string
          example: '000000000000000000000000'
        content:
          type: string
          minLength: 1
          maxLength: 140
          example: 'hello world.'
        createdAt:
          type: string
          format: date-time
          example: '2019-01-01T13:00:00.000Z'
    Tweets:
      type: array
      items:
        $ref: '#/components/schemas/Tweet'
    Error:
      required:
        - error
      properties:
        error:
          type: string
          example: 'BadRequest'
  responses:
    BadRequest:
      description: Bad request error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Not found error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

controllers/v1/tweets.jscontrollers/v1/timeline.jsにコメントでswagger定義を記述していきます。

controllers/v1/tweets.js
const express = require('express')
const model = require('../../models/tweet.js')
const Tweet = model.Tweet
const router = express.Router()

/**
 * @swagger
 *
 * /tweets:
 *   get:
 *     description: Return a list of tweets.
 *     tags:
 *       - tweets
 *     responses:
 *       '200':
 *         description: A JSON array of tweets
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/Tweets'
 */
router.get('/', (req, res, next) => {
  ;(async () => {
    const tweets = await Tweet.find({}, null, {
      sort: { createdAt: -1 }
    }).exec()
    res.status(200).json(tweets)
  })().catch(next)
})

/**
 * @swagger
 *
 * /tweets/{id}:
 *   get:
 *     description: Find tweet by ID.
 *     tags:
 *       - tweets
 *     parameters:
 *       - name: id
 *         in: path
 *         required: true
 *         description: Tweet ID.
 *         schema:
 *           type: string
 *         example: '000000000000000000000000'
 *     responses:
 *       '200':
 *         description: A JSON object of tweet.
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/Tweet'
 *       '400':
 *         $ref: '#/components/responses/BadRequest'
 *       '404':
 *         $ref: '#/components/responses/NotFound'
 */
router.get('/:id', (req, res, next) => {
  ;(async () => {
    try {
      const tweet = await Tweet.findById(req.params.id).exec()
      if (tweet) {
        res.status(200).json(tweet)
      } else {
        res.status(404).json({ error: 'NotFound' })
      }
    } catch (err) {
      console.error(err)
      res.status(400).json({ error: 'BadRequest' })
    }
  })().catch(next)
})

/**
 * @swagger
 *
 * /tweets:
 *   post:
 *     description: Create a tweet.
 *     tags:
 *       - tweets
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             properties:
 *               userId:
 *                 type: string
 *                 example: '000000000000000000000000'
 *               content:
 *                 type: string
 *                 minLength: 1
 *                 maxLength: 140
 *                 example: 'hello world.'
 *             required:
 *               - userId
 *               - content
 *     responses:
 *       '200':
 *         description: Created tweet.
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/Tweet'
 *       '400':
 *         $ref: '#/components/responses/BadRequest'
 */
router.post('/', (req, res, next) => {
  ;(async () => {
    try {
      const record = new Tweet({
        userId: req.body.userId,
        content: req.body.content
      })
      const savedRecord = await record.save()
      res.status(200).json(savedRecord)
    } catch (err) {
      console.error(err)
      res.status(400).json({ error: 'BadRequest' })
    }
  })().catch(next)
})

/**
 * @swagger
 *
 * /tweets/{id}:
 *   delete:
 *     description: Delete a tweet.
 *     tags:
 *       - tweets
 *     parameters:
 *       - name: id
 *         in: path
 *         required: true
 *         description: Tweet ID.
 *         schema:
 *           type: string
 *         example: '000000000000000000000000'
 *     responses:
 *       '200':
 *         description: Empty body.
 *       '400':
 *         $ref: '#/components/responses/BadRequest'
 *       '404':
 *         $ref: '#/components/responses/NotFound'
 */
router.delete('/:id', (req, res, next) => {
  ;(async () => {
    try {
      const removedRecord = await Tweet.findByIdAndDelete(req.params.id).exec()
      if (removedRecord) {
        res.status(200).json({})
      } else {
        res.status(404).json({ error: 'NotFound' })
      }
    } catch (err) {
      console.error(err)
      res.status(400).json({ error: 'BadRequest' })
    }
  })().catch(next)
})

module.exports = router
controllers/v1/timeline.js
const express = require('express')
const model = require('../../models/tweet.js')
const Tweet = model.Tweet
const router = express.Router()

/**
 * @swagger
 *
 * /timeline:
 *   post:
 *     description: Get user timeline.
 *     tags:
 *       - timeline
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: array
 *             items:
 *               type: string
 *             example: ['000000000000000000000000', '000000000000000000000001']
 *     responses:
 *       '200':
 *         description: A JSON array of tweets.
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/Tweets'
 */
router.post('/', (req, res, next) => {
  ;(async () => {
    const userIds = req.body
    const tweets = await Tweet.find({ userId: { $in: userIds } }, null, {
      sort: { createdAt: -1 }
    }).exec()
    res.status(200).json(tweets)
  })().catch(next)
})

module.exports = router

swagger-jsdocのCLI機能を利用してSwagger Specを生成してみましょう。

package.jsonに生成コマンドを定義します。

package.json
...
  "scripts": {
    "start": "node app.js",
    "dev": "NODE_ENV=development nodemon ./app.js",
    "lint": "eslint --ext .js --ignore-path .gitignore .",
    "swagger": "swagger-jsdoc -o ./swagger/swagger.yml -d ./swagger/swaggerDef.js ./controllers/**/*.js ./swagger/components.yml",
    "test": "ava",
    "watch": "ava --watch"
  },
...

次のコマンドでswagger/swagger.ymlにSwagger Specを出力します。

yarn swagger

生成されたファイルをSwagger Editorに貼り付けて確認してみましょう。
Swagger Editorへアクセスします。
左ペインへ生成されたswagger/swagger.ymlの中身を貼り付けます。
右ペインでSwaggerの定義が確認できるはずです。

swagger_01.png

さて、毎回Swagger Specを確認するために、yarn swaggerでファイル出力し、Swagger Editorへコピペするのは大変です。
この確認を簡単にするために、起動したサーバーから直接Swagger Specを取得できるようにします。

サーバーから直接取得できるようにすることで、jsファイルを更新すればnodemonのホットリロードで即座に反映されたり、Swagger UIを用いて簡単に生成されたSwagger Specを確認できるようになります。
また、テストサーバー等で公開することで、他のチームでも容易にSwagger Specを確認できるようになる点もメリットの一つです。

controllers/index.jsを下記のように修正します。
今回はdevelopment以外の環境ではSwagger Specファイルは取得できない実装にしています。

controllers/index.js
const express = require('express')
const tweets = require('./v1/tweets.js')
const timeline = require('./v1/timeline.js')

const router = express.Router()

// swagger
if (process.env.NODE_ENV === 'development') {
  const swaggerJSDoc = require('swagger-jsdoc')
  const options = {
    swaggerDefinition: require('../swagger/swaggerDef.js'),
    apis: [
      './controllers/v1/tweets.js',
      './controllers/v1/timeline.js',
      './swagger/components.yml'
    ]
  }
  const swaggerSpec = swaggerJSDoc(options)
  // CROS
  router.use((req, res, next) => {
    res.header('Access-Control-Allow-Origin', '*')
    res.header(
      'Access-Control-Allow-Methods',
      'GET, POST, DELETE, PUT, PATCH, OPTIONS'
    )
    res.header(
      'Access-Control-Allow-Headers',
      'Origin, X-Requested-With, Content-Type, Accept'
    )
    next()
  })
  router.get('/v1/swagger.json', (req, res) => {
    res.setHeader('Content-Type', 'application/json')
    res.send(swaggerSpec)
  })
}

router.use('/v1/tweets', tweets)
router.use('/v1/timeline', timeline)

module.exports = router

yarn devでブラウザを起動し、http://localhost:3000/v1/swagger.jsonへアクセスしてみましょう。
Swagger Specがjson形式で取得できるはずです。

次にSwagger UIを使用してSwagger Specを確認します。

swagger-uiをクローンし、dist/index.htmlをブラウザで開きましょう。
そしてページ内のアドレスバーにhttp://localhost:3000/v1/swagger.jsonと入力し、Exploreボタンを押してみましょう。
Swagger Editorで表示したのと同様の形式で表示されるはずです。

swagger_02.png

テストサーバーからSwagger Specを配信しているため、Swagger UI上からサンプルリクエストを投げることもできます。
exampleを適切に記述することで、サンプルリクエストの初期値として入力されるので、使い勝手が向上します。

このように適切にメンテナンスされたインターフェース仕様を提供することで、複数のチームでの開発を円滑に進めることが可能になるでしょう。

最終的なプロジェクト構成

最終的なプロジェクト構成を下記に示します。

microservice-sample-tweet
├── LICENSE
├── app.js                 # エントリポイント
├── controllers            # コントローラー用ディレクトリ
│   ├── index.js
│   └── v1
│       ├── timeline.js    # /timeline に関するコントローラー
│       └── tweets.js      # /tweets に関するコントローラー
├── models
│   └── tweet.js           # Tweetリソースのスキーマ定義
├── package.json
├── scripts
│   └── initialize.js      # 初期化&ダミーデータ作成スクリプト
├── swagger                # Swagger関連ディレクトリ
│   ├── components.yml
│   ├── swagger.yml        # 生成されたSwagger Specファイル
│   └── swaggerDef.js
├── test                   # テスト関連ディレクトリ
│   ├── timeline.js        # /timeline に関するテスト
│   └── tweets.js          # /tweets に関するテスト
└── yarn.lock

第2章まとめ

第2章ではNode.js + MongoDBを利用してTweetサービスを構築しました。

下記を導入し、いわゆるサンプルアプリケーションよりは、より実践的なREST APIサービスを構築しました。

  • eslintprettierによる強力なスタイルチェックとコードフォーマット
  • mongooseを利用したスキーマ定義とDBアクセス
  • avaを利用したユニットテスト
  • Swaggerを利用したインターフェース仕様の提供

次の章では本章と同様の手順でUserサービスを作成していきます。

次章: 第3章 Userサービス(そのうち投稿します)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

KubernetesとNode.jsでマイクロサービスを作成する 1/8

Node.jsとKubernetesを使い、マイクロサービスを作ってみたくなったので、このチュートリアルを作成してみました。
バグや修正した方がよい点などあれば気軽にコメントをおねがいします。

本チュートリアルは以下の8章構成になっています。

  • 第1章 概要
  • 第2章 Tweetサービス
  • 第3章 Userサービス
  • 第4章 Webサービス
  • 第5章 Docker
  • 第6章 Kubernetes with minikube
  • 第7章 Kubernetes with GCP
  • 第8章 Kubernetes with AWS

第1章 概要

この章では本チュートリアルの概要について記載します。

目的

本チュートリアルは以下の目的で作成されています。

  • Kubernetesを使い、実際にMicroserviceを構築することで、Kubernetesへの理解を深める
  • 複数チームによる開発を考慮し、リポジトリや検証環境を構築する
  • AWSのEKSとGCPのGKEを利用することで、PaaSでのKubernetes利用の知見を獲得する

作成するMicroserviceの概要

本チュートリアルではKubernetesとNode.jsを使い、TwitterライクなMicroserviceを構築します。

実装する機能としては下記になります。

  • ログイン/ログアウト機能(GitHub認証)
  • ツイート機能
  • フォロー/アンフォロー機能
  • タイムライン機能(自分 + フォローしているユーザーのツイート一覧)

構築するMicroserviceの概要図は下記のようになります。

BFF(Backend for Frontend)を採用し、Webサービスのサーバーサイドが各サービスとデータをやり取りする構成としています。

microservice-tutorial01.png

なお、Node.jsを採用した理由は下記のとおりです。

  • ミドルウェアなしに単一のプロセスで起動できること
    • Cloud Nativeなアプリケーションを作成する上で重要です
  • なるべくメジャーな言語であること
  • チュートリアル読者の実装ハードルを下げるため、各サービスとも共通した言語にしたい

各サービスについて

以下では、本チュートリアルで作成する3つのマイクロサービスについて説明します。

Webサービス

フロントエンド + BFFのサービスです。
ここでは、BFFはフロントエンドのチームが所有するものとしています。
GitHubを利用したOAuth2.0による認証機能を持ちます。

Userサービス

ユーザーやフォロー関係を扱うサービスです。
REST APIを各サービスへ提供します。

Tweetサービス

ツイートを扱うサービスです。
REST APIを各サービスへ提供します。

採用技術

本チュートリアルで作成するサンプルにおいて使用する主要な技術を列挙しておきます。
詳細は各サービスの章で説明します。

アーキテクチャ系

  • Microservice
    • 全体のアーキテクチャとして採用
  • BFF(Backend for Frontend)
    • フロントエンドと各サービスのAPI呼び出しパターンとして採用
  • REST API
    • サービス間通信のI/Fとして採用
  • MongoDB
    • NoSQLデータベース
    • Userサービス、Tweetサービスのデータベースとして採用

実装系

  • Node.js
    • 今回は全サービスの言語として採用
    • もちろん、Microserviceなので、各サービスで好きな言語を使用できる
  • Nuxt.js
    • Vue.jsを利用したSPA + SSRフレームワーク
    • WebサービスでUIフレームワークとして採用
  • Vuetify.js
    • Vue.js向けのマテリアルデザインライブラリ
    • UIの見た目をそれっぽくするためにNuxt.jsに組み込んで利用する
  • Express.js
    • Node.js製サーバーサイドのデファクトスタンダード
    • WebサービスのBFF部分やUserサービス、TweetサービスのREST APIサーバーとして利用
  • mongoose
    • Node.jsからMongoDBを利用するためのライブラリ
    • スキーマ定義も可能

第1章 まとめ

この章ではこれから作成するマイクロサービスの概要を紹介しました。
次の章からは実際に各サービスを作成していきます。

次章: 第2章 Tweetサービス

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Harborでプライベートレポジトリを立てる(CentOS、Let'sEncrypt証明書)

さくらのクラウド上にHarborを使ってプライベートレポジトリをたててみたので、そのやり方とハマりポイントをご紹介します。

背景・前提

セキュリティ上の観点などからプライベートなレポジトリを作りたいという要求があると思います。OSSで構築する方法としてDocker Registry + Portus という方法もあるようですが、ここではHarborを使う方法について書きます。(Harborも内部でDocker Registryを使っているようなので、両者は構成は似たようなものということですね。)

以下のような構成を想定しています。

  • レジストリサーバ : さくらのクラウド上のVM
  • コンテナPushノード : レジストリサーバ上に同居(別のノードでも全く問題なくいけるはず)
  • コンテナPullノード : Raspberry Piで立てたノード

これらのノードには、手元のMacからsshでアクセスして作業するという想定です。

レジストリサーバにはFQDNな名前をふります。更にLet'Encryptを利用してSSL証明書を発行します。(やり方は後述)

Harborは(というかDockerは)コンテナイメージのpushやpullに証明書を必要とするので、仮にIPアドレスのみで運用するにしてもオレオレ証明書の作成が必要です。個人でのお試し試用であればそれでも良いですが、もうちょっと真面目に使うのであればドメイン登録+Let'sEncryptで証明書発行しておいたほうが良いでしょう。

また使用したHarborのバージョンは作業時点での最新、v1.7.4です。

なお基本的には、下記のような参考資料に書いてあるように作業をすればいけます。
ですのでこの記事では、これらの記事との差異やハマりポイントを中心に記載します。

参考にした資料

Ubuntu16.04LTS上でHarborを使ってちょっぴりSecureなPrivate Docker Registryを構築する

Rancher + Harbor(プライベートレジストリ)で、簡単Dockerイメージ管理

VMの準備

さくらのクラウドのコントロールパネルからログインし、新規VMを発行します。
VMスペックはharborのドキュメントにはかなりリッチな要件が書いてありますが、1コア2GBメモリでも動きました。ちょっと使う分には問題なさげです。OSはCentOS7.6を選択しました。

VMのIPアドレスが確定したら、適当なドメインに登録して名前を振っておきます。今回の例ではfogregistry.sample.jpとしておきます。(余談ですが、名前については本件とは全く関係のない理由でfog云々としています。この記事に貼り付けた作業ログを書き直すのが大変なのでそのままで。)

Harborインストール・設定

Docker, docker-compose

まずHarborの前提となるDocker, docker-composeをインストールします。Harborはそれ自体がコンテナで動作します。
ハマりポイントは特にありません。

[root@fogregistry ~]# yum install -y yum-utils device-mapper-persistent-data lvm2
[root@fogregistry ~]# yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
[root@fogregistry ~]# yum makecache fast
[root@fogregistry ~]# yum list docker-ce.x86_64 --showduplicates
[root@fogregistry ~]# yum install docker-ce
[root@fogregistry ~]# systemctl start docker
[root@fogregistry ~]# systemctl status docker
[root@fogregistry ~]# systemctl enable docker

docker-composeも同様に。

[root@fogregistry ~]# curl -L "https://github.com/docker/compose/releases/download/1.23.2/docker-[root@fogregistry ~]# compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
[root@fogregistry ~]# chmod a+x /usr/local/bin/docker-compose
[root@fogregistry ~]# docker-compose --version
docker-compose version 1.23.2, build 1110ad01
[root@fogregistry ~]#

SSL証明書

上述の通り、harborはssl証明書を必要とするので、Let's Encryptを利用して証明書を発行します。
使ったことがない方には敷居が高いかもしれませんが、とても簡単です。

[root@fogregistry ~]# yum install certbot
[root@fogregistry ~]# certbot certonly --webroot -w /var/www/html -d fogregistry1.sample.jp

Let's Encryptの証明書発行エージェントソフトウェアのcertbotをインストールします。インストールできたら、certbotコマンドを、webサーバのドキュメントルートと当該サーバ(レジストリサーバにしたいマシン)のFQDNを引数につけて実行します。

コマンド実行するといくつか質問されるので適切に答えます。うまく実行できれば、/etc/letsencrypt/というディレクトリが作成され、その配下のarchveディレクトリにレジストリサーバのFQDN名でディレクトリが作成され、そこに発行された証明書が置かれます。

[root@fogregistry fogregistry.sample.jp]# pwd
/etc/letsencrypt/archive/fogregistry.sample.jp
[root@fogregistry fogregistry1.kikuzo.jp]# ls -l
合計 20
-rw-r--r-- 1 root root 1927  3月 20 14:36 cert1.pem
-rw-r--r-- 1 root root 1647  3月 20 14:36 chain1.pem
-rw-r--r-- 1 root root 1927  3月 20 14:49 fogregistry.sample.jp.cert
-rw-r--r-- 1 root root 3574  3月 20 14:36 fullchain1.pem
-rw------- 1 root root 1704  3月 20 14:36 privkey1.pem
[root@fogregistry fogregistry.sample.jp]#

Let's Encrytpについては、Let's Encrypt 総合ポータルに詳しい使い方などがありますので参照して下さい。

harbor本体のインストール

早速harbor本体のインストールと行きたいところですが、その前に、CentOS標準のhttpサーバを停止させます。これがharborのwebサーバとport80でconflictするからです。

[root@fogregistry ~]# systemctl stop httpd
[root@fogregistry ~]# systemctl disable httpd

ハマりポイント : この作業は、SSL証明書の発行作業が終了してからやって下さい。SSL証明書発行には当該マシンが確かに指定したFQDNでアクセス可能になっているかを確認する処理が含まれ、そのために当該サーバでwebサーバが起動していなければなりません。

で、webサーバを止めたら、harbor本体をダウンロードして展開します。

[root@fogregistry ~]# yum install wget
[root@fogregistry ~]# wget https://storage.googleapis.com/harbor-releases/release-1.7.0/harbor-online-installer-v1.7.4.tgz
[root@fogregistry ~]# tar zxvf harbor-online-installer-v1.7.4.tgz

展開した先に、harbor.cfgというファイルが含まれ、これがharborの設定ファイルになりますので、これを編集します。

[root@fogregistry ~]# cd harbor
[root@fogregistry harbor]# vi harbor.cfg

とりあえず使うために変更必要なのは、hostname, ui_url_protocol, ssl_cert, ssl_cert_keyの4行になります。

harbor.cfg
hostname = fogregistry.sample.jp

ui_url_protocol = https
ssl_cert = /etc/letsencrypt/archive/fogregistry.sample.jp/cert1.pem
ssl_cert_key = /etc/letsencrypt/archive/fogregistry.sample.jp/privkey1.pem

secretkey_path = /data

ハマりポイント : secretkey_pathはデフォルトの"/data"のまま変更しない方がよいです。このディレクトリでボリュームマウントしてコンテナに設定ファイル等を渡している(docker-compose.yml内でvolume行記述あり)ため、変更するとコンテナの起動でコケます。

設定ファイルができたら、コンテナダウンロードなどが含まれる、インストーラコマンドを実行し、完了したらdocker-compose で起動します。

[root@fogregistry harbor]# ./prepare
[root@fogregistry harbor]# docker-compose up -d

無事起動できたら、ブラウザで https://fogregistry.sample.jp/ にアクセスしてみて下さい。

ハマりポイント : webUIのページは表示されるのにadminユーザでログインできない場合は、何らかのコンテナの起動に失敗している場合があります。docker-compose logs でログを確認してみて下さい。私の場合は、上述の/dataの行を変更していたためadminserverというコンテナが必要な設定ファイルを読み込めずに起動に失敗していたことがありました。

レポジトリを使うための準備

レポジトリ作成等

webUIにアクセスしたら、以下のような作業をします。詳しくは参考文献等を参照して下さい。特に難しくないです。
- 初回ログイン時にadminユーザのパスワード変更
- ユーザの発行
- レポジトリの作成(書き込み権限を持っているユーザが作成する)

なお、以下の例では、fogadvance-exampleというレポジトリを作成しています。(余談:本件の内容に全く関係ない名前ですが、ログを書き換えるのが大変なので。)

レジストリサーバのCA証明書を使用者に渡す

後述してますが、レポジトリにコンテナイメージをpush/pullするためには、レジストリサーバのCA証明書を利用者のローカル環境に設定する必要があります。そのため、CA証明書を取り出して渡せるように準備しておきます。

CA証明書は /etc/letsencrypt/archive/fogregistry.sample.jp/fogregistry.sample.jp.cert というファイルになりますので、これをca.crtという名前にリネームして、適当な場所に置くなり利用者に送るなりします。

レポジトリへのpush/pull

以下は、dockerでプライベートレポジトリを使う基本的な操作内容なのですが、参考までにやり方を書いておきます。

pushする場合

以下の作業は、コンテナイメージを作成する(そしてレポジトリにpushする)ノードで作業する内容です。(今回の例では実際にはレジストリサーバに同居しています。わかりにくくてすみません。)

レジストリサーバのCA証明書を開発環境にインストール

dockerでコンテナをレポジトリにpushする際には、レジストリサーバのCA証明書をローカル(開発環境)側に入れておかないと認証エラーになって登録できません。別途入手したレジストリサーバのca.crtを、/etc/docker配下にcert.d/fogregistry.sample.jpというディレクトリを作成し、そこに置きます。

[root@fogregistry ~]# cd /etc/docker
[root@fogregistry docker]# mkdir certs.d
[root@fogregistry docker]# cd certs.d
[root@fogregistry certs.d]# mkdir fogregistry.sample.jp
[root@fogregistry certs.d]# cd fogregistry.sample.jp/
[root@fogregistry certs.d]# cp ~/ca.crt .
[root@fogregistry fogregistry.sample.jp]# ls -la
合計 12
drwxr-xr-x 2 root root 4096  3月 21 02:09 .
drwxr-xr-x 3 root root 4096  3月 21 02:09 ..
-rw-r--r-- 1 root root 1927  3月 21 02:09 ca.crt
[root@fogregistry fogregistry.sample.jp]#
コンテナへのタグ付け

イメージ作成は別途やっている状態で、レポジトリに登録したいイメージにタグを付与します。

[root@fogregistry ~]# docker tag hello-world:latest fogregistry.sample.jp/fogadvance-example/hello-world:latest

この例では、hello-world:latestイメージに、fogregistry.sample.jp/fogadvance-example/hello-world:latestというタグを付与しています。タグはレジストリサーバFQDN/レポジトリ名/コンテナ名:バージョンとなります。

コンテナのpush

タグ付与できたら、pushします。

[root@fogregistry ~]# docker login fogregistry.sample.jp
..レジストリサーバに発行したID/Passwordでログインします..
[root@fogregistry ~]# docker push fogregistry.sample.jp/fogadvance-example/hello-world:latest

無事にpushできたら、レポジトリのwebUIにアクセスしてみて下さい!

コンテナのpull

レジストリサーバの公開鍵を開発環境にインストール

コンテナpull時には、レポジトリへのSSL接続ができるように公開鍵を開発マシンにインストールする必要があります。レジストリサーバのca.crtが使えます。これを別途入手しておいて、

pi@raspberrypi:~/test $ cd /usr/local/share/ca-certificates/
pi@raspberrypi:/usr/local/share/ca-certificates $ cp ~/ca.crt ./fogregistry.sample.jp.crt
pi@raspberrypi:/usr/local/share/ca-certificates $ ls -l
total 4
-rw-r--r-- 1 root staff 1927 Mar 26 09:50 fogregistry.sample.jp.crt
pi@raspberrypi:/usr/local/share/ca-certificates $ update-ca-certificates

という感じで登録し、さらにdockerを再起動します(簡単なのはraspberry piの再起動)。設置先ディレクトリとupdateコマンドはOS(ディストリビューション)によって異なります。

このあたりについてはdockerのドキュメントに記述があります。

コンテナのpull

pi@raspberrypi:~ $ docker login fogregistry.sample.jp
..レジストリサーバに発行したID/Passwordでログインします...
pi@raspberrypi:~ $ docker pull fogregistry.sample.jp/fogadvance-example/hello-world:latest

やってみて下さい!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

debian(jessie)のdocker image使ってるとapt-getでエラーが出る

dockerでruby:2.3.1を使ってビルドしようとしたら

W: Failed to fetch http://deb.debian.org/debian/dists/jessie-updates/main/binary-amd64/Packages  404  Not Found

ってエラーが出る

なーぜ?

  • http://deb.debian.org/debian/dists/jessie-updates/main/binary-amd64/Packages が404になる
  • どうやらjessieがアップデートされないからhttp://deb.debian.org/debian/dists/jessie/main/binary-amd64/Packagesに変わったらしい
  • アップデートサボるなよってことなんですねー

とりあえず直す

Dockerfile
FROM ruby:2.3.1

WORKDIR /usr/src/app

RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - &&\
    apt-get update -qq &&\
    apt-get install -y nodejs

みたいなのを

Dockerfile
FROM ruby:2.3.1

WORKDIR /usr/src/app

RUN echo "deb http://deb.debian.org/debian jessie main" > /etc/apt/sources.list &&\
    echo "deb http://security.debian.org jessie/updates main" >> /etc/apt/sources.list &&\
    curl -sL https://deb.nodesource.com/setup_8.x | bash - &&\
    apt-get update -qq &&\
    apt-get install -y nodejs

にする

この修正がなにをしているのか

パッケージのダウンロード元が書かれているファイルが/etc/apt/sources.list
これを元にapt-getが動く

そしてそこには

/etc/apt/sources.list
deb http://deb.debian.org/debian jessie main
deb http://deb.debian.org/debian jessie-updates main
deb http://security.debian.org jessie/updates main

と書かれてる
まぁ見た感じ2行目3行目が悪さしてるっぽいね

だから

deb http://deb.debian.org/debian jessie main
deb http://security.debian.org jessie/updates main

で上書きしてみる
動いた!!

あくまでご参考までに

自分そこまでlinux詳しくないですし
適当に勘で直しただけなのでこれがベストな直し方かと言われると自信ないです

stretchにあげるのが一番いいんじゃないかと

追記 2019/03/28

3行目は消しちゃいけなかったようです
修正しました

@Iju
@henrich
ご指摘頂きありがとうございます

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む