Terraformのuser_dataとcloud-initでEC2を初期設定する方法|provisionerを避けるサーバー初期化設計

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)Terraform > Terraformのuser_dataとcloud-initでEC2を初期設定する方法|provisionerを避けるサーバー初期化設計
「EC2を作るたびにSSH接続してパッケージインストールや設定ファイルの配置を手動でやっている」「remote-exec provisionerを使っているが、applyのたびにSSH接続失敗でリソースがtainted状態になる」
このような問題は、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 で確認できる


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
図解60P/登録10秒/解除も3秒 / 詳細はこちら

なぜ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" } }

`<<-EOF` の `-` はインデントを除去するTerraformのheredoc構文です。bashスクリプトとして書いても、後述のcloud-config YAML形式として書いても構いません。

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"] }) }

`${path.module}/templates/cloud-config.yaml` は同じモジュールディレクトリ内のテンプレートファイルを指します。テンプレート内では `${hostname}` のようにTerraformのテンプレート構文で変数を参照できます。

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 })) }

Auto Scalingグループと組み合わせる構成ではlaunch_templateを使うことが多いため、この点は覚えておく価値があります。

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

`finished at` の行が出力されていれば cloud-init の処理が完了しています。エラーがあった場合は `CRITICAL` や `ERROR` のキーワードで絞り込むと原因を素早く特定できます。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、user_data・cloud-init設計からprovisionerを使わないEC2初期化パターンの実装まで、ハンズオン形式で体系的に学べるセミナーを開催しています。
>> 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

`user_data_replace_on_change = true` が最も確実です。ただし本番環境では「再作成=ダウンタイム」になるため、Auto ScalingグループのインスタンスリフレッシュやBlue/Greenデプロイと組み合わせることを推奨します。

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
user_data + cloud-init の組み合わせは、provisioner に比べて「インフラの宣言的記述」というTerraformの思想に沿った設計です。EC2の台数が増えても、user_dataをtemplatefile()で管理することでスクリプトを一元管理できます。

LinuxサーバーのOS設定・パッケージ管理・ネットワーク設計を体系的に押さえたい方には、ATA実践セミナーも合わせてご検討ください。
ATA実践セミナーの詳細はこちら(Linux + Terraform 運用の体系的なスキルアップ)
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、user_data設計・cloud-init実践・provisionerを使わないEC2初期化パターンまで、ハンズオン形式で習得できるセミナーを開催しています。
>> Terraform実践セミナーの詳細はこちら

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は1分、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

登録10秒/合わなければ解除3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。