Hateburo: kazeburo hatenablog

SRE / 運用系小姑 / Goを書くPerl Monger

さくらのクラウド GSLB で IPv6 対応した話

5/13 にさくらのクラウドのGSLBサービスにて、実サーバとしてIPv6のアドレスを登録できるようなリリースを行いました。

cloud-news.sakura.ad.jp

この機能拡張により、これまでのIPv4に加え、IPv6でもシステムの可用性を向上させることができるようになりました。

f:id:kazeburo:20210528145752p:plain

GSLBを簡単にいってしまうと「高可用・分散環境に置かれたヘルスチェック付きのDNSサーバ」です。

今回、ヘルスチェックの部分においてIPv6の実サーバへのリーチャビリティを確保し、実サーバとしてIPv6のサーバを登録した際に、AAAAレコードを返すことができるようになりました。今のところIPv6でのDNSの問い合わせは対応しておらず、IPv4での問い合わせのみになります。

GSLBにて重み付け応答「無効」を設定し、実サーバとして、IPv4のアドレス「203.0.113.4」「203.0.113.5」、IPv6アドレス「2001:db8::4」「2001:db8::5」を登録した際のDNSレスポンスは次のようになります

% dig +nocmd +nocomment +nostat site-NNNNNNNNNNN.gslb.sakura.ne.jp
;site-NNNNNNNNNNN.gslb.sakura.ne.jp. IN A
site-NNNNNNNNNNN.gslb.sakura.ne.jp. 0 IN A 203.0.113.4
site-NNNNNNNNNNN.gslb.sakura.ne.jp. 0 IN A 203.0.113.5

% dig +nocmd +nocomment +nostat site-NNNNNNNNNNN.gslb.sakura.ne.jp AAAA
;site-NNNNNNNNNNN.gslb.sakura.ne.jp. IN AAAA
site-NNNNNNNNNNN.gslb.sakura.ne.jp. 4 IN AAAA 2001:db8:1::4
site-NNNNNNNNNNN.gslb.sakura.ne.jp. 4 IN AAAA 2001:db8:1::5

このGSLBサービスは gdnsd というソフトウェアを利用して実現、提供させていただいております。こちらでも紹介しております。

qiita.com

qiita.com

gdnsd はWikipediaWikimedia Foundationが提供しているOSSです。

gdnsd.org

各種のヘルスチェック方式をサポートし、Geolocationや重み付けに基づいた応答などに対応している便利なDNSサーバソフトウェアです。

今回、GSLB(gdnsd)のサーバからIPv6のリーチャビリティを用意するにあたり、高可用・分散環境で動作している既存のサーバをリスクを取りながら構成を変更して直接IPv6アドレスを持たせるのではなく、別途IPv4/IPv6デュアルスタック構成のサーバを用意し、ヘルスチェックをそちらのサーバ経由で行うようにしています。

f:id:kazeburo:20210528145921p:plain

また、既存の動作への影響を減らすため、GSLBに登録されたサービスにIPv6のアドレスが含まれる際にのみ、監視代理サーバを経由するようになっております。

そして、この監視代理サーバで利用しているのが、Go言語で書いたisiusというWebサーバです。

github.com

今まで覚えきれない感じのProxyサーバを書いていますが、isiusもその一種になります。これまでサーバやコマンドを実装する際に、IPアドレスとポートを雑に「:」で結合してしまうなどIPv6に配慮しておりませんでしたが、isius実装時は反省や学びが多くありました。

isiusを起動し、

% curl -v localhost:3000/check_ping/3/10/1000/2001:db8:1::4

とアクセスすると、2001:db8:1::4 に対して、1sのタイムアウト、10msecの間隔で3回pingによる監視を行い、その結果を返します。pingだけではなく、GSLBに必要なtcp、http、httpsの監視が実装されています。

https監視の際のURLは

% curl -v localhost:3000/check_https/{method:(?:GET|HEAD|get|head)}/{ip}/{port:[0-9]+}/{expected_status:[0-9][0-9][0-9]}/{host}/{path:.*}

のようになっております。

そして、gdnsdからは外部コマンドを実行するヘルスチェック機能を使い、nagiosのhttp監視コマンド互換の「check_http2」コマンドを使って、正しいレスポンスが返ってくることを監視しています。

