追加でアタッチした EBS ボリュームをマウントする EC2 Image Builder のコンポーネントを作成する

こんにちは！インサイトテクノロジーの松尾です。

本投稿では、追加でアタッチした EBS ボリュームをマウントする EC2 Image Builder のコンポーネントについて紹介します。対象OSはRHELです。

## 背景

EC2 Image Builderを使って自社アプリのイメージを作る際に、複数ボリュームをマウントしたイメージを作りたいケースもあると思います。一方、Nitro System 上に構築されたインスタンスでNVMeブロックデバイスとして公開されるEBS ボリュームは、ブート時のデバイスが応答する順序に基づいてデバイスノードが作成されるため、fstabの作成の自動化が面倒だったりします。

https://docs.aws.amazon.com/ja_jp/ebs/latest/userguide/nvme-ebs-volumes.html#identify-nvme-ebs-device

> EBS は、シングルルート I/O 仮想化 (SR-IOV) を使用して、 NVMe仕様を使用して Nitro ベースのインスタンスにボリュームアタッチメントを提供します。これらのデバイスは、オペレーティングシステムの標準NVMeドライバーに依存しています。これらのドライバーは、通常、インスタンスのブート時にアタッチ済みのデバイスを検出し、そのデバイスがブロックデバイスマッピングでどのように指定されているかではなく、デバイスが応答する順序に基づいてデバイスノードを作成します。

EC2起動時にはデバイス名として "sdb" などと指定したのにも関わらず、EC2を起動してみたら "nvme1n1" などのデバイス名が割り振られて、しかも順番がバラバラだった、なんて経験をお持ちの方も多いと思います。

## マッピングを行うシェルが Classmethod さんのブログに！！

まずデバイスの特定(指定したデバイス名 "sdb" などとNVMEのデバイス名 "nvme1n1" とのマッピング)を行うには、nvme-cli を使用する必要があります。このコマンドを使って確認するわけですが、なんと以下の投稿に、fstab の作成まで行ってくれるシェルが公開されていました。ブログでは解説がとても詳しく書いてあり、シェルで実施している内容も大変よく理解できます。素晴らしい！👏

https://dev.classmethod.jp/articles/rhel-block-device-mapping/

マッピングの処理にはこちらのスクリプトを使うことにしましょう。

弊社のアプリケーションではOSのストレージに加え、追加のEBSボリュームが3つ必要ですので、以下のように定義します。

```bash
# マウントポイントのリストアップ
declare -A mount_points=(
    # ["EBSボリュームに指定したデバイス名"]="マウントポイント"
    ["sdb"]="/mnt/piso-data"
    ["sdc"]="/mnt/piso-backup"
    ["sdd"]="/mnt/pg-wal"
)
```

## EC2 Image Builder のコンポーネント

作成するコンポーネントでは以下のような流れで処理をすることになります。
※`map-rhel-block-device.sh`は事前に作成してS3にアップロードしておく

1. S3 からスクリプト(`map-rhel-block-device.sh`)をダウンロード
1. スクリプトに実行権限を付与
1. スクリプトの実行
1. スクリプトファイルの削除 (必要なら)

したがって、コンポーネントの定義としてはこんな感じでできそうなことがわかります。

```yml
name: MapRhelBlockDeviceEC2ImageBuilderComponentDocument
description: This is MapRhelBlockDevice EC2ImageBuilder Component document.
schemaVersion: 1.0

phases:
  - name: build
    steps:
      - name: DownloadShell
        action: S3Download
        maxAttempts: 3
        inputs:
          - source: s3://my-image-builder-test-bucket/map-rhel-block-device.sh
            destination: /tmp/map-rhel-block-device.sh

      - name: MapRhelBlockDeviceStep
        action: ExecuteBash
        inputs:
          commands:
            - |
              sudo dnf install nvme-cli -y
              chmod 755 /tmp/map-rhel-block-device.sh
              /tmp/map-rhel-block-device.sh
              rm -r -f /tmp/map-rhel-block-device.sh

  - name: validate
    steps:
      - name: MapRhelBlockDeviceStep
        action: ExecuteBash
        inputs:
          commands:
            - |
              df -h
              cat /etc/fstab

  - name: test
    steps:
      - name: MapRhelBlockDeviceStep
        action: ExecuteBash
        inputs:
          commands:
            - df -h
```

このコンポーネントの定義を使用して、コンポーネントを登録します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3480312/dd706c58-142f-8438-e71b-6b0939e98b23.png)

なお、エラーについては、現時点では何も勘案していないため、例えば、"sdb" などが定義されていなかった場合の挙動を決めて定義する必要があります。

## レシピの作成とパイプラインの実行

事前に `s3://my-image-builder-test-bucket/map-rhel-block-device.sh` からスクリプトを入手可能に設定します。また、実際のビルドプロセスにおいてS3からファイルダウンロードができるよう `EC2InstanceProfileForImageBuilder` ロールにS3の読み取り権限を追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3480312/a4aa1b0f-0bc2-f1a0-10b4-b8e2c4913ef0.png)

そして定義したコンポーネントを使ってレシピを定義し実行してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3480312/52d717f6-7ad5-445f-6d37-1eb1c64dc6cb.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3480312/8a373b90-1a4c-1116-0ce8-67fd74f26e71.png)

実行結果としては AMI が作成されますので、AMIからEC2インスタンスを起動して確認すると、期待通りマウントされていることを確認できます。

```bash
[ec2-user@ip-172-31-32-120 ~]$ df -h
Filesystem      Size  Used Avail Use% Mounted on
devtmpfs        3.7G     0  3.7G   0% /dev
tmpfs           3.7G     0  3.7G   0% /dev/shm
tmpfs           3.7G   17M  3.7G   1% /run
tmpfs           3.7G     0  3.7G   0% /sys/fs/cgroup
/dev/nvme0n1p3  9.8G  2.9G  7.0G  30% /
/dev/nvme3n1    3.0G   54M  3.0G   2% /mnt/pg-wal
/dev/nvme1n1   1014M   40M  975M   4% /mnt/piso-data
/dev/nvme2n1    2.0G   47M  2.0G   3% /mnt/piso-backup
/dev/nvme0n1p2  200M  5.8M  194M   3% /boot/efi
tmpfs           758M     0  758M   0% /run/user/1000
[ec2-user@ip-172-31-32-120 ~]$ cat /etc/fstab
UUID=9fbfebad-ad19-4692-86f0-696c37b6c4ab       /       xfs     defaults        0       0
UUID=7B77-95E7  /boot/efi       vfat    defaults,uid=0,gid=0,umask=077,shortname=winnt  0       2
UUID=224aac8b-653d-4a33-bdcf-449d799784c2       /mnt/piso-data  xfs     defaults        0       0
UUID=93cbad8f-b486-455f-9f91-b2e6e124525a       /mnt/piso-backup        xfs     defaults        0       0
UUID=de157a04-99d6-4a47-b498-28e10a1a95ec       /mnt/pg-wal     xfs     defaults        0       0
```

## おわりに

本投稿では、nvmeデバイス名と指定したデバイス名とのマッピングスクリプトをEC2 Image Builderから使用することで、EC2 Image Builderで追加ボリュームがある際のマウントとfstabの追記を行うコンポーネントを紹介しました。

本投稿が何かのヒントになれば幸いです。