AnsibleでGentoo Linuxをインストールする

Gentoo Linux のインストールは、GUI インストーラが用意された他の多くの Linux ディストリビュージョンと違い、次のようなプリミティブな方法であることが知られています。

  • ブロックデバイス上にパーティションを切る
  • / ファイルシステムの内容を Tar アーカイブから展開する
  • 必要なパッケージのソースコードをダウンロード、展開、ビルドする
    • ebuild と呼ばれる Bash スクリプトライクなテキストファイルによって行われます
  • Linux Kernel の config を作成しビルドする
  • ブロックデバイス上にブートローダを書き込む

これは柔軟なシステムを構成する上でとても有用ですが、一方で複数回繰り返す場合には煩雑なことも事実です。
そこでこのようなインストール手順を Ansible を用いて自動化することを考えます。

なぜ Ansible か?

Ansible は人気のある構成管理ツールであり、2015 年の Red Hat, Inc.による買収を経て様々な領域で使われています。
OSS として長く安定的にメンテナンスされていること、サーバーが不要なこと、YAML によるテキスト形式で処理を記述できることなどは個人的に特に扱いやすい点です。

ただし YAML 形式のトレードオフとして、複雑な処理を書く場合には Jinja2 テンプレートを駆使する必要があるかもしれません。

How to Run

私が作成した Ansible の Role は次のリポジトリで公開しています。
https://github.com/mazgi/ansible-galaxy.gentoo-systemd-remote

また _driver/provisioning/site.yml には Role を呼び出す Playbook があり、 Docker Compose を通して実行できるようにしています。

この記事では次のような構成で実行する方法を紹介します。

Hyper-V 上は Windows 標準の仮想化ソフトウェアです。
VM を作るときに Generation 1 または 2 を選べますが、Generation 2 を指定することで UEFI からの起動がサポートされます。

本記事では Hyper-V VM を例にしますが、他の仮想化ソフトウェアやベアメタルでも実行方法はあまり変わらないでしょう。
ただしベアメタルの場合、NIC や RAID コントローラのドライバを Linux Kernel の config で有効にしたり、事前に RAID を構築しておくなどの手順が必要なことがあります。

本記事でご紹介する手順で Gentoo Linux をインストールするためにはインストール対象のホストが何らかの Linux ディストリビュージョンで起動している必要があります。
具体的には SSH サービスが起動し、GUID パーティションを操作し、Tar 等のアーカイブを展開し、 /dev 等ファイルシステムが提供されている必要があります。

私はこのような用途で SystemRescue (also known as SystemRescueCd)をよく使います。
これは数百 MB の ISO イメージに収められた Linux ディストリビュージョンで、その名前の通り HDD や SSD 等のブロックデバイスから起動できなくなった OS を救済する便利なツールが組み込まれています。

また SystemRescue は起動後に何もしません。
つまり他の多くの Linux ディストリビュージョンのインストールメディアと異なり、インストールウィザードが起動することもなければ、クリック 1 つでオススメのパーティション構成がブロックデバイスに書き込まれることもありません。
単に ISO イメージと RAM 上に一時的に作られたファイルシステムから OS が起動しシェルのプロンプトが現れます。
この点も私がこのような用途で SystemRescue を好んで使う理由です。

本記事の構成では Ansible を Docker 上で実行しています。
この方法により Ansible 自体や他の依存するツール群のインストールを docker-compose up コマンド 1 つに集約できます。
もちろん Docker を介さず Ansible をインストールすることもできます。
その場合は Dockerfiledocker-compose.yml 等の内容を参考に手順を組み立ててください。

1. .env ファイルを作る

リポジトリを Clone したディレクトリに移動し Docker Compose で読み込まれる .env ファイルを作ります。
Bash や Z shell では次のように実行することで作成できます。

rm -f .env
test $(uname -s) = 'Linux' && echo "UID=$(id -u)\nGID=$(id -g)" >> .env
cat<<EOE >> .env
CURRENT_ENV_NAME=production
DOCKER_GID=$(getent group docker | cut -d : -f 3)
EOE

2. VM を SystemRescue から起動し SSHD を有効にする

Gentoo Linux をインストールする対象の VM を、SystemRescueの ISO イメージから起動します。
またこの時 SSH デーモン(SSHD)も起動します。
これは SSH 経由で Ansible を適用するためです。

