[CentOS 7]systemd-nspawnでテスト環境をつくってみた

##概要
CentOS7のホストの上にsystemd-nspawnでシステムコンテナを立てる方法の備忘録です

元となるマスターコンテナ(テンプレート)と
それのクローンコンテナを作ります

(以下のコマンドはroot/sudoで実行しています)

##環境
CentOS-7-x86_64-Minimal-1611.isoから
minimal installのちyum updateを実行(2017/06/25)

##前提
以下のものを使用するのでインストールします

- systemd-networkd
- systemd-resolved
- screen
- perl
- policycoreutils-python

```shell-session:ホスト側
yum install \
 systemd-networkd \
 systemd-resolved \
 policycoreutils-python \
 screen \
 perl \
 -y
```

環境や設定は以下のものを使うので適宜読み替えてください

```shell-session
#インターネット接続されたデバイス名、DHCP有効
EXTERNAL_DEV_NAME='enp0s3'
#↑のデバイスが属するzone
EXTERNAL_ZONE_NAME='public'

#仮想ブリッジデバイス名
INTERNAL_DEV_NAME='br0'
#仮想ブリッジデバイス、コンテナが属するzone
INTERNAL_ZONE_NAME='internal'
#仮想ブリッジデバイスのIPアドレス設定
INTERNAL_GATEWAY_ADDR='10.0.0.1'
INTERNAL_NETWORK_PREFIX='24'

#コンテナが使用するDNSサーバーのアドレス
DNS_ADDR='8.8.8.8'
```

マスターコンテナの設定

```shell-session
#初回ログイン時のrootパスワード
CONTAINERS_MASTER_FIRST_ROOT_PASSWD='password'
#コンテナのデータを置くディレクトリ
CONTAINERS_MASTER_DATA_DIR='/root/data/containers/master/'
#コンテナのIPアドレス
CONTAINERS_MASTER_NETWORK_ADDR='10.0.0.2'
```

クローンコンテナの設定

```shell-session
#クローン元のコンテナのデータがあるディレクトリ
CONTAINERS_MASTER_DATA_DIR='/root/data/containers/master/'
#クローンコンテナのデータを置くディレクトリの親
CONTAINERS_CLONE_DATA_DIR='./data/containers/clones/'

#コンテナ名
L_CONTAINER_NAME='serv1'
#初回ログイン時のrootパスワード
CONTAINERS_CLONE_FIRST_ROOT_PASSWD='pass1'
#コンテナのIPアドレス
CONTAINERS_CLONE_NETWORK_ADDR='10.0.0.11'
```

##ホストのネットワーク設定
systemd-networkdを利用するように変更

```shell-session:ホスト側
systemctl stop NetworkManager
systemctl disable NetworkManager

systemctl start systemd-networkd systemd-resolved
systemctl enable systemd-networkd systemd-resolved

#systemd-resolvedは直接/etc/resolv.confを変更しないので
mv /etc/resolv.conf /etc/resolv.conf.orig
ln -s /run/systemd/resolve/resolv.conf /etc/resolv.conf

#systemd-networkd用の設定ファイル
mkdir -p /etc/systemd/network/

cat << EOS > /etc/systemd/network/${EXTERNAL_DEV_NAME}.network
[Match]
Name=${EXTERNAL_DEV_NAME}
[Network]
DHCP=yes
EOS

chmod 644 /etc/systemd/network/${EXTERNAL_DEV_NAME}.network
```

##コンテナ用のプライベートネットワークを作成
コンテナとホストを接続するブリッジデバイスを作成

```shell-session:ホスト側
cat << EOS > /etc/systemd/network/${INTERNAL_DEV_NAME}.netdev
[Match]
[NetDev]
Name=${INTERNAL_DEV_NAME}
Kind=bridge
EOS
chmod 644 /etc/systemd/network/${INTERNAL_DEV_NAME}.netdev

cat << EOS > /etc/systemd/network/${INTERNAL_DEV_NAME}.network
[Match]
Name=${INTERNAL_DEV_NAME}
[Network]
Address=${INTERNAL_GATEWAY_ADDR}/${INTERNAL_NETWORK_PREFIX}
IPForward=yes
EOS
chmod 644 /etc/systemd/network/${INTERNAL_DEV_NAME}.network
```