service_types => {
  http_mon => {
        plugin => "extmon",
        cmd => ["/usr/local/bin/check_http2","-I","localhost","-p","3000","-u","/check_http/head/%%ITEM%%/80/200/-/live","-e","HTTP/1.1 200","-A","gdnsd-monitor"],
        interval => 10,
        timeout => 5,
        up_thresh => 5,
        ok_thresh => 3,
        down_thresh => 2
  }
}

設定中の「%%ITEM%%」が実サーバのIPアドレスに変換され、コマンドが実行されます。ヘルスチェックでアクセスする際のパスやホストヘッダ、期待するHTTPステータスはお客様がコンソールやAPIで指定できるようになっております。

check_http2ではなくcurlでも監視はできそうですが、自分でつくった監視コマンドは痒い所に手が届くのでこちらを利用しております。check_http2コマンドもgithubにあります。

github.com

check_http2の機能は段階的に増えており、さまざまなサービスで利用しています。 一社に一つは、http監視コマンドを作ると便利ですね。

このようにして、GSLBhのIPv6対応を行いました。GSLBはリージョンやデータセンターを跨ぐようなバランシングに使うというイメージがありますが、実際にはシンプルにヘルスチェック付きのDNS-RRとして利用することができます。ボトルネックの少ないロードバランサの代わりとしても利用できます。機会があればご利用くださいませ。

今後もさまざまなサービスの機能追加、改善を行っていきますので、よろしくお願いします。

HAProxy+libslzによるHTTPレスポンスのGZIP圧縮の検証

少し前になりますが、4/8 にさくらのクラウドの高機能ロードバランサーサービスである エンハンスドロードバランサ にレスポンスボディのGZIP圧縮機能を追加しました。

cloud-news.sakura.ad.jp

エンハンスドロードバランサのコントールパネルやAPIで、GZIP圧縮を有効にすることで、手軽にWebサイトの表示にかかる時間を短くすることができますので、お試しいただければと思います。

この記事にあるとおり、エンハンスドロードバランサはソフトウェアとしてHAProxyを使っています。

qiita.com

今回、レスポンスのGZIP圧縮対応にあたり、HAProxyにlibslzという圧縮ライブラリを組み込んでおります。GZIP圧縮といえばzlibがもっとも使われると思いますが、事前に比較検証を行った上でlibslzを選択しているので、その紹介です。

ライブラリとベンチマークの環境

www.libslz.org

libslzはほぼ聞いたことがない圧縮ライブラリでしhaproxyの作者と同じ方がつくっている、省メモリ、CPUコストの低いGZIP互換の圧縮用ライブラリになります。(解凍の機能はありません)

このlibslzとzlibに加え、ちょうど安定版(2.0.0)がでたzlib-ngとlibslzでベンチマークを行い比較を行いました。

github.com

zlib-ngはzlibと互換のAPIを提供しながら、新しめのCPUの命令を使うなどして高速化を行ったライブラリです。また Cloudflare-zlibやintelの最適化も取り込んでいます。

ベンチマーク環境は、さくらのクラウドにて、CPU 12コア/メモリ 128GBのサーバをたて、ローカルスイッチににて接続しています。(さくらのクラウドではサーバの搭載するメモリ量によって使える帯域が変わりますが、128GBでは5Gbpsの通信が可能になります)

manual.sakura.ad.jp

3台のサーバはそれぞれ、

  • Go app (http-dump-request)
  • haporxy: Aにreverse proxy
  • benchmark (wrk): Bに対してベンチマーク実行

としました。

haproxy サーバのセットアップ

まず zlib-ngのインストール

この記事を書いている現在の最新は、2.0.2ですが、テストした時は2.0.0でしたので、2.0.0を使っております。

# wget https://github.com/zlib-ng/zlib-ng/archive/refs/tags/2.0.0.tar.gz
# tar zxf 2.0.0.tar.gz
# cd zlib-ng-2.0.0
# ./configure --zlib-compat --prefix=/opt/zlib-ng
# make
# make test
# make install
# ll /opt/zlib-ng/lib
total 312
-rw-r--r-- 1 root root 184986 Mar 17 09:15 libz.a
lrwxrwxrwx 1 root root     22 Mar 17 09:15 libz.so -> libz.so.1.2.11.zlib-ng
lrwxrwxrwx 1 root root     22 Mar 17 09:15 libz.so.1 -> libz.so.1.2.11.zlib-ng
-rwxr-xr-x 1 root root 126736 Mar 17 09:15 libz.so.1.2.11.zlib-ng
drwxr-xr-x 2 root root   4096 Mar 17 09:15 pkgconfig

