T-CREATOR

Ansible と Terraform/Puppet/Chef 比較:宣言/手続の違いと併用戦略

Ansible と Terraform/Puppet/Chef 比較:宣言/手続の違いと併用戦略

インフラ管理ツールの選択で迷われていませんか。Ansible、Terraform、Puppet、Chef それぞれに独自の特徴があり、どれを使うべきか判断に悩むことも多いでしょう。

特に重要なのが「宣言型」と「手続型」という根本的な違いです。この違いを理解することで、各ツールの強みを活かした効果的な組み合わせが可能になります。本記事では、これらのツールを徹底比較し、実際の併用戦略まで詳しく解説していきます。

宣言型と手続型の基本概念

インフラ管理ツールを理解する上で最も重要な概念が「宣言型」と「手続型」の違いです。これらの違いを把握することで、各ツールの特性や適用場面が明確になります。

宣言型アプローチの特徴

宣言型アプローチでは、「どのような状態にしたいか」を記述します。つまり、最終的な目標状態を定義し、その状態に到達するための具体的な手順はツール側が自動的に決定します。

この概念を図で表すと以下のようになります:

mermaidflowchart LR
  current["現在の状態"] -->|自動判定| tool["管理ツール"]
  desired["望ましい状態<br/>(宣言)"] --> tool
  tool -->|最適な手順を決定| target["目標状態"]

宣言型の主な特徴は以下の通りです:

  • 冪等性:何度実行しても同じ結果になる
  • 自動判定:現在の状態と目標状態の差分を自動検出
  • 抽象化:具体的な手順を意識せずに済む

手続型アプローチの特徴

一方、手続型アプローチでは「どのような手順で実行するか」を記述します。実行すべき処理の順序やロジックを明示的に定義し、その通りに実行されます。

手続型のワークフローを図示すると:

mermaidflowchart TD
  start["開始"] --> step1["ステップ1"]
  step1 --> step2["ステップ2"]
  step2 --> step3["ステップ3"]
  step3 --> finish["完了"]

  step1 -.->|条件分岐| alt["代替処理"]
  alt --> step3

手続型の主な特徴は以下の通りです:

  • 明示的制御:実行順序を詳細に制御可能
  • 柔軟性:複雑な条件分岐やループに対応
  • 透明性:何が実行されるかが明確

これらの違いが、各ツールの設計思想や使用感に大きな影響を与えています。

各ツールのアプローチ詳解

それぞれのツールがどのようなアプローチを採用しているか、具体的に見ていきましょう。実装例も交えながら、各ツールの特徴を詳しく解説します。

Ansible:手続型アプローチの特徴

Ansible は手続型アプローチを採用しており、タスクを順次実行する形式でインフラを管理します。プレイブックと呼ばれる設定ファイルで、実行すべき処理を順序立てて記述します。

Ansible の基本構造

以下は Ansible プレイブックの基本的な例です:

yaml---
- name: Webサーバーのセットアップ
  hosts: webservers
  become: yes
  tasks:
    - name: Nginxのインストール
      package:
        name: nginx
        state: present

    - name: 設定ファイルのコピー
      copy:
        src: nginx.conf
        dest: /etc/nginx/nginx.conf
      notify: nginx restart

    - name: Nginxサービスの開始
      service:
        name: nginx
        state: started
        enabled: yes

この例では、以下の手順が順次実行されます:

  1. Nginx パッケージのインストール
  2. 設定ファイルのコピー
  3. Nginx サービスの開始と自動起動設定

Ansible の手続型特徴

実行順序の制御

Ansible では、タスクの実行順序が重要な意味を持ちます。依存関係のあるタスクは適切な順序で配置する必要があります。

yamltasks:
  - name: データベースの作成
    mysql_db:
      name: myapp
      state: present

  - name: ユーザーの作成(データベース作成後)
    mysql_user:
      name: appuser
      password: '{{ db_password }}'
      priv: 'myapp.*:ALL'
      state: present

条件分岐とループ

複雑な条件分岐やループ処理も手続型の強みです:

yaml- name: OS別パッケージインストール
  package:
    name: '{{ item }}'
    state: present
  loop:
    - "{{ 'httpd' if ansible_os_family == 'RedHat' else 'apache2' }}"
    - "{{ 'mysql-server' if ansible_os_family == 'RedHat' else 'mysql-server' }}"
  when: install_web_stack | bool

エラーハンドリング

手続型では、エラー処理も明示的に記述できます:

yaml- name: アプリケーションの配備
  copy:
    src: app.jar
    dest: /opt/myapp/
  register: deploy_result
  failed_when: false