SSHD を起動するため Booting SystemRescueCd に従って起動時に次の 2 つのパラメタを渡してください。

  • rootpass=(任意のパスワード)
    • Sets the root password of the system running on the livecd to ****. That way you can connect from the network and ssh on the livecd and authenticate using this password.
  • nofirewall
    • You need to use this option if you need to establish connections to the system running SystemRescueCd from outside (for example connections to sshd).

手順の中でここで渡すパスワードを使ってログインすることはありません。
しかし root のパスワードが設定されていないと SSH 接続できないため何か設定する必要があります。
あるいは起動後に passwd コマンドで設定し、Firewall を Off にしても同等です。

3. 公開鍵認証で SSH ログインできるよう設定する

SystemRescue で起動した VM に Ansible 実行ホストから SSH 経由で provisioning するため、公開鍵認証でログインできるように設定します。

今回は ssh-keygen コマンドで一時的に鍵ペアを生成して ~/.ssh ディレクトリを作ります。
鍵ペア自体は使わないので mkdir && chown && chmod~/.ssh ディレクトリを作っても問題ありません。

ホスト側の秘密鍵に合う公開鍵を ~/.ssh/authorized_keys に保存します。
私のように GitHub に鍵を預けている方であれば次のように cURL で取得できるでしょう。

curl -L github.com/mazgi.keys > ~/.ssh/authorized_keys

ip コマンドなどで VM に割り当てられた IP アドレスを表示し、SSH ログインできることを確認します。

ip a show

4. Provisioning 実行

リポジトリを Clone したディレクトリで docker-compose up します。
返り値 0 で終了すれば正常です。
Ansible のコレクションや Role を取得しているため、ネットワーク状況により時間がかかるかもしれません。

Ansible 上での疎通を確認します。
次のように Ansible の ping が通るか確認してください。

docker-compose run provisioning ansible --inventory 192.0.2.1, --user root --module-name ping all

以降、 192.0.2.1 は VM に割り当てられた IP アドレスを示します。
あなたの VM の IP アドレスに読み替えてください。
また複数の VM を指定する場合、 192.0.2.1,192.0.2.2 のように指定できます。

Ansible の仕様で今回のように Inventory をコマンドラインで指定する場合には 192.0.2.1, のように , が必要です。

疎通が確認できたら Playbook を適用します。

Note: ここからの手順には長い時間がかかります、また途中で失敗することがあります。

次のように ansible-playbook コマンドを実行してください。

docker-compose run provisioning ansible-playbook --inventory 192.0.2.1, /project/provisioning/site.yml

次の 3 つの質問に答えます。

Please type the main device name [sda]:
Please type the network interface name pattern [eth*]:
Please type the new root password:

途中、各種パッケージのソースコードなどをダウンロードしてビルドする際にタイムアウトやビルドエラーに遭遇することがあります。
その場合、多くは ansible-playbook コマンドを再実行することで問題が解消します。
Tarball の事前ダウンロードやバイナリパッケージの共有など、時間短縮とエラー発生の削減を工夫していきたいところです。

次のように結果が表示され、”PLAY RECAP”の unreachablefailed が共に 0 であれば成功です。
VM を再起動すると VM のブロックデバイスから Gentoo Linux が起動するはずです。

Implementations

Gentoo Linux は無数の構成を取れますが、この Role では私が現在最もよく使う次の構成にしています。

  • UEFI+GPT
  • ファイルシステムは Btrfs
  • スーパーデーモンは systemd
  • DHCP で IP アドレスを取得する
  • SSH サービスが起動し、手元の鍵ペアでログインできる
  • Docker サービスが起動する

追加で必要なサービスやアプリケーション、通常ユーザーとその環境は別途構築する想定です。
かつてはもっと多様な構成をサポートしようと考えたこともあったのですが、時代や私自身が扱う領域が変わったため単純化しました。

Gentoo Linux の構成内容と合わせて Ansible Task としての実装方法をいくつか解説します。

パーティション作成

Linux でパーティションを構成する際、対話的なツールである GParted や fdisk がよく使われます。
しかしパーティション作成を自動化する場合、非対話的に実行できるGNU Partedが便利です。

Parted を使うと例えば次のようにパーティションを作ることができます。