haproxyのインストール。libslzを有効にしたものと、GZIPを有効にしたバイナリをビルドします。 libslzはyumでインストールしました

yum install -y libslz-devel
wget http://www.haproxy.org/download/x/src/haproxy-$HAPROXY_VERSION.tar.gz

tar xvfz haproxy-$HAPROXY_VERSION.tar.gz
cd haproxy-$HAPROXY_VERSION
# USE_SLZ=1を指定すると、libslzが有効
make TARGET=linux-glibc USE_THREAD=1 USE_SLZ=1 USE_OPENSSL=1 SSL_INC=/usr/local/include/openssl SSL_LIB=/usr/local/lib64
make install
mv /usr/local/sbin/haproxy /usr/local/sbin/haproxy_slz
cd ..

rm -rf haproxy-$HAPROXY_VERSION

tar xvfz haproxy-$HAPROXY_VERSION.tar.gz
cd haproxy-$HAPROXY_VERSION
# USE_ZLIB=1を指定すると、zlibが有効
make TARGET=linux-glibc USE_THREAD=1 USE_ZLIB=1 USE_OPENSSL=1 SSL_INC=/usr/local/include/openssl SSL_LIB=/usr/local/lib64
make install

zlib-ngを有効にするときは、LD_PRELOADを使います

# /usr/local/sbin/haproxy -vv|grep zlib
Built with zlib version : 1.2.7
Running on zlib version : 1.2.7
# LD_PRELOAD=/opt/zlib-ng/lib/libz.so /usr/local/sbin/haproxy -vv|grep zlib
Built with zlib version : 1.2.7
Running on zlib version : 1.2.11.zlib-ng

haproxyの設定は、実際の環境に近づけるためエンハンスドロードバランサの開発環境からコピー・変更しています。また、httpsを有効にし、ベンチマークhttpsにて行っています。

関係ありそうなところだけ抜き出すとこうなります。

frontend test-163.43.241.14:443
        mode http

        bind 0.0.0.0:443 ssl crt /path/to/test.pem alpn h2,http/1.1 tls-ticket-keys /path/to/test-tls-ticket-keys.txt
        default_backend test-backend-default

        compression algo gzip
        compression type text/html text/plain text/plain text/css text/javascript application/x-javascript application/javascript application/json text/js text/xml application/xml application/xml+rss image/svg+xml

        use_backend test-backend-group1 if { path_reg ^/.*$ }

backend test-backend-group1
        mode http
        balance leastconn
        retries 3

        option tcp-check
        server 192.168.0.101:3000 192.168.0.101:3000 check inter 10s

haproxyは次のように起動しました。

zlib

# /usr/local/sbin/haproxy -f /path/to/proxy.cfg

zlib-ng

# LD_PRELOAD=/opt/zlib-ng/lib/libz.so /usr/local/sbin/haproxy -f /path/to/proxy.cfg

libslz

# /usr/local/sbin/haproxy_slz -f /path/to/proxy.cfg

ベンチマーク

まず、wrkを使い9KBぐらいのデータが返るURLに対してベンチマークをしました。

無圧縮

$ wrk -d 30 'https://192.168.0.102/nogzip/source?plain'
Running 30s test @ https://192.168.0.102/nogzip/source?plain
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   596.21us  231.33us   8.14ms   91.47%
    Req/Sec     8.49k     1.50k   10.88k    59.47%
  508831 requests in 30.10s, 4.40GB read
Requests/sec:  16904.68
Transfer/sec:    149.66MB

zlib

$ wrk -d 30 -H 'Accept-Encoding: deflate, gzip, br' 'https://192.168.0.102/nogzip/source?plain'
Running 30s test @ https://192.168.0.102/nogzip/source?plain
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   832.53us  315.55us  10.56ms   87.28%
    Req/Sec     6.11k   690.37     7.61k    64.45%
  365934 requests in 30.10s, 1.10GB read