コンテナからのインターネットアクセスをホスト経由で行えるように設定

```conf:/etc/systemd/network/${EXTERNAL_DEV_NAME}.network
#[Network]セクションに追加
IPForward=kernel
IPMasquerade=yes
```

以上の設定を読み込み、IPマスカレードできるよう設定

```shell-session:ホスト側
systemctl restart systemd-networkd

firewall-cmd --permanent --zone=${INTERNAL_ZONE_NAME} --change-interface=${INTERNAL_DEV_NAME}
firewall-cmd --permanent --zone=${EXTERNAL_ZONE_NAME} --change-interface=${EXTERNAL_DEV_NAME}
firewall-cmd --permanent --zone=${EXTERNAL_ZONE_NAME} --add-masquerade
firewall-cmd --reload
```

##マスターコンテナ作成
コンテナのデータを置くディレクトリを作成し、絶対パスを取得
(コンテナ内のルートディレクトリ)

```shell-session:ホスト側
mkdir -p ${CONTAINERS_MASTER_DATA_DIR}
L_CONTAINER_ROOT_DIR=$(readlink -f ${CONTAINERS_MASTER_DATA_DIR})
```

コンテナ内に必要なものをインストール
コンテナ内にカーネルは必要ないので--exclude=kernel*

```shell-session:ホスト側
yum --installroot=${L_CONTAINER_ROOT_DIR} \
 --releasever=7 --disablerepo='*' --enablerepo=base \
 group install 'core' 'base' \
 --exclude=kernel* \
 -y

yum --installroot=${L_CONTAINER_ROOT_DIR} \
 --releasever=7 --disablerepo='*' --enablerepo=base \
 install 'systemd-networkd' \
 --exclude=kernel* \
 -y

echo exclude=kernel* >> ${L_CONTAINER_ROOT_DIR}/etc/yum.conf
```

初期パスワードを設定

```shell-session:ホスト側
sed -i \
 -e "s@^root:[^:]*@root:$(perl -E "say crypt('"${CONTAINERS_MASTER_FIRST_ROOT_PASSWD}"', '\$6\$'. crypt(rand, rand 100))")@" \
 ${L_CONTAINER_ROOT_DIR}/etc/shadow
```

コンテナのネットワーク設定

```shell-session:ホスト側
systemctl disable NetworkManager --root=${L_CONTAINER_ROOT_DIR}
systemctl enable systemd-networkd --root=${L_CONTAINER_ROOT_DIR}

cat << EOS > ${L_CONTAINER_ROOT_DIR}/etc/resolv.conf
nameserver ${DNS_ADDR}
EOS

mkdir -p ${L_CONTAINER_ROOT_DIR}/etc/systemd/network/
cat << EOS > ${L_CONTAINER_ROOT_DIR}/etc/systemd/network/host0.network
[Match]
Name=host0
[Network]
Address=${CONTAINERS_MASTER_NETWORK_ADDR}/${INTERNAL_NETWORK_PREFIX}
Gateway=${INTERNAL_GATEWAY_ADDR}
EOS

touch ${L_CONTAINER_ROOT_DIR}/etc/sysconfig/network
```

コンテナのセキュリティコンテキストを初期化

```shell-session:ホスト側
restorecon -Rv ${L_CONTAINER_ROOT_DIR}
```

コンテナを起動します

```shell-session:ホスト側
screen -S 'master' systemd-nspawn -b -D ${L_CONTAINER_ROOT_DIR} --network-bridge=${INTERNAL_DEV_NAME}
```

コンテナのログインプロンプトが表示されるのでroot/初期パスワードでログインし、自由に環境を構築して下さい

###備考
コンソールから抜ける場合

```:以下の順にキーを押す
Ctrl+a
d
```

コンソールに再度入る場合

```shell-session:ホスト側
screen -r 'master'
```

##クローンコンテナの作成
(マスターコンテナはシャットダウンして下さい)

