IPv6 only環境でcertbotをpipインストールする (Let's EncryptでSSL証明書を自動更新する)

Let's EncryptでSSL証明書の更新を自動で行いたいため、Rocky Linux 8のIPv6 only環境でcertbotをpipインストールしたときのメモを残します。

TL;DR

公式のsnapからインストールする手順だとIPv6サーバがなく途中で躓いてしまったため、snap経由ではなくpipコマンドでcertbotをインストールしました。 一日一回の自動実行はcrontabではなくsystemctlでcertbot.timerを設定しました。 SELinuxのポリシー設定のあるサーバでnginxの設定に躓いたので回避策を書きました。

  1. Rocky Linux 8 (CentOS8互換ディストリビューション)
  2. snapのサーバがIPv6に対応していない
  3. 仕方なくpipでcertbotをインストールする
  4. certbotを試しに実行してみる
  5. systemctlでcertbot.timerを設定する
  6. SELinuxを無効化せずにnginxを動かす
  7. nginxのVirtualHost設定ファイル例

1. Rocky Linux 8 (CentOS8互換ディストリビューション)

今回はひとまず Rocky Linux 8 (CentOS8互換ディストリビューション) でcertbotを動かしてみます。何も考えずにsnapを使いたい場合はUbuntuを使うのが一番良いです。ただ、簡単インストールできてしまうことの裏返しとして、プログラムの動作構成をあまりよく知らずに使えてしまうので、あえて茨の道を渡ってみるのも良いと思います。ちなみにApline Linuxディストリビューションのシステム構成の問題でそもそもsnapはインストールできません(対応できておらずサポートされていません)。今回試すsnapを利用しないpipからのインストール手順であれば、Apline Linuxでもcertbotを動かすことができると思います。

2. snapのサーバがIPv6対応していない

Rocky Linux 8の環境でcertbotをインストールするために、まず公式で推奨されているsnapをインストールしようとするのですが、、、

sudo dnf install -y epel-release
sudo dnf install -y snapd
sudo systemctl enable --now snapd.socket
sudo ln -s /var/lib/snapd/snap /snap
sudo snap install core

上記コマンドをIPv6 only環境で実行すると、最後のsnapコマンド実行時に下記のようなエラーが発生してしまいます。

$ sudo snap install core
error: cannot install "core": persistent network error: Post
       "https://api.snapcraft.io/v2/snaps/refresh": dial tcp 185.125.188.58:443: connect: network
       is unreachable

これはsnapのパッケージ管理サーバ api.snapcraft.io がAAAAレコードを持たず、サーバとIPv6通信ができないことが原因です。

この件は、2017年からBug #1710022 “Snap store APIs (api.snapcraft.io) are not reachab...” : Bugs : Snap Store Serverというタイトルでバグ報告がされていますが、2022年になってもまだ何も進展がなく、Snap Store ServerにIPv6対応のAAAAレコードが追加されるような気配がありません…。

3. 仕方なくpipでcertbotをインストールする

snapはあきらめて、certbotの最新版をPython3.9のpip経由でインストールすることにします。前準備としてpip3コマンドをインストールします。

sudo dnf -y install python39-pip

pip自体のバージョンを pip3 install --upgrade pip コマンドを実行して最新版にバージョンアップします。

$ pip3 --version
pip 20.2.4 from /usr/lib/python3.9/site-packages/pip (python 3.9)
$ sudo pip3 install --upgrade pip
WARNING: Running pip install with root privileges is generally not a good idea. Try `pip3 install --user` instead.
Collecting pip
  Downloading pip-22.0.4-py3-none-any.whl (2.1 MB)
Installing collected packages: pip
  WARNING: The scripts pip, pip3 and pip3.9 are installed in '/usr/local/bin' which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
Successfully installed pip-22.0.4
$ pip3 --version
pip 22.0.4 from /usr/local/lib/python3.9/site-packages/pip (python 3.9)

certbotの動作に必要な依存ライブラリを予めOSパッケージからインストールしておきます。

sudo dnf -y install python39-devel python39-cffi python39-requests python39-urllib3
sudo dnf -y install augeas-libs libffi-devel openssl-libs

certbotをpip経由でインストールします。nginxとapacheプラグインを同時にインストールしています。

sudo pip3 install certbot certbot-nginx certbot-apache

/usr/local/binにcertbotがインストールされるため、/usr/binにシンボリックリンクを貼っておきます。

sudo ln -s /usr/local/bin/certbot /usr/bin/certbot

4. certbotを試しに実行してみる

インストールされたcertbotのバージョンを確認します。

$ certbot --version
certbot 1.27.0

--dry-runオプションを指定して、試しにcertbot renewを実行してみます。

$ sudo certbot renew --dry-run
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
No simulated renewals were attempted.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

何も前回の設定がなければ、certbotコマンドが無事動いているようなので、ひとまずこれでokです。おめでとうございます。

5. systemctlでcertbot.timerを設定する

certbot renewalを実行するサービスの定義certbot.serviceを作成します。ExecStartの行で実行するcertbot renewのコマンドを指定するのですが、--post-hookの引数をつけることで、Webサーバの設定を再起動してSSL証明書を再読み込みするようにしています。

cat<<EOF>/etc/systemd/system/certbot.service
[Unit]
Description=Let's Encrypt certbot renewal

[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet --agree-tos --post-hook "systemctl reload nginx.service"
EOF

ここではcrontabではなく、certbot.timerを定義して一日一回dailyで定時実行されるようにします。

cat<<EOF>/etc/systemd/system/certbot.timer
[Unit]
Description=Let's Encrypt certbot renewal (daily)

[Timer]
OnCalendar=0/12:00:00
RandomizedDelaySec=1h
Persistent=true

[Install]
WantedBy=timers.target
EOF

このファイルの中でRandomizedDelaySec=1hを指定することで実行時間を1時間の誤差をわざと設けて、全世界で同じ時刻にLet's Encryptのサーバにアクセスして過負荷になることを防いでおきます。このテクニックはArch Linuxcertbot設定ファイルを参考にしました。

systemctlコマンドでcertbot.timerを起動して、再起動時でも有効になるように設定します。

sudo systemctl start certbot.timer
sudo systemctl enable certbot.timer

ファイルの内容を書き換えた場合は、systemctl daemon-reloadコマンドを実行して設定ファイルを再読み込みします。 そして、systemctl list-timersコマンドを実行して、次回起動される.serviceコマンドの一覧を出力します。

sudo systemctl daemon-reload
sudo systemctl list-timers

実行結果の中にcertbot.timer certbot.serviceの行があればokです。

$ sudo systemctl list-timers
NEXT                         LEFT          LAST                         PASSED    UNIT                         ACTIVATES
Sat 2022-05-07 08:09:58 UTC  32min left    Sat 2022-05-07 07:06:44 UTC  30min ago dnf-makecache.timer          dnf-makecache.service
Sat 2022-05-07 11:13:44 UTC  3h 36min left Fri 2022-05-06 11:13:44 UTC  20h ago   systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service
Sat 2022-05-07 12:19:07 UTC  4h 41min left Sat 2022-05-07 07:37:05 UTC  11s ago   certbot.timer                certbot.service
Sun 2022-05-08 00:00:00 UTC  16h left      Sat 2022-05-07 00:00:44 UTC  7h ago    unbound-anchor.timer         unbound-anchor.service

4 timers listed.
Pass --all to see loaded but inactive timers, too.

もしも、certbotが勝手に自動起動されると困る場合は、以下のコマンドを実行して、certbot.timerを停止して無効にします。

sudo systemctl stop certbot.timer
sudo systemctl disable certbot.timer

確認のためにsystemctl list-timers --allを実行して、certbot.timerの行が n/a (not available) になっていることを確認します。

$ sudo systemctl list-timers --all
NEXT                         LEFT          LAST                         PASSED      UNIT                         ACTIVATES
Sat 2022-05-07 08:09:58 UTC  29min left    Sat 2022-05-07 07:06:44 UTC  33min ago   dnf-makecache.timer          dnf-makecache.service
Sat 2022-05-07 11:13:44 UTC  3h 33min left Fri 2022-05-06 11:13:44 UTC  20h ago     systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service
Sun 2022-05-08 00:00:00 UTC  16h left      Sat 2022-05-07 00:00:44 UTC  7h ago      unbound-anchor.timer         unbound-anchor.service
n/a                          n/a           Sat 2022-05-07 07:37:05 UTC  3min 8s ago certbot.timer                certbot.service

4 timers listed.

6. SELinuxを無効化せずにnginxを動かす

まだnginxをインストールしていない場合はOSパッケージからインストールします。

sudo dnf -y install nginx

nginxのサービスを起動して、再起動時にも自動起動を有効にします。

$ sudo systemctl start nginx
$ sudo systemctl enable nginx
Created symlink /etc/systemd/system/multi-user.target.wants/nginx.service → /usr/lib/systemd/system/nginx.service.

certbot certonly --nginxで証明書を取得します。(certonlyオプションを省略するとnginxの設定ファイルを書き換えてくれます)

$ sudo certbot certonly --nginx
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Enter email address (used for urgent renewal and security notices)
 (Enter 'c' to cancel): 自分のメールアドレスを入力する

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must
agree in order to register with the ACME server. Do you agree?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: 規約を読んで同意するならyを入力する

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing, once your first certificate is successfully issued, to
share your email address with the Electronic Frontier Foundation, a founding
partner of the Let's Encrypt project and the non-profit organization that
develops Certbot? We'd like to send you email about our work encrypting the web,
EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: 特にメールで案内が欲しくなければnを入力する
Account registered.
Please enter the domain name(s) you would like on your certificate (comma and/or
space separated) (Enter 'c' to cancel): 自分のサーバのFQDNを入力する(例:ssl.example.com)
Requesting a certificate for ssl.example.com

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/ssl.example.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/ssl.example.com/privkey.pem
This certificate expires on 2022-08-07.
These files will be updated when the certificate renews.

NEXT STEPS:
- The certificate will need to be renewed before it expires. Certbot can automatically renew the certificate in the background, but you may need to take steps to enable that functionality. See https://certbot.org/renewal-setup for instructions.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If you like Certbot, please consider supporting our work by:
 * Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
 * Donating to EFF:                    https://eff.org/donate-le
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

もしも以下のようなエラーが出る場合は途中のacme-challengeに失敗してしまっています。nginxがうまく動いていないようです。

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/ssl.example.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Simulating renewal of an existing certificate for ssl.example.com

Certbot failed to authenticate some domains (authenticator: nginx). The Certificate Authority reported these problems:
  Domain: ssl.example.com
  Type:   unauthorized
  Detail: XXXX:XXXX:100:XXX:XX:cafe:4e:1: Invalid response from https://ssl.example.com/.well-known/acme-challenge/hlbtXXXXeDKRQI9f0lNFBSLY6v4PyRfwoFY8GwOHAKo: 404

Hint: The Certificate Authority failed to download the challenge files from the temporary standalone webserver started by Certbot on port 80. Ensure that the listed domains point to this machine and that it can accept inbound connections from the internet.

Failed to renew certificate ssl.example.com with error: Some challenges have failed.

ファイルの権限設定が正しいのにnginxでエラーが発生してしまう場合(webのrootディレクトリも読み取れない場合)は、getenforceコマンドを実行してSELinuxの設定の有無を確認してみましょう。

$ getenforce
Disabled

もしもDisabledだったら、SELinuxは無効になっているので、ファイルの権限設定を見直して、Webのルートディレクトリがnginxの動作権限で読み込みができるかどうか、上位のディレクトリも含めて再確認してください。

$ getenforce
Enforcing

もしもEnforcingだったら、SELinuxが有効になっていて監査にひっかかっているので、適当な作業ディレクトリでSELinuxのポリシーでどこにひっかかっているかを確認します。

$ sudo grep nginx /var/log/audit/audit.log | audit2allow -m nginx
module nginx 1.0;

require {
        type var_t;
        type httpd_t;
        class file { getattr read };
}

#============= httpd_t ==============
allow httpd_t var_t:file { getattr read };

httpd_tコンテキスト(Apacheやnginxの動作権限)に対して、var_tファイルの読み取り権限がないようなので、許可するポリシーファイルを作成します。

$ sudo grep nginx /var/log/audit/audit.log | audit2allow -M nginx
******************** IMPORTANT ***********************
To make this policy package active, execute:

sudo semodule -i nginx.pp

少し時間がかかりますが、作成したポリシーファイルnginx.ppを適用します。

$ sudo semodule -i nginx.pp

もう一つ別の方法として、ポリシーを変更せずに、/var/www/html以下のファイルに対してrestoreconコマンドを実行してhttpd_sys_content_tのラベルを追加してあげるという方法もあります。ファイルのラベルを確認するにはls -Zオプションで、ファイルのSELinuxコンテキストを変更するには、chcon、semanage fcontext、restoreconコマンドが使えます。

$ cd /var/www/html
$ ls -Z
unconfined_u:object_r:var_t:s0 index.html
$ sudo restorecon *.html
$ ls -Z
unconfined_u:object_r:httpd_sys_content_t:s0 index.html

これでもうまくいかない場合は、セキュリティは弱くなりますが、httpd_tコンテキストに対してpermissiveにして全許可します。

$ sudo semanage permissive -a httpd_t

それでもうまくいかない場合は、SELinuxをオフにする、というのが長らくの定説ですが、ちょっと悲しいですね。

$ setenforce 0
$ getenforce
Permissive

一時的にSELinuxを無効化して、SELinuxが原因かどうかを確認して、サーバ内外のファイアウォールTCP/80,TCP/443が外から通信できないとか、どうやらSELinuxは冤罪で別のことが原因でありそうであれば元通りに設定を戻しておいてあげます。

$ setenforce 1
$ getenforce
Enforcing

以下のURLにSELinux上でnginxを動かす方法が載っていますので、SELinuxのポリシー設定は一度慣れておくと良いでしょう。

7. nginxのVirtualHost設定ファイル例

ちなみに、私が使っているnginxのVirtualHost設定ファイル例を以下に掲載します。IPv6 onlyの設定になっています。 シェル変数DOMAINに設定したいVirtualHostのFQDNを指定してVirtualHost毎に.confファイルを作成しています。

DOMAIN="ssl.example.com"
cat<<EOF| sudo tee /etc/nginx/conf.d/$DOMAIN.conf
map \$http_upgrade \$connection_upgrade {
    default upgrade;
    '' close;
}
server {
    ## listen 80; # IPv4
    listen [::]:80; # IPv6
    server_name $DOMAIN;
    return 301 https://\$host\$request_uri;
}
server {
    ## listen 443 ssl http2; # IPv4
    listen [::]:443 ssl http2; # IPv6
    server_name $DOMAIN;
    access_log /var/log/nginx/$DOMAIN.access.log;
    location / {
      root /var/www/$DOMAIN;
      index index.html;
    }
    ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem; # managed by Certbot

    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
EOF

複数の.confファイルを作成することによって、SSL証明書FQDN個別に生成して1台のnginxサーバで複数のVirtualHostを運用できます。

nginxの設定ファイルの文法チェックを-tコマンドで実施した後、systemctlで設定ファイルを再読み込みします。

sudo nginx -t
sudo systemctl reload nginx

エラーが出なければokです。それでは、良いLet's Encryptライフをお過ごしください!

参考文献

IPv6 only 環境で Docker / Alpine Linux を動かす

2022年5月、IPv4アドレスが枯渇してきていることもあって、IPv6 onlyな環境がVPSの一般サービスとして出てきました。 OSレベルではIPv4/IPv6のデュアルスタックで両方のネットワーク環境にサーバもクライアントも対応しつつあるのですが、 世の中にはまだAAAAレコードを持たないHTTP/HTTPSサーバも結構あって、DockerをIPv6 only環境で動かすのに一苦労しました。

今回、私自身が Rocky Linux 8.5 の IPv6 only 環境で Docker / Alpine Linux を動かした記録をメモで残しておきます。

Dockerのインストール

まず最初にdnfコマンドを使ってDockerをインストールします。

sudo dnf update -y
sudo dnf install -y dnf-utils device-mapper-persistent-data lvm2
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf makecache
sudo dnf install -y docker-ce
sudo systemctl start docker
sudo systemctl enable docker

(最近のCentOSLinuxではyumは推奨されていないので、dnfコマンドを使用しました。)

Dockerを動かすユーザの権限設定

root以外でdockerを動かす場合、自分自身のユーザ($USER)をdockerグループに所属させておきます。

sudo groupadd docker
# groupadd: group 'docker' already exists
sudo usermod -aG docker $USER

IPv6対応のdaemon.jsonを作成

dockerデーモンの起動オプション/etc/docker/daemon.jsonIPv6対応の設定を追加します。

sudo vi /etc/docker/daemon.json

作成するファイルの中身は以下の通りです。

{
  "ipv6": true,
  "fixed-cidr-v6": "fd11:2233:4455:6677::/64",
  "registry-mirrors": ["https://mirror.gcr.io"]
}

Dockerのデフォルトのレジストリregistry-1.docker.ioIPv4のアドレスしか返さないのでIPv6 only環境だと通信できません。この場合、IPv6に対応しているGoogleレジストリミラー https://mirror.gcr.io を設定しておきます。 fixed-cidr-v6で設定しているfd11:2233:4455:6677::/64は、自分で設定するユニークローカルアドレス (ULA: unique local address) で、IPv6のプライベートアドレスに相当するものです。自分で使う場合は、fdから始まるアドレスで任意のアドレスを指定してください。

dockerの再起動

これらの設定を有効にするにはdockerを再起動します。

sudo systemctl restart docker

もしもエラーが出た場合はdaemon.jsonの設定ファイルを見直して修正します。

ip6tablesでNATを追加

daemon.jsonのfixed-cidr-v6で設定した内部ネットワークfd11:2233:4455:6677::/64から外に通信ができるようにNATを設定します。

sudo ip6tables -t nat -A POSTROUTING -s fd11:2233:4455:6677::/64 ! -o docker0 -j MASQUERADE

引数に-t natを指定してNATテーブルが正しく追加されているかどうか確認します。

sudo ip6tables -t nat -L

以下の出力にMASQUERADE all fd11:2233:4455:6677::/64 anywhereの行があれば大丈夫です。

Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all      fd11:2233:4455:6677::/64  anywhere

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

再起動時のrc.localの設定

再起動してもNATの設定が有効となるように、rc.localの設定をします。

sudo vi /etc/rc.d/rc.local

rc.localは過去の互換性のために残されているファイルですが、最後に以下の一行を追加します。

#!/bin/bash
# THIS FILE IS ADDED FOR COMPATIBILITY PURPOSES
#
# It is highly advisable to create own systemd services or udev rules
# to run scripts during boot instead of using this file.
#
# In contrast to previous versions due to parallel execution during boot
# this script will NOT be run after all other services.
#
# Please note that you must run 'chmod +x /etc/rc.d/rc.local' to ensure
# that this script will be executed during boot.

touch /var/lock/subsys/local

ip6tables -t nat -A POSTROUTING -s fd11:2233:4455:6677::/64 ! -o docker0 -j MASQUERADE

rc.localの設定が初めてであれば、以下のコマンドを実行して再起動時に実行されるようにします。

sudo chmod +x /etc/rc.d/rc.local
sudo systemctl start rc-local

docker infoで現在の設定を確認

docker infoを実行して現在の設定を確認します。

docker info

こんな感じの出力がされるはずです。

Client:
 Context:    default
 Debug Mode: false
 Plugins:
  app: Docker App (Docker Inc., v0.9.1-beta3)
  buildx: Docker Buildx (Docker Inc., v0.8.1-docker)
  scan: Docker Scan (Docker Inc., v0.17.0)

Server:
~中略~
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Registry: https://index.docker.io/v1/
 Labels:
 Experimental: false
 Insecure Registries:
  127.0.0.0/8
 Registry Mirrors:
  https://mirror.gcr.io/
 Live Restore Enabled: false

Registry Mirrors:にhttps://mirror.gcr.io/があればokです。

Alpine Linux の Dockerfile を作成

適当なディレクトリでDockerfileを作成します。

vi Dockerfile

Dockerfileの中身は以下の通りです。

FROM alpine:3.15.4
RUN apk update && apk add bind-tools curl
CMD ["/bin/sh"]

alpine:3.15.4のイメージをpullした後、digコマンドとcurlコマンドが使いたいのでbind-toolsとcurlのapkパッケージをインストールして、デフォルトでシェルを起動しています。

docker build

タグ名は何でも良いのですが、ipv6/alpineでdocker buildします。

docker build -t ipv6/alpine .

ビルド時に以下のような感じでIPv6のサーバからapkのパッケージがダウンロードできれば成功です。

Sending build context to Docker daemon  3.072kB
Step 1/3 : FROM alpine:3.15.4
3.15.4: Pulling from library/alpine
df9b9388f04a: Pull complete
Digest: sha256:4edbd2beb5f78b1014028f4fbb99f3237d9561100b6881aabbf5acce2c4f9454
Status: Downloaded newer image for alpine:3.15.4
 ---> 0ac33e5f5afa
Step 2/3 : RUN apk update && apk add bind-tools curl
 ---> Running in a1ebb762c17e
fetch https://dl-cdn.alpinelinux.org/alpine/v3.15/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.15/community/x86_64/APKINDEX.tar.gz
v3.15.4-78-ge239b26b75 [https://dl-cdn.alpinelinux.org/alpine/v3.15/main]
v3.15.4-79-gaf675e239c [https://dl-cdn.alpinelinux.org/alpine/v3.15/community]
OK: 15855 distinct packages available
(1/18) Installing fstrm (0.6.1-r0)
(2/18) Installing krb5-conf (1.0-r2)
(3/18) Installing libcom_err (1.46.4-r0)
(4/18) Installing keyutils-libs (1.6.3-r0)
(5/18) Installing libverto (0.3.2-r0)
(6/18) Installing krb5-libs (1.19.3-r0)
(7/18) Installing json-c (0.15-r1)
(8/18) Installing protobuf-c (1.4.0-r0)
(9/18) Installing libuv (1.42.0-r0)
(10/18) Installing xz-libs (5.2.5-r1)
(11/18) Installing libxml2 (2.9.13-r0)
(12/18) Installing bind-libs (9.16.27-r0)
(13/18) Installing bind-tools (9.16.27-r0)
(14/18) Installing ca-certificates (20211220-r0)
(15/18) Installing brotli-libs (1.0.9-r5)
(16/18) Installing nghttp2-libs (1.46.0-r0)
(17/18) Installing libcurl (7.80.0-r1)
(18/18) Installing curl (7.80.0-r1)
Executing busybox-1.34.1-r5.trigger
Executing ca-certificates-20211220-r0.trigger
OK: 15 MiB in 32 packages
Removing intermediate container a1ebb762c17e
 ---> 18d7e9a3b51b
Step 3/3 : CMD ["/bin/sh"]
 ---> Running in ccda19d240b3
Removing intermediate container ccda19d240b3
 ---> ff8bbe4b67c8
Successfully built ff8bbe4b67c8
Successfully tagged ipv6/alpine:latest

エラーが出なければ成功です。おめでとうございます。

※ registry-1.docker.io でエラーが出る場合

もしもdocker build時に以下のエラーが出る場合は、registry-1.docker.io がIPv6のAAAAレコードを持っていないため、IPv4アドレスで通信しようとして失敗しています。

Sending build context to Docker daemon  4.096kB
Step 1/3 : FROM alpine:3.15.4
Get "https://registry-1.docker.io/v2/": dial tcp 34.203.135.183:443: connect: network is unreachable

この場合は、前述のdaemon.jsonで"registry-mirrors": ["https://mirror.gcr.io"]を指定して、IPv6で通信が可能なミラーサーバを指定する必要があります。 一度、失敗するとdocker内で変にダウンロードURLがキャッシュされてしまうようで、設定を変更したら、FROM alpine:3.15.4をFROM alpine:3.15.2のように別のバージョンをダウンロードして試してみてください。

※ dl-cdn.alpinelinux.org でエラーが出る場合

docker build時に以下のエラーが出る場合は、docker内からIPv6 only環境での外への通信ができないで失敗しています。

fetch https://dl-cdn.alpinelinux.org/alpine/v3.15/main/x86_64/APKINDEX.tar.gz
ERROR: https://dl-cdn.alpinelinux.org/alpine/v3.15/main: temporary error (try again later)
WARNING: Ignoring https://dl-cdn.alpinelinux.org/alpine/v3.15/main: No such file or directory
fetch https://dl-cdn.alpinelinux.org/alpine/v3.15/community/x86_64/APKINDEX.tar.gz
ERROR: https://dl-cdn.alpinelinux.org/alpine/v3.15/community: temporary error (try again later)
WARNING: Ignoring https://dl-cdn.alpinelinux.org/alpine/v3.15/community: No such file or directory

前述のdaemon.jsonで設定した"fixed-cidr-v6": "fd11:2233:4455:6677::/64"で指定した内部ネットワークから外に通信ができないといけないため、ip6tablesのNATの設定が有効になっているかどうかを確認する必要があります。

また、2022年5月3日現在、dl-cdn.alpinelinux.orgがデュアルスタックになってIPv6のAAAAレコードを持っているため、IPv6 only環境でも通信ができるはずです。

$ dig dl-cdn.alpinelinux.org. A
; <<>> DiG 9.11.26-RedHat-9.11.26-6.el8 <<>> dl-cdn.alpinelinux.org. A
~中略~
;; QUESTION SECTION:
;dl-cdn.alpinelinux.org.                IN      A

;; ANSWER SECTION:
dl-cdn.alpinelinux.org. 1420    IN      CNAME   dualstack.d.sni.global.fastly.net.
dualstack.d.sni.global.fastly.net. 30 IN A      151.101.110.133
$ dig dl-cdn.alpinelinux.org. AAAA

; <<>> DiG 9.11.26-RedHat-9.11.26-6.el8 <<>> dl-cdn.alpinelinux.org. AAAA
~中略~
;; QUESTION SECTION:
;dl-cdn.alpinelinux.org.                IN      AAAA

;; ANSWER SECTION:
dl-cdn.alpinelinux.org. 1484    IN      CNAME   dualstack.d.sni.global.fastly.net.
dualstack.d.sni.global.fastly.net. 30 IN AAAA   2a04:4e42:1a::645

もしも、AAAAレコードを返さないサーバだった場合、IPv4アドレスで通信しようとして失敗してしまいますので、この場合は、Dockerfile で RUN sed -r "s;//dl-cdn.alpinelinux.org/alpine;//ftp.udx.icscoe.jp/Linux/alpine;g" -i /etc/apk/repositories を実行するなどして、IPv6で通信が可能なミラーサーバを指定してください。ちなみにAlpine Linuxのオフィシャルのミラーサーバの一覧は https://mirrors.alpinelinux.org/ で確認できます。

docker run

ビルド時に指定したタグ名ipv6/alpineでdocker runします。-iオプションでインタラクティブに対話しながらシェルを実行できます。

docker container run -it ipv6/alpine

無事にシェルに入れたら、curlコマンドでIPv6のサーバとHTTP通信できるかどうか確認します。

/ # cd /tmp
/tmp # curl http://google.com/
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.com/">here</A>.
</BODY></HTML>
/tmp #

301のレスポンスが表示されればIPv6 onlyで疎通できたので成功です。おめでとうございます。

busyboxwgetIPv6対応が不完全

ちなみに、busyboxwgetIPv6対応が完全ではないため、apk add wgetしてwgetコマンドを置き換えておくか、curlコマンドを使うようにしておくと良いでしょう。

takesako (y0sh1) on Twitter: "IPv6 only環境でbusybox wgetで名前解決する際にIPv4アドレスを優先してしまってダウンロードできない問題:ラッパー関数xhost2sockaddrの中でstr2sockaddrに(sa_family_t)AF_UNSPECを指定してしまっていて、getaddrinfoで複数のIPアドレス(v4/v6)が返ってきたときに最初のIPをそのまま使ってしまう罠."

IPv6 only環境でインターネットがもっと使えるようになると嬉しいですね。

参考文献

回避策:日本語ユーザー名で vagrant up すると incompatible character encodings: Windows-31J and UTF-8 (Encoding::CompatibilityError)

Windowsの日本語ユーザー名でログインした状態で vagrant up を実行すると incompatible character encodings: Windows-31J and UTF-8 (Encoding::CompatibilityError) で止まってしまう件について、vagrantスクリプトの中身を見て、いろいろ試した結果、回避策が見つかりました。Googleで検索すると同じような問題で結構困っている人が多そうなので、記事にして公開します。

前提条件

  • Windowsで日本語ユーザー名を使用してログインしている(例:山田太郎など漢字を使っている)

Vagrantfile の中身

  • C:\Users\日本語利用者\vagrant\Vagrantfile
# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "generic/alpine315"
end

vagrant up のエラー内容

Windowsの日本語ユーザー名でログインした状態で vagrant up を実行すると incompatible character encodings: Windows-31J and UTF-8 (Encoding::CompatibilityError) で止まってしまう。

C:\Users\日本語利用者\vagrant>vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Box 'generic/alpine315' could not be found. Attempting to find and install...
    default: Box Provider: virtualbox
    default: Box Version: >= 0
==> default: Loading metadata for box 'generic/alpine315'
    default: URL: https://vagrantcloud.com/generic/alpine315
==> default: Adding box 'generic/alpine315' (v3.6.12) for provider: virtualbox
    default: Downloading: https://vagrantcloud.com/generic/boxes/alpine315/versions/3.6.12/providers/virtualbox.box
    default:
    default: Calculating and comparing box checksum...
==> default: Successfully added box 'generic/alpine315' (v3.6.12) for 'virtualbox'!
==> default: Importing base box 'generic/alpine315'...
Traceback (most recent call last):
        65: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/batch_action.rb:86:in `block (2 levels) in run'
        64: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/machine.rb:201:in `action'
        63: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/machine.rb:201:in `call'
        62: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/environment.rb:614:in `lock'
        61: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/machine.rb:215:in `block in action'
        60: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/machine.rb:246:in `action_raw'
        59: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/runner.rb:89:in `run'
        58: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/util/busy.rb:19:in `busy'
        57: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/runner.rb:89:in `block in run'
        56: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/builder.rb:149:in `call'
        55: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/warden.rb:48:in `call'
        54: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/plugins/providers/virtualbox/action/check_virtualbox.rb:26:in `call'
        53: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/warden.rb:48:in `call'
        52: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/builtin/call.rb:53:in `call'
        51: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/runner.rb:89:in `run'
        50: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/util/busy.rb:19:in `busy'
        49: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/runner.rb:89:in `block in run'
        48: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/builder.rb:149:in `call'
        47: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/warden.rb:48:in `call'
        46: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/warden.rb:127:in `block in finalize_action'
        45: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/warden.rb:48:in `call'
        44: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/builtin/handle_box.rb:56:in `call'
        43: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/warden.rb:48:in `call'
        42: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/warden.rb:127:in `block in finalize_action'
        41: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/warden.rb:48:in `call'
        40: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/builtin/config_validate.rb:25:in `call'
        39: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/warden.rb:48:in `call'
        38: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/builtin/call.rb:53:in `call'
        37: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/runner.rb:89:in `run'
        36: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/util/busy.rb:19:in `busy'
        35: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/runner.rb:89:in `block in run'
        34: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/builder.rb:149:in `call'
        33: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/warden.rb:48:in `call'
        32: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/warden.rb:127:in `block in finalize_action'
        31: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/warden.rb:48:in `call'
        30: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/plugins/providers/virtualbox/action/check_accessible.rb:18:in `call'
        29: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/warden.rb:48:in `call'
        28: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/plugins/providers/virtualbox/action/customize.rb:40:in `call'
        27: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/warden.rb:48:in `call'
        26: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/builtin/prepare_clone.rb:15:in `call'
        25: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/warden.rb:48:in `call'
        24: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/plugins/providers/virtualbox/action/prepare_clone_snapshot.rb:17:in `call'
        23: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/action/warden.rb:48:in `call'
        22: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/plugins/providers/virtualbox/action/import.rb:13:in `call'
        21: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/plugins/providers/virtualbox/action/import.rb:55:in `import'
        20: from C:/HashiCorp/Vagrant/embedded/mingw64/lib/ruby/2.7.0/forwardable.rb:235:in `import'
        19: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/plugins/providers/virtualbox/driver/version_6_0.rb:71:in `import'
        18: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/plugins/providers/virtualbox/driver/base.rb:398:in `execute'
        17: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/util/retryable.rb:17:in `retryable'
        16: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/plugins/providers/virtualbox/driver/base.rb:403:in `block in execute'
        15: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/plugins/providers/virtualbox/driver/base.rb:465:in `raw'
        14: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/util/busy.rb:19:in `busy'
        13: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/plugins/providers/virtualbox/driver/base.rb:466:in `block in raw'
        12: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/util/subprocess.rb:22:in `execute'
        11: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/util/subprocess.rb:154:in `execute'
        10: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/util/safe_chdir.rb:24:in `safe_chdir'
         9: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/util/safe_chdir.rb:24:in `synchronize'
         8: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/util/safe_chdir.rb:25:in `block in safe_chdir'
         7: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/util/safe_chdir.rb:25:in `chdir'
         6: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/util/safe_chdir.rb:26:in `block (2 levels) in safe_chdir'
         5: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/vagrant-2.2.19/lib/vagrant/util/subprocess.rb:155:in `block in execute'
         4: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/childprocess-4.1.0/lib/childprocess/abstract_process.rb:81:in `start'
         3: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/childprocess-4.1.0/lib/childprocess/windows/process.rb:70:in `launch_process'
         2: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/childprocess-4.1.0/lib/childprocess/windows/process_builder.rb:27:in `start'
         1: from C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/childprocess-4.1.0/lib/childprocess/windows/process_builder.rb:48:in `create_command_pointer'
C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/childprocess-4.1.0/lib/childprocess/windows/process_builder.rb:48:in `join': incompatible character encodings: Windows-31J and UTF-8 (Encoding::CompatibilityError)
C:\Users\日本語利用者\vagrant>

原因

vagrantで利用されている、いくつかのrubyスクリプトで、正しく文字エンコーディングを扱えていないため、エラーが発生してしまう。

  1. ファイルパスに日本語が含まれている「C:\Users\日本語利用者\vagrant\Vagrantfile」
  2. vagrant のデフォルトのホームディレクトリが「C:\Users\日本語利用者.vagrant.d」
  3. VirtualBox のデフォルトの仮想マシンフォルダーが「C:\Users\日本語利用者\VirtualBox VMs」

ややこしいのは、1.は気づきやすいけど、2.と3.は裏の動作で見えにくいので気づきにくいという罠。

回避策(1)

以下の環境変数を設定してから vagrant up する。

set RUBYOPT=-EUTF-8:Windows-31J
set VAGRANT_HOME=C:/HashiCorp/.vagrant.d

実行結果(1)

C:\Users\日本語利用者\vagrant>set RUBYOPT=-EUTF-8:Windows-31J
C:\Users\日本語利用者\vagrant>set VAGRANT_HOME=C:/HashiCorp/.vagrant.d
C:\Users\日本語利用者\vagrant>vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Box 'generic/alpine315' could not be found. Attempting to find and install...
    default: Box Provider: virtualbox
    default: Box Version: >= 0
==> default: Loading metadata for box 'generic/alpine315'
    default: URL: https://vagrantcloud.com/generic/alpine315
==> default: Adding box 'generic/alpine315' (v3.6.12) for provider: virtualbox
    default: Downloading: https://vagrantcloud.com/generic/boxes/alpine315/versions/3.6.12/providers/virtualbox.box
    default:
    default: Calculating and comparing box checksum...
==> default: Successfully added box 'generic/alpine315' (v3.6.12) for 'virtualbox'!
==> default: Importing base box 'generic/alpine315'...
==> default: Matching MAC address for NAT networking...
==> default: Checking if box 'generic/alpine315' version '3.6.12' is up to date...
==> default: Setting the name of the VM: vagrant_default_1650718925475_52597
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
    default: Adapter 1: nat
==> default: Forwarding ports...
    default: 22 (guest) => 2222 (host) (adapter 1)
==> default: Running 'pre-boot' VM customizations...
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
    default: SSH address: 127.0.0.1:2222
    default: SSH username: vagrant
    default: SSH auth method: private key
    default:
    default: Vagrant insecure key detected. Vagrant will automatically replace
    default: this with a newly generated keypair for better security.
    default:
    default: Inserting generated public key within guest...
    default: Removing insecure key from the guest if it's present...
    default: Key inserted! Disconnecting and reconnecting using new SSH key...
==> default: Machine booted and ready!
==> default: Checking for guest additions in VM...
C:\Users\日本語利用者\vagrant>

回避策(2)

日本語のユーザー名ではなく、英数字のユーザー名で(なければローカルアカウントを作成して)ログインし直して vagrant up を実行する。

  • Windows10でローカルアカウントを作成する方法がわかりにくいという別の罠が…
  • 設定→アカウント→家族とそのほかのユーザー→その他のユーザーをこのPCに追加
  • このユーザーはどのようにサインインしますか?→このユーザーのサインイン情報がありません
  • Microsoft アカウントを持たないユーザーを追加する(これでローカルアカウントを作成できる)
  • このPCを使うのはだれですか?→ユーザー名に英数字を入力してアカウントを作成する

回避策(3)

vagrantで正しく文字エンコーディングを扱えるようにパッチを送る。これができれば終止符。

参考文献

Alpine Linuxの自前パッケージをビルドする方法

この記事はRecruit Advent Calendar 2021 - Adventarの24日目(12/24)のエントリーです。 adventar.org

1. Alpine Linuxとは?

Alpine LinuxはDockerイメージ作成でも良く使われるLinuxディストリビューションの一つで、組み込み用途で使われていたbusyboxを標準で利用し、豪華な構文を持ったbashではなくシンプルなash、機能の多いglibcではなく簡素なmusl-libcを採用していて、トータルのバイナリサイズがとても小さいという特徴があります。Alpine Linuxでは、apkコマンド(Debian系だとapt、RedHat系だとyumに相当)を利用してパッケージインストールができるのですが、この記事では自前のapkパッケージをビルドする方法について解説します。

2. ユーザの作成

Alpine Linux上でパッケージをビルドするユーザtakesakoを作成し(このユーザ名は自分の名前に変えてください)、abuildグループに所属させます。この作業はrootで行います。

adduser takesako
addgroup takesako abuild

標準ではsudoパッケージがインストールされていないので、apkコマンドを利用してパッケージをインストールし、visudoコマンドで/etc/sudoersを編集してwheelグループに権限を付与します。

apk add sudo
visudo
addgroup takesako wheel

もしもあなたが(emacs派、nano派など)宗教上の理由でvisudoコマンドを使いたくない場合は、以下のように設定しても大丈夫です。

echo "takesako ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/takesako
chmod 440 /etc/sudoers.d/takesako

ちなみに、Alpine Linuxでは設定が豊富なsudoではなく、OpenBSDで開発が進められていたdoasコマンドも利用できますので、そちらを使うのも良いでしょう。

3. alpine-sdkのインストール

apkパッケージのビルドに必要なalpine-sdkパッケージをインストールします。

apk add alpine-sdk

これは中身のないメタパッケージとなっており、依存関係のあるパッケージ群が自動でインストールされます。

alpine-sdkのAPKBUILDファイル(~/aports/main/alpine-sdk/APKBUILD)は以下の通りです。

# Maintainer: Natanael Copa <ncopa@alpinelinux.org>
pkgname=alpine-sdk
pkgver=1.0
pkgrel=1
url="https://git.alpinelinux.org/"
pkgdesc="Alpine Software Development Kit meta package"
depends="abuild build-base git"
arch="noarch"
license="GPL-2.0"

build() {
        # meta package
        return 0
}

package() {
        mkdir -p "$pkgdir"
}

APKBUILDのリファレンスはhttps://wiki.alpinelinux.org/wiki/APKBUILD_Referenceにありますが、 depends="abuild build-base git"という個所で必要な他のパッケージの依存関係を指定していて、apk addしたときに依存関係のあるabuildパッケージと、build-baseパッケージと、gitパッケージが足りなければ自動でインストールされます。

4. 自分の名前とメールアドレスの設定

パッケージをビルドするユーザでログインして、git configで自分の名前とメールアドレスを設定します。

login takesako
git config --global user.name "Your Full Name"
git config --global user.email "your@email.address"

他に/etc/abuild.confファイルにも自分の名前とメールアドレスを記載する場所があるので、

sudo vi /etc/abuild.conf

コマンドを実行して、以下を書き換えます。

PACKAGER="Your Full Name <your@email.address>"
MAINTAINER="$PACKAGER"

5. キャッシュディレクトリの権限追加

ダウンロードしたソースコードを置くキャッシュディレクトリとして/var/cache/distfilesがあるので、abuildグループ権限で書き込みができるようにしておくと便利です。

sudo mkdir -p /var/cache/distfiles
sudo chgrp abuild /var/cache/distfiles
sudo chmod g+w /var/cache/distfiles

6. 公開鍵と秘密鍵の生成

abuild-keygenというスクリプトが用意されているので、-a(--append)と-i(--install)オプションを指定して公開鍵と秘密鍵を生成します。

abuild-keygen -a -i

鍵の保存場所を聞かれますが、そのままエンターキーを押してデフォルトの場所のままにしておいても良いでしょう。

>>> Generating public/private rsa key pair for abuild
Enter file in which to save the key [/home/takesako/.abuild/your@email.address-1a2b3c4d.rsa]:
Generating RSA private key, 2048 bit long modulus (2 primes)
................+++++
.........................+++++
e is 65537 (0x010001)
writing RSA key
>>> Installing /home/takesako/.abuild/your@email.address-1a2b3c4d.rsa.pub to /etc/apk/keys...
>>>
>>> Please remember to make a safe backup of your private key:
>>> /home/takesako/.abuild/your@email.address-1a2b3c4d.rsa
>>>

ここでビルドしたapkファイルを他のマシン上でインストールするには、ここで生成した公開鍵の.rsa.pubファイルを/etc/apk/keys/以下に置く必要があります。(※.pubのない.rsaファイルは秘密鍵です)

7. APKBUILDの雛形作成

APKBUILDファイルの雛形作成には、newapkbuildコマンドを利用すると便利です。

newapkbuild 3.7.0-r0 - generate a new APKBUILD
Usage: newapkbuild [-n PKGNAME] [-d PKGDESC] [-l LICENSE] [-u URL]
       [-a | -C | -m | -p | -y | -r] [-s] [-c] [-f] [-h]
       PKGNAME[-PKGVER] | SRCURL
Options:
  -n  Set package name to PKGNAME (only use with SRCURL)
  -d  Set package description to PKGDESC
  -l  Set package license to LICENSE, use identifiers from:
      <https://spdx.org/licenses/>
  -u  Set package URL
  -a  Create autotools package (use ./configure ...)
  -C  Create CMake package (Assume cmake/ is there)
  -m  Create meson package (Assume meson.build is there)
  -p  Create perl package (Assume Makefile.PL is there)
  -y  Create python package (Assume setup.py is there)
  -r  Crate rust package (Assume Cargo.toml is there)
  -s  Use sourceforge source URL
  -c  Copy a sample init.d, conf.d, and install script
  -f  Force even if directory already exists
  -h  Show this help

たとえば、何もオプションを指定せずに、

newapkbuild aaa

コマンドを実行すると、aaa/APKBUILDに以下のファイルが生成されます。

# Contributor: Yoshinori Takesako <takesako@namazu.org>
# Maintainer: Yoshinori Takesako <takesako@namazu.org>
pkgname=aaa
pkgver=
pkgrel=0
pkgdesc=""
url=""
arch="all"
license=""
depends=""
makedepends=""
install=""
subpackages="$pkgname-dev $pkgname-doc"
source=""
builddir="$srcdir/"

build() {
        # Replace with proper build command(s)
        :
}

check() {
        # Replace with proper check command(s)
        :
}

package() {
        # Replace with proper package command(s)
        :
}

これにビルドに必要な情報を埋めていけば、APKBUILDファイルの完成です。

8. 実用例:cycfx2progパッケージの作成

実用例として、cycfx2progパッケージを作成してみます。cycfx2progはEZ-USBなどのFX2系組み込みデバイスファームウェア書き込みに利用するコマンドです。

mkdir -p test/cycfx2prog
cd test/cycfx2prog
vi APKBUILD

作成するAPKBUILDファイルは以下の通りです。

# Contributor: Yoshinori Takesako <takesako@namazu.org>
# Maintainer: Yoshinori Takesako <takesako@namazu.org>
pkgname=cycfx2prog
pkgver=0.47
pkgrel=0
pkgdesc="download 8051 program into the FX2 board"
url="https://www.triplespark.net/elec/periph/USB-FX2/software/"
arch="all"
license="GPL2"
depends="libusb-compat"
makedepends="libusb-compat-dev"
install=""
subpackages=""
source="https://www.triplespark.net/elec/periph/USB-FX2/software/$pkgname-$pkgver.tar.gz
        Makefile.patch
        "
builddir="$srcdir/$pkgname-$pkgver"

build() {
        cd "$builddir"
        make
}

check() {
        return 0
}

package() {
        cd "$builddir"
        install -D -m 755 cycfx2prog "$pkgdir"/usr/bin/cycfx2prog
}

あと、Makefile.patchファイルも以下で作っておきます。

cat<<EOF>Makefile.patch
--- cycfx2prog-0.47/Makefile
+++ cycfx2prog-0.47-new/Makefile
@@ -9,7 +9,7 @@

 # NOTE: Also add sources to the "dist:" target!
 cycfx2prog: cycfx2prog.o cycfx2dev.o
-       $(CC) $(LDFLAGS) cycfx2prog.o cycfx2dev.o -o cycfx2prog
+       $(CC) cycfx2prog.o cycfx2dev.o -o cycfx2prog $(LDFLAGS)
EOF

これは$(LDFLAGS)を後ろに書かないと、libusbのリンクに失敗する問題を修正しています。

9. ソースコードのダウンロードとchecksumの更新

abuild checksumコマンドを実行して、ソースコードのダウンロードとchecksumの更新を行います。

abuild checksum

実行すると、以下のようにダウンロード画面が表示されます。

>>> cycfx2prog: Fetching https://www.triplespark.net/elec/periph/USB-FX2/software/cycfx2prog-0.47.tar.gz
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  8768  100  8768    0     0   5299      0  0:00:01  0:00:01 --:--:--  5301
>>> cycfx2prog: Updating the sha512sums in APKBUILD...

ダウンロード終了後、sha512ハッシュ値が計算され、APKBUILDファイルにchecksumの値が書き込まれます。

sha512sums="089895f0c4b45012f9f9fc607a30c2e2897f360d270973354fa739cc456d2728080733461f6a3681422049599947461c05e5d9e7e598fc3c9fd6d5a7d89e346c  cycfx2prog-0.47.tar.gz
ac62c7b1a13d144f5ceacc425a6c1487bf390273d4cf33cfe1c3ee5498d47b3c80c72ab5b8f189171d294da3bb001b42b1978d46937cf59578b35772c724d679  Makefile.patch"

10. パッケージビルドの実行

abuild -rコマンドを実行して、パッケージをビルドします。

alpine:~/cycfx2prog$ abuild -r
>>> cycfx2prog: Building test/cycfx2prog 0.47-r0 (using abuild 3.7.0-r0) started Fri, 24 Dec 2021 13:52:20 +0000
>>> cycfx2prog: Checking sanity of /home/test/cycfx2prog/APKBUILD...
>>> cycfx2prog: Analyzing dependencies...
>>> cycfx2prog: Installing for build: build-base libusb-compat libusb-compat-dev
(1/5) Installing libusb (1.0.24-r1)
(2/5) Installing libusb-compat (0.1.5-r4)
(3/5) Installing libusb-dev (1.0.24-r1)
(4/5) Installing libusb-compat-dev (0.1.5-r4)
(5/5) Installing .makedepends-cycfx2prog (20211224.135220)
Executing busybox-1.32.1-r6.trigger
OK: 276 MiB in 103 packages
>>> cycfx2prog: Cleaning up srcdir
>>> cycfx2prog: Cleaning up pkgdir
>>> cycfx2prog: Fetching https://www.triplespark.net/elec/periph/USB-FX2/software/cycfx2prog-0.47.tar.gz
>>> cycfx2prog: Fetching https://www.triplespark.net/elec/periph/USB-FX2/software/cycfx2prog-0.47.tar.gz
>>> cycfx2prog: Checking sha512sums...
cycfx2prog-0.47.tar.gz: OK
Makefile.patch: OK
>>> cycfx2prog: Unpacking /var/cache/distfiles/cycfx2prog-0.47.tar.gz...
>>> cycfx2prog: Makefile.patch
patching file Makefile
Hunk #1 succeeded at 9 with fuzz 2.
gcc -pipe -c -O2 -fno-rtti -fno-exceptions -DCYCFX2PROG_VERSION=\"0.47\" -W -Wall -Wformat cycfx2prog.cc
gcc -pipe -c -O2 -fno-rtti -fno-exceptions -DCYCFX2PROG_VERSION=\"0.47\" -W -Wall -Wformat cycfx2dev.cc
In file included from cycfx2dev.cc:18:
cycfx2dev.cc: In member function 'int CypressFX2Device::_ProgramIHexLine(const char*, const char*, int)':
cycfx2dev.cc:393:16: warning: comparison of unsigned expression in '>= 0' is always true [-Wtype-limits]
  393 |   assert(nbytes>=0 && nbytes<256);
      |          ~~~~~~^~~
gcc -pipe cycfx2prog.o cycfx2dev.o -o cycfx2prog -lusb
>>> cycfx2prog: Entering fakeroot...
>>> cycfx2prog*: Running postcheck for cycfx2prog
>>> cycfx2prog*: Preparing package cycfx2prog...
>>> cycfx2prog*: Stripping binaries
fatal: not a git repository (or any of the parent directories): .git
fatal: not a git repository (or any of the parent directories): .git
>>> cycfx2prog*: Scanning shared objects
>>> cycfx2prog*: Tracing dependencies...
        libusb-compat
        so:libc.musl-x86.so.1
        so:libusb-0.1.so.4
>>> cycfx2prog*: Package size: 48.0 KB
>>> cycfx2prog*: Compressing data...
>>> cycfx2prog*: Create checksum...
>>> cycfx2prog*: Create cycfx2prog-0.47-r0.apk
>>> cycfx2prog: Build complete at Fri, 24 Dec 2021 13:52:21 +0000 elapsed time 0h 0m 1s
>>> cycfx2prog: Cleaning up srcdir
>>> cycfx2prog: Cleaning up pkgdir
>>> cycfx2prog: Uninstalling dependencies...
(1/5) Purging .makedepends-cycfx2prog (20211224.135220)
(2/5) Purging libusb-compat-dev (0.1.5-r4)
(3/5) Purging libusb-compat (0.1.5-r4)
(4/5) Purging libusb-dev (1.0.24-r1)
(5/5) Purging libusb (1.0.24-r1)
Executing busybox-1.32.1-r6.trigger
OK: 275 MiB in 98 packages
>>> cycfx2prog: Updating the test/x86 repository index...
>>> cycfx2prog: Signing the index...

エラーがなければ、~/packages/test/x86/ディレクトリに以下のファイルが書き込まれます。

ls -l ~/packages/test/x86/
total 20
-rw-r--r--    1 test     test           754 Dec 24 14:09 APKINDEX.tar.gz
-rw-r--r--    1 test     test         13795 Dec 24 14:09 cycfx2prog-0.47-r0.apk

ここでcycfx2prog-0.47-r0.apkファイルができていれば完成です。

ね、簡単でしょ。

株式会社Scalarの技術顧問に就任しました

2021年度IPA未踏IT人材発掘・育成事業公募概要の竹迫PMメッセージの中で略歴として記載のある通り、2020年6月より株式会社Scalarの技術顧問に就任しました。

株式会社Scalarの技術顧問として、セキュリティ技術やエンジニア組織に関する相談や方針・設計に関するレビューを行うパートタイムのアドバイザー契約が中心となりますが、急成長するITベンチャーでよく発生する組織課題の解決や運営の相談にも乗っています。技術発のベンチャー事業が成長するにつれて0→1、1→10、10→100のそれぞれのフェーズにおいて、組織と事業のギャップを乗り越えていく必要がありますが、将来スケールした100のフェーズの景色を明確にビジョンとして持っているScalarの共同創業者CEO深津さん・山田さんのお二方に惹かれ、技術顧問を引き受けることに決めました。

当初お話をいただいた時点ではまだ緊急事態宣言は発令されておらず、初回の打ち合わせだけ実際にオフィスに出向いて対面でミーティングする機会があったのですが、出社したのはその1度きりで後は全部Zoomでの会議になりました。新型コロナウイルス感染拡大防止のため日本の働き方が大きく変わる中、色々調整いただいて、本業の仕事はそのままで、月数回のフルリモートワークで兼業として携わっています。

なぜ?国産検索エンジン開発からデータベース技術への興味

1997年、最初の私のIT業界のファーストキャリアは大学在学中にアルバイトした広島にある日本ヒューレット・パッカードの子会社でのソフトウェア開発(ECサイト構築、社内業務システム開発、SSO基盤開発、Namazu for Win32検索システムの構築など)で、当時はHP-UX上に基幹系システムのOracleデータベースを構築する百戦錬磨の精鋭部隊が近くにいたり、Intelとhpが共同開発していたItaniumプロセッサーコンパイラやOS上で発生する様々な競合問題をトラブルシュートをするサポート部隊がいたりと、非常に貴重な体験を積むことができました。当時のhpはミドルウェアから下の低レイヤーまでネットワーク・サーバ・OS・ストレージも自社開発のハードウェアのフルスタックな技術で課題解決できる強みを持っていました。ただ顧客の新規ソリューション提案においては自社のハードウェアを絡めた範囲に縛られていたりという制約もあって、次は自由度の高い独立系のソフトウェアベンチャーに身を置きたいという思いがあり、次はエンタープライズ向けのグループウェアの開発に挑戦していたドリームアーツという会社に就職することにしました。そこで開発していたプロダクトは顧客のニーズ毎にOraclePostgreSQL、PowerGres Plusをバンクエンドとして選べて(古いDB2の対応コードも少し残っていました)、LinuxのAPサーバを複数台置いてmod_perlでスケールアウト出来るようなアーキテクチャとして設計開発していました。グループウェアは情報系システムではあるものの、バックエンドはミッションクリティカルなOracle RACの構成に対応できるようにしたりと、エンタープライズな運用文化にも対峙することができました。当時はRDBMSのblobの性能に課題があり、大きなファイルは裏側のNFSサーバに格納するようなシステム構造が残っており、性能要件と一貫性のバランスを保つのが非常に難しかった記憶があります。運用現場で発生する緊急度の高い課題に対して圧倒的当事者意識で対応することもあり多くの経験を積むことが出来ましたが、運用と開発のもっと前段階のソフトウェア設計時点で解決できるアーキテクチャを中長期のR&Dとして考えられないかという思いが芽生え、サイボウズ・ラボに転職しました。

そこで機会をいただいてセキュリティ&プログラミングキャンプ(一時期データベース開発者コースも検討されていたこともありました)やShibuya.pmなどのコミュニティ活動に関わるようになり、現在の株式会社Scalar共同創業者の山田浩之さん(CEO兼CTO)と知り合うきっかけがありました。2008年のIPA未踏プロジェクトで山田さんが全文検索エンジンLux IOの開発をしていたときに国産ストレージエンジン、データベースマネージャ開発コミュニティの一員として知り合っていたのですが(当時は経済産業省が国産検索エンジン開発を支援する3年間の情報大航海プロジェクトがあり、産学官の相互人材交流が盛んでした)、昨年2月頃に「最近、改ざん検知性(ビザンチン故障検知性)を持ったデータベースを研究・開発しているスタートアップを始めたんだけど」とお誘いのお話をいただいて、今では非常に珍しくなったデータベースのコアな技術者(※喜連川先生曰く絶滅危惧種)が株式会社Scalarに複数人在籍していることと、今後の事業の発展性・スケーラビリティに非常に興味があったので「ぜひ」と二つ返事で一緒に関わらせていただくことになりました。

Scalar DB 分散データベースマネージャをOSSで公開

株式会社Scalarの主力プロダクトの一つとして開発しているScalar DBはACID準拠でない分散データベースやストレージエンジンをACID準拠にするJavaライブラリで、オープンソースApache License 2.0)と商用ライセンスの元で公開されています。現時点でScalar DBが対応しているバックエンドの分散ストレージはCassandra、Azure Cosmos DB、Amazon DynamoDB(2021年2月時点)ですが、抽象化されたアーキテクチャとして設計されているため、対応コードを実装すれば理論的にはその他のデータベースにも対応できます。

github.com

先日のApacheCon@Home 2020でCassandra上でどのように分散トランザクションを実現しているかScalarのエンジニアによる国際発表がありました。

このレイヤーでトランザクションが担保できれば、顧客サイドでのバックエンドのデータベースの選択肢に自由が生まれるので(商用のRDBMSの他にNoSQLも選択できる)、エンタープライズの現場に導入するときのスケーラビリティと信頼性の技術課題が同時に解決できるメリットが大きく、将来性の非常に高いプロダクトだと思っています。

このアイディアを聞いた時は、20年前の商用tuxedoに対抗したOpen OLTP system MONTSUQIを思い出しました。信頼性の非常に高い大型汎用機用トランザクションモニタと最新ののモダンなクラウドの分散データベース技術が交差する温故知新なワクワクする技術です。

Scalar DL 改ざん耐性とスケーラビリティの両立を実現

株式会社Scalarが開発するもう一つの主力プロダクトScalar DLは、電子署名が付与されたスマートコントラクトを分散トランザクションの形式で実行する分散型台帳ソフトウェアです。

高い改ざん耐性とスケーラビリティの両立を実現する独自の分散データベース管理技術で、帳簿など保管義務がある書類をデジタル化して改ざんされていないことを証明する必要のあるユースケースに適しています。主なアプリケーションとして、エンタープライズ向けの契約管理・情報銀行・企業間連携システム・ERPなどへの応用があります。

Scalar DL サンプルアプリケーション

分散型台帳ソフトウェアScalar DLでいくつかのアプリケーションを実装するサンプルはQiita上に記事があります。

qiita.com

モダンな技術スタックで基盤の技術を構築しているので、実際に手元でコードを動かしてみると非常に面白いです。

余談

(本業ではまだ紙の書類への捺印のために毎月オフィスに出社する必要があるのですが)株式会社Scalarとの契約ではクラウド電子署名サービスを用いて業務委託契約書を締結しました。私の中で電子署名は初めての体験でしたが、こういった新しいことを積極的に会社の業務として取り入れていくバックオフィスの柔軟かつ堅実な姿勢に感銘を受けました。今後、日本のデジタルトランスフォーメーション(DX)を進めていくためには、他社の技術も含めて、従来の紙ではない新しい電子契約の在り方をドッグフーディングして自分の身の回りでできることから地道に置き換えていくことが重要だと思っています。

Scalarでは人材を募集しています

そんな株式会社Scalarではエンジニアリングチームで一緒に働くメンバーを絶賛募集中です。データベースのコアな技術に携わりたい人、SREでクラウドの最新技術に触りたい人、コアなチームをまとめて価値を最大化したいエンジニアリングマネージャーなど様々なポジションがあります。実際どんな感じなのか、質問や相談がありましたら、ぜひ気軽に竹迫のTwitterまで直接DMください。

https://angel.co/company/scalar-inc/jobs

今後Scalarではデータベース系の学会へのスポンサーや、論文発表なども積極的に取り組んでいきたいと思います。よろしくお願いいたします。

Donuts Radio #003【竹迫良範さん】podcast出演

Donuts Radio(根岸心さんが聞くエンジニアの人生の軌跡。 子ども時代の貴重なエピソードから、心に秘めている将来の展望までとことん語りつくすpodcast)に出演しました。

waffles.donuts.ne.jp

8人きょうだいの長男として大家族で育った竹迫さんは、中学生の頃、父親が購入したパソコンを譲り受け、独学でゲームを作成したことでプログラミングと出会いました。一方で幼少期の入院経験から医者を志したこともあったといいます。エンジニアと医者、一見するとかけ離れた職業ですが、そこには意外な共通点が…?! 大家族ならではの家庭事情や、数々の偶然が重なって広島から上京した経緯など、根岸も知らなかった話はもちろん、「若い人が挑戦できる場所」を積極的に提供したいと語る竹迫さんの、熱意あふれる対談となりました。

f:id:TAKESAKO:20210212172104j:plain
Donuts Radio #003【竹迫良範さん】

ドメイン名にwafflesが入っているのが非常に良いですね。わっふるわっふる。