Nexus Repository Manager 3のDocker Registryを試す

タグ付に困る製品名。何かに使うようなので調べてます。

tl;dr
------------

- Nexus Repository Manager 3が提供するDocker Registry機能のhosted,proxy,groupの動作を試した
- 現時点(2019/07/08)で[registry-mirrors](https://docs.docker.com/registry/recipes/mirror/#configure-the-docker-daemon)としての利用が出来ていない

中身が薄いので画像入れてごまかしてます。

特徴
------------------

Nexus Repository Manager 3は、パッケージの提供場所となるリポジトリを扱うソフトウェア。キャッシュとして使ったり、プライベートに使ったり。
プラグインで扱えるリポジトリを増やすことも出来る。[一覧はDocumentで](https://help.sonatype.com/repomanager3/formats)。アンオフィシャルだけどCPANもある（Perl好き）

今回の目標
-----------------

こんな感じの構成を組む。表現力の乏しさが垣間見えるポンチ絵がこちらです。

![ポンチ絵.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52527/15ae9e81-0be3-7176-bc0c-c8aebd305f61.png)

この構成にすることで以下が実現できる（はず）

- Hostedリポジトリ
  - ローカルでイメージを管理したい
- Proxyリポジトリ
  - プロキシとしてアクセスを集約したい
  - Docker Hubさんへの通信負荷を減らしたい、Pullを早くしたい
- Groupリポジトリ
  - 独自イメージやDocker Hubイメージを意識せずに扱いたい（※今回出来てません）

頑張りましょう。


導入
------------------

以下を読みながら進めている。

https://help.sonatype.com/repomanager3/installation

導入する環境はメモリ4GBのCentOS 7.5なLinux環境。

### Java Runtime Environmentの導入

``` bash
# JREの導入
sudo yum install -y java-1.8.0-openjdk.x86_64

# 確認
java -version
```

### nexusユーザ作成

``` bash
sudo useradd nexus -s /sbin/nologin
```

### Nexus Repository Manager 3のインストール

``` bash
curl -LO https://download.sonatype.com/nexus/3/latest-unix.tar.gz
tar xzvf latest-unix.tar.gz

NEXUS_DIR_NAME=$(ls | grep "^nexus-3" | tail -n 1)

sudo mv "$NEXUS_DIR_NAME" /opt
sudo ln -ns "$NEXUS_DIR_NAME" /opt/nexus-3
sudo chown -R nexus:nexus /opt/nexus-3*
sudo chmod -R go-rw /opt/nexus-3*
```

### 作業ディレクトリの確保

Nexus Repository Manager 3は相対ディレクトリ`../sonatype-work`を参照しようとするので、ここにディレクトリを作っておく

``` bash
sudo mkdir /opt/sonatype-work
sudo chown nexus:nexus /opt/sonatype-work
sudo chmod -R go-rw /opt/sonatype-work
```


### サービス登録

https://help.sonatype.com/repomanager3/installation/run-as-a-service#RunasaService-systemd

``` bash

sudo tee /etc/systemd/system/nexus.service << 'EOF'

[Unit]
Description=nexus service
After=network.target
  
[Service]
Type=forking
LimitNOFILE=65536
ExecStart=/opt/nexus-3/bin/nexus start
ExecStop=/opt/nexus-3/bin/nexus stop
User=nexus
Restart=on-abort

[Install]
WantedBy=multi-user.target
EOF

# systemdファイル読み込み
sudo systemctl daemon-reload

# 自動起動設定 & 起動
sudo systemctl enable nexus.service
sudo systemctl start nexus.service

# 起動確認
systemctl status nexus.service
```

ブラウザで`http://localhost:8081`へアクセスできることを確認する。(リモートサーバ上ならSSHで繋いでポートフォワーディングなどする)


Nexus Repository Manager 3の設定(主にDocker Registry)
---------------------------

### adminユーザのログイン

https://help.sonatype.com/repomanager3/installation/accessing-the-user-interface#sonatype-banner

Nexus Repository Manager 3を起動するとadminのパスワードが以下に記録されるので確認する

``` bash
sudo -u nexus cat /opt/sonatype-work/nexus3/admin.password
```

ブラウザの右上のSigninからアクセスして、ユーザ名にadmin、パスワードに上記のコマンドで確認したパスワードを入力する。その後、パスワードリセットを促されるので適宜行う。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52527/4b07266a-804e-d5ed-c86b-841154ea47c3.png)

匿名のアクセスを受け付けるようにするかどうかだが、今回は無効にしておく。


### Nexus Repository Manager 3上にDockerのリポジトリを作成する

https://help.sonatype.com/repomanager3/formats/docker-registry

ここでいう「リポジトリ」はNexus Repository Manager 3の用語。

- proxy: その名の通りプロキシするリポジトリ。プロキシ先にDocker Hubを指定すれば、Docker Hubのイメージを取ってきて、溜め込むことが出来る。
- hosted: 自前イメージを置いておくリポジトリ。Pushする先。
- group: 上記を統合したリポジトリを提供できる。Hostedリポジトリにあるプライベートなイメージと。ProxyリポジトリにあるDocker Hubのイメージを一緒に扱えるようになる。

また、**pushするときはhosted、pull等をするときはgroupを使う。groupにpushしてもエラーになるので注意。**。
というわけで、使うときはhostedとgroupのために2ポート開ける必要がある。

StorageはDefault(ローカルディスク)があるので、今回はそれを使う。

### HostedなDockerリポジトリを作成

Repository →Repositoriesから作成する。

- docker (hosted)を選ぶ
- HTTPSを有効にして5001を指定する
- Enable Docker V1 APIを有効にする（手元のdockerクライアントがV1で投げるようなので。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52527/47b44163-7a36-a0e1-9bbb-67fd80ba7392.png)


