このブログを検索

この記事の内容は、個人の見解、検証の範囲のものであり、誤りがある可能性があります。
個人の責任において情報活用をお願いします。


2022年12月23日金曜日

やってみよう自動化ツール Ansible その3 k8sのコントロールプレーンを自動化してみる

今回は、前回準備を整えた環境にAnsibleをつかってKubernetesを自動インストールしていこうと思います。
実行する内容としては、Kubernetesをインストールする際に使うkubeadmのyumリポジトリを追加、kubeadmのインストール、kubeadm int でコントロールプレーンの作成、kubectlコマンド実行のためのkubeconfig用設定となります。

用意するプレイブックは以下の内容となります。
- name: setup Containerd
  hosts: kubernetes
  remote_user: root

  tasks:
  - name: kubernetes.repo create
    file:
      path: /etc/yum.repos.d/kubernetes.repo
      state: touch

  - name: add kubernetes.repo
    lineinfile:
      dest: /etc/yum.repos.d/kubernetes.repo
      line: "{{ item }}"
    with_items:
      - "[kubernetes]"
      - "name=Kubernetes"
      - "baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64"
      - "enabled=1"
      - "gpgcheck=1"
      - "repo_gpgcheck=1"
      - "gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg"

  - name: Install kubeadm
    yum:
      name: kubeadm

  - name: Kubernetes Setup
    shell : kubeadm init --apiserver-advertise-address=172.20.73.91 --pod-network-cidr=10.244.0.0/16

  - name: $HOME/.kube create
    shell : mkdir -p $HOME/.kube

  - name: copy admin.conf
    shell : sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config

  - name: chown $HOME/.kube/config
    shell : sudo chown $(id -u):$(id -g) $HOME/.kube/config

task: 以降について解説します。
『kubernetes.repo create』これは、次のタスクで内容を記述していくためのファイル作成部分になります。
『add kubernetes.repo』これは、yum で kubeadm をインストールする際に必要なリポジトリの内容を追加しています。
『Install kubeadm』これは、yum で kubeadm をインストールしています。
『Kubernetes Setup』これは、kubeadm を使用して Kubernetes を構築しています。CNI は flannel を使用しています。
『$HOME/.kube create』『copy admin.conf』『chown $HOME/.kube/config』この3つは、Kubernetes へ対して操作を行う kubectl を使うための処理となっています。
kubectl は $HOME/.kube/config に書いてある接続に必要な情報を読み取って Kubernetes とやり取りをするため、ディレクトリを作って情報が記入されているファイルをコピーして権限を整えているという処理になります。

Kubernetes をよくご存じの方は --pod-network-cidr から flannel って… と思われる方もいるかもしれませんが、私が初めて Kubernetes を構築して Pod の通信についてあれやこれやと頭を悩ませ、VXLAN やカプセル化といった仕組みを理解するまで一緒に付き合ってくれた思い入れのある機能なので使いたくなってちゃうんですよねw
どうかご容赦ください。

この後は、コントロールプレーンに追加する用のワーカーの作成になります。
こちらについては、今まで作成した OS の設定、Containerd の構築、Kubernetes の構築、これらの処理を roles を利用して呼び出す形で作成したいともいます。

今年もブログを読んでいただきありがとうございました。
来年もよろしくお願いします。

よいおとしを~

2022年11月30日水曜日

やってみよう自動化ツール Ansible その2

今回は、実際に Ansible のプレイブックを作成して自動でいろいろファイルに書き込んだり、パッケージのインストールやサービスの設定をやってみましょう。
具体的には、Docker と同じくコンテナの管理ができる Containerd のインストールを行うための各設定をおこない、インストールしてサービスの起動を行ってみます。

私が以前書いた記事を読んだことがある方は、せっかく前いろいろやったのに Docker 使わないの? と思われるかもしれません。
ちょっとした理由がありまして・・・

この環境を使って次回以降に Kubernetes(以降 k8s) を作成していこうと思ったのですが、k8s が Docker で構築できなくなりました。
K8s は Docker からコンテナを管理する部分の機能を使わせてもらいたくて Dockershim というモジュールを用意していたのですが、k8s がそれを使うのをやめることにしたためです。
昔は Docker しかなかったんですが、現在は Docker と同様にコンテナを管理する機能を提供してくれるCRIランタイム(Containerd 等)があります。
そちらを使ったほうが無駄が省けるという事のようです。(開発する部品が少なくなるほうが開発側も楽でしょうしね)

それでは、実際にどんなプレイブックを用意するのかこちらをご覧ください。
これは、ザーッっと流してみてください。
のちほど分解して説明します。(もしかしたら、コマンドとかからなんとなく処理がわかるかもしれませんが・・・)

Containerd のための設定の他に、k8s 用の設定変更も一部入っています。
参考情報:
https://kubernetes.io/ja/docs/setup/production-environment/container-runtimes/#containerd
https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/install-kubeadm/

ファイル名:containerd-set.yaml
- name: setup Containerd
  hosts: continard
  remote_user: root

  tasks:
  - name: Swap Stop
    shell : swapoff -a
    shell : sed -i '/centos-swap/s/^/# /g' /etc/fstab

  - name: SElinux Disable
    ansible.posix.selinux: state=disabled
    become: true

  - name: Reboot
    ansible.builtin.reboot: reboot_timeout=600
    become: true

  - name: FW Stop
    service:
      name: firewalld
      state: stopped
      enabled: no

  - name: kernel module file create
    file:
      path: /etc/modules-load.d/containerd.conf
      state: touch

  - name: kernel module file add line
    lineinfile:
      dest: /etc/modules-load.d/containerd.conf
      line: "{{ item }}"
    with_items:
      - "overlay"
      - "br_netfilter"

  - name: kernel module Load
    shell : modprobe overlay
    shell : modprobe br_netfilter

  - name: kernel parameter file create
    file:
      path: /etc/sysctl.d/99-kubernetes-cri.conf
      state: touch

  - name: kernel parameter file add line
    lineinfile:
      dest: /etc/sysctl.d/99-kubernetes-cri.conf
      line: "{{ item }}"
    with_items:
      - "net.bridge.bridge-nf-call-iptables  = 1"
      - "net.ipv4.ip_forward                 = 1"
      - "net.bridge.bridge-nf-call-ip6tables = 1"

  - name: System Reload
    shell : sysctl --system

  - name: Package Install
    yum:
      name: yum-utils
      name: device-mapper-persistent-data
      name: lvm2

  - name: containerd add repository
    shell : yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

  - name: Containerd Install
    yum:
      name: containerd.io

  - name: containerd Setting folder create
    file:
      path: "/etc/containerd"
      state: directory

  - name: containerd Setting file create
    shell : containerd config default | sudo tee /etc/containerd/config.toml

  - name: containerd Restart
    service:
      name: containerd
      state: restarted
      enabled: no
やってることは、サービスの起動と停止、設定ファイルの作成と内容の追記、yumのリポジトリ追加とパッケージのインストールなどです。
書き方なんですが、YAML形式となっていてインデントなどの位置が重要ですので自分で作成するときはご注意ください。
内容が長く読むのが大変だと思いますが、中身の解説に入りたいと思います。
- name: setup Containerd
  hosts: continard
  remote_user: root
この部分は処理を行わせる対象の指定と処理を行うユーザーの指定となります。
『name: 』プレイブックの名前を設定しています。
『hosts: 』で inventry ファイルに記載したどのホストを対象にするのかを指定しています。
『remote_user: 』対象にアクセスしに行く際のユーザーを指定しています。

