Jaybanuan's Blog

どうせまた調べるハメになることをメモしていくブログ

Ubuntu 20.04 LTSでKVMの構築を構築する

環境

$ cat /etc/os-release | grep PRETTY_NAME
PRETTY_NAME="Ubuntu 20.04 LTS"

事前準備

CPUがハードウェア仮想化をサポートしているかを検査する。 以下のコマンドを実行した結果、1以上が表示されればOK。

$ egrep -c '(vmx|svm)' /proc/cpuinfo

KVMをインストール

$ sudo apt install qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils

virt-managerをインストール

$ sudo apt install virt-manager

参考

ImageMagickで画像の余白を広げる

はじめに

以下のような余白に余裕のない画像は少々窮屈に見えるので、余白を広げたくなる場合がある。 ペイントソフトなどで調整するのは面倒なので、コマンドラインツールできないか調べてみた。 その結果、ImageMagickのコマンドconvertで実現できたので、メモを残しておく。

f:id:redj:20200504004431p:plain

環境

$ cat /etc/os-release | grep PRETTY_NAME
PRETTY_NAME="Ubuntu 20.04 LTS"

ImageMagickのインストール

ImageMagickがインストールされていなければ、以下のコマンドを実行してインストールする。

$ sudo apt install imagemagick-6.q16

余白の広げ方

以下のようにコマンドconvertを実行する。 ここでは、入力画像をtest.png、出力画像をtest-extent.pngとしている。

$ convert \
    test.png \
    -gravity center \
    -extent 200x130 \
    test-extent.png

オプション-extentは、画像のキャンバスを指定の大きさにリサイズする。 元の画像からどのくらい大きくすればよいかは見栄えの感覚的なものなので、試行錯誤で決める。 オプション-gravity centerにより、大きくしたキャンバスのセンターに元の画像を配置することで、上下左右に余白が生まれる。 上記のコマンドで生成されたtest-extent.pngは以下のとおり。

f:id:redj:20200504012557p:plain

オプション-extentでのデフォルトの背景色は白色だが、変更したい場合は以下のようにオプション-backgroundを指定する。

$ convert \
    test.png \
    -background SkyBlue
    -gravity center \
    -extent 200x130 \
    test-skyblue.png

上記のコマンドで生成されたtest-skyblue.pngは以下のとおり。

f:id:redj:20200504014238p:plain

付録:ここで利用した元画像の生成方法

ここで利用した余白に余裕がない元画像は、同じくコマンドconvertを利用して、以下のように生成した。

$ convert \
    -pointsize 96 \
    caption:test \
    test.png

参考

Ansible Vaultの使い方の調査

はじめに

Ansible Vaultの使い方を調べてみたので、その結果を残しておく。 Ansibleを利用してサーバ構築を行う際には、パスワードなどのセンシティブな情報をhost_varsgroup_varsに配置することがある。 Ansible Vaultを利用すると、このような秘密にしたい情報を暗号化しておくことができる。

検証環境

$ cat /etc/os-release | grep PRETTY_NAME
PRETTY_NAME="Ubuntu 20.04 LTS"

