3分でわかる，Shell Script/Dockerfileの可読性を上げるヒアドキュメントの使い方

## これは何?
shell scriptやDockerfile等を書く時にヒアドキュメントを使うと完結かつ，わかりやすく書くことができます。

例: ヒアドキュメントを使わないDockerfileのRUN

```
RUN apt-get update -y && \
    apt-get install -y --no-install-recommends sudo && \
    echo 'Creating ${USER_NAME} group.' && \
    addgroup ${USER_NAME} && \
    echo 'Creating ${USER_NAME} user.' && \
    adduser --ingroup ${USER_NAME} --gecos "my_portscanner user" --shell /bin/bash --no-create-home --disabled-password ${USER_NAME} && \
    echo 'using sudo' && \
    usermod -aG sudo ${USER_NAME} && \
    echo "${USER_NAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers && \
    rm -rf /var/lib/apt/lists/*

```
例: ヒアドキュメントを使ったDockerfileのRUN

```
RUN <<EOF
apt-get update -y
apt-get install -y --no-install-recommends sudo
echo 'Creating ${USER_NAME} group.'
addgroup ${USER_NAME}
echo 'Creating ${USER_NAME} user.'
adduser --ingroup ${USER_NAME} --gecos "my_portscanner user" --shell /bin/bash --no-create-home --disabled-password ${USER_NAME}
echo 'using sudo'
usermod -aG sudo ${USER_NAME}
echo "${USER_NAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
rm -rf /var/lib/lists
EOF
```
見た目がごちゃごちゃしないかつ，\や&&のつけ忘れでエラーになるみたいな事故を防げて便利です。
本記事ではヒアドキュメントの使い方を紹介します。

---

## catコマンドでつかう

最も有名な例はcatコマンドでヒアドキュメントを使うことです。

```shell
cat <<EOF
heredoc> hoge
heredoc> fuga
heredoc> EOF
hoge
fuga
```
こういった形でEOFを入力するまで値を連続して入力することができます。

このヒアドキュメントを使うことでシェルスクリプト中でのファイルを生成して値を書き込む処理が簡潔にかけます。
以下の例はAWSのEC2のUserDataを使って起動時にdocker.httpdを起動するためのserviceファイルを作成しています。

```shell
#!/bin/bash
yum install -y docker
cat <<'EOF' > /etc/systemd/system/docker.httpd.service
[Unit]
Description=httpd Container
After=docker.service
Requires=docker.service
[Service]
Environment="CONTAINER_NAME=httpd-container"
TimeoutStartSec=0
Restart=always
ExecStartPre=-/usr/bin/docker stop ${CONTAINER_NAME}
ExecStartPre=-/usr/bin/docker rm ${CONTAINER_NAME}
ExecStartPre=/usr/bin/docker pull httpd
ExecStart=/usr/bin/docker run --rm --name ${CONTAINER_NAME} -p 80:80 httpd
[Install]
WantedBy=multi-user.target
EOF
systemctl enable --now docker.httpd
systemctl restart docker.httpd
systemctl daemon-reload
```

:::note info
EOFを''で囲むことによりヒアドキュメント中での変数展開が行われないようにしています。
こちらの質問で@uasiさんに教えていただきました。
:::

https://qiita.com/sigma_devsecops/questions/d578850419c56829f950

---

## Dockerfileで使う

上記にも示したように，RUNで使えます。

```
RUN <<EOF
set -ex
apt-get update -y
apt-get install -y --no-install-recommends sudo
echo 'Creating ${USER_NAME} group.'
addgroup ${USER_NAME}
echo 'Creating ${USER_NAME} user.'
adduser --ingroup ${USER_NAME} --gecos "my_portscanner user" --shell /bin/bash --no-create-home --disabled-password ${USER_NAME}
echo 'using sudo'
usermod -aG sudo ${USER_NAME}
echo "${USER_NAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
rm -rf /var/lib/lists
EOF
```
:::note info
コメント欄で@ko1nksmさんにお教えいただいたのですが，
ヒアドキュメントのRUNの冒頭で`set -ex`することでデバックがしやすくなるので追記しておきます。
- `-x`でコマンドが実行する前にデバック情報を出してくれます。

  ```
  # -xなし
  5.082 6 packages can be upgraded. Run 'apt list --upgradable' to see them.
  5.085
  5.085 WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
  ```

  ```
  # -xあり
  3.654 6 packages can be upgraded. Run 'apt list --upgradable' to see them.
  3.655 + apt install -y sud
  3.657
  3.657 WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
  ```