Requests/sec:  12157.29
Transfer/sec:     37.49MB

zlib-ng

$ wrk -d 30 -H 'Accept-Encoding: deflate, gzip, br' 'https://192.168.0.102/nogzip/source?plain'
Running 30s test @ https://192.168.0.102/nogzip/source?plain
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   705.03us  371.54us  11.29ms   92.63%
    Req/Sec     7.37k   702.16     9.36k    74.04%
  440795 requests in 30.10s, 1.62GB read
Requests/sec:  14644.69
Transfer/sec:     55.07MB

libslz

$ wrk -d 30 -H 'Accept-Encoding: deflate, gzip, br' 'https://192.168.0.102/nogzip/source?plain'
Running 30s test @ https://192.168.0.102/nogzip/source?plain
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   550.74us  213.55us  10.69ms   93.44%
    Req/Sec     9.24k     1.04k   11.72k    61.56%
  552642 requests in 30.10s, 2.11GB read
Requests/sec:  18360.29
Transfer/sec:     71.94MB

無圧縮で 16904.68req/secでていたのものが、zlibで圧縮を行うようにすると、12157.29req/secまで下がります。zlib-ngになると、14644.69req/secまであがります。そしてlibslzでは 18360.29req/secとさらにあがり、無圧縮よりスループットがでるようになっています。

この際のhaproxyサーバのcpu使用率(user:vmstatで計測)ですが、無圧縮では15%前後、zlibでは44%前後に上昇、zlib-ngは35%まで下がります。libslzは37%とzlib-ngより若干高くなるようですが、25%スループットがよいのでその影響もあるでしょう。

コンテンツのサイズを、140byteほどのSmall、3KBのMedium、6KBほどのLargeとしてベンチマーク結果をまとめたのが次になります。

f:id:kazeburo:20210424114635p:plain

もっともレスポンスのサイズが小さくなるのはzlibになりますが、CPU使用率・スループットへの影響が大きくなります。それに対して、zlib-ngはレスポンスサイズはzlibより大きくなりますが、CPU使用率がさがり、スループットも改善します。ただ、小さいサイズではzlibよりもCPU使用率があがってしまうことがあるようです。libslzでは、zlib-ngよりも若干レスポンスのサイズは増えるものの、CPU使用率はより低くできるようでした。

まとめ

以上のようなベンチマークの結果や、yumでインストールが可能という入手のしやすさ、また、エンハンスドLBにおいては転送量課金ではなくCPS(秒間の新規コネクション数)なので、圧縮率が多少悪くても問題になりにくいことなどを考慮にいれ、libslzを選択し、haproxyに組み込んだ上でエンハンスドロードバランサにてGZIP圧縮オプションの提供を開始しております。

エンハンスドロードバランサをはじめ、今後もさまざまなサービスの機能追加、改善を行っていきますので、よろしくお願いします。

さくらのクラウド DNSサーバのレコード編集時の反映時間を可視化する

dehydratedとさくらのクラウドDNSを使った証明書取得の記事を書いておりますが、

kazeburo.hatenablog.com

DNS-01 チャレンジを使って証明書を作成しようとする際に気になるのはDNSレコードの登録から、実際に反映するまでの待ち時間だったりします。dehydratedのhookスクリプトの例をみていると、30秒ほどsleepしているものもあったりします。

github.com

そこで、さくらのクラウドDNS機能のクライアントである sacloudns コマンドとmackerelをつかって、ゾーンへのレコード追加削除など編集から実際にDNSへの反映にかかる時間を可視化してみました。

github.com

計測方法

sacloudnsコマンドを使って適当なレコードを更新するshell scriptを書きます。

$ cat time-taken.sh 
#!/bin/sh
set -e
export SAKURACLOUD_ACCESS_TOKEN="xxx"
export SAKURACLOUD_ACCESS_TOKEN_SECRET="xxx"
TOKEN_VALUE=$(openssl rand -hex 18)

sacloudns rset --wait --wait-timeout=80s --zone example.com --name time-taken --ttl 30 --type TXT --data ${TOKEN_VALUE}