『tasks:』以降からが実際に行ってもらう作業の内容になります。
『- name: 』で各処理に名前を付けています。 どこでエラーになったのかなどを確認する際に重要になるので重複する名前は避けたほうがいいです。
  tasks:
  - name: Swap Stop
    shell : swapoff -a
    shell : sed -i '/centos-swap/s/^/# /g' /etc/fstab
k8s の要件でスワップを停止しています。
  - name: SElinux Disable
    ansible.posix.selinux: state=disabled
    become: true

  - name: Reboot
    ansible.builtin.reboot: reboot_timeout=600
    become: true
余計なことをされると困るので、SElinux を停止します。
『ansible.posix.selinux:』モジュールを使って、設定を変更しています。
disabledにするには再起動が必要(しないと permissive)なので『ansible.builtin.reboot:』を入れています。
  - name: FW Stop
    service:
      name: firewalld
      state: stopped
      enabled: no
『service:』モジュールを使ってファイアウォールを止めています。
本来は K8s の必要ポートを開放するのですが検証環境なのでこうしました。
  - name: kernel module file create
    file:
      path: /etc/modules-load.d/containerd.conf
      state: touch

  - name: kernel module file add line
    lineinfile:
      dest: /etc/modules-load.d/containerd.conf
      line: "{{ item }}"
    with_items:
      - "overlay"
      - "br_netfilter"

  - name: kernel module Load
    shell : modprobe overlay
    shell : modprobe br_netfilter
『file:』モジュールを使って、/etc/modules-load.d/containerd.conf を作成
『lineinfile:』モジュールを使って、作成したファイルに "overlay" と "br_netfilter" を追記しています。
『shell :』モジュールを使って、modprobe を実行し読み込んでいます。
  - name: kernel parameter file create
    file:
      path: /etc/sysctl.d/99-kubernetes-cri.conf
      state: touch

  - name: kernel parameter file add line
    lineinfile:
      dest: /etc/sysctl.d/99-kubernetes-cri.conf
      line: "{{ item }}"
    with_items:
      - "net.bridge.bridge-nf-call-iptables  = 1"
      - "net.ipv4.ip_forward                 = 1"
      - "net.bridge.bridge-nf-call-ip6tables = 1"

  - name: System Reload
    shell : sysctl --system
こちらも同様に『file:』でファイルを作成し、『lineinfile:』で追記をして、『shell :』で読み込むコマンドを実行しています。
  - name: Package Install
    yum:
      name: yum-utils
      name: device-mapper-persistent-data
      name: lvm2
『yum:』モジュールを使って、必要なパッケージのインストールをしています。
  - name: containerd add repository
    shell : yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
Containerd をインストールするためのリポジトリを追加しています。
お気づきかもしれませんが Docker を入れるときのリポジトリと同じです。
実はContainerd も Docker社が作ってたものなのです。
  - name: Containerd Install
    yum:
      name: containerd.io

  - name: containerd Setting folder create
    file:
      path: "/etc/containerd"
      state: directory

  - name: containerd Setting file create
    shell : containerd config default | sudo tee /etc/containerd/config.toml

  - name: containerd Restart
    service:
      name: containerd
      state: restarted
      enabled: no
Containerd をインストールして、設定を入れるためのファイルを作成し、内容を追記してサービスを起動しています。

というわけで内容の説明はここまでです。
本当は設定変更後に、その設定を確認するコマンドを打って『register:』に格納して『debug:』で表示するとかも入れようかと思ったんですが、さすがに長くなりすぎるので次回以降で説明するタイミングがあったら入れたいと思います。

プレイブックの他に、ホストの情報を記入する hosts ファイルも修正しなければなりません。
「/etc/ansible」配下の hosts に以下のような内容を追記します。
xxx.xxx.xxx.xxx には Ansible からアクセスを行う対象のIPアドレスを入れてください。
[continard]
demo-server   ansible_host=xxx.xxx.xxx.xxx
実際に、実行してみましょう。
以下のコマンドで実行できます。
# ansible-playbook containerd-set.yaml
・・・どうでしょうか。
人によってはエラーになると思います。
というのもですね、sshでアクセスしますのでそのサーバーに初回アクセスする状態だと認証ではじかれちゃうんですよね。

というわけで、そうなっちゃった方は以下のコマンドでsshの鍵を渡してあげましょう。
xxx.xxx.xxx.xxx には Ansible からアクセスを行う対象のIPアドレスを入れてください。
# ssh-copy-id root@xxx.xxx.xxx.xxx
その後改めて実行してみてください。
そもそものプレイブックの書き方でエラーになってしまう場合は、インデントのズレなどがないか確認してみてください。

うまく処理ができた方は、Containerd をインストールしたマシンにログインして以下のコマンドを実行してください。
バージョン情報が確認できると思います。
# ctr -v
ctr containerd.io 1.6.9
これで kubernetes の構築を行う準備が整った状態となりました。 次回のブログ(来月)はいよいよ kubernetesK8s の構築を Ansible で行っていきたいと思います。 ただ、年末なのでいろいろ忙しく確認の時間が取れなかったりしたら1月のブログ投稿になるかもしれません。

2022年10月31日月曜日

やってみよう自動化ツール Ansible その1 Ansible ってどんなもの? インストールはどうやるの?

今回から、自動化ツールの Ansible を使用して今まで手動で作ったものを自動で作ってみるシリーズを開始したいと思います。
どんなことをやるかといいますと、K8s を Ansible で自動構築してそこに MetalLB なんか立ててみたいな~ って思っています。

今回は、まず Ansible ってどういうものなのかと、どうやって使うかの説明を行います。
ドキュメントは以下となっています。


https://docs.ansible.com/ansible/2.9_ja/index.html

Ansible は Ansible をインストールしたマシンから、作業を行いたいマシンへ ssh で接続します。
どうやって作業を行うかというと、playbook という作業内容を書いた yml 形式のファイルを用意します。
この yml ファイルを指定することで、作業を行ってもらうわけです。

ansibleの設定ファイルなどは、インストールしたホストの「/etc/ansible」に置かれます。
ls コマンドで確認してみると設定ファイルの ansible.cfg とアクセスするホストの情報を記入する hosts 個別に処理を用意して呼び出せるようにする role 用のディレクトリがあります。

全ての作業を一つの yml に書いて作業を行うことも可能ですが、いろんなサーバーを構築する場合でも同じ処理というものは発生すると思います。
ホスト名の設定や必ずインストールする必要があるものなどです。
そういう場合に処理をあらかじめ書いておいて必要な処理だけを呼び出してつかえるのが「roles」になります。

全体の処理をスタートさせる yml があって、その中にどの「roles」を呼び出すかという処理を書きます。
「role」ディレクトリ配下に作成したディレクトリ名がyamlで呼び出すときに指定するの名前となり、そのディレクトリに行いたい処理を記載した yml を配置する形になります。
例として、ホスト名設定、必要パッケージのインストール、という処理を roles を使用した際のファイル配置は絵でかくとこんなイメージです。

全体処理のyml
 |
 |― 「roles」←ディレクトリ
     |
     |―「OS_Setup」←ディレクトリ
     |   |
     |   |―「tasks」←ディレクトリ
     |   |     |    
     |   |     |― main.yml(ホスト名設定の処理を書いた yml )
       |
       |
     |―「Package_Insutall」←ディレクトリ
         |
         |―「tasks」←ディレクトリ
                 |         
                |― main.yml(必要パッケージのインストール処理を書いた yml )


