- 投稿日:2019-03-27T23:58:20+09:00
起動中のrailsコンテナに入るシェルスクリプトのメソッド
起動中のrailsコンテナに入るシェルスクリプトのメソッド
zshrc.function dbash() { id=$(docker ps -q --filter "name=web") command docker exec -it $id bash }参考
- 投稿日:2019-03-27T23:10:56+09:00
失敗: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
必要なファイルのコンパイルを行ったり、結構時間がかかります。途中でエラーが出たので諦めた。
- 投稿日:2019-03-27T23:10:14+09:00
失敗: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/grubGRUB_CMDLINE_LINUX= に「 rootflags=uquota,gquota,pquota 」を追記してください。
sudo grub2-mkconfig -o /boot/grub2/grub.cfg sudo rebootで適用できたが、だめ。
2
vi /etc/sysconfig/docker-storage-setupSTORAGE_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/564d51034c125d192df3See Also
- 投稿日:2019-03-27T23:06:44+09:00
【動いてないです】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.iotime="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: falsedocker 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 supportdocker 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 canceledctr で死んだ
♯ 別窓 & 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関係ありそうなところ
- runc@v1.0.0-rc6/libcontainer/process_linux.go
- BoltDB panics on cursor search since April update · Issue #3162 · Microsoft WSL
参考
- 投稿日:2019-03-27T22:02:07+09:00
DockerでSpringBoot+Mongoなアプリケーションを立ち上げる
目標
SpringBoot+MongoDBなアプリケーションをDockerで起動するサンプルを作る。
構築
docker-compose.yml
docker-compose.ymlversion: '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: - mongoDockerfile
- MongoDBの立ち上がりまでアプリの起動を待つためにDocker公式に書いてあったスクリプトを参考に。
- Gradleで
./build/libsにスクリプトをコピーする方法も考えたけど、RUNで出力するほうが性に合った。DockerfileFROM 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その他
- 開発中は
mongoをhostsに127.0.0.1として登録して、mongoだけ立ち上げていた。- もっと他に良い方法がありそう。
- 投稿日:2019-03-27T21:53:54+09:00
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.rbTakahashinoMacBook-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
- 投稿日:2019-03-27T21:46:19+09:00
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ファイルを以下の内容で作成しておきましょう。gitignorenode_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スタイルチェックとコードフォーマッタ
次にコードの品質を担保するために、
eslintとprettierを追加します。
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.jsmodule.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.jsconst a = "hoge" const b = 1+2ターミナルから
prettierを実行します。yarn run prettier --write app.jsすると、下記のように
app.jsの中身が変更されているはずです。app.jsconst a = 'hoge' const b = 1 + 2
eslintもprettierもいちいちターミナルから実行するのは面倒ですよね?
なので、VimやVisual Studio Codeといったエディタのプラグイン等を利用し、エディタから実行or自動実行されるように設定することを推奨します。
(私はVimでALEというプラグインを利用してコードが変更される度に非同期実行しています)コミット時に自動でyarn lintを実行する
前節で
eslintとprettierを追加しましたが、すべての開発者がこれらのツールを必ず実行し、コードをクリーンに保ってくれるとは限りません。実行せずにスタイルに違反したコードをコミットしてしまう開発者も存在します。(私は何度もそういった現場に遭遇してきました。)こういった問題を避けるためにはどうすればよいでしょうか?
主に2つの解決策があります。
- Gitフックを利用する
- CIで継続的にチェックする
両方実施するのが望ましいのですが、まずはGitフックを利用した自動実行を導入してみましょう。
Gitは
commitやpush等のgitコマンドの実行前/実行後に任意のスクリプトを実行する機能です。
詳しい説明は公式ドキュメントを参照してください。リポジトリ内の
.git/hooksディレクトリ以下にスクリプトを設置すれば設定できるのですが、Node.jsの場合はhuskyというnpmモジュールを利用してこれらをpackage.jsonで管理することが可能です。以下のコマンドでプロジェクトに
huskyを追加します。yarn add -D husky
package.jsonにgit commit前にyarn lintを実施する設定を追加します。
scriptsの下に下記の設定を追記しましょう。package.json... "scripts": { "lint": "eslint --ext .js --ignore-path .gitignore ." }, "husky": { "hooks": { "pre-commit": "yarn lint" } }, ...現在、
app.jsはprettierでフォーマットしましたが、変数の未使用により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モジュールexpressとmorganをプロジェクトに追加します。
yarn add express morgan body-parser
expressはNode.jsのweb serverフレームワーク、morganはexpress用のアクセスログ出力ツールになります。
body-parserはexpressのリクエストボディで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.jsconst 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.jsconst express = require('express') const tweets = require('./v1/tweets.js') const router = express.Router() router.use('/v1/tweets', tweets) module.exports = routercontrollers/v1/tweets.jsconst 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内のhelloをhogeに書き換えて見ましょう。
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.jsconst 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.jsconst 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.jsconst 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 modelsTweetモデルを次のように実装します。
models/tweet.jsconst 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.jsconst 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.jsconst 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/timelineconst 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ではavaやmocha、jest等、いくつか有名なユニットテストフレームワークが存在します。
今回は最もシンプルなavaを利用してユニットテストを記述していきます。まずはプロジェクトにnpmパッケージを追加しましょう。
今回のユニットテストで利用するsupertest、mongodb-memory-serverも一緒に追加します。yarn add -D ava supertest@^3.4.2 mongodb-memory-server簡単にテストコマンドを実行できるように
package.jsonにtestコマンドと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.jsconst 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.jsconst 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 scriptsDB中のレコードを消去し、ダミーデータを登録するスクリプトを実装していきます。
scripts/initialize.jsconst 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-jsdocswagger関連のファイルを配置するディレクトリを作成します。
mkdir swagger各APIの定義はcontrollerのそれぞれのメソッドのコメントとして記述することになるのですが、全体で共通する設定は
swagger/swaggerDef.jsに記述します。
swaggerのバージョンは最新の3.0を利用します。swagger/swaggerDef.jsconst 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.jsとcontrollers/v1/timeline.jsにコメントでswagger定義を記述していきます。controllers/v1/tweets.jsconst 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 = routercontrollers/v1/timeline.jsconst 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 Specを確認するために、
yarn swaggerでファイル出力し、Swagger Editorへコピペするのは大変です。
この確認を簡単にするために、起動したサーバーから直接Swagger Specを取得できるようにします。サーバーから直接取得できるようにすることで、jsファイルを更新すれば
nodemonのホットリロードで即座に反映されたり、Swagger UIを用いて簡単に生成されたSwagger Specを確認できるようになります。
また、テストサーバー等で公開することで、他のチームでも容易にSwagger Specを確認できるようになる点もメリットの一つです。
controllers/index.jsを下記のように修正します。
今回はdevelopment以外の環境ではSwagger Specファイルは取得できない実装にしています。controllers/index.jsconst 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 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サービスを構築しました。
eslintとprettierによる強力なスタイルチェックとコードフォーマットmongooseを利用したスキーマ定義とDBアクセスavaを利用したユニットテスト- Swaggerを利用したインターフェース仕様の提供
次の章では本章と同様の手順でUserサービスを作成していきます。
次章: 第3章 Userサービス(そのうち投稿します)
- 投稿日:2019-03-27T21:44:44+09:00
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サービスのサーバーサイドが各サービスとデータをやり取りする構成としています。なお、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サービス
- 投稿日:2019-03-27T13:27:32+09:00
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 dockerdocker-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.jpLet'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.cfghostname = 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やってみて下さい!
- 投稿日:2019-03-27T01:35:38+09:00
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に変わったらしい- アップデートサボるなよってことなんですねー
とりあえず直す
DockerfileFROM 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みたいなのを
DockerfileFROM 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.listdeb 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行目は消しちゃいけなかったようです
修正しました