sacloudnsコマンドは、TXTレコードを追加・編集した際にのみ --wait というオプションが使えます。--wait オプションを指定して実行すると、ゾーン編集リクエストをさくらのクラウドAPIに送信したのち、指定されたゾーンのNSレコードからDNSサーバを調べ、そのDNSサーバに対して、2秒おきに実際に更新されているか名前解決を行うことで、実際に更新されるまで待つことができる機能です。ACMEdns-01チャレンジをスムーズに行うためにつくったものですが、今回これを利用します。

このshell scriptをcronで実行し、mackerelに送信するのが次のcrontabの設定です。

*/5 * * * * mkr wrap -n sacloud-dns-rset -d -a -- bash -c 'set -e -o pipefail; \
  /opt/mackerel-agent/plugins/bin/mackerel-plugin-command-status \
  --timeout 90s -n sacloud-dns-rset -- sh /path/to/time-taken.sh  | mkr throw -s my-service'

mkr wrap はコマンドを実行しエラーがあった場合、mackerelのアラートを発生させるコマンドです。sacloudnsコマンドが失敗した際にわかるようにしています。

mackerel-plugin-command-status は与えられたコマンドを実行し、その終了コードとかかった時間をメトリクスにするmackerelのプラグインです。

kazeburo.hatenablog.com

こちらで紹介してますが、バージョン 0.0.2 でかかった時間もメトリクスとして取得できるようになりました。

ホストのメトリクスとして使うことを想定したプラグインではありますが、crontabを実行するサーバを変更してもメトリクスが残るよう、mackerel-plugin-command-statusの結果をサービスメトリクスとして mkr throw を使って送信しています。

これで5分ごとにDNSを更新し、かかった時間をmackerelに投稿できました。

可視化結果

結果はこんな感じ。

f:id:kazeburo:20210317003256p:plain

グラフをみると、大体は20秒強、あるいは45秒前後で終わっているようです。また、APIのレスポンスに時間がかかったのか、反映の待ち時間が長かったのかはここではわかりませんが、たまに60秒以上かかることもあるようです。

これらを指標としつつ、改善していきたい所存です。

Go net/httpで任意のタイミングでchunked レスポンスを返す

調査のため、chunked レスポンスを行っている合間に遅延をいれる必要があったので、Go net/httpでの実現方法を調べました。

こちらにあった Flusher.Flush() を使うことになります。

stackoverflow.com

golang.org

FizzBuzzっぽいものを遅延をいれつつ書き出すにはこんな感じ

func fizzBuzz(w http.ResponseWriter, r *http.Request) {
    flusher, ok := w.(http.Flusher)
    if !ok {
        w.WriteHeader(500)
        w.Write([]byte("expected http.ResponseWriter to be an http.Flusher"))
        return
    }
    for i := 1; i <= 15; i++ {
        p := fmt.Sprintf("#%03d ", i)
        if i%3 == 0 {
            p += "Fizz"
        }
        if i%5 == 0 {
            p += "Buzz"
        }
        p = strings.TrimSpace(p)
        p += "\n"
        w.Write([]byte(p))
        flusher.Flush()
        time.Sleep(300 * time.Millisecond)
    }
}

telnetで試すと意図した通り chunk に分かれてレスポンスされてくるのがわかる

]% telnet localhost 3000
Trying ::1...
Connected to localhost.
Escape character is '^]'.
GET /demo/fizzbuzz_stream HTTP/1.1
Host: example.com

HTTP/1.1 200 OK
Vary: Accept-Encoding
Date: Wed, 10 Mar 2021 01:46:27 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked

5
#001

5
#002

a
#003 Fizz

5
#004

a
#005 Buzz

a
#006 Fizz

5
#007

5
#008

このFlushは github.com/NYTimes/gziphandler にて圧縮をかけていても有効でした。

github.com

gziphandlerの簡単な使い方

// fizzbuzzのためMinSizeを小さくしておく。デフォルト 1400
gz, _ := gziphandler.NewGzipLevelAndMinSize(gzip.DefaultCompression, 5)
http.Handle("/", gz(http.HandlerFunc(fizzBuzz)))

テスト・検証用で作っている http-dump-request というサーバにこのFizzBuzzを返すエンドポイントを追加しています。このサーバを今後は監視にも利用していくつもりです。

github.com

GitHub Actionsからさくらのクラウド オブジェクトストレージへアクセスする

