これは何?
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コマンドでヒアドキュメントを使うことです。
cat <<EOF
heredoc> hoge
heredoc> fuga
heredoc> EOF
hoge
fuga
こういった形でEOFを入力するまで値を連続して入力することができます。
このヒアドキュメントを使うことでシェルスクリプト中でのファイルを生成して値を書き込む処理が簡潔にかけます。
以下の例はAWSのEC2のUserDataを使って起動時にdocker.httpdを起動するためのserviceファイルを作成しています。
#!/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
EOFを''で囲むことによりヒアドキュメント中での変数展開が行われないようにしています。
こちらの質問で@uasiさんに教えていただきました。
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
コメント欄で@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
なし --> 途中でエラーが発生しても最後のコマンドが成功していれば止まらない
FROM python:3.12.4-slim-bullseye RUN <<EOF apt update -y apt-get install -y --no-install-recommends sud # わざとタイポした ls # 成功するコマンド EOF
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
あり --> 途中でエラーが発生した段階で止まる。
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
-
追記: コメントで教えていただいたのですが,
RUN <<EOF bash -ex
のように書けるのでこっちのほうが本質的な処理がRUN
の中に入るので可読性が高そうです。
Terraformで使う
terraformでuser_dataに対してコマンドを投入する際にヒアドキュメントが使えます。
ヒアドキュメントを入れ子で使う際にはEOF2を使います。
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
}
Terraformが$
を解釈しないように$${CONTAINER_NAME}
のようにしてにしてescapeしています。
EOF2を''で囲んでいるのは上記と同じで変数展開をしないためです。
総括
- ヒアドキュメントを使うことで見た目がスタイリッシュで可読性が高くなるのでおすすめ。
- 他にも使える例を探してみたい。