この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
AWS ParallelCluster でインスタンスストアを持つ特定のインスタンスタイプをコンピュートノードで利用するとインスタンスストアを自動的にマウントされた状態で起動します。通常の EC2 インスタンスで利用すると自動マウントはされません。ParallelCluster はどの様にマウント処理を実装しているのか調べてみました。
事前調査
AWS ParallelCluster 3.1.3 と i4i インスタンスタイプの組み合わせを例にインスタンスストアのマウント設定を確認します。
i4i インスタンスタイプはストレージ最適化インスタンスだけにインスタンスストアが搭載されたインスタンスタイプです。インスタンスタイプに応じてインスタンスストアのストレージ容量が異なるため一度確認しておきます。
$ aws ec2 describe-instance-types \
--filters "Name=instance-type,Values=i4i*" "Name=instance-storage-supported,Values=true" \
--query "sort_by(InstanceTypes, &InstanceStorageInfo.TotalSizeInGB)[].[InstanceType, InstanceStorageInfo.TotalSizeInGB]" \
--region us-east-2 \
--output table
インスタンスストアの容量は 468GB 〜 30TB までとだいぶ幅がありますね。
実行結果
---------------------------
| DescribeInstanceTypes |
+---------------+---------+
| i4i.large | 468 |
| i4i.xlarge | 937 |
| i4i.2xlarge | 1875 |
| i4i.4xlarge | 3750 |
| i4i.8xlarge | 7500 |
| i4i.16xlarge | 15000 |
| i4i.32xlarge | 30000 |
+---------------+---------+
i4i インスタンスタイプを通常の EC2 と、ParallelCluster コンピュートノードで起動した場合の違いを確認します。
EC2 インスタンスを確認
i4i.large
インスタンスを普通に起動させました。OS は Ubuntu 20.04 です。インスタンスストアは自動的にマウントされないため、ストレージを確認できません。
$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/root 7.6G 1.5G 6.2G 20% /
tmpfs 7.7G 0 7.7G 0% /dev/shm
tmpfs 3.1G 836K 3.1G 1% /run
tmpfs 5.0M 0 5.0M 0% /run/lock
/dev/nvme0n1p15 105M 5.3M 100M 5% /boot/efi
tmpfs 1.6G 4.0K 1.6G 1% /run/user/1000
lsblk
コマンドでブロックデバイスの一覧を確認すると435.9G
と容量からインスタンスストアらしきデバイスを確認できます。
$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
loop0 7:0 0 26.6M 1 loop /snap/amazon-ssm-agent/5163
loop1 7:1 0 55.5M 1 loop /snap/core18/2344
loop2 7:2 0 61.9M 1 loop /snap/core20/1405
loop3 7:3 0 79.9M 1 loop /snap/lxd/22923
loop4 7:4 0 43.6M 1 loop /snap/snapd/15177
nvme0n1 259:0 0 8G 0 disk
├─nvme0n1p1 259:1 0 7.9G 0 part /
├─nvme0n1p14 259:2 0 4M 0 part
└─nvme0n1p15 259:3 0 106M 0 part /boot/efi
nvme1n1 259:4 0 435.9G 0 disk
nvme-cli
コマンドで確認するとAmazon EC2 NVMe Instance Storage
の文字列を確認できました。
$ sudo apt update -y
$ sudo apt install nvme-cli -y
$ sudo nvme id-ctrl -v /dev/nvme1n1
NVME Identify Controller:
vid : 0x1d0f
ssvid : 0
sn : AWS43B53DFA47575ECBF
mn : Amazon EC2 NVMe Instance Storage
--- snip ---
インスタンスストアをマウントする方法はファイルシステム作成して/etc/fstab
に書いておくなど方法はいくつか考えられます。
EC2 インスタンスにインスタンスストアボリュームを追加する - Amazon Elastic Compute Cloud
というわけで、インスタンスストアをデバイスとして認識しているがマウントはされていない状態です。
ParallelCluster コンピュートノードで起動
i4i.8xlarge
が起動するクラスターは以下のリンクで紹介しているコンフィグから作成した環境で確認します。
コンピュートノードが起動した時点でインスタンスストアは/scratch
ディレクトリにマウントされています。
$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/root 34G 18G 17G 51% /
devtmpfs 124G 0 124G 0% /dev
tmpfs 124G 0 124G 0% /dev/shm
tmpfs 25G 1.1M 25G 1% /run
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 124G 0 124G 0% /sys/fs/cgroup
/dev/loop0 27M 27M 0 100% /snap/amazon-ssm-agent/5163
/dev/loop2 62M 62M 0 100% /snap/core20/1405
/dev/loop1 56M 56M 0 100% /snap/core18/2344
/dev/loop3 68M 68M 0 100% /snap/lxd/22753
/dev/loop4 44M 44M 0 100% /snap/snapd/15177
/dev/loop5 45M 45M 0 100% /snap/snapd/15534
10.0.1.12:/home 34G 17G 18G 49% /home
10.0.1.12:/opt/parallelcluster/shared 34G 17G 18G 49% /opt/parallelcluster/shared
/dev/loop6 62M 62M 0 100% /snap/core20/1434
/dev/mapper/vg.01-lv_ephemeral 6.8T 89M 6.5T 1% /scratch
10.0.1.12:/opt/intel 34G 17G 18G 49% /opt/intel
10.0.1.12:/opt/slurm 34G 17G 18G 49% /opt/slurm
tmpfs 25G 40K 25G 1% /run/user/1000
ブロックデバイスは2つ確認できます。
$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
loop0 7:0 0 26.7M 1 loop /snap/amazon-ssm-agent/5163
loop1 7:1 0 55.5M 1 loop /snap/core18/2344
loop2 7:2 0 61.9M 1 loop /snap/core20/1405
loop3 7:3 0 67.8M 1 loop /snap/lxd/22753
loop4 7:4 0 43.6M 1 loop /snap/snapd/15177
loop5 7:5 0 44.7M 1 loop /snap/snapd/15534
loop6 7:6 0 55.5M 1 loop /snap/core18/2409
loop7 7:7 0 61.9M 1 loop /snap/core20/1434
loop8 7:8 0 25.1M 1 loop /snap/amazon-ssm-agent/5656
nvme0n1 259:0 0 35G 0 disk
└─nvme0n1p1 259:2 0 35G 0 part /
nvme2n1 259:1 0 3.4T 0 disk
└─vg.01-lv_ephemeral 253:0 0 6.8T 0 lvm /scratch
nvme1n1 259:3 0 3.4T 0 disk
└─vg.01-lv_ephemeral 253:0 0 6.8T 0 lvm /scratch
i4i.8xlarge
のローカルストレージはなぜ2つあるのかと言うと
aws ec2 describe-instance-types \
--filters "Name=instance-type,Values=i4i.8xlarge" \
--query "InstanceTypes[].InstanceStorageInfo" \
--region us-east-2
3750GBのストレージが2つで、7.5TBのインスタンスストアを提供されています。
実行結果
-------------------------------------------------------
| DescribeInstanceTypes |
+-------------------+---------------+-----------------+
| EncryptionSupport | NvmeSupport | TotalSizeInGB |
+-------------------+---------------+-----------------+
| required | required | 7500 |
+-------------------+---------------+-----------------+
|| Disks ||
|+--------------+---------------------+--------------+|
|| Count | SizeInGB | Type ||
|+--------------+---------------------+--------------+|
|| 2 | 3750 | ssd ||
|+--------------+---------------------+--------------+|
i4i.large
のブロックデバイスは1つでしたが、大きなタイプだと個数を増やして大きなストレージ容量を提供しています。
たとえばi4i.32xlarge
は 3750GB * 8個で 30TB のインスタンスストアを提供しています。
というわけで、インスタンスストアはマウントされた状態で起動することを確認できました。
ここまで確認できたこと
- 通常の EC2 インスタンスを起動するとインスタンスストアは自動マウントされない
- ParallelCluster のコンピュートノードで起動するとインスタンスは自動マウントされる
次は ParallelCluster のコンピュートノードで起動時、どやってインスタンスストアをマウントしたのかを確認します。
コンピュートノードのマウント処理を追う
やっと本題のどうやって ParallelCluster はインスタンスストアをマウントしているのか確認します。
コンピュートノード起動時のログ確認
/var/log/cloud-init-output.log
からインスタンスストアをマウント時のログを確認できました。
ローカルストレージが2つあるため論理ボリュームグループ(LVM)を作成してからext4
ファイルシステムを作成しています。その後/scratch
にマウントしています。
/var/log/cloud-init-output.log 抜粋
--- snip ---
* service[setup-ephemeral] action enable[2022-05-14T09:56:02+00:00] INFO: Processing service[setup-ephemeral] action enable (aws-parallelcluster-config::base line 30)
[2022-05-14T09:56:02+00:00] INFO: service[setup-ephemeral] enabled
- enable service service[setup-ephemeral]
* execute[Setup of ephemeral drivers] action run[2022-05-14T09:56:02+00:00] INFO: Processing execute[Setup of ephemeral dri
vers] action run (aws-parallelcluster-config::base line 36)
[execute] ParallelCluster - This instance type has (2) device(s) for instance store: (/dev/nvme2n1 /dev/nvme1n1)
ParallelCluster - LVM (/dev/vg.01/lv_ephemeral) does not exist
ParallelCluster - Creating LVM (/dev/vg.01/lv_ephemeral)
Physical volume "/dev/nvme2n1" successfully created.
Physical volume "/dev/nvme1n1" successfully created.
Volume group "vg.01" successfully created
Wiping atari signature on /dev/vg.01/lv_ephemeral.
Logical volume "lv_ephemeral" created.
ParallelCluster - LVM (/dev/vg.01/lv_ephemeral) created successfully
ParallelCluster - Found LVM (/dev/vg.01/lv_ephemeral) in state (a)
ParallelCluster - Found LVM (/dev/vg.01/lv_ephemeral) FS type ()
ParallelCluster - Formatting LVM (/dev/vg.01/lv_ephemeral) with FS type (ext4)
mke2fs 1.45.5 (07-Jan-2020)
Discarding device blocks: done
Creating filesystem with 1831053312 4k blocks and 228884480 inodes
Filesystem UUID: d5240c5a-f705-4841-ba1d-f6684b3b1672
Superblock backups stored on blocks:
32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968,
102400000, 214990848, 512000000, 550731776, 644972544
Allocating group tables: done
Writing inode tables: done
Creating journal (262144 blocks): done
Writing superblocks and filesystem accounting information: done
ParallelCluster - LVM (/dev/vg.01/lv_ephemeral) formatted successfully
ParallelCluster - LVM (/dev/vg.01/lv_ephemeral) not mounted, mounting on (/scratch)
mount: /dev/mapper/vg.01-lv_ephemeral mounted on /scratch.
ParallelCluster - LVM (/dev/vg.01/lv_ephemeral) mounted successfully
[2022-05-14T09:56:15+00:00] INFO: execute[Setup of ephemeral drivers] ran successfully
- execute /usr/local/sbin/setup-ephemeral-drives.sh
--- snip ---
確認したログについて
コンピュートノードを調査したいときは/var/log/cloud-init-output.log
を確認すれば概ねあたりつけられます。
コンピュートノードが起動した場合、まず /var/log/cloud-init-output.log を確認します。ヘッドノードの /var/log/chef-client.log ログと同様のセットアップログが含まれているはずです。セットアップ中に発生するほとんどのエラーは、/var/log/cloud-init-output.log ログにエラーメッセージが表示されます。 AWS ParallelCluster のトラブルシューティング - AWS ParallelCluster
setup-ephmeral.service を調べる
抜粋したcloud-init-output.log
の冒頭setup-ephemeral
とはなにかを探しました。
/var/log/cloud-init-output.log 該当箇所
[2022-05-14T09:56:02+00:00] INFO: service[setup-ephemeral] enabled
Chef の cookbook がコンピュートノードの以下のパスに保存されており、その中の receipe に該当の設定がありました。
パス: /etc/chef/cookbooks/aws-parallelcluster-config/recipes/base.rb
base.rb 抜粋
--- snip ---
service "setup-ephemeral" do
supports restart: false
action :enable
end
# Execution timeout 3600 seconds
unless virtualized?
execute "Setup of ephemeral drivers" do
user "root"
command "/usr/local/sbin/setup-ephemeral-drives.sh"
end
end
--- snip ---
aws-parallelcluster-cookbookは、ParallelCluster のソースコードとは別のリポジトリ管理されています。
setup-ephemeral.service
をenableにすると何が行われるのかコンピュートノードのサービスを確認しました。内容は/usr/local/sbin/setup-ephemeral-drives.sh
を実行するサービスでした。
/etc/systemd/system/setup-ephemeral.service
[Unit]
Description=Setup ephemeral drives service
After=network.target
[Service]
ExecStart=/usr/local/sbin/setup-ephemeral-drives.sh
[Install]
WantedBy=multi-user.target
/etc/systemd/system/setup-ephemeral.service
setup-ephemeral-drives.sh を調べる
/usr/local/sbin/setup-ephemeral-drives.sh
の実行内容はcloud-init-output.log
で確認できたメッセージの生成元でした。インスタンスストアのマウント処理を行っているのはsetup-ephemeral-drives.sh
であることがわかりました。
スクリプト全文は長いため折りたたんであります。
折りたたみ
#!/bin/bash
# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the
# License. A copy of the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and
# limitations under the License.
. /etc/parallelcluster/cfnconfig
LVM_VG_NAME="vg.01"
LVM_NAME="lv_ephemeral"
LVM_PATH="/dev/${LVM_VG_NAME}/${LVM_NAME}"
LVM_ACTIVE_STATE="a"
FS_TYPE="ext4"
MOUNT_OPTIONS="noatime,nodiratime"
# cfn_ephemeral_dir is set in the environment by cfnconfig sourcing
INPUT_MOUNTPOINT="${cfn_ephemeral_dir}"
function log {
SCRIPT=$(basename "$0")
MESSAGE="$1"
echo "ParallelCluster - ${MESSAGE}"
}
function error_exit {
log "[ERROR] $1"
exit 1
}
function exit_noop {
log "[INFO] $1"
exit 0
}
function parameter_check {
if [[ -z "${INPUT_MOUNTPOINT}" ]]; then
exit_noop "Mount point not specified"
fi
}
function set_imds_token {
if [[ -z "${IMDS_TOKEN}" ]];then
IMDS_TOKEN=$(curl --retry 3 --retry-delay 0 --fail -s -f -X PUT -H "X-aws-ec2-metadata-token-ttl-seconds: 900" http://169.254.169.254/latest/api/token)
if [[ "$?" -gt 0 ]] || [[ -z "${IMDS_TOKEN}" ]]; then
error_exit "Could not get IMDSv2 token. Instance Metadata might have been disabled or this is not an EC2 instance"
fi
fi
}
function get_metadata {
QUERY=$1
local IMDS_OUTPUT
IMDS_OUTPUT=$(curl --retry 3 --retry-delay 0 --fail -s -q -H "X-aws-ec2-metadata-token:${IMDS_TOKEN}" -f "http://169.254.169.254/latest/${QUERY}")
echo -n "${IMDS_OUTPUT}"
}
function print_block_device_mapping {
echo 'block-device-mapping: '
DEVICE_MAPPING_LIST=$(get_metadata meta-data/block-device-mapping/)
if [[ -n "${DEVICE_MAPPING_LIST}" ]]; then
for DEVICE_MAPPING in ${DEVICE_MAPPING_LIST}; do
echo -e '\t' "${DEVICE_MAPPING}: $(get_metadata meta-data/block-device-mapping/"${DEVICE_MAPPING}")"
done
else
echo "NOT AVAILABLE"
fi
}
function check_instance_store {
if ls /dev/nvme* >& /dev/null; then
IS_NVME=1
MAPPINGS=$(realpath --relative-to=/dev/ -P /dev/disk/by-id/nvme*Instance_Storage* | grep -v "*Instance_Storage*" | uniq)
else
IS_NVME=0
set_imds_token
MAPPINGS=$(print_block_device_mapping | grep ephemeral | awk '{print $2}' | sed 's/sd/xvd/')
fi
NUM_DEVICES=0
for MAPPING in ${MAPPINGS}; do
umount "/dev/${MAPPING}" &>/dev/null
STAT_COMMAND="stat -t /dev/${MAPPING}"
if ${STAT_COMMAND} &>/dev/null; then
DEVICES+=("/dev/${MAPPING}")
NUM_DEVICES=$((NUM_DEVICES + 1))
fi
done
if [[ "${NUM_DEVICES}" -gt 0 ]]; then
log "This instance type has (${NUM_DEVICES}) device(s) for instance store: (${DEVICES[*]})"
else
exit_noop "This instance type doesn't have instance store"
fi
if [[ "${IS_NVME}" -eq 0 ]]; then
log "This instance store may suffer first-write penalty unless initialized: please have a look at https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/disk-performance.html"
# Initialization can take long time, even hours
# for DEVICE in "${DEVICES[@]}"; do
# dd if=/dev/zero of="${DEVICE}" bs=1M
# done
fi
}
function create_lvm {
log "Creating LVM (${LVM_PATH})"
pvcreate -y "${DEVICES[@]}"
vgcreate -y "${LVM_VG_NAME}" "${DEVICES[@]}"
LVM_CREATE_COMMAND="lvcreate -y -i ${NUM_DEVICES} -I 64 -l 100%FREE -n ${LVM_NAME} ${LVM_VG_NAME}"
if ! ${LVM_CREATE_COMMAND}; then
error_exit "Failed to create LVM"
else
log "LVM (${LVM_PATH}) created successfully"
fi
}
function check_lvm_exist {
LVM_EXIST_COMMAND="lvs ${LVM_PATH} --nosuffix --noheadings -q"
if ! ${LVM_EXIST_COMMAND} &>/dev/null; then
log "LVM (${LVM_PATH}) does not exist"
create_lvm
else
log "LVM (${LVM_PATH}) already exists"
fi
}
function activate_lvm {
LVM_STATE=$(lvs "${LVM_PATH}" --nosuffix --noheadings -o lv_attr | xargs | cut -c5)
log "Found LVM (${LVM_PATH}) in state (${LVM_STATE})"
if [[ "${LVM_STATE}" != "${LVM_ACTIVE_STATE}" ]]; then
log "Activating LVM (${LVM_PATH})"
LVM_ACTIVATE_COMMAND="lvchange -ay ${LVM_PATH}"
if ! ${LVM_ACTIVATE_COMMAND}; then
error_exit "Failed to activate LVM"
else
log "LVM (${LVM_PATH}) activated successfully"
fi
fi
}
function format_lvm {
LVM_FS_TYPE=$(lsblk "${LVM_PATH}" --noheadings -o FSTYPE | xargs)
log "Found LVM (${LVM_PATH}) FS type (${LVM_FS_TYPE})"
if [[ "${LVM_FS_TYPE}" != "${FS_TYPE}" ]]; then
log "Formatting LVM (${LVM_PATH}) with FS type (${FS_TYPE})"
LVM_FORMAT_COMMAND="mkfs -t ${FS_TYPE} ${LVM_PATH}"
if ! ${LVM_FORMAT_COMMAND}; then
error_exit "Failed to format LVM"
else
log "LVM (${LVM_PATH}) formatted successfully"
fi
sync
sleep 1
else
log "LVM (${LVM_PATH}) already formatted with FS type (${LVM_FS_TYPE})"
fi
}
function mount_lvm {
LVM_MOUNTPOINT=$(lsblk "${LVM_PATH}" -o MOUNTPOINT --noheadings | xargs)
if [[ -z ${LVM_MOUNTPOINT} ]]; then
log "LVM (${LVM_PATH}) not mounted, mounting on (${INPUT_MOUNTPOINT})"
# create mount
mkdir -p "${INPUT_MOUNTPOINT}"
LVM_MOUNT_COMMAND="mount -v -t ${FS_TYPE} -o ${MOUNT_OPTIONS} ${LVM_PATH} ${INPUT_MOUNTPOINT}"
if ! ${LVM_MOUNT_COMMAND}; then
error_exit "Failed to mount LVM"
else
log "LVM (${LVM_PATH}) mounted successfully"
fi
# set mount permission
chmod 1777 "${INPUT_MOUNTPOINT}"
else
log "LVM (${LVM_PATH}) already mounted on (${LVM_MOUNTPOINT})"
fi
}
function main {
parameter_check
check_instance_store
check_lvm_exist
activate_lvm
format_lvm
mount_lvm
}
main
ここまで確認できたこと
/usr/local/sbin/setup-ephemeral-drives.sh
が実行されるとインスタンスストアをマウントする/usr/local/sbin/setup-ephemeral-drives.sh
は/etc/systemd/system/setup-ephemeral.service
サービス起動により実行される/etc/systemd/system/setup-ephemeral.service
サービス起動は Chef の cookbook で管理されている
次は Chef の cookbook はどの時点でコンピュートノードに保存されていたのか確認します。
コンピュートノードのユーザーデータ
コンピュートノードの起動時の設定は起動テンプレートに入っています。起動テンプレートのユーザーデータで何を実行しているのか確認します。起動テンプレートの作成自体はpcluster create-cluster
実行時に CDK(CloudFormation)によって起動テンプレートを作成されています。
ユーザーデータの内容から以下のことがわかりました。
- ParallelCluster 用の cookbook をダウンロードします
- Chef は localmode で実行します
ParallelCluster の cookbook については事前に保存してあるわけではなく、コンピュートノードが起動する都度ダウンロードして展開していることがわかりました。
この先は Chef 動作に詳しくないため正しく理解できていないのですが...
最初の方に確認していた/etc/chef/cookbooks/aws-parallelcluster-config/recipes/base.rb
にインスタンスストアをマウントするサービスを起動するレシピがありましたので、aws-parallelcluster::config
の箇所で実行していものかと思います。
UserData 抜粋
--- snip ---
curl --retry 3 -v -L -o /etc/chef/aws-parallelcluster-cookbook.tgz ${cookbook_url}
--- snip ---
jq --argfile f1 /tmp/dna.json --argfile f2 /tmp/extra.json -n '$f1 * $f2' > /etc/chef/dna.json || ( echo "jq not installed or invalid extra_json"; cp /tmp/dna.json /etc/chef/dna.json)
{
pushd /etc/chef &&
cinc-client --local-mode --config /etc/chef/client.rb --log_level info --force-formatter --no-color --chef-zero-port 8889 --json-attributes /etc/chef/dna.json --override-runlist aws-parallelcluster::init &&
/opt/parallelcluster/scripts/fetch_and_run -preinstall &&
cinc-client --local-mode --config /etc/chef/client.rb --log_level info --force-formatter --no-color --chef-zero-port 8889 --json-attributes /etc/chef/dna.json --override-runlist aws-parallelcluster::config &&
/opt/parallelcluster/scripts/fetch_and_run -postinstall &&
cinc-client --local-mode --config /etc/chef/client.rb --log_level info --force-formatter --no-color --chef-zero-port 8889 --json-attributes /etc/chef/dna.json --override-runlist aws-parallelcluster::finalize &&
popd
} || error_exit 'Failed to run bootstrap recipes. If --norollback was specified, check /var/log/cfn-init.log and /var/log/cloud-init-output.log.'
--- snip ---
ユーザーデータ全文は長いため折りたたんであります。
折りたたみ
Content-Type: multipart/mixed; boundary="==BOUNDARY=="
MIME-Version: 1.0
--==BOUNDARY==
Content-Type: text/cloud-boothook; charset="us-ascii"
MIME-Version: 1.0
#!/bin/bash -x
which dnf 2>/dev/null; dnf=$?
which yum 2>/dev/null; yum=$?
if [ "${dnf}" == "0" ]; then
echo "proxy=" >> /etc/dnf/dnf.conf
elif [ "${yum}" == "0" ]; then
echo "proxy=_none_" >> /etc/yum.conf
else
echo "Not yum system"
fi
which apt-get && echo "Acquire::http::Proxy \"false\";" >> /etc/apt/apt.conf || echo "Not apt system"
proxy=NONE
if [ "${proxy}" != "NONE" ]; then
proxy_host=$(echo "${proxy}" | awk -F/ '{print $3}' | cut -d: -f1)
proxy_port=$(echo "${proxy}" | awk -F/ '{print $3}' | cut -d: -f2)
echo -e "[Boto]\nproxy = ${proxy_host}\nproxy_port = ${proxy_port}\n" >/etc/boto.cfg
cat >> /etc/profile.d/proxy.sh <<PROXY
export http_proxy="${proxy}"
export https_proxy="${proxy}"
export no_proxy="localhost,127.0.0.1,169.254.169.254"
export HTTP_PROXY="${proxy}"
export HTTPS_PROXY="${proxy}"
export NO_PROXY="localhost,127.0.0.1,169.254.169.254"
PROXY
fi
--==BOUNDARY==
Content-Type: text/cloud-config; charset=us-ascii
MIME-Version: 1.0
package_update: false
package_upgrade: false
repo_upgrade: none
datasource_list: [ Ec2, None ]
output:
all: "| tee -a /var/log/cloud-init-output.log | logger -t user-data -s 2>/dev/console"
write_files:
- path: /tmp/dna.json
permissions: '0644'
owner: root:root
content: |
{
"cluster": {
"stack_name": "I4iParallelcluster",
"enable_efa": "NONE",
"raid_parameters": "NONE,NONE,NONE,NONE,NONE,NONE,NONE,NONE,NONE",
"base_os": "ubuntu2004",
"preinstall": "NONE",
"preinstall_args": "NONE",
"postinstall": "s3://hpc-dev-postinstall-files/sample-ubuntu-docker/postinstall.sh",
"postinstall_args": "NONE",
"region": "us-east-2",
"efs_fs_id": "NONE",
"efs_shared_dir": "NONE,NONE,NONE,NONE,NONE,NONE,NONE,NONE,NONE",
"fsx_fs_id": "NONE",
"fsx_mount_name": "",
"fsx_dns_name": "",
"fsx_options": "NONE,NONE,NONE,NONE,NONE,NONE,NONE,NONE,NONE,NONE,NONE,NONE,NONE,NONE,NONE,NONE,NONE",
"scheduler": "slurm",
"disable_hyperthreading_manually": "false",
"ephemeral_dir": "/scratch",
"ebs_shared_dirs": "NONE,NONE,NONE,NONE,NONE",
"proxy": "NONE",
"ddb_table": "parallelcluster-I4iParallelcluster",
"log_group_name": "/aws/parallelcluster/I4iParallelcluster-202205090742",
"dns_domain": "i4iparallelcluster.pcluster.",
"hosted_zone": "Z02054841BVWJ1BFSLAC3",
"node_type": "ComputeFleet",
"cluster_user": "ubuntu",
"enable_intel_hpc_platform": "false",
"cw_logging_enabled": "true",
"scheduler_queue_name": "i4i",
"scheduler_compute_resource_name": "large8x",
"enable_efa_gdr": "NONE",
"custom_node_package": "",
"custom_awsbatchcli_package": "",
"use_private_hostname": "false",
"head_node_private_ip": "10.0.1.12",
"directory_service": {
"enabled": "false"
}
}
}
- path: /etc/chef/client.rb
permissions: '0644'
owner: root:root
content: cookbook_path ['/etc/chef/cookbooks']
- path: /tmp/extra.json
permissions: '0644'
owner: root:root
content: |
{}
--==BOUNDARY==
Content-Type: text/x-shellscript; charset="us-ascii"
MIME-Version: 1.0
#!/bin/bash -x
function error_exit
{
echo "Bootstrap failed with error: $1"
# wait logs flush before signaling the failure
sleep 10
# TODO: add possibility to override this behavior and keep the instance for debugging
shutdown -h now
exit 1
}
function vendor_cookbook
{
mkdir /tmp/cookbooks
cd /tmp/cookbooks
tar -xzf /etc/chef/aws-parallelcluster-cookbook.tgz
HOME_BAK="${HOME}"
export HOME="/tmp"
for d in `ls /tmp/cookbooks`; do
cd /tmp/cookbooks/$d
LANG=en_US.UTF-8 /opt/cinc/embedded/bin/berks vendor /etc/chef/cookbooks --delete || error_exit 'Vendoring cookbook failed.'
done;
export HOME="${HOME_BAK}"
}
[ -f /etc/profile.d/proxy.sh ] && . /etc/profile.d/proxy.sh
custom_cookbook=NONE
export _region=us-east-2
s3_url=amazonaws.com
if [ "${custom_cookbook}" != "NONE" ]; then
if [[ "${custom_cookbook}" =~ ^s3://([^/]*)(.*) ]]; then
bucket_region=$(aws s3api get-bucket-location --bucket ${BASH_REMATCH[1]} | jq -r '.LocationConstraint')
if [[ "${bucket_region}" == null ]]; then
bucket_region="us-east-1"
fi
cookbook_url=$(aws s3 presign "${custom_cookbook}" --region "${bucket_region}")
else
cookbook_url=${custom_cookbook}
fi
fi
export PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/opt/aws/bin
export parallelcluster_version=aws-parallelcluster-3.1.3
export cookbook_version=aws-parallelcluster-cookbook-3.1.3
export chef_version=17.2.29
export berkshelf_version=7.2.0
if [ -f /opt/parallelcluster/.bootstrapped ]; then
installed_version=$(cat /opt/parallelcluster/.bootstrapped)
if [ "${cookbook_version}" != "${installed_version}" ]; then
error_exit "This AMI was created with ${installed_version}, but is trying to be used with ${cookbook_version}. Please either use an AMI created with ${cookbook_version} or change your ParallelCluster to ${installed_version}"
fi
else
error_exit "This AMI was not baked by ParallelCluster. Please use pcluster createami command to create an AMI by providing your AMI as parent image."
fi
if [ "${custom_cookbook}" != "NONE" ]; then
curl --retry 3 -v -L -o /etc/chef/aws-parallelcluster-cookbook.tgz ${cookbook_url}
vendor_cookbook
fi
cd /tmp
mkdir -p /etc/chef/ohai/hints
touch /etc/chef/ohai/hints/ec2.json
jq --argfile f1 /tmp/dna.json --argfile f2 /tmp/extra.json -n '$f1 * $f2' > /etc/chef/dna.json || ( echo "jq not installed or invalid extra_json"; cp /tmp/dna.json /etc/chef/dna.json)
{
pushd /etc/chef &&
cinc-client --local-mode --config /etc/chef/client.rb --log_level info --force-formatter --no-color --chef-zero-port 8889 --json-attributes /etc/chef/dna.json --override-runlist aws-parallelcluster::init &&
/opt/parallelcluster/scripts/fetch_and_run -preinstall &&
cinc-client --local-mode --config /etc/chef/client.rb --log_level info --force-formatter --no-color --chef-zero-port 8889 --json-attributes /etc/chef/dna.json --override-runlist aws-parallelcluster::config &&
/opt/parallelcluster/scripts/fetch_and_run -postinstall &&
cinc-client --local-mode --config /etc/chef/client.rb --log_level info --force-formatter --no-color --chef-zero-port 8889 --json-attributes /etc/chef/dna.json --override-runlist aws-parallelcluster::finalize &&
popd
} || error_exit 'Failed to run bootstrap recipes. If --norollback was specified, check /var/log/cfn-init.log and /var/log/cloud-init-output.log.'
if [ ! -f /opt/parallelcluster/.bootstrapped ]; then
echo ${cookbook_version} | tee /opt/parallelcluster/.bootstrapped
fi
# End of file
--==BOUNDARY==
おまけ Pre / Postinstall 処理の実態
ユーザーデータで cookbook と同時に実行される/opt/parallelcluster/scripts/fetch_and_run
スクリプトが気になったので確認しました。
UserData抜粋
/opt/parallelcluster/scripts/fetch_and_run -preinstall &&
/opt/parallelcluster/scripts/fetch_and_run -postinstall &&
コンフィグファイルが別途あり、Postinstallで指定した S3 バケットとスクリプト名が書き込まれていました。Preinstall は使用していないため Postinstall について確認します。
/etc/parallelcluster/cfnconfig
stack_name=I4iParallelcluster
cfn_preinstall=NONE
cfn_preinstall_args=(NONE)
cfn_postinstall=s3://hpc-dev-postinstall-files/sample-ubuntu-docker/postinstall.sh
cfn_postinstall_args=(NONE)
cfn_region=us-east-2
cfn_scheduler=slurm
cfn_scheduler_slots=vcpus
cfn_instance_slots=16
cfn_ephemeral_dir=/scratch
cfn_ebs_shared_dirs=NONE,NONE,NONE,NONE,NONE
cfn_proxy=NONE
cfn_node_type=ComputeFleet
cfn_cluster_user=ubuntu
cfn_head_node=ip-10-0-1-12
cfn_head_node_private_ip=10.0.1.12
cfn_scheduler_queue_name=i4i
対象が S3 バケットなら aws s3 cp でダウンロードし、実行権限を付与してスクリプトを実行していました。
/opt/parallelcluster/scripts/fetch_and_run
#!/bin/bash
cfnconfig_file="/etc/parallelcluster/cfnconfig"
. ${cfnconfig_file}
# Check expected variables from cfnconfig file
function check_params () {
if [ -z "${cfn_region}" ] || [ -z "${cfn_preinstall}" ] || [ -z "${cfn_preinstall_args}" ] || [ -z "${cfn_postinstall}" ] || [ -z "${cfn_postinstall_args}" ]; then
error_exit "One or more required variables from ${cfnconfig_file} file are undefined"
fi
}
# Error exit function
function error_exit () {
script=`basename $0`
echo "parallelcluster: ${script} - $1"
logger -t parallelcluster "${script} - $1"
exit 1
}
function download_run (){
url=$1
shift
scheme=$(echo "${url}"| cut -d: -f1)
tmpfile=$(mktemp)
trap "/bin/rm -f $tmpfile" RETURN
if [ "${scheme}" == "s3" ]; then
/opt/parallelcluster/pyenv/versions/3.7.10/envs/cookbook_virtualenv/bin/aws --region ${cfn_region} s3 cp ${url} - > $tmpfile || return 1
else
wget -qO- ${url} > $tmpfile || return 1
fi
chmod +x $tmpfile || return 1
$tmpfile "$@" || error_exit "Failed to run ${ACTION}, ${file} failed with non 0 return code: $?"
}
function run_preinstall () {
if [ "${cfn_preinstall}" != "NONE" ]; then
file="${cfn_preinstall}"
if [ "${cfn_preinstall_args}" != "NONE" ]; then
download_run ${cfn_preinstall} "${cfn_preinstall_args[@]}"
else
download_run ${cfn_preinstall}
fi
fi || error_exit "Failed to run preinstall"
}
function run_postinstall () {
RC=0
if [ "${cfn_postinstall}" != "NONE" ]; then
file="${cfn_postinstall}"
if [ "${cfn_postinstall_args}" != "NONE" ]; then
download_run ${cfn_postinstall} "${cfn_postinstall_args[@]}"
else
download_run ${cfn_postinstall}
fi
fi || error_exit "Failed to run postinstall"
}
check_params
ACTION=${1#?}
case ${ACTION} in
preinstall)
run_preinstall
;;
postinstall)
run_postinstall
;;
*)
echo "Unknown action. Exit gracefully"
exit 0
esac
Postinstall 処理の実態は/opt/parallelcluster/scripts/fetch_and_run
のスクリプトが実行されて、そこからユーザーが用意したスクリプトを実行する流れだったんですね。
確認結果
インスタンスストアがマウントされるまで過程
pcluster create-cluster
により CDK(CloudFormation)で起動テンプレートが作成される- 起動テンプレートのユーザーデータにコンピュートノードの起動処理が書き込まれている
- コンピュートノードが起動するとユーザーデータに従い Chef の cookbook のダウンロードと実行が走る
- cookbook の実行で
/etc/systemd/system/setup-ephemeral.service
サービスが起動する /etc/systemd/system/setup-ephemeral.service
サービスが起動すると/usr/local/sbin/setup-ephemeral-drives.sh
が実行される/usr/local/sbin/setup-ephemeral-drives.sh
の実行によりインスタンスストアのマウントが行われる
おわりに
AWS ParallelCluster というフレームワークを使うことで手軽にクラウドHPC 環境を手に入れることができます。裏側ではないをやってくれているのか?を確認してみるといろいろと発見がありました。今回は興味本位でインスタンスストアのマウント処理を調べていので面白かったので良いのですが、調べるのに半日かかりましたのでほどほどにですね。
トラブルシュートするときには一見知らなくて良さそうな知識がいきてくるはずので ParallelClusterで トラブったら呼んでください。なにかお力になれるかもしれません。
最後にインスタンスストアがマウントされるパス/scratch
は変更可能です。こちらはドキュメントに載っています。
Scheduling section - AWS ParallelCluster