2月1日よりオープンβとなった新しいさくらのクラウド オブジェクトストレージサービスにGitHub Actionsからアクセスしてみるサンプルです。

オブジェクトストレージは2021年3月31日までオープンベータ期間となります。お試しいただいてフィードバックいただけると嬉しいです! オブジェクトストレージ(β)の利用についてはこちらのニュースを確認してください。

cloud-news.sakura.ad.jp

この新しいオブジェクトストレージの特徴の一つとして、Amazon S3と高い互換性を持つAPIの提供があげられ、AWS CLIを通してオブジェクトの操作が可能です。マニュアルはこちらから

manual.sakura.ad.jp

ということで、GitHub ActionsからAWS CLIを使ってオブジェクトストレージにアクセスを試してみました。

アクセストークンについて

オブジェクトストレージ利用開始時に、 アクセスキーID と シークレットアクセスキー が表示されます。こちらのアクセスキーは全てのバケットを操作できてしまうので、パーミッションメニューからバケットごとにアクセス件が設定なアクセスキーを生成するのがお勧めです。

f:id:kazeburo:20210215094509p:plain (画像はマニュアルより)

アクセスキーを作成したら、GitHub Actionsを設定するrepositoryのSecretsに登録します。

f:id:kazeburo:20210215095118p:plain

Workflow の作成

GitHub ActionsでAWSへのアクセスを行う際には、

github.com

configure-aws-credentials を使うことで設定をシンプルにすることができますが、このactionの中で aws sts get-caller-identity 相当のAPI呼び出しを行うため、オブジェクトストレージのアクセスキーではエラーになってしまい、今回は使うことができません。

なので、環境変数でアクセスキーを渡すようにしてワークフローを作ったところ動きました。

name: release
on:
  push:
    branches:
      - main

jobs:
  renew-cert:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
        with:
          fetch-depth: 0

      - name: sync to object storage
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_REGION: eu-west-1

        run: |
          aws --endpoint-url=https://s3.isk01.sakurastorage.jp s3 --only-show-errors --delete sync public_html/ s3://my-website-data/public_html/

aws コマンドに --endpoint-url=https://s3.isk01.sakurastorage.jp を渡せばオブジェクトストレージにアクセス可能です。

どうぞお試しあれ~

2021/03/03 追記

GitHub Actionsで

<botocore.awsrequest.AWSRequest object at 0x7f22f2d7d630>

のようなエラーが出る際は環境変数 AWS_REGION を指定するか、awsコマンドに --region オプションの追加をするとなおります。

参考

github.com

dehydrated とさくらのクラウドのDNS機能をつかってワイルドカード・マルチドメイン証明書取得

dehydrated はshell scriptでできた letsencrypt/acme のクライアントです。certbotlegoなど様々なツールがある中で、わりと長いこと使ってますが、安定してワイルドカード・マルチドメイン対応の証明書の取得・更新ができています。

github.com

dehydratedとさくらのクラウドDNS機能と先週末つくったクライアント sacloudns を使うことで、ほぼ自動的に証明書の取得と更新が行えるので、その紹介です。

github.com

こちらのツールは、リリースページからのダウンロードもしくはMacなら homebrew でインストールが可能です。

% brew install kazeburo/tap/sacloudns

dehydrated の準備

dehydrated はGitHubからcloneしてきます

$ git clone https://github.com/dehydrated-io/dehydrated.git
$ cd dehydrated

そして設定ファイルを用意します。

まず、 config ファイルを作成

CERTDIR="${BASEDIR}/certs"
ACCOUNTDIR="${BASEDIR}/accounts"
HOOK="${BASEDIR}/hook.sh"
HOOK_CHAIN="no"
CHALLENGETYPE="dns-01"
KEY_ALGO="prime256v1"

configのサンプルは docs/example 以下にあります。KEY_ALGOにprime256v1を指定することで楕円曲線暗号であるECDSAを使った証明書を作成できます。デフォルトはRSAになります

つぎに証明書を作成するドメイン一覧を記した domains.txt を作ります

# スペース区切りで複数のドメインを記せます。ワイルドカードも使えますが行頭にはかけません
example.com *.example.com example.jp *.example.jp > certalias
chocon.me *.chocon.me