- name: 配備失敗時のロールバック
  command: /opt/myapp/rollback.sh
  when: deploy_result.failed

Terraform:宣言型アプローチの特徴

Terraform は宣言型アプローチの代表的なツールで、インフラの最終的な状態を記述することに特化しています。HCL(HashiCorp Configuration Language)を使用して、リソースの望ましい状態を定義します。

Terraform の基本構造

以下は Terraform の基本的な設定例です:

hcl# プロバイダーの設定
provider "aws" {
  region = "ap-northeast-1"
}

# VPCの定義
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "main-vpc"
  }
}

# サブネットの定義
resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "ap-northeast-1a"
  map_public_ip_on_launch = true

  tags = {
    Name = "public-subnet"
  }
}

この設定では、以下のリソースの最終状態を宣言しています:

  • VPC が存在し、指定の CIDR ブロックを持つ
  • サブネットが存在し、VPC に関連付けられている
  • 適切なタグが設定されている

Terraform の宣言型特徴

状態管理の自動化

Terraform は現在の状態を自動的に追跡し、差分を検出します:

bash# 現在の状態と設定の差分を確認
terraform plan

# 差分を適用して目標状態に到達
terraform apply

依存関係の自動解決

リソース間の依存関係は暗黙的に解決されます:

hclresource "aws_security_group" "web" {
  name_prefix = "web-"
  vpc_id      = aws_vpc.main.id  # VPCへの暗黙的依存

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "web" {
  ami                    = "ami-12345678"
  instance_type          = "t3.micro"
  subnet_id              = aws_subnet.public.id      # サブネットへの暗黙的依存
  vpc_security_group_ids = [aws_security_group.web.id] # セキュリティグループへの暗黙的依存
}

冪等性の保証

同じ設定を何度実行しても、結果は変わりません:

hcl# この設定を何度実行しても、1つのS3バケットのみ存在
resource "aws_s3_bucket" "example" {
  bucket = "my-unique-bucket-name"
}

resource "aws_s3_bucket_versioning" "example" {
  bucket = aws_s3_bucket.example.id
  versioning_configuration {
    status = "Enabled"
  }
}

Puppet:宣言型アプローチの特徴

Puppet も宣言型アプローチを採用しており、システムの望ましい状態をマニフェストと呼ばれるファイルで定義します。Puppet 独自の DSL(Domain Specific Language)を使用します。

Puppet の基本構造

以下は Puppet マニフェストの例です:

puppet# Webサーバーの設定
class webserver {
  # パッケージの管理
  package { 'apache2':
    ensure => installed,
  }

  # サービスの管理
  service { 'apache2':
    ensure  => running,
    enable  => true,
    require => Package['apache2'],
  }

  # 設定ファイルの管理
  file { '/etc/apache2/sites-available/default':
    ensure  => file,
    content => template('webserver/default.erb'),
    notify  => Service['apache2'],
    require => Package['apache2'],
  }
}

# クラスの適用
include webserver

Puppet の宣言型特徴

リソースの抽象化

Puppet では、OS の違いを抽象化してリソースを管理できます:

puppet# OS に関係なく、適切なパッケージマネージャーを使用
package { 'git':
  ensure => installed,
}

# OS に応じてサービス名を自動選択
service { $apache_service_name:
  ensure => running,
  enable => true,
}

依存関係の明示

リソース間の依存関係を明示的に定義できます:

puppetfile { '/var/www/html/index.html':
  ensure  => file,
  content => 'Hello World',
  require => Package['apache2'],  # Apacheインストール後に実行
  notify  => Service['apache2'],  # ファイル変更時にサービス再起動
}

設定のテンプレート化

ERB テンプレートを使用して、動的な設定を生成できます:

puppetfile { '/etc/mysql/my.cnf':
  ensure  => file,
  content => template('mysql/my.cnf.erb'),
  notify  => Service['mysql'],
}

Chef:手続型アプローチの特徴

Chef は Ruby ベースの手続型ツールで、レシピと呼ばれる Ruby スクリプトでインフラの設定手順を記述します。プログラミング言語の柔軟性を活かした複雑な処理が可能です。

Chef の基本構造

以下は Chef レシピの例です:

ruby# パッケージのインストール
package 'nginx' do
  action :install
end

# 設定ファイルのテンプレート生成
template '/etc/nginx/nginx.conf' do
  source 'nginx.conf.erb'
  variables({
    worker_processes: node['cpu']['total'],
    worker_connections: 1024
  })
  notifies :restart, 'service[nginx]', :delayed
end

# サービスの管理
service 'nginx' do
  action [:enable, :start]
  supports restart: true, reload: true
end

Chef の手続型特徴

Ruby の活用

Chef では Ruby の機能をフルに活用できます:

ruby# 動的な設定生成
servers = search(:node, 'role:webserver')
servers.each do |server|
  template "/etc/nginx/conf.d/#{server.name}.conf" do
    source 'vhost.conf.erb'
    variables server: server
  end
end

# 条件分岐
if node['platform_family'] == 'debian'
  package 'apache2'
elsif node['platform_family'] == 'rhel'
  package 'httpd'
end

カスタムリソースの作成

独自のリソースタイプを定義できます:

ruby# カスタムリソースの定義
provides :my_application

property :app_name, String, name_property: true
property :version, String, required: true
property :port, Integer, default: 8080

action :deploy do
  # アプリケーション配備のロジック
  remote_file "/opt/#{new_resource.app_name}" do
    source "https://releases.example.com/#{new_resource.version}.tar.gz"
  end

  # サービス設定
  systemd_unit "#{new_resource.app_name}.service" do
    content({
      Unit: { Description: "My Application #{new_resource.app_name}" },
      Service: {
        ExecStart: "/opt/#{new_resource.app_name}/bin/start",
        Environment: "PORT=#{new_resource.port}"
      }
    })
  end
end

データバッグの活用

外部データソースとの連携も容易です:

ruby# データバッグから設定情報を取得
db_config = data_bag_item('configs', 'database')

template '/etc/myapp/database.yml' do
  source 'database.yml.erb'
  variables({
    host: db_config['host'],
    username: db_config['username'],
    password: db_config['password']
  })
end

宣言型 vs 手続型の比較分析

各アプローチの特徴を理解したところで、実際の運用における違いを詳しく比較してみましょう。どちらを選ぶべきかの判断材料として、具体的な場面での違いを解説します。

設定管理における違い

設定管理の手法は、宣言型と手続型で大きく異なります。この違いを理解することで、プロジェクトに適したアプローチを選択できます。

設定記述の考え方

以下の図は、同じ Web サーバー構築を両アプローチで実現する際の思考プロセスの違いを示しています:

mermaidflowchart TD
  subgraph declarative["宣言型アプローチ"]
    d1["Webサーバーが稼働している状態"] --> d2["必要なパッケージが存在"]
    d2 --> d3["設定ファイルが適切"]
    d3 --> d4["サービスが起動中"]
  end

  subgraph procedural["手続型アプローチ"]
    p1["1. パッケージをインストール"] --> p2["2. 設定ファイルを配置"]
    p2 --> p3["3. サービスを起動"]
    p3 --> p4["4. 動作確認"]
  end

宣言型での設定例(Terraform)

hcl# 望ましい状態を記述
resource "aws_instance" "web" {
  ami           = "ami-12345678"
  instance_type = "t3.micro"

  # セキュリティグループが適用されている状態
  vpc_security_group_ids = [aws_security_group.web.id]

  # タグが設定されている状態
  tags = {
    Name = "WebServer"
    Environment = "Production"
  }
}

# セキュリティグループが存在し、適切なルールを持つ状態
resource "aws_security_group" "web" {
  name = "web-sg"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

手続型での設定例(Ansible)

yaml# 実行手順を記述
- name: Webサーバーセットアップ手順
  hosts: webservers
  tasks:
    - name: Step1:Nginxをインストール
      package:
        name: nginx
        state: present

    - name: Step2:設定ファイルを配置
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
      notify: restart nginx

    - name: Step3:サービスを開始
      service:
        name: nginx
        state: started
        enabled: yes

    - name: Step4:動作確認
      uri:
        url: 'http://{{ ansible_default_ipv4.address }}'
        method: GET
      register: health_check

設定変更時の対応

宣言型の場合

設定変更は定義の更新のみで完了します:

hcl# 変更前
resource "aws_instance" "web" {
  instance_type = "t3.micro"
}

# 変更後(インスタンスタイプを変更)
resource "aws_instance" "web" {
  instance_type = "t3.small"  # この行のみ変更
}

Terraform が自動的に差分を検出し、必要な変更操作を実行します。

手続型の場合

変更手順を明示的に記述する必要があります:

yaml- name: インスタンスタイプ変更手順
  hosts: webservers
  tasks:
    - name: サービス停止
      service:
        name: nginx
        state: stopped

    - name: インスタンス停止
      aws_ec2:
        instance_ids: '{{ instance_id }}'
        state: stopped

    - name: インスタンスタイプ変更
      aws_ec2:
        instance_ids: '{{ instance_id }}'
        instance_type: t3.small

    - name: インスタンス起動
      aws_ec2:
        instance_ids: '{{ instance_id }}'
        state: running

    - name: サービス開始
      service:
        name: nginx
        state: started

実行プロセスの違い

実行プロセスの違いは、運用時の作業効率やトラブルシューティングに大きな影響を与えます。

実行前の確認プロセス

宣言型の確認プロセス

bash# Terraform での確認
terraform plan
# Output:
# Plan: 1 to add, 0 to change, 0 to destroy
#
# + aws_instance.web
#   + ami           = "ami-12345678"
#   + instance_type = "t3.micro"

宣言型では、現在の状態と目標状態の差分を自動計算し、実行予定の変更を事前に確認できます。

手続型の確認プロセス

yaml# Ansible での確認(ドライラン)
ansible-playbook site.yml --check --diff
# タスクごとに実行予定の変更が表示される
# TASK [nginx : Install Nginx]
# changed: [server1] => (item=nginx)
#
# TASK [nginx : Configure Nginx]
# --- before: /etc/nginx/nginx.conf
# +++ after: /tmp/nginx.conf

手続型では、各タスクの実行結果を順次確認していく形になります。

エラー発生時の対応

実行中にエラーが発生した場合の対応も、アプローチによって大きく異なります:

mermaidsequenceDiagram
  participant User as 管理者
  participant Tool as ツール
  participant Infra as インフラ

  Note over User,Infra: 宣言型の場合
  User->>Tool: terraform apply
  Tool->>Infra: 状態確認
  Infra-->>Tool: 現在の状態
  Tool->>Infra: 差分適用
  Note over Tool,Infra: エラー発生
  Tool-->>User: エラー報告(ロールバック自動)

  Note over User,Infra: 手続型の場合
  User->>Tool: ansible-playbook
  Tool->>Infra: タスク1実行
  Tool->>Infra: タスク2実行
  Note over Tool,Infra: エラー発生
  Tool-->>User: エラー報告(手動対応必要)

宣言型でのエラー処理例

bash# Terraform実行時エラー
terraform apply
# Error: Error creating instance: InvalidAMI.NotFound
#
# 自動的に他の変更もロールバックされる

手続型でのエラー処理例

yaml# Ansibleでのエラーハンドリング
- name: アプリケーション更新
  copy:
    src: app-v2.jar
    dest: /opt/app/
  register: copy_result

- name: 更新失敗時のロールバック
  copy:
    src: app-v1.jar
    dest: /opt/app/
  when: copy_result.failed

- name: サービス再起動
  service:
    name: myapp
    state: restarted
  when: not copy_result.failed

メンテナンス性の違い

長期的な運用を考える上で、メンテナンス性は重要な要素です。チーム体制や技術的負債の観点から比較してみましょう。

コードの可読性と保守性

宣言型の保守性

宣言型では、設定の意図が明確で、変更影響範囲が把握しやすいという特徴があります:

hcl# 意図が明確な設定
variable "environment" {
  description = "Environment name (dev/staging/prod)"
  type        = string
  default     = "dev"
}

# 環境ごとの設定差分が明確
resource "aws_instance" "web" {
  instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"

  tags = {
    Environment = var.environment
    Purpose     = "web-server"
  }
}

設定の変更時には、差分が自動的に計算されるため、影響範囲を事前に把握できます。

手続型の保守性

手続型では、処理の詳細が明確で、カスタマイズの自由度が高いという特徴があります:

yaml# 処理手順が明確
- name: 環境別Webサーバー構築
  hosts: '{{ target_env }}_servers'
  vars:
    instance_type: "{{ 't3.large' if target_env == 'prod' else 't3.micro' }}"

  tasks:
    - name: '{{ target_env }}環境用設定ファイル配置'
      template:
        src: 'nginx-{{ target_env }}.conf.j2'
        dest: /etc/nginx/nginx.conf
      when: configure_nginx | default(true)

    - name: カスタム処理(本番環境のみ)
      script: prod-specific-setup.sh
      when: target_env == "prod"

チーム運用での違い

チーム開発における運用面の違いを表にまとめました:

観点宣言型手続型
学習コスト専用 DSL の習得が必要一般的なプログラミング知識で対応可能
レビュー観点最終状態の妥当性確認処理手順の妥当性確認
並行開発状態定義の競合リスク有タスク単位での分担が容易
デバッグ状態差分の確認が中心ステップ実行での詳細確認
テスト結果状態のテストが中心処理過程のテストも可能

技術的負債の蓄積パターン

宣言型での技術的負債

hcl# 悪い例:設定の重複と不整合
resource "aws_instance" "web1" {
  ami           = "ami-12345678"  # 古いAMI
  instance_type = "t2.micro"     # 旧世代
}

resource "aws_instance" "web2" {
  ami           = "ami-87654321"  # 新しいAMI
  instance_type = "t3.micro"     # 新世代
}

# 改善例:共通設定の抽象化
locals {
  common_config = {
    ami           = "ami-87654321"
    instance_type = "t3.micro"
  }
}

resource "aws_instance" "web" {
  count = 2
  ami           = local.common_config.ami
  instance_type = local.common_config.instance_type
}

手続型での技術的負債

yaml# 悪い例:手順の重複と保守性の悪化
- name: サーバー1のセットアップ
  hosts: server1
  tasks:
    - name: Nginxインストール
      package: name=nginx state=present
    - name: 設定ファイル配置
      copy: src=nginx.conf dest=/etc/nginx/
    - name: サービス起動
      service: name=nginx state=started

- name: サーバー2のセットアップ
  hosts: server2
  tasks:
    - name: Nginxインストール # 重複
      package: name=nginx state=present
    - name: 設定ファイル配置 # 重複
      copy: src=nginx.conf dest=/etc/nginx/
    - name: サービス起動 # 重複
      service: name=nginx state=started

# 改善例:ロール化による共通化
- name: Webサーバーセットアップ
  hosts: webservers
  roles:
    - nginx

併用戦略とベストプラクティス

実際のプロジェクトでは、単一のツールですべてを解決するよりも、各ツールの強みを活かした併用が効果的です。特に Ansible と Terraform の組み合わせは、多くの組織で採用されている実績ある戦略です。

Ansible + Terraform の組み合わせ

Ansible と Terraform の併用は、インフラ管理における最も一般的なパターンの一つです。それぞれの役割を明確に分離することで、効率的な運用が可能になります。

役割分担の基本方針

以下の図は、Ansible と Terraform の典型的な役割分担を示しています:

mermaidflowchart TD
  subgraph terraform["Terraform 担当領域"]
    infra["インフラリソース"]
    infra --> compute["コンピュートリソース<br/>EC2, ECS, Lambda"]
    infra --> network["ネットワークリソース<br/>VPC, Subnet, Route"]
    infra --> storage["ストレージリソース<br/>S3, EBS, RDS"]
    infra --> security["セキュリティリソース<br/>IAM, Security Group"]
  end

  subgraph ansible["Ansible 担当領域"]
    config["設定管理"]
    config --> os["OS設定<br/>パッケージ, サービス"]
    config --> app["アプリケーション<br/>配備, 設定"]
    config --> monitor["監視設定<br/>ログ, メトリクス"]
    config --> maintenance["メンテナンス<br/>更新, バックアップ"]
  end

  terraform -->|インフラ情報| ansible

Terraform の責任範囲

  • クラウドリソースの作成・管理
  • ネットワーク構成の定義
  • セキュリティポリシーの設定
  • リソース間の依存関係管理

Ansible の責任範囲

  • OS レベルの設定
  • アプリケーションの配備
  • サービス設定の管理
  • 運用タスクの自動化

実装例:Web アプリケーション環境の構築

以下は、Terraform で AWS インフラを作成し、Ansible で Web アプリケーションを配備する例です。

Step1: Terraform でインフラ作成

hcl# main.tf
provider "aws" {
  region = "ap-northeast-1"
}

# VPC とネットワーク
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  tags = { Name = "webapp-vpc" }
}

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "ap-northeast-1a"
  map_public_ip_on_launch = true
  tags = { Name = "public-subnet" }
}