### ProxyなDockerリポジトリを作成

- docker (Proxy)を選ぶ
- Enable Docker V1 APIを有効にする
- Remote Storageに`https://registry-1.docker.io`を指定
- Docker Indexに `Use Docker Hub` を指定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52527/fb0528f4-a676-54f9-adc8-c794a6d748b8.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52527/0a31abf1-e9d9-8905-4a37-e6fbac8247f7.png)


### GroupなDockerリポジトリを作成

- docker (Group)を選ぶ
- HTTPSを有効にして5000を指定する
- Enable Docker V1 APIを有効にする
- Member repositoriesへ先ほど作成したリポジトリをMembersに追加する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52527/2fc57f4a-8749-f47f-06d6-d95a38f76349.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52527/59b822fc-6c3a-d8d1-d109-563981519521.png)



自己署名証明書によるSSL対応
--------------------------

今回はローカルのみの動作で良いものとして自己署名証明書を使う。以下の記事を参考に構築してた

- https://help.sonatype.com/repomanager3/formats/docker-registry/ssl-and-repository-connector-configuration
- https://help.sonatype.com/repomanager3/security/configuring-ssl
- https://support.sonatype.com/hc/en-us/articles/217542177

keystoreの場所は`/opt/nexus-3/etc/jetty/jetty-https.xml`に記述されている位置を参照する。デフォルトで`<install-dir>/etc/ssl/keystore.jks`を参照する。しかし、アップデート時に再設定が面倒になりそうなので別の場所に配置する。

keystoreのパスワードはすべて`password`にしている。もし変更する場合は、`/opt/nexus-3/etc/jetty/jetty-https.xml`の修正も必要なので注意。


``` bash
# keystoreを作成する先のpath
KEYSTORE_PATH=/opt/sonatype-work/nexus3/etc/ssl/keystore.jks

# ドメイン
NEXUS_DOMAIN=$(hostname)

# keystoreを保存するディレクトリを作成
sudo -u nexus mkdir -p "$(dirname $KEYSTORE_PATH)"

# keystoreを作成
sudo -u nexus keytool -genkeypair -keystore "$KEYSTORE_PATH" -storepass password -keypass password -alias jetty -keyalg RSA -keysize 2048 -validity 3650 -deststoretype pkcs12 -dname "CN=*.${NEXUS_DOMAIN}, OU=Example, O=Sonatype, L=Unspecified, ST=Unspecified, C=US" -ext "SAN=DNS:${NEXUS_DOMAIN},DNS:localhost" -ext "BC=ca:true"
sudo chmod 0600 "$KEYSTORE_PATH"

# keystoreを確認
sudo -u nexus keytool -v -list -keystore "$KEYSTORE_PATH" -storepass password
```

設定でkeystoreの配置を変え、HTTPS接続を有効化する。

``` bash
sudo -u nexus tee -a /opt/sonatype-work/nexus3/etc/nexus.properties << 'EOF'

# keystore等のパスを変更
ssl.etc = /opt/sonatype-work/nexus3/etc/ssl
# `${jetty.etc}/jetty-https.xml` を追加し、HTTPSをサポートさせる
nexus-args=${jetty.etc}/jetty.xml,${jetty.etc}/jetty-http.xml,${jetty.etc}/jetty-https.xml,${jetty.etc}/jetty-requestlog.xml
# （おまけ）HTTPSで管理画面にアクセスできるようにする
application-port-ssl = 8443
EOF
```

