🕌

CentOS7でもHTTP/3+Brotil+TLS1.3なnginxを使いたい(RPMパッケージ)

2022/03/21に公開

表題の通りです。未だにCentOS7で運用している複数のWebサーバでHTTP/3(QUIC)やBrotil圧縮を試したくなってしまいました。しかし,yumで入るnginxは相当古く,そもそもTLS1.3にすら対応していないので,自前でビルドします。その際,複数のWebサーバへの適用を考え,RPMパッケージ化を行ったので,その方法をまとめておきます。

事前準備

RPMのビルド用ユーザとフォルダの作成

RPMパッケージ関係の作業はrootで作業しない方がよいため,builderというアカウントを作ります。今回は全ての作業をbuilderアカウントでやろうと思うので,アカウントとフォルダを一緒に作成します。

$ useradd builder
$ su builder -
$ mkdir ~/rpmbuild
$ echo '%_topdir %(echo $HOME)/rpmbuild' > ~/.rpmmacros

ビルドに必要なパッケージのインストール

以下のとおりインストールします。

$ yum -y epel-release
$ yum -y --enablerepo=epel install gcc gcc-c++ pcre-devel redhat-lsb-core zlib-devel pcre-devel rpm-build perl-devel perl-ExtUtils-Embed GeoIP-devel libxslt-devel gd-devel ninja-build go libunwind-devel

注意が必要なパッケージたちの説明です。まず,http3関係のパッケージをビルドするのに必要なcmakeをインストールします。CentOS7の標準リポジトリではcmake2がインストールされてしまうためepel経由でcmake3をインストールします(CMake 3.5 or later is required.)。しかし,これではバイナリファイルが /usr/bin/cmake3 となってしまいます。のちのち面倒なので,cmakeからシンボリックリンクを張っちゃいましょう。

$ yum -y --enablerepo=epel install cmake3
$ ln -s /usr/bin/cmake3 /usr/bin/cmake
$ cmake --version
cmake3 version 3.17.5  # >3.5

同様に,gccも8.x系をインストールします(C and C++ compilers with C++11 support are required. )。Developer Toolset 8を使うとsclコマンドでgccを置き換えることができます。このセッションだけ有効になるようにしているので,シェルを閉じたときは再度sclコマンドを実行しなくてはならない点に注意が必要です。

$ gcc --version
gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-44)
$ yum -y install centos-release-scl
$ yum -y --enablerepo=centos-sclo-rh install devtoolset-8
$ scl enable devtoolset-8 bash
$ gcc --version
gcc (GCC) 8.3.1 20190311 (Red Hat 8.3.1-3)

ちなみに,goも相当古いバージョンが入るので不安でしたが,なんとかなりました。

関連モジュールの準備

Boringssl(ビルド)

nginx-quicは,TLS部分をGoogleの開発したBoringsslに依存しています。これをソースからコンパイルしておきます。

$ cd ~
$ git clone https://boringssl.googlesource.com/boringssl
$ cd ./boringssl
$ mkdir build
$ cd build
$ cmake -GNinja ..
$ ninja
$ make

cmakeでなんどもこけたのですが,最初のyumコマンドで依存パッケージを入れておけば大丈夫なはずです。

nginxのSRPM入手

既存のnginx.spec など必要なファイルを使い回したかったので,nginxのSPRMを入手することにしました。すべて一から作成するのであれば本来は不要な工程で,あくまでのちのち楽するために必要な工程です。
http://nginx.org/packages/mainline/centos/7/SRPMS/ を見て,SRPMの最新版のURLを調べ,ダウンロードしました。今回はまだ1.21.7はなかったようで,1.21.6でした。

$ cd ~
$ curl -L -O https://nginx.org/packages/centos/7/SRPMS/nginx-1.21.6-1.el7.ngx.src.rpm
$ rpm -ivh nginx-1.21.6-1.el7.ngx.src.rpm
$ ls ~/rpmbuild
SOURCES  SPECS

なお,~/rpmbuild/SOURCES にある .tar.gzファイルにダウンロードしたnginxのソースコードがありますが,これはnginx-quicに置き換えるので一切使いません。後々混同しないように消しておきます。

$ rm ~/rpmbuild/SOURCES/nginx-1.12.6.tar.gz

nginx-quicの入手とお試しビルド

先ほど消した .tar.gzファイルに置き換わる,nginx-quicのソースコードを持ってきます。本来はhg clone でソースコードを持ってくるのですが,どうもCentOSと相性が悪く前に進まなかったので,.tar.gzを直接curlで持ってきちゃいましょう。