- `-e`でエラーが発生した際に途中でbuildを止めてくれます。
    - `-e`なし --> 途中でエラーが発生しても最後のコマンドが成功していれば止まらない

    ```dockerfile
    FROM python:3.12.4-slim-bullseye
    RUN <<EOF
    apt update -y
    apt-get install -y --no-install-recommends sud # わざとタイポした
    ls # 成功するコマンド
    EOF
    ```

    ```shell
    docker buildx build .
    [+] Building 1.2s (6/6) FINISHED                                                                                                   docker:default
     => [internal] load build definition from Dockerfile                                                                                         0.0s
     => => transferring dockerfile: 148B                                                                                                         0.0s
     => [internal] load metadata for docker.io/library/python:3.12.4-slim-bullseye                                                               1.0s
     => [internal] load .dockerignore                                                                                                            0.0s
     => => transferring context: 2B                                                                                                              0.0s
     => [1/2] FROM docker.io/library/python:3.12.4-slim-   bullseye@sha256:26ce493641ad3b1c8a6202117c31340c7bbb2dc126f1aeee8ea3972730a81dc6         0.0s
     => CACHED [2/2] RUN <<EOF (apt update -y...)                                                                                                0.0s
     => exporting to image                                                                                                                       0.0s
     => => exporting layers                                                                                                                      0.0s
     => => writing image sha256:fc59842d7a59c1937163ce3712d71d2d56fd4fe76a7649f5f9665c7d7ef67e9a                                               0.0s
    ```
    - `-e`あり --> 途中でエラーが発生した段階で止まる。

    ```shell
    docker buildx build .
     > [2/2] RUN <<EOF (set -e...):
    0.192
    0.192 WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
    0.192
    0.342 Get:1 http://deb.debian.org/debian bullseye InRelease [116 kB]
    0.373 Get:2 http://deb.debian.org/debian-security bullseye-security InRelease [27.2 kB]
    0.386 Get:3 http://deb.debian.org/debian bullseye-updates InRelease [44.1 kB]
    0.430 Get:4 http://deb.debian.org/debian bullseye/main amd64 Packages [8066 kB]
    1.644 Get:5 http://deb.debian.org/debian-security bullseye-security/main amd64 Packages [287 kB]
    1.679 Get:6 http://deb.debian.org/debian bullseye-updates/main amd64 Packages [18.8 kB]
    2.506 Fetched 8559 kB in 2s (3711 kB/s)
    2.506 Reading package lists...
    2.843 Building dependency tree...
    2.929 Reading state information...
    2.938 6 packages can be upgraded. Run 'apt list --upgradable' to see them.
    2.944 Reading package lists...
    3.260 Building dependency tree...
    3.344 Reading state information...
    3.394 E: Unable to locate package sud
    ------
    Dockerfile:2
    --------------------
       1 |     FROM python:3.12.4-slim-bullseye
       2 | >>> RUN <<EOF
       3 | >>> set -e
       4 | >>> apt update -y
       5 | >>> apt-get install -y --no-install-recommends sud
       6 | >>> ls
       7 | >>> EOF
       8 |
    --------------------
    ERROR: failed to solve: process "/bin/sh -c set -e\napt update -y\napt-get install -y --no-install-recommends sud\nls\n" did not complete successfully: exit code: 100
    ```
追記: コメントで教えていただいたのですが，

```dockerfile
RUN <<EOF bash -ex
```
のように書けるのでこっちのほうが本質的な処理が`RUN`の中に入るので可読性が高そうです。

:::


---

## Terraformで使う

terraformでuser_dataに対してコマンドを投入する際にヒアドキュメントが使えます。
ヒアドキュメントを入れ子で使う際にはEOF2を使います。

```terraformf
resource "aws_instance" "example" {
  ami           = "ami-00c79d83cf718a893"
  instance_type = "t3.micro"
  # attach IAM Role
  iam_instance_profile = aws_iam_instance_profile.ssm_instance_profile.name

  tags = {
    Name = "sigma-ec2"
  }

  user_data = <<EOF
#!/bin/bash
yum install -y docker
cat <<'EOF2' > /etc/systemd/system/docker.httpd.service
[Unit]
Description=httpd Container
After=docker.service
Requires=docker.service
[Service]
Environment="CONTAINER_NAME=httpd-container"
TimeoutStartSec=0
Restart=always
ExecStartPre=-/usr/bin/docker stop $${CONTAINER_NAME}
ExecStartPre=-/usr/bin/docker rm $${CONTAINER_NAME}
ExecStartPre=/usr/bin/docker pull httpd
ExecStart=/usr/bin/docker run --rm --name $${CONTAINER_NAME} -p 80:80 httpd
[Install]
WantedBy=multi-user.target
EOF2
systemctl enable --now docker.httpd
systemctl restart docker.httpd
systemctl daemon-reload
EOF
}
```

:::note info
Terraformが`$`を解釈しないように`$${CONTAINER_NAME}`のようにしてにしてescapeしています。
EOF2を''で囲んでいるのは上記と同じで変数展開をしないためです。
:::

---

## 総括

- ヒアドキュメントを使うことで見た目がスタイリッシュで可読性が高くなるのでおすすめ。
- 他にも使える例を探してみたい。