「main.yml」となっているのは、各ディレクトリの「main.yml」を読み込むように設定されているためです。
全体の yml にはどのロールを使うのかの指定を行うフィールドを書いて「OS_Setup」や「Package_Insutall」のディレクトリ部分を指定します。
※本当は「tasks」と同列に「handlers」と「templates」というディレクトリも行わせる処理によって用意します。

とまぁ、複雑なこともできるようになっているので Ansible は使えるようになっておいて損はないと思います。
構築だけでなく繰り返し同じ処理を行うようなテストでも使えそうですからね。

とはいえすぐにいろいろ機能を使ったものを作るのも理解するのも無理なので、まずは基本となる1つの yml に全部を書く形でやってみましょう。
最後はそれを元に roles に分解していきたいと思います。

では、Ansible のインストールをしてみましょう。
ホストOSはCentos 7 を使います。
公式マニュアルはこちら
https://docs.ansible.com/ansible/2.9_ja/installation_guide/intro_installation.html

yum でサクッとインストールするのですが、リポジトリが epel の様なのでそちらをインストールします

# yum install epel-release

リポジトリが使えるようになったので Ansible をインストールしましょう

# yum install ansible

インストール自体はこれで完了です。
確認として ansible のバージョンを表示してみましょう
# ansible --version
ansible 2.9.27
config file = /etc/ansible/ansible.cfg
configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python2.7/site-packages/ansible
executable location = /usr/bin/ansible
python version = 2.7.5 (default, Nov 16 2020, 22:23:17) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]


このように出力されていれば成功です。
次回は、Docker と同様にコンテナを動かすことができる containerd のインストールを ansible で行う playbook の作成をやってみようと思います。

2022年9月30日金曜日

調べてみようコンテナセキュリティ その6 ~Harborつかってみた~

 前回はHarborの構築を行いました。

今回はHarborにログインしてイメージのプッシュを行いたいと思います。


まずはGUIからログインしてみましょう。

ちなみに、adminの初期パスワードは harbor.yml に記載があります。

ログインしてみると「library」というプロジェクトが1つだけある状態です。


「library」をクリックしてみると、当然ながら何もありません。

今回は、ここにイメージを追加する作業を行うのですがその前に1つやっておきたいことがあります。


今回は前回構築したHarbor用のサーバーからイメージのアップロードを行うのでこの作業を必要としませんが、他のマシンからイメージをアップロードしようとする場合に証明書が必要となりますのでその証明書をダウンロードできるリンクを用意します。

実は、設定してあるとこの画面に表示されるのです。


何をするかというと、Harborのサーバーに「/data/ca_download/」に証明書を置いてあげるのです。

証明書関係のファイルはいっぱいありますが、拡張子が ca.crt となっているものがそうなります。

前回構築した環境ですと、以下のコマンドで配置できます。

# cp /root/CA/ca.crt /data/ca_download/


ブラウザを更新すると「REGISTRY CERTIFICATE」というリンクが出てきます。

これで証明書をGUIからダウンロード可能になりました。

では続いて、一旦GUIからは離れコマンドラインでの操作となります。


docker login コマンドを使用して Harbor へログインします。

# docker login <harborのIPアドレスかFQDN> -u admin -p <パスワード>


CUI のログインができたところで、GUI に戻ります。

先ほどの画面で、「PUSH COMMAND」というリンクがありますのでそちらをクリックします。

Harbor のリポジトリにpushを行うために、TAG を付ける命名規則と push の際のコマンドが表示されます。


これに従ってまずイメージの TAG 付けを行いましょう。

こちらは docker tag コマンドを使用します。


現在ローカルに保存してあるイメージは docker images で表示できるので好きなものを TAG 付けします。

ちなみにこの時、Harbor で使用されているイメージも確認できます。

ここでは CentOS 7 を利用します。


# docker tag centos:7 <HarborのIPアドレスかFQDN>/library/centos:harbor-test

このコマンドは、元となるイメージを指定(centos:7)し、付与するTAGを指定された命名規則に従って<HarborのIPアドレスかFQDN>/<転送するプロジェクト名>/<任意のイメージ名:任意のTAG名>となっています。


それでは push します。

# docker push <harborのIPアドレスかFQDN>/library/centos:harbor-test


ブラウザを更新すると、以下の様に push されたイメージが表示されます。

イメージを確認してみると、「Tags」に先ほどアップロードした tag 情報も確認することができます。


push ができたので pull してみましょう。

「Tags」のとなりに「Pull Command」がありますのでクリックしてコピーしてCUIで実行できますが、実行前にローカルに存在しているイメージを消しておいた方がわかりやすいと思います。

以下のコマンドで削除してみましょう


docker rmi <harborのIPアドレスかFQDN>/library/centos:harbor-test


先ほどコピーしたコマンドを確認すると、@sha256:~~~~ と追加されている部分があると思います。

これはダイジェストというそのイメージに付与された普遍的な識別子です。

これを付けることで、確実にそのイメージをダウンロードすることができるという事ですね。

実際にやってみましょう。


docker pull <harborのIPアドレスかFQDN>/library/centos@sha256:xxxxxx~~~~


ダイジェストを使ってダウンロードしてきたイメージは、tagが付与されていません。

そのため docker tag コマンドを使用して tag を付与することになりますが、イメージの指定にはイメージの名前ではなく「IMAGE ID」を使用します。


docker tag <IMAGE ID> <harborのIPアドレスかFQDN>/library/centos:harbor-test


先ほどまで none だった tag が付与されたと思います。


これで、イメージを検査しプライベートなリポジトリを用意してそこへイメージを push し pull して使うことができるようになりました。


コンテナのセキュリティに関してはこれで一旦終わりとしたいと思います。

読んでくださった方々ありがとうございました。


次は、今まで作ったものを自動で構築とかしてみましょうかね。

以前書いたk8sの構築もdockerではできなくなりましたし、その辺も絡めたりとか。

もしくは、Tanzu製品があれよあれよとたくさん増えて何する製品なのかが分からなくなってきたので、それぞれどういう事を目的とした製品で何ができるのかをまとめた記事を書こうかと考えています。

2022年8月31日水曜日

調べてみようコンテナセキュリティ その5 ~harbor作ってみた~

前回までで、イメージの脆弱性や構成上の問題をチェックし対応することが可能になりました。

コンテナ用のイメージは普段DockerHubからpullしてくると思いますが、セキュリティ的な観点からプライベートなコンテナレジストリを用意したいと思うこともあると思います。

そこで、自分用のコンテナレジストリとしてharborの作成(今回)と使い方(次回)について記載したいと思います。


harborを何で作るかですが、いろんな作り方があるんですけど今回はdocker composeという複数のコンテナを管理するツールを使いたいと思います。

元になるOSは毎度おなじみCentos 7でやります。

それではやっていきましょう。


コンテナとして展開するのでdockerを使います。

まずはdockerの構築が必要ですが、ここではdockerの構築は省略させていただきdocker-composeの構築からやっていきたいと思います。


利用するdocker-composeとharborのバージョンは以下

 [バージョン]

  - docker-compose v2.5.1

  - harbor v2.5.1


まず、docker composeの導入です。

手順はこちら。

https://docs.docker.jp/compose/install.html#linux