$ ansible --version
ansible 2.9.7
  config file = None
  configured module search path = ['/home/redj/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/redj/.local/lib/python3.8/site-packages/ansible
  executable location = /home/redj/.local/bin/ansible
  python version = 3.8.2 (default, Mar 13 2020, 10:14:16) [GCC 9.3.0]

検証の題材のPlaybook

まずは、Ansible Vaultによる暗号化/復号化の検証の題材にするPlaybookを示す。 ファイル構成は以下。

vault-test
|-- host_vars
|   `-- localhost.yml
`-- playbook.yml

今回の検証ではhost_vars/localhost.ymlの中に秘密情報が入っていると仮定し、その秘密情報をAnsible Vaultで暗号化/復号化してみる。 このファイルの内容を以下に示す。

foo: FOO
bar: BAR

また、Playbook実行のエントリーポイントとなるplaybook.ymlの内容を以下に示す。

- hosts: localhost
  connection: local
  gather_facts: no
  tasks:
  - debug:
      msg: "foo={{ foo }}, bar={{ bar }}"

変数foobarの内容を表示しているだけの単純なもの。 暗号化と復号化の検証さえできればよいので、ローカルコネクションで済ませている。

そして、このPlaybookを実行すると、以下のようになる。

$ ansible-playbook playbook.yml 

[WARNING]: (……インベントリの指定がないという指摘なので、警告は無視する……)

PLAY [localhost] **************************************************************************************************

TASK [debug] ******************************************************************************************************
ok: [localhost] => {
    "msg": "foo=FOO, bar=BAR"
}

PLAY RECAP ********************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

出力結果に"msg": "foo=FOO, bar=BAR"とあり、期待通り変数展開されていることが分かる。 今回の検証では、Ansible Vaultによる暗号化/復号化を行っても、同様に期待通りに変数展開されることを確認する。

Ansible Vaultの基本的な仕組み

秘密にしたい情報は、コマンドansible-vaultを利用して事前に暗号化しておく。 暗号化に際して、コマンドansible-vaultはVault Passwordと呼ばれる共通鍵(自分で決めた任意の文字列)を利用する。 図示すると、以下のようになる。

f:id:redj:20200502042603p:plain

そして、その暗号化された情報をhost_varsなどに配置して、コマンドansible-playbookでPlaybookを実行する。 暗号化された情報は、共通鍵であるVault Passwordを利用して、Playbookの実行中に自動的に復号化される。 図示すると、以下のようになる。

f:id:redj:20200502042618p:plain

Vault Passwordとは

Vault Passwordとは、Ansible Vaultが暗号化と復号化で利用する共通鍵で、利用者が自分で決める任意の文字列である。 通常は、Vault Passwordをファイルに書き込んでおいて、コマンドラインパラメータでファイルパスを指定することになる。 例えば、Vault Passwordがhogehogeの場合は、単に以下のようなファイルtest_vault_passwordを準備すればよい。

$ echo -n "hogehoge" > test_vault_password

$ cat test_vault_password 
hogehoge

特定のコマンドラインパラメータを指定することで、実行中に対話的にVault Passwordを入力することもできるが、そういった方法はここでは言及しない。

暗号化の対象

Ansible Vaultの暗号化の対象には、以下の2種類がある。

  • ファイルレベルの暗号化 (File-level encryption)
  • 変数レベルの暗号化 (Variable-level encryption)

変数レベルの暗号化は、YAMLにおける値の暗号化を想定したものである。

ファイルレベルの暗号化

ファイル全体を暗号化する方式を、ファイルレベルの暗号化という。 以下のコマンドを実行することで、ファイルhost_vars/localhost.yml全体が暗号化される。

$ ansible-vault encrypt \
    --vault-id test@test_vault_password \
    host_vars/localhost.yml 

ここで、コマンドライン中の--vault-id test@test_vault_passwordの部分でVault Passwordを指定しているが、詳細は後述する。 ファイルhost_vars/localhost.ymlの内容を確認してみると、以下のように書き換わっている。

$ANSIBLE_VAULT;1.2;AES256;test
34633833356339363463306664666561353663363934646334383338636465323765373235366231
6236613861336239626338383030613863303066636561320a653132643463393735306563653138
33343831653434373862653363373531313264383830313635636135373531373066643363303239
6631663031343063370a663230616362386162643436333036353035613033636531363233666335
35383962663038363431643631646631643934616164626362663366366134656362

これで暗号化は完了。 復号化の確認のために、以下のようにコマンドansible-playbookを実行する。 Vault Passwordは、暗号化した時と同様にコマンドラインパラメータ--vault-idで指定する。

$ ansible-playbook \
    --vault-id test@test_vault_password \
    playbook.yml 

[WARNING]: (……警告は無視する……)

PLAY [localhost] **************************************************************************************************

TASK [debug] ******************************************************************************************************
ok: [localhost] => {
    "msg": "foo=FOO, bar=BAR"
}

PLAY RECAP ********************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

出力結果を確認すると、自動的にhost_vars/localhost.ymlが復号化されて、期待通りの変数展開が行われていることが確認できる。

変数レベルの暗号化

YAMLファイル中の個々の変数定義を暗号化する方式を、変数レベルの暗号化という。 以下のコマンドを実行することで、FOOという値を暗号化することができる。 Vault Passwordはコマンドラインパラメータ--vault-idで指定する。

$ ansible-vault encrypt_string \
    --vault-id test@test_vault_password \
    'FOO'

!vault |
          $ANSIBLE_VAULT;1.2;AES256;test
          32393933393231336263333538323866633037616337663730633339303431313038343935383834
          6237326433333437393366313039653065636630333837350a623339353136343537306132306663
          63306433643336626533383733666439323836363136346635333230653765303965356364333438
          6137656561313130330a346235646633366562343032326166663939343165346364396164393465
          3138
Encryption successful

そして、以下のようにhost_vars/localhost.ymlこの出力結果をコピペする

foo: !vault |
          $ANSIBLE_VAULT;1.2;AES256;test
          32393933393231336263333538323866633037616337663730633339303431313038343935383834
          6237326433333437393366313039653065636630333837350a623339353136343537306132306663
          63306433643336626533383733666439323836363136346635333230653765303965356364333438
          6137656561313130330a346235646633366562343032326166663939343165346364396164393465
          3138
bar: BAR

これで暗号化は完了。 復号化の確認のために、以下のようにコマンドansible-playbookを、パラメータ--vault-id付きで実行する。

$ ansible-playbook \
    --vault-id test@test_vault_password \
    playbook.yml 

[WARNING]: (……警告は無視する……)

PLAY [localhost] **************************************************************************************************

TASK [debug] ******************************************************************************************************
ok: [localhost] => {
    "msg": "foo=FOO, bar=BAR"
}

PLAY RECAP ********************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

出力結果を確認すると、自動的に変数fooの値が復号化されて、期待通りの変数展開が行われていることが確認できる。 もちろん、変数fooに加えて、変数barの値も暗号化しても、同様に期待通りに動作する。

余談だが、以下のようにコマンドラインパラメータ--nameを利用することで、YAMLのキーも出力結果に含まれるようになるので、コピペが少し楽になる

$ ansible-vault encrypt_string \
    --vault-id test@test_vault_password \
    --name foo \
    'FOO'

foo: !vault |
          $ANSIBLE_VAULT;1.2;AES256;test
          34373064336233363430636134383135303233323333633834336337333966346662363964363635
          3430326265333237313664386131363666636264303531370a303939376139313336333961313439
          32356431316231303636623462613232323432346261316561666333323439623934313938666536
          3530613033633963330a623038383834303866653339303331333930653832613431653062663830
          6430
Encryption successful

また、複数の変数を一度に暗号化することもできるが、詳細は割愛する。

Ansibleのベストプラクティス

Ansibleのベストプラクティスでは、実際に利用される変数を含むファイルと、暗号化された変数を含むファイルの、2種類のファイルに分割する方式を提示している。 例えば、以下のようにhost_vars(あるいはgroup_vars)の中にホスト名(あるいはグループ名)と同じ名前のディレクトリを作成し、その中に実際に利用される変数を含むファイルvarsと、暗号化された変数を含むファイルvaultを配置する。

vault-test
|-- host_vars
|   `-- localhost
|       |-- vars
|       `-- vault
`-- playbook.yml

ファイルvarsの内容の例を以下に示す。

foo: "{{ vault_foo }}"
bar: "{{ vault_bar }}"

ここで、変数名にプレフィックスvault_を付与した変数は、ファイルvaultで暗号化された変数である。 ファイルvaultの内容の例を以下に示す。 ただし、説明の都合上ここでは平文で示しているが、本来は暗号化されている。

vault_foo: FOO
vault_bar: BAR

つまり、Playbookなどの変数の利用者は、実際に利用される変数(例えばfoo)を通して、暗号化された変数(例えばvault_foo)を間接参照する方式である。 この方式の動機は、ファイルレベルの暗号化では変数名が判別不能なのだが、これによるPlaybook内の変数の検索性(grep)の低下を防ぐところにある。

ちなみに、ベストプラクティスの例では、ファイル名はvarsvaultになっているが、ファイル名およびその個数は任意との記述がある。

Vault IDとは

ここでようやく--vault-idで指定する、Vault IDを説明する。 なぜ最後の方で説明するかというと、無駄にややこしいから。

Vault IDとは、Playbook中で複数のVault Passwordを区別して利用するためのIDであり、そのフォーマットは以下の通り。

label@source

labelはVault Passwordに付けられた名前であり、sourceはVault Passwordが保存されているファイルのパスである。

使い方の例を示す。 まずhost_vars/localhost.ymlについて、変数レベルの暗号化を利用して、変数fooをVault ID test@test_vault_passwordで、変数barをVault ID other@other_vault_passwordで暗号化したとする。 コマンドラインは以下の通り。

$ ansible-vault encrypt_string \
    --vault-id test@test_vault_password \
    --name foo \
    'FOO'

(出力結果は省略)

$ ansible-vault encrypt_string \
    --vault-id other@other_vault_password \
    --name bar \
    'BAR'

(出力結果は省略)

この出力結果をhost_vars/localhost.ymlにコピペすると、内容は以下のようになる。

foo: !vault |
          $ANSIBLE_VAULT;1.2;AES256;test
          63643064613338386334323730613439313164626338386464623033616138386662653462376133
          (以下略)

bar: !vault |
          $ANSIBLE_VAULT;1.2;AES256;other
          34653962306238643839303634636438396239623838656631333562353138303865633962386564
          (以下略)

ここで、変数fooの暗号文の以下の部分を見てみると、末尾のtestの部分が暗号化時に指定したラベルであり、また復号化でもこのラベルを指定することになる。

          $ANSIBLE_VAULT;1.2;AES256;test

同様に変数barotherというラベルが関連付けられていることが分かる。 このような複数のVault Passwordを利用して暗号化した情報を扱うPlaybookを実行するには、コマンドラインパラメータ--vault-idを複数個指定すればよい。

$ ansible-playbook \
    --vault-id test@test_vault_password \
    --vault-id other@other_vault_password \
    playbook.yml

[WARNING]: (……警告は無視する……)

PLAY [localhost] **************************************************************************************************

TASK [debug] ******************************************************************************************************
ok: [localhost] => {
    "msg": "foo=FOO, bar=BAR"
}

PLAY RECAP ********************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

・・・ところが。

Ansible Vaultのドキュメントには以下のようにある。

By default the vault-id label is only a hint, any values encrypted with the password will be decrypted. The config option DEFAULT_VAULT_ID_MATCH can be set to require the vault id to match the vault ID used when the value was encrypted. This can reduce errors when different values are encrypted with different passwords.

以下に、個人的な主観を含めた意訳を示す。

デフォルトではVault IDのラベルはただのヒントであり、Vault Passwordで暗号化された値は手当たり次第に復号化されます。 設定オプションDEFAULT_VAULT_ID_MATCHをセットすることで、暗号化で指定したVault IDと一致するVault IDを要求することができます。 このデフォルトの挙動は、それぞれの値が異なるVault Passwordを利用して暗号化されている場合に、復号化のエラーを減らすことができます。

デフォルトの挙動が緩すぎて、ラベルの意味がないじゃない。。。

試しに、暗号化では指定していない適当なラベルでPlaybookを実行すると、確かに復号化が成功した。 ダメでしょう、この挙動は。

$ ansible-playbook \
    --vault-id xxxxx@test_vault_password \
    --vault-id yyyyy@other_vault_password \
    playbook.yml

[WARNING]: (……警告は無視する……)

PLAY [localhost] **************************************************************************************************

TASK [debug] ******************************************************************************************************
ok: [localhost] => {
    "msg": "foo=FOO, bar=BAR"
}

PLAY RECAP ********************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

付録:ファイルレベルの暗号化のサブコマンド

ファイルレベルの暗号化のサブコマンドは6つ存在していて、ややこしい。

  • ansible-vault create
  • ansible-vault edit
  • ansible-vault rekey
  • ansible-vault encrypt
  • ansible-vault decrypt
  • ansible-vault view

そのため、成果物との関係性を以下に図示してみた。

f:id:redj:20200502042731p:plain

実際に実行するサブコマンドのほとんどは、encryptだと思う。 真面目に運用するなら、rekeydecyptviewも時々利用すると思う。 個人的には、createeditはまず使わない。

参考

SnapでインストールしたVS Codeで日本語入力ができない

環境

$ cat /etc/os-release | grep PRETTY_NAME
PRETTY_NAME="Ubuntu 20.04 LTS"

SnapでVS Codeをインストール

UbuntuがSnapを推してきているので、VS CodeをSnapでインストールしてみた。

$ sudo snap install code

難なくインストールが完了し、起動も成功した。

しかし日本語入力ができない

[全角/半角]キーを押下しても、VS Codeで日本語入力が有効にならない。 Mozcの設定も見直したが、状況は変わらず。 日本語が入力できないのは、日本人にとって致命的。 ググってみると、どうもSnapでインストールしたVS CodeはInput Methodの制御が怪しいようだ。

github.com

解決策

SnapでインストールしたVS Codeをアンインストールしたあと、.debパッケージをダウンロードしてaptでインストールし直すことで解決した。

$ sudo snap remove code
$ sudo apt install ./code_1.44.2-1587059832_amd64.deb 

Snapは、まだまだか。。。

Ubuntu 20.04 LTS Desktopをインストールする

はじめに

Ubuntu 20.04 LTSがリリースされたので、開発作業用のマシンのOSリプレースに取り掛かった。 Ubuntu 20.04 LTSのメディアをマシンに差し込んで、インストーラの画面に従って進めていけば、難なくインストールできる。 ただ、一部インストーラの画面がはみ出して操作不能になったりと、いくらかトラブルがあったので、メモしておく。

Ubuntu 20.04 LTSのインストール

(1) メディアから起動

インストール対象のマシンにメディアを差し込んで起動する。 今回はVMを利用したので、ISOイメージをCDドライブにアタッチして起動している。

(2) 表示する言語の選択

インストーラの画面表示で利用する言語を選択する。 「日本語」を選択し、Enterを押下する。

f:id:redj:20200426161818p:plain

(3) インストーラの起動

Ubuntuをインストール」を選択し、Enterを押下する。

f:id:redj:20200426162113p:plain

(4) Ubuntuで利用する言語の選択

インストールするUbuntuで利用する言語を選択する。 「日本語」を選択し、「続ける」をクリックする。

f:id:redj:20200426162333p:plain

(5) キーボードレイアウトの選択

キーボードのレイアウトを選択する。 画面左側で「キーボードレイアウトの選択」で「Japanese」を選択し、画面右側で「Japanese」を選択する。 そして「続ける」をクリックする。

f:id:redj:20200426162711p:plain

(6) インストールするソフトウェアの構成を選択

インストールするソフトウェアの構成を選択する。 今回構築するのは開発環境なので、不要なソフトウェアはあまり入れたくない。 なので「あらかじめどのアプリケーションをインストールしますか?」で「最小インストール」を選択する。 そして「続ける」をクリックする。

f:id:redj:20200426163150p:plain

(7) パーティションの構成

クリーンインストールするため「ディスクを削除してUbuntuをインストール」を選択する。 そして「インストール」をクリックする。

f:id:redj:20200426164001p:plain

以下の確認画面がポップアップするので、内容を確認して「続ける」をクリックする。

f:id:redj:20200426164112p:plain

(8) タイムゾーンの選択

タイムゾーンを選択する。 「Tokyo」を選択し、「続ける」をクリックする。

f:id:redj:20200426164821p:plain

(9) ユーザの作成など

各項目を適切に入力し、「続ける」をクリックする。

f:id:redj:20200426165230p:plain

(10) Ubuntuのインストール

Ubuntuのインストールが開始される。

f:id:redj:20200426165404p:plain

インストールが完了すると以下の画面が表示されるので、「今すぐ再起動する」をクリックする。

f:id:redj:20200426165446p:plain

これで、インストールは完了。

画面がはみ出して操作不能になった場合の対処

インストーラの表示言語を日本語に設定しているためか、一部の画面が大きなサイズになり、画面からはみ出してボタンのクリックなどができなくなることがある。 例えば次の画面は、縦に長過ぎて画面下部にあるはずのボタンなどが表示されていない。

f:id:redj:20200426215711p:plain

この現象に遭遇した場合は、システムで利用するフォントのサイズ(font scaling)を調整することで、表示を画面内に収めることができる。 この回避方法は「kledgeb - Ubuntu 18.04 その96 - Ubuntuやフレーバーインストール時、インストーラーの画面がディスプレイからはみ出す場合は」を参考にした。

まず、[ctrl] + [alt] + [t]で、ターミナルを起動する。

f:id:redj:20200426215827p:plain

そして、以下のコマンドを実行して、フォントのスケーリングを変更する。デフォルトが1なので、ここでは0.75にする。

$ gsettings set org.gnome.desktop.interface text-scaling-factor 0.75

すると、以下のようにフォントのサイズが小さくなり、画面の下部まで表示される。

f:id:redj:20200426215924p:plain

インストール後に行う作業

インストール直後は、ホームディレクトリの中のユーザディレクトリが「ダウンロード」や「ドキュメント」と日本語になっている。

$ ls -l ~/
合計 32
drwxr-xr-x 2 redj redj 4096  4月 26 17:21 ダウンロード
drwxr-xr-x 2 redj redj 4096  4月 26 17:21 テンプレート
drwxr-xr-x 2 redj redj 4096  4月 26 17:21 デスクトップ
drwxr-xr-x 2 redj redj 4096  4月 26 17:21 ドキュメント
drwxr-xr-x 2 redj redj 4096  4月 26 17:21 ビデオ
drwxr-xr-x 2 redj redj 4096  4月 26 17:21 ピクチャ
drwxr-xr-x 2 redj redj 4096  4月 26 17:21 ミュージック
drwxr-xr-x 2 redj redj 4096  4月 26 17:21 公開

これではコマンドラインからは扱いづらいので、英語表記の「Downloads」や「Documents」に変更する。 まずは、以下のコマンドを実行する。

$ LANG=C xdg-user-dirs-gtk-update

表示された画面で「Don't ask me this again」にチェックを入れて、「Update Names」をクリックする。

f:id:redj:20200426205311p:plain

結果、以下のようにユーザディレクトリが英語に変更されている。

$ ls -l ~/
合計 32
drwxr-xr-x 2 redj redj 4096  4月 26 17:24 Desktop
drwxr-xr-x 2 redj redj 4096  4月 26 17:24 Documents
drwxr-xr-x 2 redj redj 4096  4月 26 17:24 Downloads
drwxr-xr-x 2 redj redj 4096  4月 26 17:24 Music
drwxr-xr-x 2 redj redj 4096  4月 26 17:24 Pictures
drwxr-xr-x 2 redj redj 4096  4月 26 17:24 Public
drwxr-xr-x 2 redj redj 4096  4月 26 17:24 Templates
drwxr-xr-x 2 redj redj 4096  4月 26 17:24 Videos

参考

RedmineをDocker Composeで構築

はじめに

バグの管理などは通常ITS (Issue Tracking System)を利用するが、どういう形態のITSを使うかは状況によって異なる。 GitHubのIssues機能などSaaSで提供されているITSは手軽に使い始められるものの、センシティブな情報はクラウドへの配置が許されない事も多いので、そういった場合はオンプレで運用できるRedmineが重宝する。 そこで、RedmineをDocker Composeで構築して、ライトに運用してみることにしたので、その手順を記しておく。

環境

$ cat /etc/os-release | grep PRETTY_NAME
PRETTY_NAME="Ubuntu 18.04.4 LTS"

$ docker --version
Docker version 19.03.5, build 633a0ea838

$ docker-compose --version
docker-compose version 1.25.4, build 8d51620a

方針

  • 運用
    • 簡易な運用にする。サービスレベルは気にしない。落ちたら上げる、ぐらいで。
  • Docker Composeを利用
    • 可読性を保ったままパラメータの記述ができて便利。コンテナを起動するためのスクリプトを書かずにすむ。
  • 永続データ (DB)
    • DBサーバの運用はしない。SQLiteを利用して永続化する。
  • 永続データ (ファイル)

構築手順

以下に構築手順を示す。 永続データについては色々調べまくったが、ここでは触れずに後続の節でメモを残しておく。

(1) Composeファイルの作成

以下の内容でComposeファイルを作成する。 Composeファイルのパスは任意だが、ここでは/opt/docker-compose-service/docker-compose.ymlとして説明をすすめる。

version: '3.7'

services:
  redmine:
    image: redmine:4.1
    container_name: redmine
    volumes:
      - /var/lib/docker-compose-service/redmine/files:/usr/src/redmine/files
      - /var/lib/docker-compose-service/redmine/plugins:/usr/src/redmine/plugins
      - /var/lib/docker-compose-service/redmine/public/themes:/usr/src/redmine/public/themes
      - /var/lib/docker-compose-service/redmine/sqlite:/usr/src/redmine/sqlite
    ports:
      - 10080:3000
    environment:
      - TZ=Asia/Tokyo

RedmineのイメージはRedmine公式のものを利用する。 Webサーバはコンテナ内部ではポート番号3000で立ち上がるが、コンテナ外部にはポート番号10080で公開する。 環境変数TZタイムゾーンを指定して、表示時刻を日本標準時にしておく。 SQLiteを利用する場合は、以下のDockerHubのRedmineのサイトからの引用のとおり、単にDBの設定(環境変数の定義)をしないでおけばよい。 そうすれば、自動的にSQLiteにフォールバックされる。

If neither variable is set, the image will fall back to using SQLite.

(2) 永続データ用のディレクトリを作成

ホスト側に、docker-compise.ymlvolumesの項目で指定する、永続データ用のディレクトリを作成する。

$ sudo mkdir -p /var/lib/docker-compose-service/redmine/files
$ sudo mkdir -p /var/lib/docker-compose-service/redmine/plugins
$ sudo mkdir -p /var/lib/docker-compose-service/redmine/public/themes
$ sudo mkdir -p /var/lib/docker-compose-service/redmine/sqlite

コンテナ内で動作するRedmineのプロセスのuidとgidは、それぞれ999である。 そのため、これらディレクトリのオーナーのuidとgidも、それぞれ999にする。

$ sudo chown -R 999:999 /var/lib/docker-compose-service/redmine

(3) デフォルトデータのロード

コンテナを立ち上げて、デフォルトデータをロードする。

$ docker-compose -f /opt/docker-compose-service/docker-compose.yml up

$ docker exec -it redmine bundle exec rake redmine:load_default_data RAILS_ENV=production REDMINE_LANG=ja
W, [2020-02-16T17:59:24.731200 #32]  WARN -- : Creating scope :system. Overwriting existing method Enumeration.system.
Default configuration data loaded.

警告が気になるが、メッセージ的に環境の問題ではないように見える。 Rubyのことは詳しくないのだが、おそらくRedmineのコードの中で意図的にEnumeration.systemを再定義(?)しているのだと思われる。

動作確認

詳細は割愛するが、以下の手順を実施した後でも期待通りの状態でRedmineが利用できるなら、永続データが欠落しておらず動作確認が取れたと言える。

  1. docker-compose upを実行し、コンテナを開始
  2. チケットの作成、プラグインの追加、テーマの追加など、永続データの変更を伴う作業を実施
  3. docker-compose downを実行し、コンテナを停止
  4. docker rm redmineを実行し、コンテナを削除
  5. docker-compose upを実行し、コンテナを再び開始

ついでにsystemdでサービス化

以下の過去の記事で説明したように、Docker Composeをsystemdでサービス化しておくと、ライフサイクル(起動/停止など)の管理が容易になる。

redj.hatenablog.com

構築は以上で終了。 以降は今回の構築にあたっての調査メモ。

メモ: 永続データの配置場所のまとめ

DockerHubのRedmineのサイトには、永続データを保存しておくためには、どのディレクトリに外部のストレージをマウントすればよいかが明記されていない。 そのため、Redmineのドキュメントを元に永続データが配置されているディレクトリを以下の通りに特定した。 ドキュメントは全部は読めていないので、抜け漏れはあるかもしれない。

項目 永続データの配置場所 参考ドキュメント
データベース /usr/src/redmine/sqlite Backing up and restoring Redmine
添付ファイル /usr/src/redmine/files Backing up and restoring Redmine
プラグイン /usr/src/redmine/plugins Plugin Tutorial
テーマ /usr/src/redmine/public/themes HowTo create a custom Redmine theme

以下、永続データの配置場所の詳細を説明する。

メモ: データベースの配置場所

データベース(SQLite)について、ドキュメント「Backing up and restoring Redmine」には、以下の記述がある。

SQLite databases are all contained in a single file, so you can back them up by copying the file to another location. You can determine the file name of SQLite database by looking at config/database.yml.

日本語訳すると、以下になる。

SQLiteデータベースは単一のファイルに必要なものがすべてが含まれているため、そのファイルを他の場所にコピーすることによりバックアップを取ることができます。 SQLiteデータベースのファイル名はconfig/database.ymlの中を確認することで特定できます。

そこで確認のために、ボリュームの設定を行わずに直接DockerからRedmineを起動し、config/database.ymlの中身を表示してみる。

$ docker run -it --rm --name redmine redmine:4.1

$ docker exec -it redmine /bin/cat /usr/src/redmine/config/database.yml
production:
  adapter: "sqlite3"
  host: "localhost"
  username: "redmine"
  database: "sqlite/redmine.db"
  encoding: "utf8"

表示結果には、database: "sqlite/redmine.db"とあるので、ディレクト/usr/src/redmine/sqliteに永続用のストレージをマウントすればよい。

メモ: 添付ファイルの配置場所

添付ファイルについて、ドキュメント「Backing up and restoring Redmine」には、以下の記述がある。

All file uploads are stored in attachments_storage_path (defaults to the files/ directory). You can copy the contents of this directory to another location to easily back it up.

WARNING: attachments_storage_path may point to a different directory other than files/. Be sure to check the setting in config/configuration.yml to avoid making a useless backup.

言葉を補いつつ日本語訳すると、以下になる。

すべてのファイルは、設定項目attachments_storage_pathで指定された場所(デフォルトはディレクトリfiles/)に保存されます。 このディレクトリの内容を他の場所にコピーすることで、簡単にバックアップすることができます。

警告: 設定項目attachments_storage_pathはfiles/以外の場所を指している場合があります。 無効なバックアップを防ぐために、config/configuration.ymlの中の設定を必ず確認してください。

ということで、config/configuration.ymlの中を確認する必要がありそうだ。 しかしながら、ドキュメント「Installing Redmine」には、以下の記述がある。

If you need to override default application settings, simply copy config/configuration.yml.example to config/configuration.yml and edit the new file; the file is well commented by itself, so you should have a look at it.

言葉を補いつつ日本語訳すると、以下になる。

アプリケーション(=Redmine)のデフォルトの設定をオーバーライドする必要がある場合、config/configuration.yml.exampleをconfig/configuration.ymlにコピーして編集するだけでよいです。 そのファイルのコメントには十分な説明が書かれているので、よく読んでください。

言い換えると「すべてデフォルト値でよければconfig/configuration.ymlは不要」ということだと理解した。 Redmineの公式イメージの中を確認したがconfig/configuration.ymlは存在しなかったので、全てデフォルト値が利用されていることになる。

結局のところ、デフォルトであるディレクトfiles、つまりフルパスではディレクト/usr/src/redmine/filesに永続用のストレージをマウントすればよい。

メモ: プラグインの配置場所

プラグインについては、ドキュメント「Backing up and restoring Redmine」では何故か言及がない。 そこで、プラグイン開発のドキュメント「Plugin Tutorial」を確認すると、プラグインに必要なファイルはディレクトplugins/[プラグイン名]に集約しているようだ。 そのため、ディレクトplugins、つまりフルパスではディレクト/usr/src/redmine/pluginsに永続用のストレージをマウントすればよい。

ちなみに、公式のRedmineのイメージではプリインストールされているプラグインはないが、もしプラグインがプリインストールされているイメージを利用する場合は、アタッチするボリュームに事前にディレクトpluginsの内容をコピーするなどの追加の手順が必要になる。

メモ: テーマの配置場所

テーマについても、ドキュメント「Backing up and restoring Redmine」では何故か言及がない。 そこで、テーマのカスタマイズのドキュメント「HowTo create a custom Redmine theme」を確認すると、テーマに必要なファイルはディレクトpublic/themes/[テーマ名]に集約しているようだ。 そのため、ディレクトpublic/themes、つまりフルパスではディレクト/usr/src/redmine/public/themesに永続用のストレージをマウントすればよい。

ここで注意点がひとつある。 ディレクト/usr/src/redmine/public/themesにはプリインストールのテーマの「Alternate」と「Classic」が含まれている。 そのため、単に永続用のストレージをマウントするとこれらのテーマが見えなくなってしまう。 必要であれば、事前にこれらのテーマを永続用のストレージにコピーするなどの追加の手順が必要になる。

ちなみに、デフォルトのテーマは/usr/src/redmine/public/stylesheets/application.cssなので、/usr/src/redmine/public/themesの内容に関わらず利用できる。

メモ: ディレクトリのオーナー

コンテナにマウントするホスト側のディレクトリは、単に作成するだけではRedmineから書き込みが出来ず、以下のようにSQLiteのファイルの作成に失敗する。

$ docker-compose up
Creating redmine ... done
Attaching to redmine
(略)
redmine    | SQLite3::CantOpenException: unable to open database file
(略)
redmine exited with code 1

原因は、ホスト側のディレクトリのオーナーとRedmineのプロセスのオーナーが異なるからである。 以下のようにコンテナに入ってRedmineのプロセスの情報を確認すると、ユーザはredmine(uidは999)で、グループもredmine(gidは999)で動作していることが分かる。

$ docker run -it --rm --name redmine redmine:4.1

$ docker exec -it redmine /bin/ps -e -o user,uid,group,gid,cmd
USER       UID GROUP      GID CMD
redmine    999 redmine    999 /usr/local/bin/ruby bin/rails server -b 0.0.0.0
root         0 root         0 /bin/ps -e -o user,uid,group,gid,cmd

念のため、/etc/passwd/etc/groupを確認してみると、以下のように確かにredmineのエントリがある。

$ docker exec -it redmine /bin/cat /etc/passwd | grep redmine
redmine:x:999:999::/home/redmine:/bin/sh

$ docker exec -it redmine /bin/cat /etc/group | grep redmine 
redmine:x:999:

そのため、以下のようにホスト側で/var/lib/docker-compose-service/redmineとそのサブディレクトリの所有ユーザーと所有グループを、それぞれ999に変更する。

$ sudo chown -R 999:999 /var/lib/docker-compose-service/redmine

ただし、ホスト側ではuid 999とgid 999は別の目的で利用されている可能性がある。 セキュリティの観点では、コンテナの都合に無理やり合わせてchownするのはあまり良くないので、理想的にはユーザーネームスペースのリマッピングなど、何らかの対策が必要になる。

参考

Docker Composeをsystemdのサービスにする

はじめに

サーバの簡易な運用をする場合、Docker Composeを利用してコンテナを立ち上げると簡単に環境を構築できる。 さらにDocker Composeをsystemdのサービスにすることで、起動や停止などのライフサイクルも管理できる。

ここでは、コンテナを利用した簡易なサーバ運用のために、Docker Composeをsystemdのサービスにする方法をメモしておく。

環境

$ cat /etc/os-release | grep PRETTY_NAME
PRETTY_NAME="Ubuntu 18.04.4 LTS"

$ docker --version
Docker version 19.03.5, build 633a0ea838

$ docker-compose --version
docker-compose version 1.25.4, build 8d51620a

ユニットファイルの作成

基本的には、どのようなコンテナを利用する場合もdocker-compose updocker-compose downを実行するだけなので、ユニットファイルのテンプレート化が可能である。 ここでは、次に示すテンプレート化したユニットファイル/etc/systemd/system/docker-compose-service@.serviceを作成する。

[Unit]
Description=%i managed by docker-compose
Requires=docker.service

[Service]
Type=simple

Environment=COMPOSE_FILE=/opt/docker-compose-service/%i/docker-compose.yml

ExecStartPre=-/usr/local/bin/docker-compose -f ${COMPOSE_FILE} down --volumes
ExecStart=/usr/local/bin/docker-compose -f ${COMPOSE_FILE} up
ExecStop=/usr/local/bin/docker-compose -f ${COMPOSE_FILE} down --volumes 

[Install]
WantedBy=multi-user.target

ExecStartPredocker-compose downを実行しているのは、ExecStartの前に念のためゴミ掃除をしておくため。 また、読み込むComposeファイルを指定するために、このユニットファイルの%iを活用してパスを作成している。 例えば、jenkinsnginxredmineの3つのサービスを運用する場合は、次のようなファイル構成になる。

/opt
`-- docker-compose-service
    |-- jenkins
    |   `-- docker-compose.yml
    |-- nginx
    |   `-- docker-compose.yml
    `-- redmine
        `-- docker-compose.yml

Composeファイルの準備とサービスの登録

ここでは例として、ポート10080でHTTPを受け付けるNginxのサービスを登録する。 まずは、以下の内容でComposeファイル/opt/docker-compose-service/nginx/docker-compose.ymlを作成する。 これは、ポート番号10080でアクセス可能なNginxを実行するだけのComposeファイルである。

version: '3.7'
services:
  nginx:
    image: nginx:1.17
    ports:
      - 10080:80

サービスとして登録するには、以下の以下のコマンドを実行する。

$ sudo systemctl enable --now docker-compose-service@nginx

動作確認

まずはsystemdからステータスを確認してみる。

$ systemctl status docker-compose-service@nginx
● docker-compose-service@nginx.service - nginx managed by docker-compose
   Loaded: loaded (/etc/systemd/system/docker-compose-service@.service; indirect; vendor preset: enabled)
   Active: active (running) since Tue 2020-02-11 13:11:38 JST; 57min ago
  Process: 715 ExecStartPre=/usr/local/bin/docker-compose -f ${COMPOSE_FILE} down --volumes (code=exited, status=0/SUCCESS)
 Main PID: 2018 (docker-compose)
    Tasks: 4 (limit: 4637)
   CGroup: /system.slice/system-docker\x2dcompose\x2dservice.slice/docker-compose-service@nginx.service
           ├─2018 /usr/local/bin/docker-compose -f /opt/docker-compose-service/nginx/docker-compose.yml up
           └─2063 /usr/local/bin/docker-compose -f /opt/docker-compose-service/nginx/docker-compose.yml up

 2月 11 13:11:27 ubuntu1804 systemd[1]: Starting nginx managed by docker-compose...
 2月 11 13:11:37 ubuntu1804 docker-compose[715]: Removing nginx_nginx_1 ...
 2月 11 13:11:37 ubuntu1804 docker-compose[715]: [75B blob data]
 2月 11 13:11:38 ubuntu1804 systemd[1]: Started nginx managed by docker-compose.
 2月 11 13:11:39 ubuntu1804 docker-compose[2018]: Creating network "nginx_default" with the default driver
 2月 11 13:11:40 ubuntu1804 docker-compose[2018]: Creating nginx_nginx_1 ...

Nginxのコンテナがdocker-compose経由で正常に立ち上がっていることが分かる。 dockerコマンドからも確認してみる。

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                   NAMES
b4f4c0a9b306        nginx:1.17          "nginx -g 'daemon of…"   57 minutes ago      Up 57 minutes       0.0.0.0:10080->80/tcp   nginx_nginx_1

dockerコマンドの結果からも、Ningxのコンテナが起動していることが確認できる。 では、curlでNginxにアクセスしてみる。

$ curl http://localhost:10080/
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

(以下略)

正常にNginxにアクセスできたことが分かる。 最後に、サービスを停止してみる。

$ sudo systemctl stop docker-compose-service@nginx

systemdからステータスを確認してみる。

$ systemctl status docker-compose-service@nginx
● docker-compose-service@nginx.service - nginx managed by docker-compose
   Loaded: loaded (/etc/systemd/system/docker-compose-service@.service; indirect; vendor preset: enabled)
   Active: inactive (dead) since Tue 2020-02-11 14:16:05 JST; 23s ago
  Process: 14572 ExecStop=/usr/local/bin/docker-compose -f ${COMPOSE_FILE} down --volumes (code=exited, status=0/SUCCESS)
  Process: 2018 ExecStart=/usr/local/bin/docker-compose -f ${COMPOSE_FILE} up (code=exited, status=0/SUCCESS)
  Process: 715 ExecStartPre=/usr/local/bin/docker-compose -f ${COMPOSE_FILE} down --volumes (code=exited, status=0/SUCCESS)
 Main PID: 2018 (code=exited, status=0/SUCCESS)

 2月 11 13:11:40 ubuntu1804 docker-compose[2018]: Creating nginx_nginx_1 ...
 2月 11 14:07:24 ubuntu1804 docker-compose[2018]: [71B blob data]
 2月 11 14:07:24 ubuntu1804 docker-compose[2018]: nginx_1  | 172.18.0.1 - - [11/Feb/2020:05:07:24 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.58.0" "-"
 2月 11 14:16:02 ubuntu1804 systemd[1]: Stopping nginx managed by docker-compose...
 2月 11 14:16:03 ubuntu1804 docker-compose[14572]: Stopping nginx_nginx_1 ...
 2月 11 14:16:05 ubuntu1804 docker-compose[2018]: nginx_nginx_1 exited with code 0
 2月 11 14:16:05 ubuntu1804 docker-compose[14572]: [71B blob data]
 2月 11 14:16:05 ubuntu1804 docker-compose[14572]: [75B blob data]
 2月 11 14:16:05 ubuntu1804 docker-compose[2018]: 
 2月 11 14:16:05 ubuntu1804 systemd[1]: Stopped nginx managed by docker-compose.

正常にサービスが停止したことが分かる。 dockerコマンドからも確認してみる。

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

dockerコマンドの結果より、起動しているコンテナがないことが分かる。 また、systemdの停止のコマンドとしてdocker-compose downを利用しているので、ゴミも後始末されていることが確認できる。

参考