この記事で紹介する方法で証明書を作成する場合、こちらのドメインはすべてさくらのクラウドDNS機能で管理されている必要があります。試してはないですが、Let's Encryptではマルチドメイン証明書のSAN(Subject Alternative Name)に100個までFQDNを追加することができるようです。

そして最後に、dns-01 で利用するDNSレコードの作成などを行う hook.sh を用意します。

こちらはAmazon Route 53のクライアント、 cli53 を使う以下コードを参考にして作りました

https://github.com/whereisaaron/dehydrated-route53-hook-script/blob/master/hook.sh

#!/bin/bash
set -e
# This hook script is written based on dehydrated-route53-hook-script
# https://github.com/whereisaaron/dehydrated-route53-hook-script

deploy_challenge() {
    local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"

    local ZONE=$(find_zone "${DOMAIN}")
    
    if [[ -n "$ZONE" ]]; then
        echo "Creating challenge record for ${DOMAIN} in zone ${ZONE}"
        sacloudns radd --wait --zone ${ZONE} --name _acme-challenge.${DOMAIN}. --ttl 60 --type TXT --data ${TOKEN_VALUE}
    else
        echo "Could not find zone for ${DOMAIN}"
        exit 1
    fi
}

clean_challenge() {
    local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"

    local ZONE=$(find_zone "${DOMAIN}")
    
    if [[ -n "$ZONE" ]]; then
        echo "Deleting challenge record for ${DOMAIN} from zone ${ZONE}"
        sacloudns rdelete --zone ${ZONE} --name _acme-challenge.${DOMAIN}. --type TXT --data ${TOKEN_VALUE}
    else
        echo "Could not find zone for ${DOMAIN}"
        exit 1
    fi
}

deploy_cert() {
    local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}"
    # NOP
}

unchanged_cert() {
    local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}"
    # NOP
}

function invalid_challenge {
    local DOMAIN="${1}" RESPONSE="${2}"

    local HOSTNAME="$(hostname)"

    (>&2 echo "Failed to issue SSL cert for ${DOMAIN}: ${RESPONSE}")
}

function get_base_name() {
    local HOSTNAME="${1}"

    if [[ "$HOSTNAME" == *"."* ]]; then
      HOSTNAME="${HOSTNAME#*.}"
      echo "$HOSTNAME"
      return 0
    else
      echo ""
      return 1
    fi
}

function find_zone() {
  local DOMAIN="${1}"

  local ZONELIST=$(sacloudns list | jq -r '.DNS[].DNSZone'|xargs echo -n)

  local TESTDOMAIN="${DOMAIN}"

  while [[ -n "$TESTDOMAIN" ]]; do
    for zone in $ZONELIST; do
      if [[ "$zone" == "$TESTDOMAIN" ]]; then
        echo "$zone"
        return 0
      fi
    done
    TESTDOMAIN=$(get_base_name "$TESTDOMAIN")
  done

  return 1
}

#
# This hook is called at the end of a dehydrated command and can be used
# to do some final (cleanup or other) tasks.
#
exit_hook() {
  :
}

HANDLER="$1"; shift
if [[ "${HANDLER}" =~ ^(deploy_challenge|clean_challenge|deploy_cert|unchanged_cert|invalid_challenge|request_failure|exit_hook)$ ]]; then
  "$HANDLER" "$@"
fi

さくらのクラウドAPIキーを環境変数を設定

さくらのクラウドのコントロールパネルからAPIキーを作成し、SAKURACLOUD_ACCESS_TOKENSAKURACLOUD_ACCESS_TOKEN_SECRET環境変数に設定します。

export SAKURACLOUD_ACCESS_TOKEN=xxx
export SAKURACLOUD_ACCESS_TOKEN_SECRET=yyy

sacloudns は作業ディレクトリに、.env ファイルを作成し、

SAKURACLOUD_ACCESS_TOKEN=xxx
SAKURACLOUD_ACCESS_TOKEN_SECRET=yyy

のように記すと、コマンド実行時に自動で読み込むこともできます。

dehydrated の実行

準備ができたのでdehydratedを実行

# 初回はアカウントの作成が必要
$ ./dehydrated --register --accept-terms -f config 
$ ./dehydrated -c -f config 

これで、DNSレコードの作成と検証が行われたのち、証明書が certs ディレクトリ以下に作成されます。