・利用するv2.5.1を指定してコマンドを実行します。

 # curl -L https://github.com/docker/compose/releases/download/v2.5.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose

 # chmod +x /usr/local/bin/docker-compose


・バージョンの確認をします。

 # docker-compose version

 # Docker Compose version v2.5.1


docker composeはこれで準備完了です。

驚きの簡単さですね。


続いてharborになります。

こっちは、証明書の作成やそれに合わせたファイルの書き換えなどがあるので手順も多くしんどいです。

それゆえに、うまくいかないことも多いですので心を強く持って挑んでください。


まずはharborのバイナリをダウンロードしてきて、構築用のOSに転送します。

開発速度が速いので、今回記載しているバージョンより新しいものが出ているかと思いますが、その場合でもおそらく構築手順自体は同じだと思います。

今回は/root直下でやっています。

  - harborのバイナリダウンロード先

      - https://github.com/goharbor/harbor/releases

        使いたいバージョンの「Assets」からダウンロードできます。


・CentOS上で解凍します。今回は/root 直下

 # tar zxvf harbor-offline-installer-v2.5.1.tgz


証明書関連の作成に着手します。

これは正しく作成配置ができていないと、イメージを置いたりダウンロードしたりするときに認証が通らなくて失敗します。

個人的にはこれに一番苦労しました。


・作業用のディレクトリを作成し移動ます。

 # mkdir CA

 # cd CA


・CA証明書の秘密鍵を生成します。

 # openssl genrsa -out ca.key 4096


・CA証明書を生成 ※CNにharborホスト名をFQDNで入れます

 # openssl req -x509 -new -nodes -sha512 -days 3650 \

  -subj "/C=CN/ST=Beijing/L=Beijing/O=example/OU=Personal/CN=xxxx.xxxx" \

  -key ca.key \

  -out ca.crt


・秘密鍵を生成します。

 # openssl genrsa -out myharbor.key 4096


・証明書署名要求(CSR)を生成します。

 # openssl req -sha512 -new \

     -subj "/C=CN/ST=Beijing/L=Beijing/O=example/OU=Personal/CN=xxxx.xxxx" \

     -key myharbor.key \

     -out myharbor.csr


・x509v3拡張ファイルを生成します。

 # cat > v3.ext <<-EOF

 authorityKeyIdentifier=keyid,issuer

 basicConstraints=CA:FALSE

 keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment

 extendedKeyUsage = serverAuth

 subjectAltName = @alt_names


 [alt_names]

 IP.1=OSのIPアドレスを入力

 DNS.1=harborホストのFQDNを入力

 DNS.2=harborホストのショートネームを入力

 EOF


・このファイルを使用してv3.ext、harborホストの証明書を生成します。

 # openssl x509 -req -sha512 -days 3650 \

     -extfile v3.ext \

     -CA ca.crt -CAkey ca.key -CAcreateserial \

     -in myharbor.csr \

     -out myharbor.crt


・ホストにサーバー証明書と秘密鍵を置きます。

 # mkdir -p /data/cert/

 # cp myharbor.crt /data/cert/

 # cp myharbor.key /data/cert/


・サーバー証明書、秘密鍵、CAファイルをharborホストのDocker証明書フォルダーにコピーします。

 # mkdir -p /etc/docker/certs.d/harborホストのFQDN/

 # cp myharbor.key /etc/docker/certs.d/harborホストのFQDN/

 # cp ca.crt /etc/docker/certs.d/harborホストのFQDN/


・harborの設定ファイルharbor.ymlに必要な情報を記入します。

 テンプレート(harbor.yml.tmpl)があるのでコピーして.ymlにします。

 # cd ../harbor

 # cp harbor.yml.tmpl harbor.yml

 # vi harbor.yml


 編集個所は以下

 # Configuration file of Harbor

 # The IP address or hostname to access admin UI and registry service.

 # DO NOT use localhost or 127.0.0.1, because Harbor needs to be accessed by external clients.

 hostname: reg.mydomain.com ※harborホストのFQDNに変更


 # http related config

 http:

   # port for http, default is 80. If https enabled, this port will redirect to https port

   port: 80

 

 # https related config

 https:

   # https port for harbor, default is 443

   port: 443

   # The path of cert and key files for nginx

   certificate: /your/certificate/path ※/data/cert/myharbor.crt に変更

   private_key: /your/private/key/path ※/data/cert/myharbor.key に変更


・インストールのスクリプトを実行します

 # ./prepare

 # ./install.sh


・ブラウザから以下へ接続を確認します。

 http://harborホストのIPアドレス

 

これで構築は完了です。

作業が多く何を作ってるのか見失いがちになりますが、ゆっくり一つ一つやっていきましょう。

初めて作るときはチェックシートなんかを用意してみてもいいかもしれませんね。


次回は、出来上がったharborへログインしてイメージのpushやpullを実行してみましょう。

2022年7月29日金曜日

調べてみようコンテナセキュリティ その4 ~dockle 使ってみた~

 今回は、dockleについてインストールして実際に使ってみたいと思います。

使ってるのはCentOS 7 なので、インストール方法はこちら。


  VERSION=$(

   curl --silent "https://api.github.com/repos/goodwithtech/dockle/releases/latest" | \

   grep '"tag_name":' | \

   sed -E 's/.*"v([^"]+)".*/\1/' \

  ) && rpm -ivh https://github.com/goodwithtech/dockle/releases/download/v${VERSION}/dockle_${VERSION}_Linux-64bit.rpm


