AWS ParallelCluster インスタンスストアを自動マウントする仕組みを調べてみた
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
にマウントしています。
--- 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
とはなにかを探しました。
[2022-05-14T09:56:02+00:00] INFO: service[setup-ephemeral] enabled
Chef の cookbook がコンピュートノードの以下のパスに保存されており、その中の receipe に該当の設定がありました。
パス: /etc/chef/cookbooks/aws-parallelcluster-config/recipes/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
を実行するサービスでした。
[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
の箇所で実行していものかと思います。
--- 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
スクリプトが気になったので確認しました。
/opt/parallelcluster/scripts/fetch_and_run -preinstall && /opt/parallelcluster/scripts/fetch_and_run -postinstall &&
コンフィグファイルが別途あり、Postinstallで指定した S3 バケットとスクリプト名が書き込まれていました。Preinstall は使用していないため Postinstall について確認します。
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 でダウンロードし、実行権限を付与してスクリプトを実行していました。
#!/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