再起動

``` bash
sudo systemctl restart nexus
```

再起動してからListenするまで時間がかかるのでしばらく待つ。

最後にdockerの信頼できる証明書として登録・更新

``` bash
keytool -printcert -sslserver ${NEXUS_DOMAIN}:5000 -rfc | sudo tee /etc/pki/ca-trust/source/anchors/${NEXUS_DOMAIN}.crt
keytool -printcert -sslserver localhost:5000 -rfc | sudo tee /etc/pki/ca-trust/source/anchors/localhost.crt
sudo update-ca-trust
```

registry-mirrorの設定（※2019/07/08時点では機能しない）
----------

https://docs.docker.com/registry/recipes/mirror/#configure-the-docker-daemon

Dockerデーモンの設定を変更し、イメージをプライベートレジストリから返すようにする。

しかし、Docker側のバグがあり期待通りに機能しない。[理由は後ろの方に書いた](#registry-mirrorsが機能しない)

``` bash
sudo tee /etc/docker/daemon.json << EOF
{
  "registry-mirrors": ["https://${NEXUS_DOMAIN}:5000"]
}
EOF

# デーモン再起動して設定読み直し
sudo systemctl restart docker
```


動作確認
-------------

- hostedの動作をpushできることで確認する
- groupとproxyの動作をpull、run、searchを用いて行う

プライベートレジストリを経由してイメージをpullしてrun。

``` bash
mkdir sample
cd sample
echo 'console.log("Hello World")' > app.js

# プライベートレジストリへログイン（とりあえずadminで繋がる、Userを作りRoleを与えればそのユーザで繋がる）
sudo docker login localhost:5000

sudo docker run -v $(pwd):/tmp localhost:5000/node:10 node /tmp/app.js
```

次にDockerfileからイメージを作成し、それをプライベートレジストリにPush。

``` bash
cat << 'EOF' > Dockerfile
from localhost:5000/node:10
COPY app.js /tmp/app.js
EOF

sudo docker build -t  fukasawah/fukasawah-node-hello:1 .

# イメージを試す（イメージにapp.jsが含まれているのでマウント無しで使える）
sudo docker run fukasawah/fukasawah-node-hello:1 node /tmp/app.js
```

イメージが問題ないようなのでHostedリポジトリ(ポート番号:5001)へPushする。

``` bash
# タグをPush用に複製する
sudo docker tag fukasawah/fukasawah-node-hello:1 localhost:5001/fukasawah-node-hello:1
# プライベートレジストリへログイン（とりあえずadminで繋がる）
sudo docker login localhost:5001
# イメージをPushする
sudo docker push localhost:5001/fukasawah-node-hello:1
```

PushしたイメージをGroupリポジトリ(ポート番号:5000)経由で取得して実行（Hostedではないことに注意）

``` bash
sudo docker run -v $(pwd):/tmp localhost:5000/fukasawah-node-hello:1 node /tmp/app.js
```

プライベートレジストリを使い、`node`に関するイメージを検索。

``` bash
# docker-hub上にあるイメージをプロキシリポジトリを介して検索
sudo docker search localhost:5000/node

# pushしたイメージを検索
sudo docker search localhost:5000/fukasawah-node-hello

# Hostedリポジトリからnodeを検索
sudo docker search localhost:5001/node
```




お試し計測
-------------

一番気になってたregistry-mirrorとして使ったときのPullにかかる時間の改善具合を見たかった。
しかし、registry-mirrorが機能しなかったし、明示的に指定してPullしてもあまり効果なかった。

<details>
<summary>そして環境が貧弱で真の結果ではない気がするので伏せておく。興味があれば開いてお読みください。</summary><div>

- Azure VM DS1v2を2台用意
  - docker registry
  - docker daemon + docker client (操作側)
- ディスクは Standard HDD (これが良くない)
- 対象は「`node:10`（プライベートレジストリ未使用）」「`$NEXUS_DOMAIN:5000/node:10`(プライベートレジストリ利用)」
  - registry-mirrorは諸事情で機能しなかったので。



前準備

``` bash
NEXUS_DOMAIN=foo.example

# 自己署名証明書を信頼する
openssl s_client -showcerts -connect ${NEXUS_DOMAIN}:5000 < /dev/null | awk "/BEGIN CERTIFICATE/,/END CERTIFICATE/" | sudo tee /etc/pki/ca-trust/source/anchors/${NEXUS_DOMAIN}.crt
sudo update-ca-trust

# ログイン
sudo docker login "${NEXUS_DOMAIN}:5000"

# registry-mirrorを有効
sudo tee /etc/docker/daemon.json << EOF
{
  "registry-mirrors": ["https://${NEXUS_DOMAIN}:5000"]
}
EOF

# registry-mirrorを無効
sudo tee /etc/docker/daemon.json << EOF
{
}
EOF

sudo systemctl restart docker 

# docker infoを見ると、Registry Mirrosの項目が追加される
sudo docker info
```

1回測る毎に以下を実行

``` bash
# 情報でRegistry Mirrorsの情報を確認する
sudo docker info | grep -A5 "Registry Mirrors"

# イメージを全て削除したりゴミを削除したり
sudo docker images -q | sudo xargs docker rmi -f
sudo docker system prune

# 実行
time sudo docker pull node:10
# or
time sudo docker pull $NEXUS_DOMAIN:5000/node:10
```

|     | Mirror有 | Mirror無|
|-----|----------|--------|
|1回目|1:22.09|1:17.36|
|2回目|1:18.53| 1:18.10|
|3回目|1:15.62|1:18.22|

(単位は `分:秒.ミリ秒`)

うーん、誤差ですね。展開に時間がかかりすぎている。
ダウンロード自体はどちらも10秒ぐらいで終わるが、その後の展開に70秒以上かかっているという感じ（Standard HDDのせいだと思う）

</div></details>


感想
---------------

自己証明書のSSL対応しようとしたり、ベンチマーク取ろうとしたり、実はまだregistry-mirrorが使えなかったり、Anonymouseも試したけどようわからん、という感じで無駄に時間がかかってしまった。
意外とLinux上でごにょごにょとやってしまったのでDockerで1発で動かせるようにした方が良さそう。

ボタンポチポチでリポジトリが立ち上がるのでお手軽感はある。Mavenやnpmのリポジトリを使うとなっても、ある程度は操作が一緒なので、同じ感覚で建てられて良さそう。（実際に運用できるかは自信がない）

registry-mirrorが使えれば・・・という感じ。





遭遇したエラー
--------------

先にDockerの環境を。ちょっと古いが、CentOS 7の安定板がこれらしい。

```
$ sudo docker version
Client:
 Version:      17.05.0-ce
 API version:  1.29
 Go version:   go1.7.5
 Git commit:   89658be
 Built:        Thu May  4 22:06:25 2017
 OS/Arch:      linux/amd64

Server:
 Version:      17.05.0-ce
 API version:  1.29 (minimum version 1.12)
 Go version:   go1.7.5
 Git commit:   89658be
 Built:        Thu May  4 22:06:25 2017
 OS/Arch:      linux/amd64
 Experimental: false
```


### HostedリポジトリへPushしてエラー

以下のような感じ。

```
# sudo docker push localhost:5000/fukasawah-node-hello:1
The push refers to a repository [localhost:5000/fukasawah-node-hello]
ea53379b117c: Layer already exists
954f92adc866: Layer already exists
adca1e83b51a: Layer already exists
73982c948de0: Layer already exists
84d0c4b192e8: Layer already exists
a637c551a0da: Layer already exists
2c8d31157b81: Layer already exists
7b76d801397d: Layer already exists
f32868cde90b: Layer already exists
0db06dff9d9a: Layer already exists
error parsing HTTP 404 response body: invalid character  '<' looking for beginning of value: 
```

Push先を間違えているかもしれない。ちゃんとHostedなDockerのRepositoryのポートになっているかどうか確認する。~~これで4時間溶かした~~


### 再起動時に`Could not configure HTTPS connector on port (ポート番号) for docker repository (リポジトリ名)`のログが出てHTTPS接続できない

DockerのHTTPSコネクタを有効にした後に出た。

``` /opt/sonatype-work/nexus3/log/nexus.log
2019-07-07 19:20:44,714+0000 WARN  [FelixStartLevel]  *SYSTEM org.sonatype.nexus.repository.docker.internal.DockerConnectorFacetImpl - Could not configure HTTPS connector on port 5000 for docker repository docker
org.sonatype.nexus.bootstrap.jetty.UnsupportedHttpSchemeException: Unsupported HTTP Scheme: https
        at org.sonatype.nexus.internal.jetty.ConnectorRegistrarImpl.validate(ConnectorRegistrarImpl.java:128)
        at org.sonatype.nexus.internal.jetty.ConnectorRegistrarImpl.addConnector(ConnectorRegistrarImpl.java:84)
        at org.sonatype.nexus.repository.docker.internal.DockerConnectorFacetImpl.doStart(DockerConnectorFacetImpl.java:121)
        at org.sonatype.nexus.repository.FacetSupport.start(FacetSupport.java:156)
        at org.sonatype.nexus.repository.docker.internal.DockerConnectorFacetImpl$$EnhancerByGuice$$613f4292.CGLIB$start$20(<generated>)
        at org.sonatype.nexus.repository.docker.internal.DockerConnectorFacetImpl$$EnhancerByGuice$$613f4292$$FastClassByGuice$$c00a5460.invoke(<generated>)
     
```

原因はjettyがhttpsの設定ファイルを読みこんでおらず、Keystoreを認識できていなかった。デフォルトでは読んでくれない。

なのでプロパティで読むようにする必要があった。以下のファイルを以下のように変更

``` /opt/sonatype-work/nexus3/etc/nexus.properties

# ${jetty.etc}/jetty-https.xml を追加している
nexus-args=${jetty.etc}/jetty.xml,${jetty.etc}/jetty-http.xml,${jetty.etc}/jetty-https.xml,${jetty.etc}/jetty-requestlog.xml

```


### registry-mirrorsが機能しない

これに関するIssueが上がっていた。見た感じ同じ現象に見える。

- [Docker is not passing auth informations when pulling from a mirror registry #30880](https://github.com/moby/moby/issues/30880)

registry-mirrorsは設定しておくと、`docker pull node:10`とすれば、registry-mirrorsにあるレジストリから取得を試みる、というのが期待する動きなのだが認証の問題でそうならない。

以下、そのときのdockerデーモンのログ。プライベートレジストリにアクセスしたが認証失敗し、Docker Hubの方を見に行ってしまっている。

```
Jul  8 19:03:52 localhost dockerd: time="2019-07-08T08:03:52.920737964Z" level=debug msg="Trying to pull node from https://remote-addrss:5000/ v2"
Jul  8 19:03:52 localhost dockerd: time="2019-07-08T08:03:52.965148289Z" level=debug msg="Increasing token expiration to: 60 seconds"
Jul  8 19:03:53 localhost dockerd: time="2019-07-08T08:03:53.002164344Z" level=info msg="Attempting next endpoint for pull after error: unauthorized: access to the requested resource is not authorized"
Jul  8 19:03:53 localhost dockerd: time="2019-07-08T08:03:53.002212643Z" level=debug msg="Trying to pull node from https://registry-1.docker.io v2"
Jul  8 19:03:55 localhost dockerd: time="2019-07-08T08:03:55.126096378Z" level=debug msg="Pulling ref from V2 registry: node:10"
```

この時、認証情報は使っていないのではなく、docker hubにログインしたときの認証情報を使っているらしい。
実際にdocker hubに登録したID/Passwordを使って、Nexus上のユーザを作成し適切にRoleを割り当てたら、一応registory-mirrorとして機能した。**しかしnexusのログレベルをDebugにするとログにパスワードが平文で流れていく**ので、おとなしく修正を待った方が良さそう。

後は、Issueにも書いてありますが、前段にNginx等のリバースプロキシを置いて、認証情報をNginx側で上書きして、Dockerデーモンからは認証関係なくアクセスできるようにする、という手法もあるみたい。Nginxにアクセスできる人は誰でも使えるようになってしまいますが、プライベートレジストリなのでそんなに問題はない気がする。

これがうまく扱えないと魅力が9割減だと思うので、改善されると良いなー。


不明点
--------------

- イメージの管理方法に何かあるか（ブラックリスト、ホワイトリスト方式とか）
- 古くなったイメージの更新をやってくれるのか（腐ったイメージを使うのは避けたい）
- キャッシュ用途の場合、ディスクの空き容量が心配（いい感じに古いのは削除してくれるのかどうか）
- 冗長構成 ([High Availability](https://help.sonatype.com/repomanager3/high-availability))
- 他の機能
  - Blob Storage(他のバックエンド、増設など)
  - Routing Rules
  - Cleanup Policies
  - Content Selectors
  - Task 
  - Anonymous認証
  - LDAP連携
  - IQ Server連携
  - Webhooks
  - Rest API