公式(https://github.com/goodwithtech/dockle#installation)にこれだけしか書いてないんですが、これで動くのだろうか・・・

あ、いや、動くんだとは思いますよ、OSにあったバージョンのrpmを持ってきてインストールする内容だと思いますし。

むかしは、何もわからず「こうやったら動くって書いてあるからこうやればいい」と思ってやっていましたが、いまは「こういうことをするんだろうなこのコマンドで」とある程度はわかるようになってきたので、長い年月続ける意味ってあるんだなぁって・・・


では、やっていきましょう

実行すると以下のような出力が出てインストールが実行されます。

  https://github.com/goodwithtech/dockle/releases/download/v0.4.5/dockle_0.4.5_Linux-64bit.rpm を取得中

  準備しています...              ################################# [100%]

  更新中 / インストール中...

     1:dockle-0:0.4.5-1                 ################################# [100%]


動くものですね、結構マニュアルとかで「こうやったら動くよ」って書いてあってもそのままやったら動かないことも多く経験してきたものでいまだに動くと驚いちゃうんですよねぇ。

たいてい、「いや~、この製品使うならこういうことはやっておくのが常識でしょう???」みたいな、前提条件が省かれて手順が書かれてるようなことが多くて、この手順を成功させるために前もって必要なものがあったりしないかな?

という事を考えたり調べたりしちゃう書いてあることが信用できない悲しいSEなのです。


インストールできたので脆弱性のチェックをやっていきます。


  2022-07-22T17:09:32.027+0900    INFO    Failed to check latest version. not found version patterns

  WARN    - CIS-DI-0001: Create a user for the container

          * Last user should not be root

  INFO    - CIS-DI-0005: Enable Content trust for Docker

          * export DOCKER_CONTENT_TRUST=1 before docker pull/build

  INFO    - CIS-DI-0006: Add HEALTHCHECK instruction to the container image

          * not found HEALTHCHECK statement

  INFO    - CIS-DI-0008: Confirm safety of setuid/setgid files

          * setuid file: urwxr-xr-x usr/sbin/pam_timestamp_check

          * setuid file: urwxr-xr-x usr/bin/newgrp

          * setuid file: urwxr-xr-x usr/sbin/unix_chkpwd

          * setuid file: urwxr-xr-x usr/bin/passwd

          * setuid file: urwxr-x--- usr/libexec/dbus-1/dbus-daemon-launch-helper

          * setuid file: urwx--x--x usr/bin/chfn

          * setgid file: grwxr-xr-x usr/bin/write

          * setuid file: urwxr-xr-x usr/bin/su

          * setuid file: urwxr-xr-x usr/bin/gpasswd

          * setuid file: urwx--x--x usr/bin/chsh

          * setuid file: urwxr-xr-x usr/bin/mount

          * setuid file: urwxr-xr-x usr/bin/umount

          * setuid file: urwxr-xr-x usr/bin/chage

          * setgid file: grwx--x--x usr/libexec/utempter/utempter

  INFO    - DKL-LI-0003: Only put necessary files

          * Suspicious directory : tmp



このチェックでの判定は5段階のようです


『Level』

  https://github.com/goodwithtech/dockle#level


いろいろ出てきましたが、trivyの時ほどいっぱい怒られませんでしたね。

どういうところで引っかかったのかは以下で確認できます。


 『Checkpoint Summary』

  https://github.com/goodwithtech/dockle#checkpoint-summary


どうやって改善したらいいのかも見ることができます。

これを参考に対処してねという事なのでしょう。


 『Checkpoint Details』

   https://github.com/goodwithtech/dockle/blob/master/CHECKPOINT.md


ちょうど「WARN CIS-DI-0001」の出力がありましたので、そちらの対処を行ってみたいと思います。

今回は、イメージを作成する設計書のDockerfileと、Dockerfileの内容を読み込んでイメージを作成する docker build コマンドを使用し、修正を組み込んだイメージcentos:dockle の作成をします。

Dockerfile の中身は以下の様になります。


  ====

  # STEP1 CentOS 7 のイメージをベースにする

  FROM centos:centos7

  # STEP2 CIS-DI-0001の対処 

  RUN useradd -d /home/dockle -m -s /bin/bash dockle

  USER dockle

  ====


ではイメージを作成してみます。


  # docker build -t centos:dockle dockle/

  Sending build context to Docker daemon  2.048kB

  Step 1/3 : FROM centos:centos7

   ---> eeb6ee3f44bd

  Step 2/3 : RUN useradd -d /home/dockle -m -s /bin/bash dockle

   ---> Running in 48139360d8c8

  Removing intermediate container 48139360d8c8

   ---> 4cf2480c9454

  Step 3/3 : USER dockle

   ---> Running in 950fa59666e5

  Removing intermediate container 950fa59666e5

   ---> d5cb3dc4dd14

  Successfully built d5cb3dc4dd14

  Successfully tagged centos:dockle


出来上がったイメージがちゃんと存在するか確認します。


  # docker images

  REPOSITORY   TAG       IMAGE ID       CREATED          SIZE

  centos       dockle    d5cb3dc4dd14   16 seconds ago   204MB


では、修正を盛り込んだイメージのチェックをしてみます。


  # dockle centos:dockle

  2022-07-25T10:18:17.309+0900    INFO    Failed to check latest version. not found version patterns

  2022-07-25T10:18:19.255+0900    FATAL   unable to initialize a image struct: failed to initialize source: reading manifest dockle in docker.io/library/centos: manifest unknown: manifest unknown


エラーとなってしまいました。

メッセージを検索等してみたところ、どうやらローカルにあるイメージをスキャンしたいんだけど実際はリモート(docker hubかな?)にスキャン対象イメージを探しに行ってしまっているということみたいです。

その対処は以下にありました。


  『Use Docker』

    https://github.com/goodwithtech/dockle#use-docker


dockleのコンテナを起動してスキャンしてもらって、結果が出たらdockleのコンテナは削除するという使い方のようですね。

ここに「You only need -v /var/run/docker.sock:/var/run/docker.sock when you'd like to scan the image on your host machine.」と記載があり、ローカルにあるイメージををスキャンするときはこうする必要があるみたいです。

では、コマンドを今回の環境に合わせて、バージョンとイメージを指定したいと思います。


バージョンの確認


  # dockle -v

  dockle version 0.4.5


コマンドはこうなりました。


  # docker run \

      --rm \

      -v /var/run/docker.sock:/var/run/docker.sock \

      -v $(pwd)/.dockleignore:/.dockleignore \

      goodwithtech/dockle:v0.4.5 \

      centos:dockle


実行してみたいと思います。


  Unable to find image 'goodwithtech/dockle:v0.4.5' locally

  v0.4.5: Pulling from goodwithtech/dockle

  97518928ae5f: Pull complete

  fc586ee9e6b2: Pull complete

  Digest: sha256:ebd36f6c92ac850408c7cbbf1eddf97e53565da9d661c5fb8ec184ad82a5358d

  Status: Downloaded newer image for goodwithtech/dockle:v0.4.5

  2022-07-25T01:41:20.053Z        INFO    Failed to check latest version. not found version patterns

  INFO    - CIS-DI-0005: Enable Content trust for Docker

          * export DOCKER_CONTENT_TRUST=1 before docker pull/build

  INFO    - CIS-DI-0006: Add HEALTHCHECK instruction to the container image

          * not found HEALTHCHECK statement

  INFO    - CIS-DI-0008: Confirm safety of setuid/setgid files

          * setuid file: urwxr-xr-x usr/bin/chage

          * setuid file: urwxr-xr-x usr/bin/passwd

          * setuid file: urwxr-xr-x usr/bin/su

          * setuid file: urwxr-xr-x usr/sbin/unix_chkpwd

          * setuid file: urwxr-xr-x usr/bin/umount

          * setuid file: urwx--x--x usr/bin/chfn

          * setuid file: urwxr-xr-x usr/bin/gpasswd

          * setuid file: urwxr-xr-x usr/sbin/pam_timestamp_check

          * setuid file: urwx--x--x usr/bin/chsh

          * setuid file: urwxr-xr-x usr/bin/newgrp

          * setgid file: grwxr-xr-x usr/bin/write

          * setuid file: urwxr-x--- usr/libexec/dbus-1/dbus-daemon-launch-helper

          * setgid file: grwx--x--x usr/libexec/utempter/utempter

          * setuid file: urwxr-xr-x usr/bin/mount

  INFO    - DKL-LI-0003: Only put necessary files

          * Suspicious directory : tmp


修正を盛り込んだことで「WARN CIS-DI-0001」の出力がなくなりました。

trivyとdockleを組み合わせて、より強固で安全なイメージの作成ができそうですね。


次回は、せっかく作ったイメージを置いておくコンテナレジストリを自分で作って使ってみるという事をやりたいと思います。

2022年6月30日木曜日

調べてみようコンテナセキュリティ その3 ~trivy使ってみた~

今回はコンテナイメージの脆弱性を確認してくれるtrivy編となります。


実際にインストールをしてみましょう。

まずはtrivy をyum でインストールするための作業です。


1.使ってるOSのバージョンを変数に入れます。
# RELEASE_VERSION=$(grep -Po '(?<=VERSION_ID=")[0-9]' /etc/os-release) 


2.1.の値を使ってyumのレポジトリを追加します。
# cat << EOF | sudo tee -a /etc/yum.repos.d/trivy.repo
[trivy]
name=Trivy repository
baseurl=https://aquasecurity.github.io/trivy-repo/rpm/releases/$RELEASE_VERSION/\$basearch/
gpgcheck=0
enabled=1
EOF


3.一旦全体のUpdateをおこないます。
# sudo yum -y update


4.trivyをインストールします。
# sudo yum -y install trivy


これでインストールできたようです。

コマンドを打ってみましょう・・・のまえに、スキャンするイメージが必要なので適用にCentos7でもイメージを引っ張っておきましょう。
# docker pull centos:centos7


それでは、trivyによるコンテナイメージのスキャンを実施です。
# trivy i centos:centos7


実行結果がかなり長く全部乗せられませんので見づらいかもしれないですがスクショを・・・








思ってたよりは全然きれいにテーブル表示形式で出力が見れました。

一番左が対象となるもので、そこからCVEの何に該当しているか、脆弱性の深刻さ、インストールされているバージョン、問題が解決しているバージョン、一番右はCVEのタイトルですかね。

で、これってどうやって対処するんでしょうと思ったあなた。
私も当然思いました。
仮になんですが、このイメージから展開したコンテナにログインしてyum update でもすればいいんでしょうか?(権限的にできる?
ま、とりあえずやってみましょ。

コンテナを起動します。
# docker run -d -i -t centos:centos7 /bin/bash

コンテナにログインするため、CONTAINER IDを確認します。
# docker ps
CONTAINER ID   IMAGE                                COMMAND                  CREATED         STATUS                             PORTS                       NAMES
8c70ec6e46e2   centos:centos7                       "/bin/bash"              6 minutes ago   Up 6 minutes                                                   eager_mclean

コンテナにログインしてyum update を実行します。
# docker exec -i -t 8c70ec6e46e2 /bin/bash
# yum update -y


先ほど「HIGH」で表示されていた「bind-license」もUpdateされていることが確認できました。
# cat /var/log/yum.log | grep bind
Nov 13 01:55:50 Erased: 32:bind-utils-9.11.4-26.P2.el7.x86_64
Nov 13 01:55:51 Erased: 32:bind-libs-9.11.4-26.P2.el7.x86_64
Nov 13 01:55:51 Erased: 32:bind-libs-lite-9.11.4-26.P2.el7.x86_64
Jun 09 01:36:23 Updated: 32:bind-license-9.11.4-26.P2.el7_9.9.noarch


yum update -y できたので、一旦exit でコンテナから抜けます。

この状態でイメージスキャンをしても、変更が保存されていないイメージしかいないため結果は変わりません。


そのため、docker commit で更新したコンテナから新しくイメージを「centos:update」として作成します。
# docker commit 8c70ec6e46e2 centos:update
sha256:6bc0f6964c196dc38a3b00bcf79a9b586306253808b08e8e26b16c80290831fc


作成されたものを確認してみると、SIZEが増えているのでちゃんとyum update の内容が反映されてそうですね。
# docker images
REPOSITORY                      TAG       IMAGE ID       CREATED              SIZE
centos                          update    6bc0f6964c19   About a minute ago   510MB
centos                          centos7   eeb6ee3f44bd   8 months ago         204MB


では改めて作成したイメージをスキャンしてみましょう。
# trivy i centos:update










Fixed Version があったものが全部表示されなくなったようですね。

アップデートで解消できる問題については、trivyで発見と対応後の状態を確認できることは確認できました。

ちょっと長くなってしまったので、dockleは次回に回したいと思います。


2022年5月31日火曜日

調べてみようコンテナやPodのセキュリティ その2 ~コンテナイメージの脆弱性確認ツールについて~

 コンテナイメージの脆弱性確認ツールについて


こちらは、コンテナのセキュリティに関して勉強していくシリーズの第2回となっています。

今回は、コンテナイメージの脆弱性確認ツールについて調べたことを書いていこうと思います。


コンテナイメージというものは、実はWindowsやCent OSのインストール時に使うisoとかと違って動かすアプリケーションに必要なファイルやディレクとりなどの階層が固められたものになります。

コンテナはホストOSのカーネルを利用するので、それ以外の部分で動かすアプリケーションに必要なものが固められたものがコンテナイメージというわけです。

なので、一般的なウイルス対策ソフトでファイルのスキャンをするというのとはちょっと内容が違ってきます。


一般的なウイルスソフトのスキャンは、スパイウェアやマルウェアの発見と駆除をしてくれます。

コンテナイメージでも同じようにファイルに問題がないか、変なプログラムがいないか、と言ったことをスキャンしてくれるのかというとそうではありません。

簡単に言いますと、イメージの中にあるアプリケーション等が既存の脆弱性(CVE)に該当していないかどうかという事を報告してくれます。


ここで覚えておいてほしいのが、脆弱性を報告してくれますがその対処は人間が行うという点です。

スパイウェアやマルウェアの駆除をしてくれる一般的なウイルスソフトは自動で駆除なり隔離なりをしてくれると思いますが、コンテナイメージの場合は人間が対処を行って大丈夫な状態のコンテナイメージを作成する必要があるという事ですね。

システム任せにできないのでかなり大変になりそうだなと思いました。


コンテナイメージのスキャンで有名なのが、Trivyという製品です。

先ほどの脆弱性に該当してないかどうかの確認と、設定ファイルの不備もチェックしてくれるようです。

こういったものが一般的に使われているようですね。


最初はTrivyを調べて記事にしようかと思ったんですが、これとはちょっと違うDockelというものもあるようでせっかくなら比較してみようかと思いました。

Dockelもコンテナイメージの脆弱性を確認してくれるツールなのですが、こちらはCIS Benchmark というシステムを安全に構築するためのベストプラクティスがありまして、そちらのコンテナに関係する部分についてもチェックを行ってくれるようです。


次回は実際にやってみた結果でブログを書きたいと思いますが、もしかしたらどちらか一方の記事になるかもしれません…

2022年4月28日木曜日

調べてみようコンテナやPodのセキュリティ

 今回から、Podやコンテナのセキュリティについて学習したことをまとめていきたいと思います。

なぜそんなことをしようと思ったのかといいますと、たまたまPodを実際に外部公開して使いましょう

となった場合、セキュリティの考え方は従来通りでいいんだろうか?


Podやコンテナにアンチウィルスソフトとかって入れるの? それってどうなんだろ?

という事を考えて気になり始めたので、実際に調べてみたところだいぶ考え方が違うしやる事も違う

という事がわかったためです。


従来のWindows OSなどでは、パッチを適用したりアンチウィルスを入れてスキャンしたり、ファイアウォールで

防御したりという事がセキュリティ的な対応としてすぐに思いつく事ではないでしょうか。

しかし、コンテナやPodでは少々事情が異なります。


コンテナやPodというのは、使いたいOSやアプリのイメージを使用して構築します。

この時に、脆弱性のチェックを行って脆弱性の対応が済んでいる状態のイメージを使用することで脆弱性の対応を

完了してしまうのです。


時間がたてば、新しい脆弱性等が発見されたりしますので、その際は対応を行ったイメージを再度作成しそれを利用する。

こういった作業を繰り返すのがPodやコンテナにおける脆弱性への対応の一つとなります。

使うイメージの状態に問題がなければ大丈夫という考え方という事ですね。


外部からアクセスされて乗っ取られたらどうするかという部分については、コンテナやPod内で動くユーザーを、非root

権限のユーザーにするといった対応を行っていくことになります。


今後、このブログでは、イメージの脆弱性を確認するツール、イメージのマルウェアを確認するツール、外部リポジトリ

を使用した脆弱性対応などについて調べたり確認したことを書いていこうと思いますので、ご興味ありましたら読んでみてください。

2022年3月25日金曜日

gRPCはどのくらい速いのか?

検証目的


gRPCは通信プロトコルにHTTP/2を使うため、高速化・双方向通信・streaming等を実現できる…というような記事をよく見るのですが、この「高速化」について、正直、普段の業務アプリ開発でHTTP/2を使った通信をしたことないため、どのくらい速くなるのかよくわかりません。。

そこで、実際にgRPCを使って速さを測定し、「gRPCは業務アプリ開発で使ったほうがいいのか?」を確認しようと思います。

※使うとしても、もちろん「ブラウザ↔サーバ」間ではなく、マイクロサービスを想定した「サーバ↔サーバ」間で使います。


検証内容


なるべく簡単にgRPCを使いたいなと思い、調べたところ、 NestJS でgRPCが使えるということを知ったので、今回は NestJS の gRPC を使って検証しようと思います。

比較対象が必要なので、普段の業務で使っている「Json API」を比較対象とし、「Json API」と「gRPC API」を作って比較してみます。

あと、やり取りするデータとリクエスト回数についてですが、今回は、

  • ①:大量データを1リクエストでやり取り
  • ②:軽量データを大多数リクエストでやり取り

を試そうと思います。
それぞれのイメージは以下のとおりです。

①のイメージ

以下のN件データを1リクエストでやり取りする
{
    {
      rowNum: 0,
      columnA: 'columnA0',
      columnB: 'columnB0',
      columnC: 'columnC0',
      columnD: 'columnD0',
      columnE: 'columnE0',
      columnF: 'columnF0',
      columnG: 'columnG0',
      columnH: 'columnH0',
      columnI: 'columnI0',
      columnJ: 'columnJ0'
    },
    {
      rowNum: 1,
      columnA: 'columnA1',
      columnB: 'columnB1',
      columnC: 'columnC1',
      columnD: 'columnD1',
      columnE: 'columnE1',
      columnF: 'columnF1',
      columnG: 'columnG1',
      columnH: 'columnH1',
      columnI: 'columnI1',
      columnJ: 'columnJ1'
    },
    …以下N件のデータが続く…
}

②のイメージ

以下の1件データをNリクエストでやり取りする
{
    {
      rowNum: 0,
      columnA: 'columnA0',
      columnB: 'columnB0',
      columnC: 'columnC0',
      columnD: 'columnD0',
      columnE: 'columnE0',
      columnF: 'columnF0',
      columnG: 'columnG0',
      columnH: 'columnH0',
      columnI: 'columnI0',
      columnJ: 'columnJ0'
    }
}


それと、「クライアントサーバ」「Json API サーバ」「gRPC API サーバ」の構成、および、計測箇所のイメージは以下のとおりです。



※補足1:検証の結果、最終的に「Json API」と「gRPC API」で若干のデータ構造の違いがでてしまいましたが、概ね同じ構造だと思ってください。

※補足2:今回はデータ量をバイト数で表記しません。普段の業務でデータの数を意識することはあっても、バイト数を意識することはあまり無いため、表記したところでピンと来ないと思ったためです。(意識しろよって話かもですが。。)

※補足3:今回はネットワークの遅延を含めたくなかったので、全サーバをローカルで起動して計測します。(ネットワークの通信速度によってgRPCが速くなる…ってことはないよね?たぶん。。間違ってたらすみません)

検証結果


検証結果は以下のとおりとなりました。

ケース番号データ件数リクエスト数API呼び出し回数計測時間(ms)平均時間(ms)
①ー1
1
1
Json API
1回目3.049
2.940
2回目2.756
3回目3.015
gRPC API
1回目2.766
2.830
2回目2.767
3回目2.958
①ー2
10,000
1
Json API
1回目56.332
47.220
2回目54.471
3回目30.857
gRPC API
1回目86.188
94.660
2回目106.468
3回目91.324
①ー3
1,000,000
1
Json API
1回目5,302.862
5,177.148
2回目4,803.492
3回目5,425.089
gRPC API
1回目12,201.268
12,075.561
2回目11,939.809
3回目12,085.605
②ー1
1
10
Json API
1回目9.551
9.758
2回目10.846
3回目8.878
gRPC API
1回目9.279
8.483
2回目8.422
3回目7.749
②ー2
1
100
Json API
1回目75.867
72.088
2回目74.990
3回目65.406
gRPC API
1回目56.728
50.794
2回目50.003
3回目45.650
②ー3
1
1,000
Json API
1回目655.518
551.685
2回目537.163
3回目462.374
gRPC API
1回目341.100
295.650
2回目315.221
3回目230.630

まとめると、以下のとおりです。

  • ①:大量データを1リクエストでやり取り
    • 結果:「Json API」のほうが速い。

  • ②:軽量データを大多数リクエストでやり取り
    • 結果:「gRPC API」のほうが速い。

HTTP/2は複数のリクエストを同時に処理できるだけあって、②はgRPCのほうが速いようですね。

でも正直なところ、業務アプリで同時にデータ取得するマスタ/トランザクションの数って、せいぜい4つくらいな気がする(=それぞれのマスタ/トランザクションがマイクロサービス化されているとしたら4リクエストくらいな気がする)ので、そう考えると上記のgRPCが速さを発揮するケースって業務アプリ開発上だとほとんど無いんじゃ…。

「20,000件データを4リクエストでやり取り」が現実的に有り得そうなケースなので、ちょっと計測してみました。結果は以下のとおりです。

※後述のソースコードでは計測できません。ちょっとイジって計測しました。

ケース番号データ件数リクエスト数API呼び出し回数計測時間(ms)平均時間(ms)
おまけ
20,000
4
Json API
1回目228.920
220.704
2回目219.529
3回目213.664
gRPC API
1回目677.273
700.893
2回目656.078
3回目769.329


意外な結果になりました。HTTP/2は複数のリクエストを同時に処理できるので、gRPCのほうが速い結果になると予想していたのですが、遅い結果になりましたね。。しかも結構の差がある。。


結論


業務アプリ開発ではgRPCは使わず、Json API を使うことにします。
(速さ以外にgRPCのメリットはあるかと思いますが、やはりユーザが一番嬉しいのは「速いこと」だと思うので)

どこか検証方法でまずいところあったんですかね。。勉強し直して、誤りがあればまたブログに書くかもです。


参考:ソースコード


NestJS CLI でプロジェクトを作ったあと、自分が追加・変更したソースだけ参考までに掲載しておきます。(NestJSのバージョンは「8.2.6」です)

「クライアントサーバ」「Json API サーバ」「gRPC API サーバ」それぞれでプロジェクトを作った形となります。

◆クライアントサーバ

事前に以下のnpmコマンドを実行してライブラリをインストールしました。
npm i --save @nestjs/axios
npm i --save axios

nest-cli.json (変更)
{
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "assets": ["**/*.proto"],
    "watchAssets": true
  }
}


src/main.ts (変更)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();


src/app.module.ts (変更)
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { ClientController } from './client.controller';
import { AppService } from './app.service';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { join } from 'path/posix';
import { HttpModule } from '@nestjs/axios';

@Module({
  imports: [
    HttpModule,
    ClientsModule.register([
      {
        name: 'SAMPLE_PACKAGE',
        transport: Transport.GRPC,
        options: {
          package: 'sample',
          protoPath: join(__dirname, 'proto/sample.proto'),
          url: 'localhost:3002',
          maxSendMessageLength: 1024 * 1024 * 1024 * 7, // ★大量のデータをやり取りするため設定。
          maxReceiveMessageLength: 1024 * 1024 * 1024 * 7, // ★大量のデータをやり取りするため設定。
        },
      },
    ]),
  ],
  controllers: [AppController, ClientController],
  providers: [AppService],
})
export class AppModule {}


src/client.controller.ts (追加)
import { HttpService } from '@nestjs/axios';
import { Controller, Get, Inject, OnModuleInit, Param } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { AxiosResponse } from 'axios';
import { forkJoin, Observable } from 'rxjs';

@Controller('client')
export class ClientController implements OnModuleInit {
  private sampleService: any;

  constructor(
    private httpService: HttpService,
    @Inject('SAMPLE_PACKAGE') private client: ClientGrpc,
  ) {}

  onModuleInit() {
    this.sampleService = this.client.getService<any>('SampleService');
  }

  @Get('call-a/:count')
  callServerA(@Param() params): string {
    const count = Number(params.count);

    const startTime = performance.now();
    this.httpService
      .get('http://localhost:3001/server-a/' + count)
      .subscribe((res) => {
        // console.log(res.data.samples[count - 1]); // ★データを見たかったらコメントアウトを外してください。
        const endTime = performance.now();
        console.log(endTime - startTime + ' ms');
      });

    return 'success';
  }

  @Get('call-a-parallel/:count')
  callServerAParallel(@Param() params): string {
    const count = Number(params.count);

    const reqList: Observable<AxiosResponse<any>>[] = [];
    for (let i = 0; i < count; i++) {
      reqList.push(this.httpService.get('http://localhost:3001/server-a/' + 1));
    }

    const startTime = performance.now();
    forkJoin(reqList).subscribe((res) => {
      /* // ★データを見たかったらコメントアウトを外してください。
      for (let i = 0; i < count; i++) {
        console.log(res[i].data.samples[0]);
      }
      */
      const endTime = performance.now();
      console.log(endTime - startTime + ' ms');
    });

    return 'success';
  }

  @Get('call-b/:count')
  callServerB(@Param() params): string {
    const count = Number(params.count);

    const startTime = performance.now();
    this.sampleService.getSamples({ count: count }).subscribe((res) => {
      // console.log(res.samples[count - 1]); // ★データを見たかったらコメントアウトを外してください。
      const endTime = performance.now();
      console.log(endTime - startTime + ' ms');
    });

    return 'success';
  }

  @Get('call-b-parallel/:count')
  callServerBParallel(@Param() params): string {
    const count = Number(params.count);

    const reqList: Observable<any>[] = [];
    for (let i = 0; i < count; i++) {
      reqList.push(this.sampleService.getSamples({ count: 1 }));
    }

    const startTime = performance.now();
    forkJoin(reqList).subscribe((res) => {
      /* // ★データを見たかったらコメントアウトを外してください。
      for (let i = 0; i < count; i++) {
        console.log(res[i].samples[0]);
      }
      */
      const endTime = performance.now();
      console.log(endTime - startTime + ' ms');
    });

    return 'success';
  }
}


src/proto/sample.proto (追加)
// proto/sample.proto
syntax = "proto3";

package sample;

service SampleService {
  rpc GetSamples (GetSamplesParam) returns (Samples) {}
}

message GetSamplesParam {
  int32 count = 1;
}

message Samples {
  repeated Sample samples = 1;
}

message Sample {
  int32 rowNum = 1;
  string columnA = 2;
  string columnB = 3;
  string columnC = 4;
  string columnD = 5;
  string columnE = 6;
  string columnF = 7;
  string columnG = 8;
  string columnH = 9;
  string columnI = 10;
  string columnJ = 11;
}

◆Json API サーバ(サーバA)

src/main.ts (変更)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3001);
}
bootstrap();