# セキュリティグループ
resource "aws_security_group" "web" {
  name   = "web-sg"
  vpc_id = aws_vpc.main.id

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

EC2 インスタンスの作成

hcl# EC2インスタンス
resource "aws_instance" "web" {
  ami                    = "ami-0c3fd0f5d33134a76"
  instance_type          = "t3.micro"
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.web.id]
  key_name               = "my-keypair"

  tags = {
    Name = "web-server"
    Role = "webserver"
  }
}

# Ansible用のインベントリ情報を出力
resource "local_file" "ansible_inventory" {
  content = templatefile("${path.module}/inventory.tpl", {
    web_ip = aws_instance.web.public_ip
  })
  filename = "../ansible/inventory/hosts"
}

インベントリテンプレート

ini# inventory.tpl
[webservers]
${web_ip} ansible_user=ec2-user ansible_ssh_private_key_file=~/.ssh/my-keypair.pem

[webservers:vars]
ansible_ssh_common_args='-o StrictHostKeyChecking=no'

Step2: Ansible でアプリケーション配備

yaml# site.yml
---
- name: Webアプリケーション配備
  hosts: webservers
  become: yes
  vars:
    app_name: mywebapp
    app_port: 3000
    nginx_port: 80

  tasks:
    - name: 必要パッケージのインストール
      yum:
        name:
          - nginx
          - nodejs
          - npm
          - git
        state: present

    - name: アプリケーション用ユーザー作成
      user:
        name: '{{ app_name }}'
        system: yes
        home: '/opt/{{ app_name }}'
        create_home: yes

    - name: アプリケーションコードのクローン
      git:
        repo: 'https://github.com/example/mywebapp.git'
        dest: '/opt/{{ app_name }}/src'
        version: main
      become_user: '{{ app_name }}'
      notify: restart app

アプリケーション設定とサービス起動

yaml- name: NPM依存関係のインストール
  npm:
    path: '/opt/{{ app_name }}/src'
  become_user: '{{ app_name }}'

- name: アプリケーション設定ファイル配置
  template:
    src: app.env.j2
    dest: '/opt/{{ app_name }}/.env'
    owner: '{{ app_name }}'
    group: '{{ app_name }}'
    mode: '0600'
  notify: restart app

- name: Systemdサービスファイル作成
  template:
    src: mywebapp.service.j2
    dest: '/etc/systemd/system/{{ app_name }}.service'
  notify:
    - reload systemd
    - restart app

- name: Nginx設定ファイル配置
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  notify: restart nginx

- name: サービス起動と自動起動設定
  service:
    name: '{{ item }}'
    state: started
    enabled: yes
  loop:
    - '{{ app_name }}'
    - nginx

ハンドラーの定義

yamlhandlers:
  - name: reload systemd
    systemd:
      daemon_reload: yes

  - name: restart app
    service:
      name: '{{ app_name }}'
      state: restarted

  - name: restart nginx
    service:
      name: nginx
      state: restarted

データ連携のベストプラクティス

Terraform と Ansible 間でのデータ連携には、いくつかの効果的な方法があります。

方法 1: Terraform Output と Local File

hcl# outputs.tf
output "web_server_info" {
  value = {
    public_ip    = aws_instance.web.public_ip
    private_ip   = aws_instance.web.private_ip
    instance_id  = aws_instance.web.id
  }
}

# インベントリファイル自動生成
resource "local_file" "ansible_vars" {
  content = yamlencode({
    instance_id = aws_instance.web.id
    public_ip   = aws_instance.web.public_ip
    private_ip  = aws_instance.web.private_ip
    vpc_id      = aws_vpc.main.id
  })
  filename = "../ansible/group_vars/webservers.yml"
}

方法 2: 外部データソースの活用

yaml# Ansible側でTerraform stateを参照
- name: Terraform状態からインスタンス情報取得
  shell: |
    cd ../terraform && terraform output -json web_server_info
  register: tf_output
  delegate_to: localhost

- name: 出力値の解析
  set_fact:
    instance_info: '{{ (tf_output.stdout | from_json) }}'

- name: インスタンス情報を使用した処理
  debug:
    msg: 'Instance {{ instance_info.instance_id }} has IP {{ instance_info.public_ip }}'

適材適所の使い分け指針

各ツールの特性を理解した上で、具体的にどのような場面でどのツールを選択すべきかのガイドラインを提示します。

シナリオ別推奨ツール

以下の表は、一般的なシナリオにおける推奨ツールを示しています:

シナリオ推奨ツール理由
クラウドインフラ構築Terraform宣言型でリソース間依存関係を自動解決
マルチクラウド対応Terraformプロバイダーが豊富で統一的な管理が可能
OS 設定・パッケージ管理Ansible/PuppetOS レベルの詳細な制御が必要
アプリケーション配備Ansible/Chef複雑な配備手順に対応可能
設定ファイル管理Puppet設定の一元管理と監視が強力
一時的なタスク実行Ansibleエージェントレスで即座に実行可能
大規模な設定管理Puppet/Chefスケーラビリティとレポート機能が充実

技術的制約による選択

ネットワーク制約がある環境

yaml# エージェントレスのAnsibleが有効
- name: 閉域網内サーバー管理
  hosts: isolated_servers
  connection: ssh
  vars:
    ansible_ssh_common_args: '-o ProxyCommand="ssh -W %h:%p bastion-host"'

  tasks:
    - name: ベースラインセキュリティ設定
      include_role:
        name: security_baseline

Windows 環境の管理

yaml# Windows環境ではAnsibleのWinRM接続を活用
- name: Windows Server管理
  hosts: windows_servers
  vars:
    ansible_connection: winrm
    ansible_winrm_transport: kerberos

  tasks:
    - name: IIS役割のインストール
      win_feature:
        name: IIS-WebServerRole
        state: present

    - name: アプリケーションプール設定
      win_iis_webapppool:
        name: MyAppPool
        state: present
        attributes:
          processModel.identityType: ApplicationPoolIdentity

レガシーシステムとの共存

puppet# Puppetでレガシーシステムとの段階的移行
class legacy_migration {
  # 既存システムとの互換性を保持
  if $facts['legacy_app_installed'] {
    service { 'legacy-app':
      ensure => stopped,
      enable => false,
    }

    # 新システムへの移行処理
    exec { 'migrate-data':
      command => '/opt/migration/migrate.sh',
      creates => '/var/lib/migration.complete',
      require => Service['legacy-app'],
    }
  }

  # 新システムの配備
  package { 'new-app':
    ensure  => installed,
    require => Exec['migrate-data'],
  }
}

実践的な併用パターン

実際のプロジェクトで使われている効果的な併用パターンを、具体的な実装例とともに紹介します。

パターン 1: フェーズ分離型

開発フェーズごとに異なるツールを使い分けるパターンです:

mermaidflowchart LR
  subgraph phase1["Phase 1: インフラ構築"]
    terraform["Terraform<br/>・VPC/Subnet<br/>・EC2/RDS<br/>・Load Balancer"]
  end

  subgraph phase2["Phase 2: 基盤設定"]
    ansible1["Ansible<br/>・OS設定<br/>・監視エージェント<br/>・セキュリティ設定"]
  end

  subgraph phase3["Phase 3: アプリ配備"]
    ansible2["Ansible<br/>・アプリケーション<br/>・設定ファイル<br/>・サービス起動"]
  end

  phase1 --> phase2
  phase2 --> phase3

実装例: CI/CD パイプライン

yaml# .github/workflows/deploy.yml
name: Infrastructure and Application Deployment

on:
  push:
    branches: [main]

jobs:
  infrastructure:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v2

      - name: Terraform Plan
        run: |
          cd terraform
          terraform init
          terraform plan -out=tfplan

      - name: Terraform Apply
        run: |
          cd terraform
          terraform apply tfplan

      - name: Generate Ansible Inventory
        run: |
          cd terraform
          terraform output -json > ../ansible/terraform_outputs.json

  configuration:
    needs: infrastructure
    runs-on: ubuntu-latest
    steps:
      - name: Setup Ansible
        run: |
          pip install ansible boto3

      - name: Base Configuration
        run: |
          cd ansible
          ansible-playbook -i inventory/aws_ec2.yml base-config.yml

      - name: Application Deployment
        run: |
          cd ansible
          ansible-playbook -i inventory/aws_ec2.yml app-deploy.yml

パターン 2: レイヤー分離型

インフラの各レイヤーごとに最適なツールを選択するパターンです:

mermaidflowchart TD
  subgraph layer1["ネットワークレイヤー"]
    terraform1["Terraform<br/>VPC, Subnet, Route, NAT"]
  end

  subgraph layer2["コンピュートレイヤー"]
    terraform2["Terraform<br/>EC2, ECS, Lambda"]
  end

  subgraph layer3["プラットフォームレイヤー"]
    ansible1["Ansible<br/>Docker, Kubernetes"]
  end

  subgraph layer4["アプリケーションレイヤー"]
    ansible2["Ansible<br/>App Deployment, Configuration"]
  end

  layer1 --> layer2
  layer2 --> layer3
  layer3 --> layer4

実装例: マイクロサービス環境

hcl# terraform/network/main.tf
module "vpc" {
  source = "terraform-aws-modules/vpc/aws"

  name = "microservices-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["ap-northeast-1a", "ap-northeast-1c"]
  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
}
hcl# terraform/compute/main.tf
module "eks" {
  source = "terraform-aws-modules/eks/aws"

  cluster_name    = "microservices-cluster"
  cluster_version = "1.24"

  vpc_id     = data.terraform_remote_state.network.outputs.vpc_id
  subnet_ids = data.terraform_remote_state.network.outputs.private_subnets

  node_groups = {
    main = {
      desired_capacity = 3
      max_capacity     = 10
      min_capacity     = 1
      instance_types   = ["t3.medium"]
    }
  }
}
yaml# ansible/k8s-setup.yml
---
- name: Kubernetes クラスター設定
  hosts: localhost
  vars:
    cluster_name: microservices-cluster

  tasks:
    - name: kubectl 設定更新
      shell: |
        aws eks update-kubeconfig --name {{ cluster_name }} --region ap-northeast-1

    - name: Helm リポジトリ追加
      kubernetes.core.helm_repository:
        name: '{{ item.name }}'
        repo_url: '{{ item.url }}'
      loop:
        - {
            name: ingress-nginx,
            url: 'https://kubernetes.github.io/ingress-nginx',
          }
        - {
            name: prometheus-community,
            url: 'https://prometheus-community.github.io/helm-charts',
          }

    - name: NGINX Ingress Controller デプロイ
      kubernetes.core.helm:
        name: ingress-nginx
        chart_ref: ingress-nginx/ingress-nginx
        namespace: ingress-nginx
        create_namespace: true

パターン 3: 機能分離型

機能やチームの責任範囲に応じてツールを分離するパターンです:

チーム担当領域使用ツール理由
Platform Teamインフラ基盤Terraform宣言型でインフラの一元管理
Security Teamセキュリティ設定Puppet設定の標準化と監視
DevOps TeamCI/CD・運用Ansible柔軟な自動化とオーケストレーション
Development TeamアプリケーションAnsible + Dockerアプリ固有の要件に対応

実装例: チーム間連携

yaml# Platform Team: インフラ情報のExport
# terraform/outputs.tf
output "platform_info" {
value = {
vpc_id              = module.vpc.vpc_id
private_subnet_ids  = module.vpc.private_subnets
security_group_id   = aws_security_group.app.id
database_endpoint   = module.rds.db_instance_endpoint
}
sensitive = true
}
puppet# Security Team: セキュリティベースライン
# puppet/modules/security_baseline/manifests/init.pp
class security_baseline {
  # ファイアウォール設定
  firewall { '100 allow ssh':
    dport  => [22],
    proto  => tcp,
    action => accept,
  }

  # セキュリティ監査ログ
  file { '/etc/audit/rules.d/base.rules':
    ensure  => file,
    content => template('security_baseline/audit.rules.erb'),
    notify  => Service['auditd'],
  }

  # 定期的なセキュリティアップデート
  cron { 'security-updates':
    command => '/usr/bin/yum update --security -y',
    user    => 'root',
    hour    => 2,
    minute  => 0,
    weekday => 0,
  }
}
yaml# DevOps Team: アプリケーション配備
# ansible/app-deploy.yml
---
- name: アプリケーション配備
  hosts: app_servers
  vars:
    app_version: "{{ lookup('env', 'APP_VERSION') | default('latest') }}"

  tasks:
    - name: Platform情報の取得
      set_fact:
        platform_info: "{{ lookup('file', '../terraform/platform_outputs.json') | from_json }}"

    - name: アプリケーションコンテナ配備
      docker_container:
        name: 'myapp-{{ app_version }}'
        image: 'myregistry/myapp:{{ app_version }}'
        ports:
          - '3000:3000'
        env:
          DATABASE_URL: '{{ platform_info.database_endpoint }}'
          REDIS_URL: '{{ platform_info.cache_endpoint }}'
        restart_policy: always

まとめ

インフラ管理ツールの選択において最も重要なのは、「宣言型」と「手続型」という根本的なアプローチの違いを理解することです。

**宣言型アプローチ(Terraform、Puppet)**の特徴:

  • 最終的な状態を記述し、達成手順はツールに委ねる
  • 冪等性が保証され、状態管理が自動化される
  • インフラリソースの管理や標準化された設定管理に適している

**手続型アプローチ(Ansible、Chef)**の特徴:

  • 実行すべき手順を明示的に記述する
  • 複雑な条件分岐やカスタムロジックに対応可能
  • アプリケーション配備や運用タスクの自動化に適している

実際のプロジェクトでは、これらのツールを併用することで、それぞれの強みを最大限に活用できます。特に Terraform と Ansible の組み合わせは、インフラ構築と設定管理の責任を明確に分離し、効率的な運用を実現する実績ある戦略です。

ツール選択の際は、技術的制約だけでなく、チーム体制、プロジェクトの規模、長期的な保守性も考慮することが重要です。適切な使い分けにより、安定性と生産性を両立したインフラ管理が実現できるでしょう。

関連リンク