Amazon Linux と Jetty で HTTP/2 な GitBucket サーバーを建てる。

GitBucket の運用サーバーを HTTPS 対応で建てたいという動機からスタート。Tomcat と Apache のような組み合わせではなく、もっとライトに建てたかったので、GitBucket のビルトインサーバーでも採用されている Jetty を試した。

HTTP サーバー兼 Java サーブレットを一つで提供してくれて、先進機能も豊富、設定項目の自由度、ポータビリティと言った利点に惹かれた。

**注意**: だいたい設定できたけどデーモン化して起動しようとすると `java.lang.NoClassDefFoundError` というエラーで失敗する問題が未解決です。助けて。


# tl;dr

サーバーの構成は次のように定義した:

| 環境変数       | 値               | 説明                                 |
|----------------|------------------|--------------------------------------|
| JETTY_HOME     | `/opt/jetty`     | Jetty 実行ディレクトリのパス。       |
| JETTY_BASE     | `/var/jetty`     | Jetty データディレクトリのパス。     |
| JETTY_USER     | `jetty`          | Jetty を実行する Linux ユーザー名。  |
| GITBUCKET_HOME | `/var/gitbucket` | GitBucket データディレクトリのパス。 |
| *N/A*          | `/opt/gitbucket` | GitBucket 実行ディレクトリのパス。   |

- 上記ディレクトリは `jetty` ユーザーの所有とした。
- `JETTY_HOME` は実ディレクトリ `/opt/jetty-distribution-9.3.7.v20160115` のシンボリックリンクとした。
- この他 GitBucket の実行ファイルパスは `/opt/gitbucket/gitbucket.war` としている。


# Amazon Linux

## セットアップ

Security Group で 80 と 443 ポートを開放する。

`iptables` で Jetty 標準の HTTP ポート `8080` と HTTPS ポート `8443` をポートフォワーディングして、`80` と `443` に設定する:

```sh
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080
sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8443
sudo service iptables save
```

- `80` と `443` に Jetty のポートを変更するには実行ユーザーを `root` にする必要がある。それを避けるためにポートフォワーディングを使用する。[^1]

Jetty の実行ユーザー `jetty` を作成する:

```sh
sudo adduser \
  --comment 'User running Jetty server' \
  --home /home/jetty \
  --create-home \
  --system \
  --shell /bin/bash \
  --user-group \
  jetty
```


# Java

## インストール

Amazon Linux は OpenJDK を `yum` コマンドからインストールできる。しかし、HTTPS 暗号化通信のアルゴリズムの一部が実装されていないとかで、Oracle 純正 JDK をダウンロードして使用する。