コンテナのデータを置くディレクトリを作成し、絶対パスを取得

```shell-session:ホスト側
L_CONTAINER_ROOT_DIR=${CONTAINERS_CLONE_DATA_DIR}/${L_CONTAINER_NAME}
mkdir -p ${L_CONTAINER_ROOT_DIR}
L_CONTAINER_ROOT_DIR=$(readlink -f ${L_CONTAINER_ROOT_DIR})
```

マスターコンテナのデータディレクトリにオーバーレイしたディレクトリを作成
(コンテナ内のルートディレクトリ)

```shell-session:ホスト側
mkdir -p ${L_CONTAINER_ROOT_DIR}/work
mkdir -p ${L_CONTAINER_ROOT_DIR}/upper
mkdir -p ${L_CONTAINER_ROOT_DIR}/merge
mount -t overlay overlay -o lowerdir=${CONTAINERS_MASTER_DATA_DIR},upperdir=${L_CONTAINER_ROOT_DIR}/upper,workdir=${L_CONTAINER_ROOT_DIR}/work ${L_CONTAINER_ROOT_DIR}/merge
```

初期パスワードを設定

```shell-session:ホスト側
sed -i \
 -e "s@^root:[^:]*@root:$(perl -E "say crypt('"${CONTAINERS_CLONE_FIRST_ROOT_PASSWD}"', '\$6\$'. crypt(rand, rand 100))")@" \
 ${L_CONTAINER_ROOT_DIR}/merge/etc/shadow

```

コンテナのネットワーク設定

```shell-session:ホスト側
mkdir -p ${L_CONTAINER_ROOT_DIR}/merge/etc/systemd/network
cat << EOS > ${L_CONTAINER_ROOT_DIR}/merge/etc/systemd/network/host0.network
[Match]
Name=host0
[Network]
Address=${CONTAINERS_CLONE_NETWORK_ADDR}/${INTERNAL_NETWORK_PREFIX}
Gateway=${INTERNAL_GATEWAY_ADDR}
EOS
```

コンテナのセキュリティコンテキストを初期化し、一旦アンマウント

```shell-session:ホスト側
restorecon -Rv ${L_CONTAINER_ROOT_DIR}/merge
umount ${L_CONTAINER_ROOT_DIR}/merge
```

コンテナを起動します

```shell-session:ホスト側
#(systemd-nspawn単体でもoverlayできるらしいのですが試していません)
mount -t overlay overlay -o lowerdir=${CONTAINERS_MASTER_DATA_DIR},upperdir=${L_CONTAINER_ROOT_DIR}/upper,workdir=${L_CONTAINER_ROOT_DIR}/work ${L_CONTAINER_ROOT_DIR}/merge
screen -S ${L_CONTAINER_NAME} systemd-nspawn -b -D ${L_CONTAINER_ROOT_DIR}/merge --network-bridge=${INTERNAL_DEV_NAME} --machine=${L_CONTAINER_NAME}
```

コンテナのログインプロンプトが表示されるのでroot/初期パスワードでログインし、自由にガチャガチャして下さい

###備考
コンソールから抜ける場合

```:以下の順にキーを押す
Ctrl+a
d
```

コンソールに再度入る場合

```shell-session:ホスト側
screen -r ${L_CONTAINER_NAME}
```

##外部からコンテナへアクセス
ポートフォワードの設定

```shell-session:ホスト側
#受け付けるホスト側のポート
L_HOST_PORT=11022
#プロトコル
L_PROTO=tcp
#フォワード先のコンテナのアドレス
L_CONTAINER_ADDR=10.0.0.11
#フォワード先のコンテナのポート
L_CONTAINER_PORT=22
```

ホストからのポートフォワードを設定

```shell-session:ホスト側
firewall-cmd --zone=${EXTERNAL_ZONE_NAME} --add-forward-port="port=${L_HOST_PORT}:proto=${L_PROTO}:toaddr=${L_CONTAINER_ADDR}:toport=${L_CONTAINER_PORT}"
firewall-cmd --zone=${EXTERNAL_ZONE_NAME} --add-port=${L_HOST_PORT}/${L_PROTO}
```