src/app.module.ts (変更)
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { ServerAController } from './server-a.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController, ServerAController],
  providers: [AppService],
})
export class AppModule {}


src/server-a.controller.ts (追加)
import { Controller, Get, Param } from '@nestjs/common';

@Controller('server-a')
export class ServerAController {
  @Get(':count')
  getSamples(@Param() params): any {
    const count = Number(params.count);
    const samples = [];
    for (let i = 0; i < count; i++) {
      samples.push({
        rowNum: i,
        columnA: 'columnA' + i,
        columnB: 'columnB' + i,
        columnC: 'columnC' + i,
        columnD: 'columnD' + i,
        columnE: 'columnE' + i,
        columnF: 'columnF' + i,
        columnG: 'columnG' + i,
        columnH: 'columnH' + i,
        columnI: 'columnI' + i,
        columnJ: 'columnJ' + i,
      });
    }
    return { samples: samples };
  }
}


◆gRPC API サーバ(サーバB)

事前に以下のnpmコマンドを実行してライブラリをインストールしました。
npm i --save @grpc/grpc-js @grpc/proto-loader
npm i --save @nestjs/microservices


nest-cli.json (変更)
{
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "assets": ["**/*.proto"],
    "watchAssets": true
  }
}


src/main.ts (変更)
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { join } from 'path/posix';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    AppModule,
    {
      transport: Transport.GRPC,
      options: {
        package: 'sample',
        protoPath: join(__dirname, 'proto/sample.proto'),
        url: 'localhost:3002',
      },
    },
  );
  await app.listen();
}
bootstrap();