まず,最新版のコミットハッシュを調べます(tip.tar.gzを指定するとハッシュを調べなくとも最新版を入手できますが,後々.specファイルにコミットハッシュを書きたくなるので,この段階で調べておきましょう)。その後, https://hg.nginx.org/nginx-quic/ にアクセスし,最新のコミットへのリンクをクリックすると表示されるURLの末尾が最新のコミットハッシュです。今回は55b38514729bでした。 IETF QUIC バージョン1に対応しています(インターネットドラフトにはもう対応していません)。

$ cd ~
$ curl -L -O https://hg.nginx.org/nginx-quic/archive/55b38514729b.tar.gz

ここで,本当にビルドに必要なパッケージが足りているか確認するため,一度nginx-quicを手動でビルドしてみます。先ほど設定した boringssl を指定すること,および --with-http_v3_moduleを引数に加えることを忘れないようにします。本番ビルド自体は後ほどrpmbuildコマンドの中で実施するので,スキップしても大丈夫です。

$ cd ~
$ tar xvfz 55b38514729b.tar.gz
$ cd nginx-quic-55b38514729b
$ ./auto/configure --with-debug --with-http_v3_module         \
                       --with-cc-opt="-I../boringssl/include"     \
                       --with-ld-opt="-L../boringssl/build/ssl    \
                                      -L../boringssl/build/crypto"
$ make

うまくいくことが確認できたら,tar.gzファイルを rpmbuild 用のフォルダにコピーしておきます。

$ cp ~/55b38514729b.tar.gz ~/rpmbuild/SOURCES/

Brotliモジュールの入手

Brotliはnginxのmoduleとして提供されています。適当な場所にダウンロードしておきます。また,libbrotliが必要なので,yumでインストールしておきます。

$ cd ~
$ git clone https://github.com/google/ngx_brotli.git
$ cd ngx_brotli
$ git submodule update --init
$ yum -y install brotli-devel

ビルド

.spec の設定

元々入手した nginx.spec を以下のように編集します。

$ vi ~/rpmbuild/SPECS/nginx.spec

%define nginx_home %{_localstatedir}/cache/nginx
%define nginx_user nginx
%define nginx_group nginx
%define nginx_loggroup adm

BuildRequires: systemd
Requires(post): systemd
Requires(preun): systemd
Requires(postun): systemd

### distribution specific definitions は全て削除 ###

%define base_version 1.21.7         # nginx-quic-55b38514729bのnginxバージョンに合わせて修正
%define base_release 2%{?dist}.ngx  # 競合を防ぐため+1する

%define nginx_commit_hash 55b38514729b           # 追加
%define boringssl_home /home/builder/boringssl   # 追加
%define ngx_brotli_home /home/builder/ngx_brotli # 追加

%define bdir %{_builddir}/%{name}-%{nginx_commit_hash} # 

%define BASE_CONFIGURE_ARGS $(echo "--prefix=%{_sysconfdir}/nginx --sbin-path=%{_sbindir}/nginx --modules-path=%{_libdir}/nginx/modules --conf-path=%{_sysconfdir}/nginx/nginx.conf --error-log-path=%{_localstatedir}/log/nginx/error.log --http-log-path=%{_localstatedir}/log/nginx/access.log --pid-path=%{_localstatedir}/run/nginx.pid --lock-path=%{_localstatedir}/run/nginx.lock --http-client-body-temp-path=%{_localstatedir}/cache/nginx/client_temp --http-proxy-temp-path=%{_localstatedir}/cache/nginx/proxy_temp --http-fastcgi-temp-path=%{_localstatedir}/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=%{_localstatedir}/cache/nginx/uwsgi_temp --http-scgi-temp-path=%{_localstatedir}/cache/nginx/scgi_temp --user=%{nginx_user} --group=%{nginx_group} --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module")


Summary: High performance web server
Name: nginx-quic
Version: %{base_version}
Release: %{base_release}
URL: https://nginx.org/
Group: %{_group}

Source0: https://hg.nginx.org/nginx-quic/archive/%{nginx_commit_hash}.tar.gz
Source1: logrotate
Source2: nginx.conf
Source3: nginx.default.conf
Source4: nginx.service
Source5: nginx.upgrade.sh
Source6: nginx.suse.logrotate
Source7: nginx-debug.service
Source8: nginx.copyright
Source9: nginx.check-reload.sh

License: 2-clause BSD-like license

### BuildRoot,BuildRequiresを削除 ###

%description
nginx [engine x] is an HTTP and reverse proxy server, as well as
a mail proxy server.

%prep
### nginx-quic合わせて置き換え ###
%setup -n nginx-quic-%{nginx_commit_hash}

