Docker Composeを使ってHTTPSで通信できるSonarQubeを爆速で構築する

## SonarQubeとは
オープンソースのコード品質管理ツールです。
下記のようなことを行ってくれます。

* 重複しているコードの検出
* クラスや関数の複雑度の計測
* バグや脆弱性のチェック

より詳しい説明や具体的な使い方は [公式サイト](https://www.sonarqube.org/) やこちらの記事などを参考に・・・。

* [SonarQubeサクッといじってみた](https://qiita.com/yasumon/items/52702df02aaf6201bf30)
* [SonarQubeでソースコードの品質チェック](https://qiita.com/teradonburi/items/776e4735395af872320a)

SonarQubeいいじゃん！うちのチームでも使いたい！！ってなった方は [こちら](https://github.com/jkff-mv/docker-compose-sonarqube) へどうぞ。
以下 `docker-compose.yaml` が出来上がるまでの過程です。

## 背景
公式が用意してくれている [docker-composeのサンプル](https://github.com/SonarSource/docker-sonarqube/blob/master/example-compose-files/sq-with-postgres/docker-compose.yml) 、普通はこれで十分事足ります。
ただこれだと通信がHTTPで丸腰です・・・。 :joy:
社内や閉じたネットワーク内で使うならまだしも、お外に置くならHTTPSで通信させたいところ。
ということで、HTTPSで利用できるSonarQubeをサクッと立ち上げるための設定を作りたい！と思った次第です。

##事前に必要な作業
今回利用するサービスの制限として、ドメインを持っていたり外から通信できることが条件となっているため、下記の作業は必須となります。
**なお「爆速で構築する」にこれらの作業は含みませんので、事前に準備しておいてください。**

* ホストにDockerとDocker Composeをインストールする（必要に応じてカーネルも調整する）。
* ドメインを取得し、DNSの設定を行う。
* ホストに対して外部からアクセスできるようにする（80番と443番ポートを開放する）。

## Docker Composeの設定

### まずは雛形を調達
公式が用意してくれているサンプルがあるので、有り難く使わせてもらいましょう！
この内容をベースにあれこれいじっていきます。

```yaml:docker-compose.yaml
version: "2"

services:
  sonarqube:
    image: sonarqube:7.9.1-community
    ports:
      - "9000:9000"
    networks:
      - sonarnet
    environment:
      - sonar.jdbc.url=jdbc:postgresql://db:5432/sonar
    volumes:
      - sonarqube_conf:/opt/sonarqube/conf
      - sonarqube_data:/opt/sonarqube/data
      - sonarqube_extensions:/opt/sonarqube/extensions

  db:
    image: postgres
    networks:
      - sonarnet
    environment:
      - POSTGRES_USER=sonar
      - POSTGRES_PASSWORD=sonar
    volumes:
      - postgresql:/var/lib/postgresql
      - postgresql_data:/var/lib/postgresql/data

networks:
  sonarnet:
    driver: bridge

volumes:
  sonarqube_conf:
  sonarqube_data:
  sonarqube_extensions:
  postgresql:
  postgresql_data:
```

### とりあえずV3へ移行
公式のサンプルはバージョンが2系です・・・。
せっかくなので新しい3系に書き換えます。
加えて、サービスの依存関係や起動の設定、名前など、個人的に気になる部分をちょっと修正します。

```yaml:docker-compose.yaml
version: "3"

services:
  sonarqube-web:
    container_name: sonarqube-web
    image: sonarqube:7.9.1-community
    restart: always
    ports:
      - "9000:9000"
    environment:
      - sonar.jdbc.url=jdbc:postgresql://sonarqube-db:5432/sonar
    volumes:
      - sonarqube-conf:/opt/sonarqube/conf
      - sonarqube-data:/opt/sonarqube/data
      - sonarqube-extensions:/opt/sonarqube/extensions
    depends_on:
      - sonarqube-db
    networks:
      - sonarqube

  sonarqube-db:
    container_name: sonarqube-db
    image: postgres:12.1
    restart: always
    environment:
      - POSTGRES_USER=sonar
      - POSTGRES_PASSWORD=sonar
    volumes:
      - postgresql-root:/var/lib/postgresql
      - postgresql-data:/var/lib/postgresql/data
    networks:
      - sonarqube

networks:
  sonarqube:
    driver: bridge

volumes:
  sonarqube-conf:
  sonarqube-data:
  sonarqube-extensions:
  postgresql-root:
  postgresql-data:
```

### HTTPSで通信させるには
[公式](https://docs.sonarqube.org/latest/setup/operate-server/#header-5) いわくnginxとか使ってリバースプロキシを構成せいとのことです。

> To run the SonarQube server over HTTPS, you must build a standard reverse proxy infrastructure.

リバースプロキシ用意するの面倒くさい・・・。
nginxとか名前しか知らないし、SonarQube使いたいだけなのに何でnginxの勉強しなきゃならないのん・・・。

リバースプロキシしてくれるのDockerイメージとかないのかなぁ・・・、ありました！
[jwilder/nginx-proxy](https://hub.docker.com/r/jwilder/nginx-proxy) というイメージがそのまま使えそうです。 :v:

### リバースプロキシを設定
nginx-proxyを組み込みます。
通常は、サーバの `VIRTUAL_HOST` にアクセス時のドメイン名を設定するだけで、勝手にリバースプロキシしてくれるとのこと（すげぇ・・・）。
ただ [ドキュメント](https://github.com/jwilder/nginx-proxy#multiple-ports) にある通り、デフォルトで見に行くのは80番ポートらしいので、今回はSonarQubeの9000番ポートも `VIRTUAL_PORT` に設定します。

> If your container exposes multiple ports, nginx-proxy will default to the service running on port `80` . If you need to specify a different port, you can set a `VIRTUAL_PORT` env var to select a different one. If your container only exposes one port and it has a `VIRTUAL_HOST` env var set, that port will be selected.

合わせて9000番ポートのホストへのバインドが不要になるので解除します。

```yaml:docker-compose.yaml
version: "3"

services:
  sonarqube-web:
    container_name: sonarqube-web
    image: sonarqube:7.9.1-community
    restart: always
    environment:
      - sonar.jdbc.url=jdbc:postgresql://sonarqube-db:5432/sonar
      # Settings for nginx reverse proxy.
      - VIRTUAL_HOST=your.sonarqube.domain.jp  # 変えてね！
      - VIRTUAL_PORT=9000
    volumes:
      - sonarqube-conf:/opt/sonarqube/conf
      - sonarqube-data:/opt/sonarqube/data
      - sonarqube-extensions:/opt/sonarqube/extensions
    depends_on:
      - sonarqube-db
    networks:
      - sonarqube

  sonarqube-db:
    container_name: sonarqube-db
    image: postgres:12.1
    restart: always
    environment:
      - POSTGRES_USER=sonar
      - POSTGRES_PASSWORD=sonar
    volumes:
      - postgresql-root:/var/lib/postgresql
      - postgresql-data:/var/lib/postgresql/data
    networks:
      - sonarqube

  sonarqube-reverse-proxy:
    container_name: sonarqube-reverse-proxy
    image: jwilder/nginx-proxy:latest
    restart: always
    ports:
      - "80:80"
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro
    depends_on:
      - sonarqube-web
    networks:
      - sonarqube

networks:
  sonarqube:
    driver: bridge

volumes:
  sonarqube-conf:
  sonarqube-data:
  sonarqube-extensions:
  postgresql-root:
  postgresql-data:
```

### 証明書を発行／更新するには
HTTPSで通信する際に必須の証明書についてですが、別件で [Let's Encrypt](https://letsencrypt.org/ja/) を使った経験はあるので、適当にスクリプト組んで自分でDockerイメージ作れるだろうと思っていました。
が、なんとnginx-proxyの説明を読んでいくと [jrcs/letsencrypt-nginx-proxy-companion](https://hub.docker.com/r/jrcs/letsencrypt-nginx-proxy-companion) というイメージが使えるとあるじゃないですか！
証明書の発行／更新を自動で行ってくれるので、こちらの作業も不要になりました。 :v:

### 証明書管理機能の追加
letsencrypt-nginx-proxy-companionを組み込みます。
基本的な改修ポイントはドキュメントにある通りです。

1. nginxプロキシコンテナの443番ポートを、ホストにバインドする。
1. `/etc/nginx/certs` 、 `/etc/nginx/vhost.d` 、 `/usr/share/nginx/html` をnginxプロキシコンテナと共有する。
1. SonarQubeコンテナの `LETSENCRYPT_HOST` に、証明書発行のためのドメイン名を設定する（ `VIRTUAL_HOST` と同じでOK）。
1. SonarQubeコンテナの `LETSENCRYPT_EMAIL` に、Let's Encryptからの通知を受け取れるメールアドレスを設定する（証明書の有効期限が切れそうになると通知がきます）。

一点ドキュメントには特に書いてなく手間取ったのが、 `NGINX_PROXY_CONTAINER` と `NGINX_DOCKER_GEN_CONTAINER` の設定です。
実行時にnginxプロキシコンテナが見つからないと怒られたので焦りましたが、言われた通りに設定したらちゃんと見つけてくれました（言われたとおり `NGINX_PROXY_CONTAINER` を設定すると、次は `NGINX_DOCKER_GEN_CONTAINER` を設定しろと言ってきます・・・）。

```
sonarqube-lets-encrypt | Error: can't get nginx-proxy container ID !
sonarqube-lets-encrypt | Check that you are doing one of the following :
sonarqube-lets-encrypt |        - Use the --volumes-from option to mount volumes from the nginx-proxy container.
sonarqube-lets-encrypt |        - Set the NGINX_PROXY_CONTAINER env var on the letsencrypt-companion container to the name of the nginx-proxy container.
sonarqube-lets-encrypt |        - Label the nginx-proxy container to use with 'com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy'.
```

また、Let's Encryptを使ったことある方はご存知かと思いますが、無料で証明書発行できるからと言ってやりたい放題というわけではありません。
例えば [レート制限](https://letsencrypt.org/ja/docs/rate-limits/) というものがあり、テストや検証段階でトライアンドエラーを繰り返していると引っかかる可能性があります・・・。
Let's Encryptにはそのためのテスト環境も用意されており、 [テスト環境の制限](https://letsencrypt.org/ja/docs/staging-environment/) は本番環境に比べてゆるゆるなので、まずはテスト環境を利用してきちんと動作することを確認しましょう。

[こちら](https://github.com/JrCs/docker-letsencrypt-nginx-proxy-companion/blob/master/docs/Let's-Encrypt-and-ACME.md#test-certificates) にある通りテスト用の証明書発行設定も用意されているので、まずは設定しておきます。

> The `LETSENCRYPT_TEST` environment variable, when set to `true` on a proxyed application container, will create a test certificates that don't have the [5 certs/week/domain limits](https://letsencrypt.org/docs/rate-limits/) and are signed by an untrusted intermediate (they won't be trusted by browsers).
>
> If you want to do this globally for all containers, set `ACME_CA_URI` as described in [Container configuration](https://github.com/JrCs/docker-letsencrypt-nginx-proxy-companion/blob/master/docs/Container-configuration.md) .

```yaml:docker-compose.yaml
version: "3"

services:
  sonarqube-web:
    container_name: sonarqube-web
    image: sonarqube:7.9.1-community
    restart: always
    environment:
      - sonar.jdbc.url=jdbc:postgresql://sonarqube-db:5432/sonar
      # Settings for nginx reverse proxy.
      - VIRTUAL_HOST=your.sonarqube.domain.jp  # 変えてね！
      - VIRTUAL_PORT=9000
      # Settings for Let's Encrypt.
      - LETSENCRYPT_HOST=your.sonarqube.domain.jp  # 変えてね！
      - LETSENCRYPT_EMAIL=your.email.address@gmail.com  # 変えてね！
      # Settings for test.
      - LETSENCRYPT_TEST=true
    volumes:
      - sonarqube-conf:/opt/sonarqube/conf
      - sonarqube-data:/opt/sonarqube/data
      - sonarqube-extensions:/opt/sonarqube/extensions
    depends_on:
      - sonarqube-db
    networks:
      - sonarqube

  sonarqube-db:
    container_name: sonarqube-db
    image: postgres:12.1
    restart: always
    environment:
      - POSTGRES_USER=sonar
      - POSTGRES_PASSWORD=sonar
    volumes:
      - postgresql-root:/var/lib/postgresql
      - postgresql-data:/var/lib/postgresql/data
    networks:
      - sonarqube

  sonarqube-reverse-proxy:
    container_name: sonarqube-reverse-proxy
    image: jwilder/nginx-proxy:latest
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - nginx-certs:/etc/nginx/certs:ro
      - nginx-vhost:/etc/nginx/vhost.d
      - nginx-html:/usr/share/nginx/html
    depends_on:
      - sonarqube-web
    networks:
      - sonarqube

  sonarqube-lets-encrypt:
    container_name: sonarqube-lets-encrypt
    image: jrcs/letsencrypt-nginx-proxy-companion:latest
    restart: always
    environment:
      # Find nginx proxy container.
      - NGINX_PROXY_CONTAINER=sonarqube-reverse-proxy
      - NGINX_DOCKER_GEN_CONTAINER=sonarqube-reverse-proxy
      # Settings for test.
      - ACME_CA_URI=https://acme-staging-v02.api.letsencrypt.org/directory
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - nginx-certs:/etc/nginx/certs:rw
      - nginx-vhost:/etc/nginx/vhost.d
      - nginx-html:/usr/share/nginx/html
    depends_on:
      - sonarqube-reverse-proxy
    networks:
      - sonarqube

networks:
  sonarqube:
    driver: bridge

volumes:
  sonarqube-conf:
  sonarqube-data:
  sonarqube-extensions:
  postgresql-root:
  postgresql-data:
  nginx-certs:
  nginx-vhost:
  nginx-html:
```

### HTTPSで通信できるSonarQubeを構築するための設定
動作確認が済んだらテスト用の設定を削除します。
`docker-compose up -d` だけで、HTTPSで通信できるSonarQubeが爆誕します。 :tada:

```yaml:docker-compose.yaml
version: "3"

services:
  sonarqube-web:
    container_name: sonarqube-web
    image: sonarqube:7.9.1-community
    restart: always
    environment:
      - sonar.jdbc.url=jdbc:postgresql://sonarqube-db:5432/sonar
      # Settings for nginx reverse proxy.
      - VIRTUAL_HOST=your.sonarqube.domain.jp  # 変えてね！
      - VIRTUAL_PORT=9000
      # Settings for Let's Encrypt.
      - LETSENCRYPT_HOST=your.sonarqube.domain.jp  # 変えてね！
      - LETSENCRYPT_EMAIL=your.email.address@gmail.com  # 変えてね！
    volumes:
      - sonarqube-conf:/opt/sonarqube/conf
      - sonarqube-data:/opt/sonarqube/data
      - sonarqube-extensions:/opt/sonarqube/extensions
    depends_on:
      - sonarqube-db
    networks:
      - sonarqube

  sonarqube-db:
    container_name: sonarqube-db
    image: postgres:12.1
    restart: always
    environment:
      - POSTGRES_USER=sonar
      - POSTGRES_PASSWORD=sonar
    volumes:
      - postgresql-root:/var/lib/postgresql
      - postgresql-data:/var/lib/postgresql/data
    networks:
      - sonarqube

  sonarqube-reverse-proxy:
    container_name: sonarqube-reverse-proxy
    image: jwilder/nginx-proxy:latest
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - nginx-certs:/etc/nginx/certs:ro
      - nginx-vhost:/etc/nginx/vhost.d
      - nginx-html:/usr/share/nginx/html
    depends_on:
      - sonarqube-web
    networks:
      - sonarqube

  sonarqube-lets-encrypt:
    container_name: sonarqube-lets-encrypt
    image: jrcs/letsencrypt-nginx-proxy-companion:latest
    restart: always
    environment:
      # Find nginx proxy container.
      - NGINX_PROXY_CONTAINER=sonarqube-reverse-proxy
      - NGINX_DOCKER_GEN_CONTAINER=sonarqube-reverse-proxy
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - nginx-certs:/etc/nginx/certs:rw
      - nginx-vhost:/etc/nginx/vhost.d
      - nginx-html:/usr/share/nginx/html
    depends_on:
      - sonarqube-reverse-proxy
    networks:
      - sonarqube

networks:
  sonarqube:
    driver: bridge

volumes:
  sonarqube-conf:
  sonarqube-data:
  sonarqube-extensions:
  postgresql-root:
  postgresql-data:
  nginx-certs:
  nginx-vhost:
  nginx-html:
```

上のYAMLが全てですが [GitHub](https://github.com/jkff-mv/docker-compose-sonarqube) にも一応置いておきます。
気が向いたらSonarQubeのバージョンアップに合わせて、バージョン上げていこうと思います。

## さいごに
シンプルにグローバルIPを割り当てたGCE上で動かしていますが、 **GCP使うならGCEの前にロードバランサ立てて、そこHTTPSの終端にすればよくね？** と悟りを開いたところです。 :innocent: