Posted on :: 3853 Words :: Tags: , ,

Core Terraform Workflow

HCL構文、変数、そしてstate管理について学びました。では次に、実際のworkflowについて説明します。これはインフラに触れるたびに使う日常的なサイクルです。

これはPart 6 of 12です。この記事を読み終えるころには、必要なコマンド、使うタイミング、そして問題が起きたときのデバッグ方法を理解できます。

📦 Code Examples

Repository: terraform-hcl-tutorial-series This Part: Part 6 - Workflow Examples

動作するサンプルを取得:

git clone https://github.com/khuongdo/terraform-hcl-tutorial-series.git
cd terraform-hcl-tutorial-series
git checkout part-06
cd examples/part-06-workflow/

# workflowを練習
terraform init
terraform fmt
terraform validate
terraform plan

The Terraform Lifecycle

すべてのインフラ変更は、このパターンに従います:

Write → Init → Format → Validate → Plan → Apply → Destroy
   ↑______________________________________________|
              (Repeat for changes)

各ステップには役割があります。一つでもスキップすると、なぜそれが存在するのか困難な方法で理解することになります。

StepWhat It DoesWhat Breaks If You Skip It
Write欲しいものを定義デプロイするものがない
InitProviderをダウンロード、backendをセットアップ"not initialized"でコマンドが失敗
Formatスペースとインデントをクリーンアップ乱雑なdiff、面倒なコードレビュー
Validate実行前に構文チェックplan/apply中のエラー
Plan変更をプレビュー予期しない削除、事故
Apply実際にリソースを作成インフラがない
Destroyすべて削除孤立したリソース、予期しない請求

それでは、各ステップを見ていきましょう。


Step 1: Write Configuration

まず、欲しいものを書きます。基本的なEC2インスタンスのセットアップ例です:

# main.tf
terraform {
  required_version = ">= 1.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

resource "aws_instance" "web" {
  ami           = var.ami_id
  instance_type = var.instance_type

  tags = {
    Name        = "${var.project_name}-web-server"
    Environment = var.environment
  }
}
# variables.tf
variable "aws_region" {
  description = "AWS region for resources"
  type        = string
  default     = "us-east-1"
}

variable "ami_id" {
  description = "AMI ID for EC2 instance"
  type        = string
}

variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t3.micro"
}

variable "project_name" {
  description = "Project name for resource tagging"
  type        = string
}

variable "environment" {
  description = "Environment (dev/staging/prod)"
  type        = string
}
# outputs.tf
output "instance_id" {
  description = "ID of the EC2 instance"
  value       = aws_instance.web.id
}

output "public_ip" {
  description = "Public IP address of the instance"
  value       = aws_instance.web.public_ip
}

Step 2: Initialize the Project

Command: terraform init

これは常に最初に実行するコマンドです。以下のことを行います:

  1. Providerプラグインをダウンロード(AWS、GCPなど)
  2. Backendをセットアップ(stateの保存場所)
  3. .terraformディレクトリを作成
$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.31.0...
- Installed hashicorp/aws v5.31.0 (signed by HashiCorp)

Terraform has been successfully initialized!

initを再実行するタイミング:

  • 新しいproviderを追加した
  • Backend設定を変更した
  • Gitを新しくcloneした
  • .terraform/ディレクトリを削除した

Checkpoint!

terraform initは何度実行しても安全です。必要かどうか分からない場合は、実行してください。


Step 3: Format Your Code

Command: terraform fmt

.tfファイルをTerraformの標準スタイルに自動フォーマットします:

$ terraform fmt
main.tf
variables.tf

修正される内容:

  • インデント(2スペース)
  • =周りのスペース
  • アライメント
  • 末尾の空白

フォーマット前:

resource "aws_instance" "web"{
ami="ami-12345"
  instance_type =    "t3.micro"
    tags={
Name="web-server"
  }
}

terraform fmt実行後:

resource "aws_instance" "web" {
  ami           = "ami-12345"
  instance_type = "t3.micro"
  tags = {
    Name = "web-server"
  }
}

これをGitのpre-commitフックに追加すれば、誰も乱雑なコードをpushできなくなります:

#!/bin/bash
# .git/hooks/pre-commit
terraform fmt -check -recursive
if [ $? -ne 0 ]; then
  echo "Error: Terraform files not formatted. Run 'terraform fmt'"
  exit 1
fi

Step 4: Validate Configuration

Command: terraform validate

Cloud APIに触れることなく、構文エラーや必須フィールドの欠落をチェックします:

$ terraform validate
Success! The configuration is valid.

検出される内容:

  • 不正なHCL構文
  • 必須引数の欠落
  • 無効なリソース参照
  • 型の不一致

Validation失敗の例:

resource "aws_instance" "web" {
  ami = var.ami_id
  # おっと、instance_typeを忘れた
}
$ terraform validate

Error: Missing required argument

  on main.tf line 10, in resource "aws_instance" "web":
  10: resource "aws_instance" "web" {

The argument "instance_type" is required, but no definition was found.

Tip!

terraform validateは完全にローカルで実行されます。API呼び出しは一切行いません。早めに頻繁に使用してください。


Step 5: Preview Changes (Plan)

Command: terraform plan

これが最も重要なステップです。何かを変更する前に、正確に何が起こるかを表示します:

$ terraform plan -var-file="dev.tfvars" -out=tfplan

Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_instance.web will be created
  + resource "aws_instance" "web" {
      + ami                          = "ami-0c55b159cbfafe1f0"
      + instance_type                = "t3.micro"
      + id                           = (known after apply)
      + public_ip                    = (known after apply)
      + tags                         = {
          + "Environment" = "dev"
          + "Name"        = "myapp-web-server"
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + instance_id = (known after apply)
  + public_ip   = (known after apply)

────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

Planシンボルの理解:

SymbolMeaningRisk Level
+リソースが作成されるLow
~リソースがその場で変更されるMedium
-/+リソースが破棄され再作成されるHigh
-リソースが破棄されるCritical

より安全なapplyのためにplanを保存:

# planファイルを生成
terraform plan -out=tfplan

# planをレビュー
terraform show tfplan

# 正確なplanを適用
terraform apply tfplan

planを保存する理由:

-outなしでterraform applyを実行すると、apply時に新しいplanが生成されます。planとapplyの間に何かが変更された場合(drift、他の人の変更)、レビューしたものとは異なるものを適用する可能性があります。

Production Best Practice

本番環境では常にterraform plan -out=tfplanの後にterraform apply tfplanを使用してください。保存されたplanをレビューせずにterraform applyを実行しないでください。


Step 6: Apply Changes

Command: terraform apply

これはplanを実行し、インフラを作成/変更/破棄します:

# Interactive apply(確認を求める)
$ terraform apply -var-file="dev.tfvars"

# Auto-approve(CI/CDでのみ使用)
$ terraform apply -var-file="dev.tfvars" -auto-approve

# 保存されたplanを適用(推奨)
$ terraform apply tfplan

Output:

aws_instance.web: Creating...
aws_instance.web: Still creating... [10s elapsed]
aws_instance.web: Still creating... [20s elapsed]
aws_instance.web: Creation complete after 25s [id=i-0abcd1234efgh5678]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

instance_id = "i-0abcd1234efgh5678"
public_ip = "54.123.45.67"

Apply中に起こること:

  1. Terraformがstateファイルをロック(他の実行を防止)
  2. Cloud providerにAPI呼び出し
  3. 新しいリソースIDでterraform.tfstateを更新
  4. Stateファイルをアンロック
  5. Outputsを表示

Apply実行中に失敗した場合:

クラッシュした場合(ネットワークエラー、APIリミット、タイムアウト)、stateファイルは実際に作成されたものを反映します。問題を修正してterraform applyを再実行すれば、Terraformは中断したところから再開します。


Step 7: Inspect Your Infrastructure

Apply後、作成したものを確認します:

# state内のすべてのリソースをリスト
$ terraform state list
aws_instance.web

# 詳細なリソース属性を表示
$ terraform state show aws_instance.web
# aws_instance.web:
resource "aws_instance" "web" {
    ami                          = "ami-0c55b159cbfafe1f0"
    instance_type                = "t3.micro"
    id                           = "i-0abcd1234efgh5678"
    public_ip                    = "54.123.45.67"
    tags                         = {
        "Environment" = "dev"
        "Name"        = "myapp-web-server"
    }
}

# Outputsを再度表示
$ terraform output
instance_id = "i-0abcd1234efgh5678"
public_ip = "54.123.45.67"

# 特定のoutput値を取得(スクリプト用)
$ terraform output -raw public_ip
54.123.45.67

Step 8: Destroy Infrastructure

Command: terraform destroy

終了したら(dev環境、テストなど)、削除します:

$ terraform destroy -var-file="dev.tfvars"

Terraform will perform the following actions:

  # aws_instance.web will be destroyed
  - resource "aws_instance" "web" {
      - ami           = "ami-0c55b159cbfafe1f0" -> null
      - instance_type = "t3.micro" -> null
      - id            = "i-0abcd1234efgh5678" -> null
    }

Plan: 0 to add, 0 to change, 1 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

aws_instance.web: Destroying... [id=i-0abcd1234efgh5678]
aws_instance.web: Destruction complete after 35s

Destroy complete! Resources: 1 destroyed.

ターゲット指定での削除(特定のリソース):

# Webインスタンスのみ削除
terraform destroy -target=aws_instance.web

Warning!

terraform destroyは元に戻せません。常にterraform plan -destroyを最初に実行して、何が削除されるかをプレビューしてください。本番環境では、lifecycle { prevent_destroy = true }で重要なリソースを保護してください。


Debugging Terraform

問題が発生したとき、Terraformは詳細なログを提供します:

Enable Debug Logging

# ログレベルを設定(TRACE、DEBUG、INFO、WARN、ERROR)
export TF_LOG=DEBUG

# ログをファイルに保存
export TF_LOG_PATH=terraform-debug.log

# Terraformコマンドを実行
terraform apply

# ロギングを無効化
unset TF_LOG TF_LOG_PATH

Log levels:

LevelUse Case
TRACE最大の詳細度 - すべてのAPI呼び出しを表示
DEBUG詳細なデバッグ情報
INFO標準的な操作メッセージ
WARN警告メッセージのみ
ERRORエラーのみ

Common Debugging Scenarios

Problem: "Error: Provider configuration not present"

# Solution: initを実行
terraform init

Problem: "Error: Inconsistent dependency lock file"

# Solution: lockファイルを再生成
terraform init -upgrade

Problem: "Error: Backend initialization required"

# Solution: backendを移行
terraform init -migrate-state

Problem: "Error: Resource already exists"

# Solution: 既存のリソースをimport
terraform import aws_instance.web i-1234567890abcdef0

Importing Existing Infrastructure

手動で作成したリソースがありますか?Terraformにimportしましょう:

Step 1: リソース設定を書く

# main.tf
resource "aws_instance" "legacy" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"

  tags = {
    Name = "legacy-server"
  }
}

Step 2: リソースをimport

# 構文: terraform import <resource_type>.<name> <cloud_resource_id>
$ terraform import aws_instance.legacy i-1234567890abcdef0

aws_instance.legacy: Importing from ID "i-1234567890abcdef0"...
aws_instance.legacy: Import prepared!
  Prepared aws_instance for import
aws_instance.legacy: Refreshing state... [id=i-1234567890abcdef0]

Import successful!

Step 3: importを確認

$ terraform plan

No changes. Your infrastructure matches the configuration.

Planが変更を表示する場合は、実際のリソースに合わせて設定を更新してください。

Tip!

Import後にterraform showを使用して完全なリソース設定を確認し、.tfファイルにコピーしてください。


State Operations

Terraform stateコマンドを使用すると、cloudリソースに触れることなくstateを検査および操作できます:

List Resources

$ terraform state list
aws_instance.web
aws_security_group.web_sg
aws_vpc.main

Show Resource Details

$ terraform state show aws_instance.web
# aws_instance.web:
resource "aws_instance" "web" {
    ami           = "ami-0c55b159cbfafe1f0"
    id            = "i-0abcd1234efgh5678"
    instance_type = "t3.micro"
}

Move Resources (Refactoring)

コード内でリソースの名前を変更したら、stateを更新して一致させます:

# コード内で名前変更: aws_instance.web → aws_instance.app_server
$ terraform state mv aws_instance.web aws_instance.app_server

Move "aws_instance.web" to "aws_instance.app_server"
Successfully moved 1 object(s).

Remove Resources from State

リソースを破棄せずにTerraform管理から削除:

$ terraform state rm aws_instance.web

Removed aws_instance.web
Successfully removed 1 resource instance(s).

インスタンスはAWSに残っていますが、Terraformはもう管理しません。


Advanced CLI Operations

Refresh State

Terraform stateを実際のcloudリソースと同期:

$ terraform refresh

使用するタイミング:

  • 誰かがcloudコンソールで手動変更を行った
  • stateと実際の状態の差分をチェック

Note: Terraformはplanapply中に自動的にrefreshするため、明示的なrefreshはほとんど必要ありません。

Replace Resources

リソースの強制置換(破棄して再作成):

# 特定のリソースを置換
$ terraform apply -replace=aws_instance.web

# 以下の場合に有用:
# - リソースが破損している
# - 新しいインスタンスIDが必要
# - 災害復旧のテスト

Targeted Apply

特定のリソースのみに変更を適用:

# Webインスタンスのみ作成/更新
$ terraform apply -target=aws_instance.web

# 複数のターゲット
$ terraform apply -target=aws_instance.web -target=aws_security_group.web_sg

Warning!

-targetの使用は不整合なstateを作成する可能性があります。部分的な失敗からの回復以外では、本番環境での使用を避けてください。


The Complete Workflow Script

本番環境対応のworkflowスクリプトです:

#!/bin/bash
# deploy.sh - Safe Terraform deployment script

set -e  # エラー時に終了

ENVIRONMENT=${1:-dev}
VAR_FILE="${ENVIRONMENT}.tfvars"

echo "========================================="
echo "Deploying to: $ENVIRONMENT"
echo "========================================="

# Step 1: Initialize
echo "→ Initializing Terraform..."
terraform init

# Step 2: Format check
echo "→ Checking code formatting..."
terraform fmt -check -recursive
if [ $? -ne 0 ]; then
  echo "❌ Code not formatted. Run: terraform fmt -recursive"
  exit 1
fi

# Step 3: Validate
echo "→ Validating configuration..."
terraform validate

# Step 4: Generate plan
echo "→ Generating plan..."
terraform plan -var-file="$VAR_FILE" -out=tfplan

# Step 5: Show plan
echo "→ Plan generated. Review changes above."
read -p "Apply this plan? (yes/no): " CONFIRM

if [ "$CONFIRM" != "yes" ]; then
  echo "❌ Deployment cancelled."
  rm tfplan
  exit 0
fi

# Step 6: Apply plan
echo "→ Applying changes..."
terraform apply tfplan

# Cleanup
rm tfplan

echo "✅ Deployment complete!"
terraform output

使用方法:

chmod +x deploy.sh
./deploy.sh dev      # devにデプロイ
./deploy.sh staging  # stagingにデプロイ
./deploy.sh prod     # productionにデプロイ

Checkpoint Questions

理解度をテストしましょう:

  1. terraform initは何をしますか? planapplyの前に実行しなければならないのはなぜですか?

  2. terraform planterraform applyの違いは? Planせずにapplyできますか?

  3. コード変更なしでterraform applyを2回実行するとどうなりますか?

  4. すべてを削除せずに特定のリソースのみを削除するには?

  5. なぜ-out=tfplanでplanを保存するのですか? どんな問題を解決しますか?

  6. terraform importを使用するのはいつですか? Terraformが作成したリソースをimportできますか?

  7. terraform state rmterraform destroyの違いは何ですか?

クリックして回答を表示
  1. terraform initはproviderプラグインをダウンロードし、backendを初期化します。すべてのTerraform操作の前に必要です。

  2. planは変更を加えずにプレビューし、applyは変更を実行します。明示的なplanなしでapplyできますが、リスクがあります(planがインラインで実行される)。

  3. 何も変更されません。Terraformはインフラが望ましい状態と一致していることを検出し、操作を実行しません。

  4. terraform destroy -target=resource_type.name

  5. 保存されたplanは、レビューした変更が正確に適用されることを保証します(planとapplyの間にdriftがない)。

  6. 手動で作成されたリソースをTerraform管理下に置くためにimportを使用します。はい、stateを失った場合、Terraformが作成したリソースをimportできます。

  7. state rmはTerraformの追跡から削除しますが、リソースはそのまま残ります。destroyは実際のcloudリソースを削除します。


What's Next: Part 7 - Modules for Organization

Terraform workflowをマスターしましたが、コードベースが数百のリソースに成長したらどうなりますか?同じVPC設定を10のプロジェクトにコピーしますか?関連リソース間の依存関係を管理しますか?

Part 7では、これらの問題をTerraformモジュールで解決します - 再利用可能なコンポーネントで:

  • コードの重複を排除
  • 組織の標準を強制
  • チームコラボレーションを可能に
  • 複雑なインフラを簡素化

学ぶこと:

  • ゼロからカスタムモジュールを作成
  • Terraform Registryのパブリックモジュールを使用
  • モジュール間で変数とoutputsを渡す
  • 内部モジュールのバージョン管理と公開
  • 大規模なTerraformプロジェクトの構造化

プレビュースニペット(Part 7):

# VPCコードを繰り返す代わりに...
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.1.0"

  name = "${var.project}-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24"]

  enable_nat_gateway = true
  enable_vpn_gateway = false
}

# モジュールのoutputsを参照
resource "aws_instance" "web" {
  subnet_id = module.vpc.public_subnets[0]
}

Part 7でお会いしましょう。


Series Navigation

  • Part 1: Why Infrastructure as Code?
  • Part 2: Setting Up Terraform (Coming soon)
  • Part 3: Your First Cloud Resource (Coming soon)
  • Part 4: HCL Fundamentals (Coming soon)
  • Part 5: Variables, Outputs & State (Coming soon)
  • Part 6: Core Terraform Workflow (You are here)
  • Part 7: Modules for Organization (Coming soon)
  • Part 8: Multi-Cloud Patterns (Coming soon)
  • Part 9: State Management & Team Workflows (Coming soon)
  • Part 10: Testing & Validation (Coming soon)
  • Part 11: Security & Secrets Management (Coming soon)
  • Part 12: Production Patterns & DevSecOps (Coming soon)

References


連載ナビゲーション:


質問やフィードバックがありますか? 下にコメントを残すか、LinkedInでつながってください。