src/app.module.ts (変更)
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { ServerBController } from './server-b.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController, ServerBController],
  providers: [AppService],
})
export class AppModule {}


src/server-b.controller.ts (追加)
import { Metadata, ServerUnaryCall } from '@grpc/grpc-js';
import { Controller } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';

@Controller()
export class ServerBController {
  @GrpcMethod('SampleService', 'GetSamples')
  getSamples(
    data: any,
    metadata: Metadata,
    call: ServerUnaryCall<any, any>,
  ): any {
    const count = Number(data.count);
    const samples = [];
    for (let i = 0; i < count; i++) {
      samples.push({
        rowNum: i,
        columnA: 'columnA' + i,
        columnB: 'columnB' + i,
        columnC: 'columnC' + i,
        columnD: 'columnD' + i,
        columnE: 'columnE' + i,
        columnF: 'columnF' + i,
        columnG: 'columnG' + i,
        columnH: 'columnH' + i,
        columnI: 'columnI' + i,
        columnJ: 'columnJ' + i,
      });
    }
    return { samples: samples };
  }
}


src/proto/sample.proto (追加)
// proto/sample.proto
syntax = "proto3";

package sample;

service SampleService {
  rpc GetSamples (GetSamplesParam) returns (Samples) {}
}

message GetSamplesParam {
  int32 count = 1;
}

message Samples {
  repeated Sample samples = 1;
}

message Sample {
  int32 rowNum = 1;
  string columnA = 2;
  string columnB = 3;
  string columnC = 4;
  string columnD = 5;
  string columnE = 6;
  string columnF = 7;
  string columnG = 8;
  string columnH = 9;
  string columnI = 10;
  string columnJ = 11;
}