このような問題は、Terraformでのサーバー初期化設計を見直すことで解決できます。
この記事では、user_dataブロックとcloud-initを組み合わせたEC2初期化設計を実践的に解説します。provisionerが持つ構造的な問題点の整理から、HCLでのheredoc・templatefile()関数・base64encode()の使い方、cloud-config形式のYAML実践例、初期化ログの確認方法、よくあるトラブルの対処まで、本番で使えるレベルで体系化します。
この記事のポイント
・user_dataでEC2起動時にcloud-init処理を実行するのがprovisionerより安全な設計
・HCLのheredocまたはtemplatefile()で外部cloud-config YAMLを渡せる
・変更を反映するにはEC2の再作成(replace)が必要な点に注意する
・実行結果は /var/log/cloud-init-output.log で確認できる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜprovisionerではなくuser_dataなのか
Terraformのprovisionerは「インフラ構築ツールとしてのTerraformが本来カバーしない領域を埋める最後の手段」とHashiCorpが公式に位置づけています。それにもかかわらず、実務では安易に使われがちです。provisionerが抱える問題を3つ整理します。・SSH/WinRM依存:`remote-exec` provisionerはSSH(またはWinRM)で対象EC2に接続し、コマンドを実行します。EC2が起動してから接続可能になるまでのタイミング、セキュリティグループの22番ポート開放、鍵の管理など、インフラ側の設定が複雑になります。自分のPCからSSHが届かないCI/CD環境では使い物になりません
・Terraformの状態管理外になる:provisionerの実行結果はtfstateに記録されません。applyが途中で失敗するとリソースはtainted状態になり、次回のapplyで強制再作成が走ります。「何を実行したか」がコードではなく人の記憶に依存する状態になります
・冪等性が保証されない:provisionerは「applyを実行したときに一度だけ動く」設計であり、同じスクリプトを何度実行しても同じ結果になるかどうかはスクリプト自体の実装に委ねられます。Infrastructure as Codeの原則と相性が悪い仕組みです
一方、user_dataはEC2インスタンスが初回起動した時にIMDS(Instance Metadata Service)を経由してcloud-initに渡され、OS起動プロセスの中で自動実行されます。SSH接続は不要で、実行ログは `/var/log/cloud-init-output.log` に記録されます。Terraform planの対象にもなるため、変更の意図が可視化されます。
user_dataブロックの基本構造とHCL記法
1. heredocでcloud-configを直接書く
最もシンプルな書き方は、`aws_instance` リソースの `user_data` 引数にheredocで直接cloud-configを記述する方法です。# main.tf resource "aws_instance" "web" { ami = "ami-0c3fd0f5d33134a76" # Amazon Linux 2023 instance_type = "t3.micro" subnet_id = aws_subnet.public.id user_data = <<-EOF #!/bin/bash yum update -y yum install -y nginx systemctl enable nginx systemctl start nginx echo "初期化完了: $(date)" >> /var/log/my-init.log EOF tags = { Name = "web-server" } }
2. templatefile()関数で外部ファイルを参照する
スクリプトが長くなったり、変数を埋め込みたい場合は `templatefile()` 関数を使います。コードとテンプレートを分離することで、HCLファイルの可読性が大きく改善されます。# main.tf resource "aws_instance" "web" { ami = "ami-0c3fd0f5d33134a76" instance_type = "t3.micro" user_data = templatefile("${path.module}/templates/cloud-config.yaml", { hostname = "web-01.example.com" environment = var.environment packages = ["nginx", "git", "jq"] }) }
3. base64encode()を明示的に使う場面
AWS APIの仕様上、user_dataはBase64エンコードされて渡されます。`aws_instance` リソースはTerraformが自動でBase64エンコードするため通常は不要です。ただし `aws_launch_template` を使う場合は `base64encode()` を明示的に呼ぶ必要があります。# aws_launch_templateを使う場合はbase64encode()が必要 resource "aws_launch_template" "web" { name_prefix = "web-lt-" image_id = "ami-0c3fd0f5d33134a76" instance_type = "t3.micro" # aws_instanceと異なり、自動Base64エンコードが行われないため明示が必要 user_data = base64encode(templatefile("${path.module}/templates/cloud-config.yaml", { environment = var.environment })) }
cloud-config形式(#cloud-config)の実践
bashスクリプト形式でも動きますが、`#cloud-config` で始まるYAML形式にするとcloud-initの各モジュールが活用できるため、よりIaC的な書き方になります。1. cloud-configのYAML基本構造
#cloud-config # ファイル: templates/cloud-config.yaml # パッケージの更新(アップグレードはしない) package_update: true package_upgrade: false # インストールするパッケージ(OSのパッケージマネージャで処理) packages: - nginx - git - jq - python3 # ファイルの作成・配置 write_files: - path: /etc/nginx/conf.d/app.conf permissions: '0644' content: | server { listen 80; server_name ${hostname}; location / { root /var/www/html; } } # コマンドの実行(パッケージインストール後に実行される) runcmd: - systemctl enable nginx - systemctl start nginx - hostnamectl set-hostname ${hostname} - echo "${environment} 環境の初期化完了" >> /var/log/cloud-init-app.log
・packages:yum/aptで自動インストール。OSの種別検出はcloud-initが自動で行う
・write_files:ファイルパス・パーミッション・内容を宣言的に定義。bashの `cat >>` よりも安全かつ冪等
・runcmd:パッケージインストール完了後に実行されるコマンドリスト。初回起動時に一度だけ実行される
2. パッケージ・ファイル作成・コマンドを組み合わせた実例
本番環境でよく使うパターンとして、アプリケーションユーザーの作成とディレクトリ構成の初期化を組み合わせた例を示します。#cloud-config packages: - nginx - amazon-cloudwatch-agent # アプリケーション実行ユーザーの作成 users: - name: appuser groups: [nginx] shell: /bin/bash sudo: false write_files: - path: /opt/app/config.json permissions: '0640' owner: appuser:appuser content: | { "env": "${environment}", "log_level": "info" } runcmd: - mkdir -p /var/www/html /var/log/app - chown -R appuser:appuser /var/www/html /var/log/app - systemctl enable nginx amazon-cloudwatch-agent - systemctl start nginx amazon-cloudwatch-agent
3. 初期化ログで実行結果を確認する
user_data / cloud-initの実行結果は以下の2ファイルで確認できます。EC2にSSHしてtailするか、CloudWatch Logs エージェントを通じて集約するのが実務では一般的です。・/var/log/cloud-init-output.log:runcmdで実行した各コマンドの標準出力・標準エラーが記録される。初期化に失敗した場合はここを最初に確認する
・/var/log/cloud-init.log:cloud-init自体の動作ログ。モジュールの実行順序・エラーが詳細に記録される
実際のログ出力例(Amazon Linux 2023環境)は以下のようになります。
# /var/log/cloud-init-output.log の抜粋 Cloud-init v. 23.1.1 running 'modules:final' at Thu, 10 Jul 2025 10:23:41 +0000. Running module runcmd with frequency always. 2025-07-10 10:23:42 - cc_runcmd.py[DEBUG]: Running ['systemctl', 'enable', 'nginx'] Created symlink /etc/systemd/system/multi-user.target.wants/nginx.service -> /usr/lib/systemd/system/nginx.service. 2025-07-10 10:23:43 - cc_runcmd.py[DEBUG]: Running ['systemctl', 'start', 'nginx'] 2025-07-10 10:23:44 - prod 環境の初期化完了 Cloud-init v. 23.1.1 finished at Thu, 10 Jul 2025 10:23:44 +0000. Datasource DataSourceEc2. Up 63.42 seconds
>> Terraform実践セミナーの詳細はこちら
よくあるトラブルと対処法
1. user_dataが実行されない
よくある原因と対処を3つ挙げます。・IMDSv2の設定問題:EC2メタデータサービスがIMDSv2のみ許可している場合、cloud-initはPUTリクエストでトークンを取得してからメタデータを取得します。cloud-initのバージョンが古いとIMDSv2に対応していないことがあります。Amazon Linux 2023では問題ありません。Terraformでは `metadata_options` ブロックで `http_tokens = "required"` を指定している場合は、AMIのcloud-initバージョンを確認してください
・user_dataのサイズ超過:user_dataは16KBが上限です。スクリプトが大きい場合はS3にスクリプト本体を置き、user_dataでは `curl` でダウンロード → 実行する設計に切り替えます
・YAML構文エラー:cloud-config形式の場合、YAMLの構文エラーはcloud-initが起動時に検出します。`cloud-init devel schema --config-file cloud-config.yaml` でローカル検証が可能です
2. 設定を変更してもEC2に反映されない
これはuser_dataの仕様に起因する、最もよく誤解されるポイントです。user_dataはEC2インスタンスの初回起動時にのみ実行されます。Terraformで `user_data` を変更して `terraform apply` しても、既存のEC2インスタンスは再作成されません(Terraformはこれを変更と認識しますが、インスタンスはそのまま稼働し続けます)。
変更を反映するには、インスタンスを強制的に再作成する必要があります。
# user_dataの変更時にEC2を自動で再作成させる設定(AWS Provider 4.x以降) resource "aws_instance" "web" { ami = "ami-0c3fd0f5d33134a76" instance_type = "t3.micro" user_data = templatefile("${path.module}/templates/cloud-config.yaml", { environment = var.environment }) # user_dataが変わった場合にEC2を再作成(destroy > create)する user_data_replace_on_change = true } # または手動でtaint > apply # terraform taint aws_instance.web # terraform apply
3. runcmdが部分実行で止まる
`runcmd` の各コマンドはリストの順に実行されますが、一つのコマンドが非ゼロの終了コードを返すと後続が実行されないことがあります。・原因:パッケージのインストールが途中で失敗している、依存サービスがまだ起動していないタイミングで `systemctl start` を実行しているなど
・対処:`/var/log/cloud-init-output.log` でどのコマンドが失敗したかを確認する。問題のあるコマンドを `- bash -c "コマンド || true"` で包んで継続実行するか、cloud-initの `bootcmd` モジュールでより早いフェーズに実行タイミングを移動させる
・予防策:runcmdのコマンドは副作用が少なく、失敗しても安全なものにとどめる。複雑なロジックはS3に置いたシェルスクリプトにまとめてruncmdから呼び出す設計が保守しやすい
本記事のまとめ
| やりたいこと | 方法 |
|---|---|
| EC2初期化スクリプトをHCLに直接書く | user_data = <<-EOF ... EOF(heredoc形式) |
| テンプレートファイルから変数を埋め込む | templatefile("${path.module}/templates/init.yaml", {...}) |
| launch_templateでBase64エンコードが必要な場合 | base64encode(templatefile(...)) |
| cloud-initでパッケージをインストールする | cloud-configの packages: セクションに記述 |
| cloud-initでファイルを配置する | cloud-configの write_files: セクションに記述 |
| 初期化が成功したか確認する | /var/log/cloud-init-output.log を確認 |
| user_data変更を既存EC2に反映する | インスタンスを再作成(user_data_replace_on_change = true) |
LinuxサーバーのOS設定・パッケージ管理・ネットワーク設計を体系的に押さえたい方には、ATA実践セミナーも合わせてご検討ください。
・ATA実践セミナーの詳細はこちら(Linux + Terraform 運用の体系的なスキルアップ)
>> Terraform実践セミナーの詳細はこちら
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
登録10秒/合わなければ解除3秒 / 詳細はこちら

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は1分、解除もいつでも可。
登録無料・いつでも解除できます