% ls -l certs/chocon.me 
cert-1612763291.csr
cert-1612763291.pem
cert.csr -> cert-1612763291.csr
cert.pem -> cert-1612763291.pem
chain-1612763291.pem
chain.pem -> chain-1612763291.pem
fullchain-1612763291.pem
fullchain.pem -> fullchain-1612763291.
privkey-1612763291.pem
privkey.pem -> privkey-1612763291.pem

証明書は nginx や Apache などのWebサーバや各クラウドにアップロードして使うこともできます。

おまけ: さくらのクラウド エンハンスドロードバランサーへの証明書アップロード

取得した証明書をさくらのクラウドのエンハンスドロードバランサーに設定してみます。

エンハンスドロードバランサーとは大規模なHTTP/HTTPSサービスに最適な高性能・高機能なロードバランサアプライアンスです。詳しくはこちら

manual.sakura.ad.jp

こちらも参考になります。

qiita.com

エンハンスドロードバランサーの構築が済んでいるといるとして、以下のようにすると証明書が登録できます。

$ cat template.jq
{
  "ProxyLB": {
    "PrimaryCert": {
      "ServerCertificate": $ServerCertificate,
      "IntermediateCertificate": $IntermediateCertificate,
      "PrivateKey": $PrivateKey
    },
    "AdditionalCerts": []
  }
}
$ jq -n \
-f template.jq 
--rawfile ServerCertificate certs/chocon.me/cert.pem \
--rawfile IntermediateCertificate certs/chocon.me/chain.pem \
--rawfile PrivateKey certs/chocon.me/privkey.pem \
| \
curl -d @- -X PUT \
-H "Content-Type: application/json" \
--user $SAKURACLOUD_ACCESS_TOKEN:$SAKURACLOUD_ACCESS_TOKEN_SECRET \
https://secure.sakura.ad.jp/cloud/zone/is1a/api/cloud/1.1/commonserviceitem/${リソース番号}/proxylb/sslcertificate

jqのテンプレート機能と、rawfileオプションとても便利ですね!

なお、この方法で証明書を登録した場合は、let's encryptでの証明書取得・更新機能は停止しておく必要があります。

エンハンスドロードバランサーのlet's encryptを利用した証明書取得では、1つのロードバランサーあたり、1個のドメインの証明書しか作れませんが、この方法だとそれよりも多くの証明書を登録できるので、たくさんのドメインを利用される場合は便利かもしれません。

最後に、ここまでの方法を一つのシェルスクリプトにまとめるなど自動化しておくのがいいでしょう。

何かのお役に立てれば幸いです。

さくらインターネット株式会社に入社しました

あっという間に1週間経ってしまいましたが、2021年1月18日より、さくらインターネットにてお仕事をしております。

前職中は本当にたくさんの皆様にお世話になりました。ありがとうございます。挨拶もほとんど出ておらず申し訳ありません。

さくらインターネットは20年ほどユーザとして使っており、またmixiやメルカリでもサーバを利用しており、公私共に馴染みのある会社です。

2018年の北海道胆振東部地震の際には対応はもちろん、情報がまとまってない中で直接お話を聞く機会をいただいたり、逐次DMなどで情報をいただくなど大変お世話になったのは強く印象に残っています。そのほか、ISUCON9でも問題の出題を一緒にやらせていただいたり、サイボウズ様、jig.jp様との共催で行われた小学生向けのIoTプログラム教室などにも息子とともに参加させていただきました。このためかさくらインターネットに転職をすることを息子に伝えると、ニンマリと喜んでおりました。

これまでのキャリアではWebサービスのパフォーマンス改善、運用などに携わっていたわけですが、さくらインターネットは少し異なるレイヤーへの挑戦になります。今までの経験を生かしつつ、新しいチャレンジも行い、さくらインターネットのお客様とそのお客様にとってよりよいサービスを提供していけるようやっていきます。

もし、さくらインターネットの利用に関わらずWebサービスのパフォーマンスや運用での悩みなどがありましたら、力になれることがあるかと思いますのでtwitterなどで相談くださいませ。

COVID-19の緊急事態宣言が出ているなか、初日からリモートとなかなか難しい船出になっておりますが、素晴らしい同僚の力を借りつつ、一つ一つやりはじめております。今後ともよろしくお願いします。