- [Java SE Development Kit 8 - Downloads](http://www.oracle.com/technetwork/java/javase/downloads/index.html)

現状最新の `jdk-8u74-linux-x64.rpm` をダウンロードしてインストールする:

```sh
curl -L \
  http://download.oracle.com/otn-pub/java/jdk/8u74-b02/jdk-8u74-linux-x64.rpm \
  | sudo rpm -Uvh -
```

`alternatives` コマンドでデフォルトの Java 環境をインストールしたものに変更する:

```sh
sudo alternatives --config java
```

```shell-session
There is 3 program that provides 'java'.

  Selection    Command
-----------------------------------------------
   1           /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/java
   2           /usr/lib/jvm/jre-1.8.0-openjdk.x86_64/bin/java
*+ 3           /usr/java/jdk1.8.0_74/jre/bin/java

Enter to keep the current selection[+], or type selection number: 3
```


# GitBucket

## インストール

GitBucket は手動インストールのみ。

- [Releases · gitbucket/gitbucket](https://github.com/gitbucket/gitbucket/releases)

最新の `gitbucket.war` をダウンロードして設置する:

```sh
# /opt/gitbucket 以下にダウンロード
sudo mkdir -p /opt/gitbucket
cd /opt/gitbucket
sudo curl -LO https://github.com/gitbucket/gitbucket/releases/download/3.12/gitbucket.war
# 所有者は jetty ユーザーに
sudo chown jetty:jetty -R /opt/gitbucket
```

`GITBUCKET_HOME` として `/var/gitbucket` ディレクトリを作成する:

```sh
# GITBUCKET_HOME ディレクトリを作成
sudo mkdir -p /var/gitbucket
# 所有者は jetty ユーザーに
sudo chown jetty:jetty -R /var/gitbucket
```


# Jetty

## インストール

Amazon Linux は `yum` コマンドで Jetty のインストールに対応していないので、手動で導入する。

- [Eclipse Jetty Downloads](http://download.eclipse.org/jetty/)

現状最新の `jetty-distribution-9.3.7.v20160115.tar.gz` をダウンロードしてインストールする:

```sh
# ダウンロードした書庫を /opt 以下に展開
curl -L \
  http://download.eclipse.org/jetty/9.3.7.v20160115/dist/jetty-distribution-9.3.7.v20160115.tar.gz \
  | sudo tar zxvf - -C /opt
# /opt/jetty としてシンボリック化
ln -s /opt/jetty-distribution-9.3.7.v20160115 /opt/jetty
```

`/opt/jetty-distribution-9.3.7.v20160115` (`/opt/jetty`) 以下は `jetty` ユーザーの所有に変更する:

```sh
sudo chown jetty:jetty -R /opt/jetty-distribution-9.3.7.v20160115
```

前述の Java 環境 `jdk-8u74` に対応する Jetty ライブラリ `alpn-1.8.0_74.mod` は、上記パッケージには含まれていない。なので別途ダウンロードして、然るべきパスに配置する:

```sh
# jetty ユーザーで作業
sudo su - jetty
# /opt/jetty/modules/alpn-impl 以下に alpn-1.8.0_74.mod をダウンロード
cd /opt/jetty/modules/alpn-impl
curl -LO https://raw.githubusercontent.com/eclipse/jetty.project/master/jetty-start/src/test/resources/dist-home/modules/alpn-impl/alpn-1.8.0_74.mod
```

- [jetty.project/jetty-start/src/test/resources/dist-home/modules/alpn-impl at master · eclipse/jetty.project](https://github.com/eclipse/jetty.project/tree/master/jetty-start/src/test/resources/dist-home/modules/alpn-impl)

## セットアップ

`JETTY_BASE` となるディレクトリを作成し、必要なモジュールをインストールする:

```sh
# jetty ユーザーで作業
sudo su - jetty
# JETTY_BASE ディレクトリを作成
mkdir -p /var/jetty
# JETTY_BASE ディレクトリにモジュールをインストール
cd /var/jetty
java -jar /opt/jetty/start.jar \
  --add-to-start=deploy,http,https,http2,logging
```

作成された `start.ini` に次の設定を追加し、HTTP 通信でアクセスがあった時のリダイレクト先 HTTPS 通信のポート番号を指定する[^2]:

```sh
# jetty ユーザーで作業
sudo su - jetty
# start.ini に設定項目を追加
vi /var/jetty/start.ini
```

```diff:/var/jetty/start.ini
229,235d228
< # HTTP 通信でアクセスがあった時のリダイレクト先 HTTPS 通信のポート番号
< # http://www.eclipse.org/jetty/documentation/current/configuring-ssl.html#d0e4938
< jetty.httpConfig.securePort=443
<
```

### GitBucket 用のセットアップ

`JETTY_BASE` の `webapps` ディレクトリ下に `gitbucket.xml` を作成し;

- サーバールートで GitBucket を動作させる設定と
- HTTPS 通信にリダイレクトする設定[^3]

を定義する:

```sh
# jetty ユーザーで作業
sudo su - jetty
# gitbucket.xml を作成
touch /var/jetty/webapps/gitbucket.xml
```

```xml:/var/jetty/webapps/gitbucket.xml
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">

<Configure class="org.eclipse.jetty.webapp.WebAppContext">
  <!-- ルートパスで GitBucket を動作させる -->
  <Set name="contextPath">/</Set>
  <Set name="war">/opt/gitbucket/gitbucket.war</Set>

  <!-- HTTPS 通信を宣言する -->
  <!-- https://wiki.eclipse.org/Jetty/Howto/Configure_SSL#Redirecting_http_requests_to_https -->
  <Set name="securityHandler">
    <New class="org.eclipse.jetty.security.ConstraintSecurityHandler">
      <Call name="addConstraintMapping">
        <Arg>
          <New class="org.eclipse.jetty.security.ConstraintMapping">
            <Set name="pathSpec">/*</Set>
            <Set name="constraint">
              <New class="org.eclipse.jetty.util.security.Constraint">
                <!-- 2 means CONFIDENTIAL. 1 means INTEGRITY -->
                <Set name="dataConstraint">2</Set>
              </New>
            </Set>
          </New>
        </Arg>
      </Call>
    </New>
  </Set>
</Configure>
```

- `gitbucket.war` を `webapps` 内に設置しなくても `gitbucket.xml` に war ファイルのパスが設定されてれば問題ないみたいだ。

## デーモン化

デーモン化するため `/etc/default/jetty` を作成し、必要な環境変数を定義する:

```sh
sudo touch /etc/default/jetty
```

```:/etc/default/jetty
JETTY_HOME=/opt/jetty
JETTY_BASE=/var/jetty
JETTY_USER=jetty
GITBUCKET_HOME=/var/gitbucket
```

デーモン化用スクリプトは Jetty のパッケージに含まれているので、これをコピーして配置する:

```sh
sudo cp /opt/jetty/bin/jetty.sh /etc/init.d/jetty
```

サーバー起動時にも自動起動するよう設定する:

```sh
sudo chkconfig --add /etc/init.d/jetty
sudo chkconfig jetty on
```

自動起動が設定されているのを確認する:

```sh
sudo chkconfig --list jetty
```

```shell-session
jetty          	0:off	1:off	2:on	3:on	4:on	5:on	6:off
```

デーモンを起動する:

```sh
sudo service jetty start
```

**ここまでやったが、デーモン起動するとエラーで終了してしまう...**

```shell-session
Starting Jetty: StartLog to /var/jetty/logs/start.log
2016-03-15 00:00:00.986:INFO::main: Logging initialized @142ms
2016-03-15 00:00:01.086:INFO::main: Redirecting stderr/stdout to /home/jetty/logs/2016_03_15.stderrout.log
FAILED Tue Mar 15 00:00:04 UTC 2016
```

```:/home/jetty/logs/2016_03_15.stderrout.log
Exception in thread "main" java.lang.NoClassDefFoundError: org/eclipse/jetty/alpn/ALPN$ServerProvider
        at java.lang.ClassLoader.defineClass1(Native Method)
        at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
        at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
        at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
        at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
        at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
        at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        at java.lang.Class.getDeclaredConstructors0(Native Method)
        at java.lang.Class.privateGetDeclaredConstructors(Class.java:2671)
        at java.lang.Class.getConstructors(Class.java:1651)
        at org.eclipse.jetty.util.TypeUtil.construct(TypeUtil.java:567)
        at org.eclipse.jetty.xml.XmlConfiguration$JettyXmlConfiguration.newObj(XmlConfiguration.java:787)
        at org.eclipse.jetty.xml.XmlConfiguration$JettyXmlConfiguration.itemValue(XmlConfiguration.java:1233)
        at org.eclipse.jetty.xml.XmlConfiguration$JettyXmlConfiguration.value(XmlConfiguration.java:1138)
        at org.eclipse.jetty.xml.XmlConfiguration$JettyXmlConfiguration.access$500(XmlConfiguration.java:274)
        at org.eclipse.jetty.xml.XmlConfiguration$JettyXmlConfiguration$AttrOrElementNode.getList(XmlConfiguration.java:1366)
        at org.eclipse.jetty.xml.XmlConfiguration$JettyXmlConfiguration$AttrOrElementNode.getList(XmlConfiguration.java:1341)
        at org.eclipse.jetty.xml.XmlConfiguration$JettyXmlConfiguration.call(XmlConfiguration.java:704)
        at org.eclipse.jetty.xml.XmlConfiguration$JettyXmlConfiguration.configure(XmlConfiguration.java:417)
        at org.eclipse.jetty.xml.XmlConfiguration$JettyXmlConfiguration.configure(XmlConfiguration.java:358)
        at org.eclipse.jetty.xml.XmlConfiguration.configure(XmlConfiguration.java:259)
        at org.eclipse.jetty.xml.XmlConfiguration$1.run(XmlConfiguration.java:1498)
        at java.security.AccessController.doPrivileged(Native Method)
        at org.eclipse.jetty.xml.XmlConfiguration.main(XmlConfiguration.java:1435)
Caused by: java.lang.ClassNotFoundException: org.eclipse.jetty.alpn.ALPN$ServerProvider
        at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        ... 29 more
```

とり急ぎ `nohup` でバックグラウンド実行してお茶を濁している:

```sh
sudo su - jetty
cd /var/jetty
GITBUCKET_HOME=/var/gitbucket nohup java -jar /opt/jetty/start.jar 2>&1 | logger &
```


# 参考

- [Jetty 9.3でHTTP/2対応Webサーバを作ってみよう - Qiita](http://qiita.com/tomoyukilabs/items/7f3f09a68a2c014ef04a)
- [Jetty上にAD連携したgitbucketを構築する - Qiita](http://qiita.com/hikaruworld@github/items/e02459b9ef9bc7d2a7f8)
- [Jettyのインストール（EC2 ver9.1） - Qiita](http://qiita.com/supertaihei02/items/a53ce41d82c4b5ab0efa)


# TODO

- デーモンで起動するとエラーで失敗する問題の解決
- ~~HTTP 通信を HTTPS 通信に自動リダイレクトさせる設定~~
- SSL 証明書の発行と設定
- cloud-init 化


[^1]: [Setting Port 80 Access for a Non-Root User](http://www.eclipse.org/jetty/documentation/current/setting-port80-access.html)
[^2]: [Configuring SSL/TLS](http://www.eclipse.org/jetty/documentation/current/configuring-ssl.html#d0e4938)
[^3]: [Jetty/Howto/Configure SSL - Eclipsepedia](https://wiki.eclipse.org/Jetty/Howto/Configure_SSL#Redirecting_http_requests_to_https)