%build
### nginx-quic合わせて置き換え ###
./auto/configure --with-debug --with-http_v3_module %{BASE_CONFIGURE_ARGS} \
--with-cc-opt="-I%{boringssl_home}/include"  \
--with-ld-opt="-L%{boringssl_home}/build/ssl -L%{boringssl_home}/build/crypto" \
--add-module="%{ngx_brotli_home}" 

make

%install
### nginx_debug関係を削除 ###
%{__rm} -rf $RPM_BUILD_ROOT
%{__make} DESTDIR=$RPM_BUILD_ROOT INSTALLDIRS=vendor install

%{__mkdir} -p $RPM_BUILD_ROOT%{_datadir}/nginx
%{__mv} $RPM_BUILD_ROOT%{_sysconfdir}/nginx/html $RPM_BUILD_ROOT%{_datadir}/nginx/

%{__rm} -f $RPM_BUILD_ROOT%{_sysconfdir}/nginx/*.default
%{__rm} -f $RPM_BUILD_ROOT%{_sysconfdir}/nginx/fastcgi.conf

%{__mkdir} -p $RPM_BUILD_ROOT%{_localstatedir}/log/nginx
%{__mkdir} -p $RPM_BUILD_ROOT%{_localstatedir}/run/nginx
%{__mkdir} -p $RPM_BUILD_ROOT%{_localstatedir}/cache/nginx

%{__mkdir} -p $RPM_BUILD_ROOT%{_libdir}/nginx/modules
cd $RPM_BUILD_ROOT%{_sysconfdir}/nginx && \
    %{__ln_s} ../..%{_libdir}/nginx/modules modules && cd -

%{__mkdir} -p $RPM_BUILD_ROOT%{_datadir}/doc/%{name}-%{base_version}
%{__install} -m 644 -p %{SOURCE8} \
    $RPM_BUILD_ROOT%{_datadir}/doc/%{name}-%{base_version}/COPYRIGHT

%{__mkdir} -p $RPM_BUILD_ROOT%{_sysconfdir}/nginx/conf.d
%{__rm} $RPM_BUILD_ROOT%{_sysconfdir}/nginx/nginx.conf
%{__install} -m 644 -p %{SOURCE2} \
    $RPM_BUILD_ROOT%{_sysconfdir}/nginx/nginx.conf
%{__install} -m 644 -p %{SOURCE3} \
    $RPM_BUILD_ROOT%{_sysconfdir}/nginx/conf.d/default.conf

%{__install} -p -D -m 0644 %{bdir}/objs/nginx.8 \
    $RPM_BUILD_ROOT%{_mandir}/man8/nginx.8

%{__mkdir} -p $RPM_BUILD_ROOT%{_unitdir}
%{__install} -m644 %SOURCE4 \
    $RPM_BUILD_ROOT%{_unitdir}/nginx.service
%{__mkdir} -p $RPM_BUILD_ROOT%{_libexecdir}/initscripts/legacy-actions/nginx
%{__install} -m755 %SOURCE5 \
    $RPM_BUILD_ROOT%{_libexecdir}/initscripts/legacy-actions/nginx/upgrade
%{__install} -m755 %SOURCE9 \
    $RPM_BUILD_ROOT%{_libexecdir}/initscripts/legacy-actions/nginx/check-reload

# install log rotation stuff
%{__mkdir} -p $RPM_BUILD_ROOT%{_sysconfdir}/logrotate.d
%if 0%{?suse_version}
%{__install} -m 644 -p %{SOURCE6} \
    $RPM_BUILD_ROOT%{_sysconfdir}/logrotate.d/nginx
%else
%{__install} -m 644 -p %{SOURCE1} \
    $RPM_BUILD_ROOT%{_sysconfdir}/logrotate.d/nginx
%endif

%{__rm} $RPM_BUILD_ROOT%{_sysconfdir}/nginx/koi-utf
%{__rm} $RPM_BUILD_ROOT%{_sysconfdir}/nginx/koi-win
%{__rm} $RPM_BUILD_ROOT%{_sysconfdir}/nginx/win-utf

### %checkセクションはまるごと削除(nginx_debugが必要なため) ###

%clean
%{__rm} -rf $RPM_BUILD_ROOT

%files
### nginx_debug関係を削除 ###
%defattr(-,root,root)

%{_sbindir}/nginx

%dir %{_sysconfdir}/nginx
%dir %{_sysconfdir}/nginx/conf.d
%{_sysconfdir}/nginx/modules

%config(noreplace) %{_sysconfdir}/nginx/nginx.conf
%config(noreplace) %{_sysconfdir}/nginx/conf.d/default.conf
%config(noreplace) %{_sysconfdir}/nginx/mime.types
%config(noreplace) %{_sysconfdir}/nginx/fastcgi_params
%config(noreplace) %{_sysconfdir}/nginx/scgi_params
%config(noreplace) %{_sysconfdir}/nginx/uwsgi_params

%config(noreplace) %{_sysconfdir}/logrotate.d/nginx
%{_unitdir}/nginx.service
%dir %{_libexecdir}/initscripts/legacy-actions/nginx
%{_libexecdir}/initscripts/legacy-actions/nginx/*

%attr(0755,root,root) %dir %{_libdir}/nginx
%attr(0755,root,root) %dir %{_libdir}/nginx/modules
%dir %{_datadir}/nginx
%dir %{_datadir}/nginx/html
%{_datadir}/nginx/html/*

%attr(0755,root,root) %dir %{_localstatedir}/cache/nginx
%attr(0755,root,root) %dir %{_localstatedir}/log/nginx

%dir %{_datadir}/doc/%{name}-%{base_version}
%doc %{_datadir}/doc/%{name}-%{base_version}/COPYRIGHT
%{_mandir}/man8/nginx.8*

%pre
# 〜〜〜〜以下省略(これ以下は元のファイルと同じ)〜〜〜〜

変更点は以下のとおりです。

  • ディストリビューション特定の部分は全て削除
  • BuildRoot,BuildRequiresの部分はすべて削除
  • %define base_version 1.21.6%define base_version 1.21.7 (nginx-quic-55b38514729bのnginxバージョンに合わせるため)
  • %define base_release 1%{?dist}.ngx%define base_release 2%{?dist}.ngx (yumで管理しやすくするため)
  • %prep%buidを今回のnginx-quicおよびbrotliに合わせて修正
    • ビルドオプションには既存のパラメータを全てそのまま入れておきました。
  • %install%file のnginx_debug関係の部分,および %checkを全て削除 (debugをmakeしていないため)

RPMの作成

いよいよRPMパッケージをビルドします。

$ rpmbuild -bb ~/rpmbuild/SPECS/nginx.spec 
$ ls ~/rpmbuild/RPMS/x86_64/
nginx-quic-1.21.7-2.el7.ngx.x86_64.rpm  nginx-quic-debuginfo-1.21.7-2.el7.ngx.x86_64.rpm

無事RPMパッケージが作成されました。

インストールと動作確認

ここから先は,実際にnginxをインストールしたいサーバで作業します。

RPMパッケージのダウンロード

先ほどビルドした .rpm を何らかの方法でダウンロードします。

インストール

rpmコマンドで直接インストールすることも可能ですが,今回は真面目にyumでインストールします。

$ yum -y install createrepo
$ mkdir -p /tmp/nginx-quic
$ chmod 757 /tmp/nginx-quic
$ cp nginx-quic-1.21.7-2.el7.ngx.x86_64.rpm /tmp/nginx-quic # 先ほどダウンロードしたファイルを作成したフォルダへコピーしておきます
$ createrepo /tmp/nginx-quic
$ vi /etc/yum.repos.d/nginx-quic.repo
[nginx-quic]
name=nginx-quic
baseurl=file:///tmp/nginx-quic
gpgcheck=0
enabled=0

$ yum clean all
$ yum -y --enablerepo=nginx-quic install nginx

起動後,正しくインストールされていることを確認しておきます。

$ systemctl start nginx
$ systemctl status nginx # エラー無く立ち上がっていることを確認
$ curl -i localhost # サーバがレスポンスを返すことを確認
HTTP/1.1 200 OK
(以下略)
$ nginx -V  # コンパイルオプションを確認し,古いnginxが入っていないことを確認
nginx version: nginx/1.21.7
built by gcc 8.3.1 20190311 (Red Hat 8.3.1-3) (GCC) 
built with OpenSSL 1.1.1 (compatible; BoringSSL) (running with BoringSSL)
TLS SNI support enabled
configure arguments: --with-debug --with-http_v3_module --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt=-I/home/builder/boringssl/include --with-ld-opt='-L/home/builder/boringssl/build/ssl -L/home/builder/boringssl/build/crypto' --add-module=/home/builder/ngx_brotli

証明書の取得

letsencryptを使いました。おそらくこの記事を読んでいる方(http3とかbrotliとかを試そうとしている方)には特に説明不要な項かと思いますので,細かい説明は省略します。

# HTTP-01認証用設定
$ vi /etc/nginx/conf.d/default.conf
### server_nameを正しく設定 ###
server_name  localhost;
↓
server_name  localhost, nginx-quic-test.example.com;

$ systemctl reload nginx

# 証明書取得
$ yum --enablerepo=epel -y install certbot
$ certbot certonly --webroot -w /usr/share/nginx/html -d nginx-quic-test.example.com --email hogehoge@example.com

nginxの設定

https://quic.nginx.org/readme.html および https://github.com/google/ngx_brotli を参考に以下のような設定を投入しました。

$ vi /etc/nginx/conf.d/nginx-quic-test.conf

server {
   listen 443 ssl http2;
   listen [::]:443 ssl http2;
   listen 443 http3 reuseport;      # HTTP/3(QUIC)の設定(IPv4)
   listen [::]:443 http3 reuseport; # HTTP/3(QUIC)の設定(IPv6)
      
   server_name nginx-quic-test.example.com;

   root /usr/share/nginx/html;

   brotli on;              # brotliの設定
   brotli_comp_level 6;    # brotliの設定(オプション)
   brotli_types *;         # brotliの設定(オプション)

   ssl_protocols TLSv1.3;          # TLS1.3
   ssl_prefer_server_ciphers off;  # TLS1.3
   ssl_early_data on;              # TLS1.3の0-RTTを設定(HTTP/3が使われやすくなる)
   ssl_certificate /etc/letsencrypt/live/nginx-quic-test.example.com/fullchain.pem;   # Letsencryptの場合
   ssl_certificate_key /etc/letsencrypt/live/nginx-quic-test.example.com/privkey.pem;     # Letsencryptの場合
   
   location / {
      try_files $uri $uri/ =404;
      add_header Alt-Svc 'h3=":443"; h3-29=":443"; ma=86400';  # HTTP/2,HTTP/1.1でアクセスした際に次回以降のアクセスをHTTP/3で行わせるための設定(今後はh3-29は不要かもしれません)
   }
}

$ nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

$ systemctl restart nginx

ファイアウォールの設定

私はこれを忘れていてかなりの時間を無駄にしました。HTTP/3(QUIC)では,TCPの代わりにUDPが使われます。そのため,ファイアウォールなどでHTTP(TCP80番)とHTTPS(TCP443番)のみを開けていると,HTTP/3の通信が行われず,HTTP/2にフォールバックします。必ずUDP443番を開けましょう

AWSの場合のセキュリティグループ設定例を貼っておきます。

動作確認

HTTP/3対応のcurlコマンドもありますが,最新のGoogle ChromeであればHTTP/3で通信できます。そこで,Google Chrome(99.0.4844.74)の開発者ツールを使って確認しました。

HTTP/3となっていることは,開発者ツールのNetworkタブで「Protocol」の列を表示するようことで,簡単に確認できます。h3となっていればHTTP/3です。

注意点として,ブラウザはまずTLS上でHTTPのバージョンのネゴシエーションを行いますが,これはHTTP/1.1またはHTTP/2のどちらかを使うかのネゴシエーションしか行われず,初回のアクセス時はHTTP/2になる場合があるようです。その場合,ブラウザが初回アクセスのレスポンスヘッダに「alt-svc: h3=":443";」が含まれていることを確認できたら,次回以降HTTP/3でアクセスを試みます。nginx.confに0-RTTの設定をいれてあげているので比較的HTTP/3にアップグレードしやすくなっているはずですが,1度目がHTTP/2になってしまった場合はもう一度アクセスしてみてください。それでも上手くいかない場合,ブラウザキャッシュが原因かもしれませんので,Networkタブの「Disable Cache」にチェックを入れておきましょう。

ちなみにキャプチャでも確認しました。WireSharkではQUICと表示されるようです。

次に,圧縮を確認しましょう。同じくNetworkタブでName欄をクリックし,ヘッダを確認します。すると,Google Chromeが accept-encoding: gzip, deflate, br をリクエストし,nginxが content-encoding: brでレスポンスしていることが分かります。brはbrotliのことなので,無事brotliで圧縮されているようです。

圧縮度の検証

Brotli圧縮のすごさを知りたかったので,nginxのデフォルトページで評価してみました。評価では,リクエストヘッダを編集できるChrome拡張を使い, Accept-Encodingヘッダを変更してそのときのコンテンツサイズを比較しました。

  • デフォルト(圧縮無し)
    →771B

  • gzip
    →551B

  • Brotli(brotli_comp_level 6)
    →458B

gzipと比べると20%程度容量が削減できていました。そもそもが容量が小さいファイルなのでこの値が参考になるかはよく分かりません。。

参考にさせて頂いたところ

公式サイト

ブログなど

Discussion