parted --script --align optimal /dev/sda -- mkpart uefi_boot fat32 1MiB 128MiB

Tarball のダウンロードと検証

Tarball などのアーカイブをダウンロードし、独自の方法で検証したい場合があります。
例えば提供元からチェックサムが提供されている場合などです。
その場合、次のように register を使うことで実現できます。

- name: Verify exists stage3 archive
  changed_when: no
  ignore_errors: yes
  shell: >
        sha512sum --check /mnt/gentoo/{{ latest_stage3_filename.stdout | basename }}.DIGESTS 2> /dev/null | grep -E '^{{ latest_stage3_filename.stdout | basename }}:\s+OK$'
  args:
    chdir: /mnt/gentoo/
  register: latest_stage3_archive_verified
- name: Download latest stage3 archive
  get_url:
    dest: /mnt/gentoo/
    url: "{{ portage.mirror_uri }}/releases/amd64/autobuilds/{{ latest_stage3_filename.stdout }}"
  when: latest_stage3_archive_verified is failed

あらゆる設定ファイルのテンプレート化

Linux を含め UNIX like な OS はほとんどの設定をテキストファイルで管理しています。
またファイルシステムを / を起点とした 1 つの Tree として扱います。
また Ansible では jinja2 テンプレートをテンプレートの Tree に対して適用できます。
これらの特徴を活かして設定ファイルをあらかじめ適用先に合わせた Tree として配置しておくことで簡単に適用できます。

テンプレートは例えば次のように書けます。
{{}} が Ansible により置き換えられて配置されます。

/dev/{{ main_block_device }}3	/	btrfs	defaults,subvol=gentoo	0 1
/dev/{{ main_block_device }}3	/var/log	btrfs	defaults,subvol=var-log	0 1
/dev/{{ main_block_device }}1	/boot	vfat	defaults	0 1
/dev/{{ main_block_device }}2	none		swap		sw		0 0

もちろん置き換えるものがなければテンプレートの内容がそのまま反映されますので、構成を管理したい全てのファイルをテンプレート化しておくことができます。
例えば Linux Kernel の config は現時点では既存環境の /proc/config.gzzcat したものをそのまま配置しています。

このように設定ファイルを実際のファイルシステムに合わせてテンプレートとして配置しておくことで、将来内容が変わっても同じように適用できます。
このテンプレートを適用するために必要な Ansible Task は次の 2 つだけです。

- name: Create directories by the templates
  file:
    path: "{{ item | regex_replace('^('+role_path+'/templates)(.*)', \"/mnt/gentoo\\2\") }}"
    state: directory
  with_lines: find {{ role_path }}/templates -type d
- name: Apply templates
  template:
    dest: "{{ item | regex_replace('^('+role_path+'/templates)(.*)(.j2$)', \"/mnt/gentoo\\2\") }}"
    src: "{{ item }}"
  with_lines: find {{ role_path }}/templates -type f -name '*.j2'

標準出力の内容による判別

Provisioning を行う上で「適用が変更を及ぼしたか」判別したいことがあります。
特に「変更を及ぼしてもそうではなくても返り値が同じ場合」、標準出力や標準エラー出力の内容を読み取って判断する必要が生じます。

これは Ansbile Task では次のように registerchanged_when を組み合わせることで実現できます。

- name: Generate locales
  changed_when:
    - '" Adding locales to archive ..." in _result.stdout'
  shell: >
        chroot /mnt/gentoo /bin/bash -lc 'locale-gen --update'
  register: _result

ブートローダの簡略化

Linux を起動できるブートローダとして GRUB や ELILO が有名です。
しかし Linux Kernel には UEFI から直接起動できる機能があります( CONFIG_EFI_STUB )。
これを有効にすることで特定のブートローダをインストールせずに OS を起動できるようになります。

具体的には efibootmgr を用いて次のように実行することで実現できます。

efibootmgr --create --part 1 --disk /dev/sda --label Gentoo --loader "\\EFI\\Gentoo\\vmlinuz-gentoo.efi" --unicode "root=/dev/sda3 rootflags=subvol=gentoo rootfstype=btrfs initrd=\\EFI\\Gentoo\\initramfs-gentoo.img"

以上、このようなハードウェアとアプリケーションの中間にあたるレイヤーを扱う際に参考になれば幸いです。