0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Ansible】有名だが意外と詳しく知らないIaCツール、Ansibleとは?ハンズオンもしてみよう

0
Posted at

はじめに

こんばんは、mirukyです。

皆さんは、IaCと聞いてどんなツールを思い浮かべますか?
AWSで言えばCloudFormation、CDK、SAM、汎用的なものだとTerraformあたりを思い浮かべる人が多いかな、と思います。一方で、AnsibleもIaCの文脈でよく名前が挙がるツールです。個人的に、名前は有名なのに、TerraformやCloudFormationと同じ仲間なのか、別物なのか、どこで使うものなのかが少し曖昧に語られる印象を持っています。

Ansibleとは、クラウドリソース作成だけでなく、サーバーの中身をコードで合わせる場面に強いツールです。たとえば、Nginxを入れる、設定ファイルを配置する、ディレクトリの所有者や権限を決める、systemdサービスを起動する、環境ごとに設定値を変える、といった作業をPlaybookとして管理できます。

Ansibleが担当するサーバー内部構成とクラウドIaCの違い

私の理解では、Ansibleは広い意味でIaCの一部として扱えます。ただし、TerraformやCloudFormationのようにクラウドリソースの作成を中心にしたIaCとは得意領域が違います。クラウドの土台はTerraformやCloudFormationで作り、サーバー内の構成はAnsibleで合わせる。この分担が、実務でも使われる構成かな、と思います。

今回は、AnsibleをTerraformやCloudFormationと横並びにするのではなく、どの階層を担当するツールなのかから確認します。最後にはAWS上のEC2へAnsibleでNginxを入れる小さなハンズオンまで行おうと思いますので、最後までご覧いただけますと幸いです!

目次

  1. AnsibleがIaCツールとして語られる理由
  2. Ansibleの正体とは?真相に迫る
  3. TerraformやCloudFormationとの違い
  4. Ansibleでコード化できるもの
  5. Ansibleの基本要素
  6. ファイル構成まで管理できる理由
  7. 他のIaCツールとの重ね使い
  8. リポジトリ構成例
  9. 最小のPlaybookを書く
  10. 【ハンズオン】AWS上のEC2でAnsibleを試す
  11. 運用で気をつけること

1. AnsibleがIaCツールとして語られる理由

1-1. IaCを広めにとらえる

IaCは、インフラの状態をコードとして管理する考え方です。狭く捉えると、VPC、Subnet、Security Group、RDS、S3、IAM Roleなどのクラウドリソースをテンプレートで作る話になります。この範囲なら、CloudFormationやTerraformが代表例です。

ただ、インフラの状態はもちろんクラウドリソースだけで決まらないことが殆どです。全てがサーバレスとは限らないからですね。EC2やVMを使う構成では、OSの中に入っているパッケージ、設定ファイル、ユーザー、権限、サービス状態もインフラの一部です。ここを手作業で変更すると、Git上のテンプレートだけでは現在の状態を把握できなくなります。IaCとして表現しているはずなのに、乖離が生まれるわけです。

Ansibleは、このサーバー内の状態をコードで表します。Nginxが入っている、/etc/nginx/nginx.conf が決めた内容になっている、/opt/app の所有者が app ユーザーである、サービスが起動済みで自動起動も有効になっている、といった状態をPlaybookで定義できます。

IaCに含まれるクラウドリソース管理とサーバー内部構成管理

つまり、Ansibleはクラウドリソース作成だけのIaCではなく、構成管理寄りのIaCです。インフラをコードで扱うという大きな枠ではIaCに含めて考えられますが、TerraformやCloudFormationとは担当する階層が違いますね。

1-2. AnsibleをIaCとして使う意味

先程も説明した通り、AnsibleをIaCとして使う意味は、サーバー内の変更をレビューできるコードへ移すことです。手順書を見ながらSSHで作業すると、その場では作業が終わっても、後から同じ作業を別環境へ反映する負担が残ります。さらに、誰がどの設定を変えたのかも曖昧になりますよね。

Playbookに書けば、変更内容をPull Requestで確認できます。設定ファイルをテンプレートとして持てます。複数台へ同じ設定を反映できます。反映前に --check--diff で変更内容を確認できます。

手作業のサーバー変更をAnsible Playbookでレビュー可能にする流れ

AnsibleがIaCの文脈で語られるのは、この性質があるからです。現代でIaCが多用される原因も、こういった部分が大きいと考えています。

2. Ansibleの正体とは?真相に迫る

2-1. 構成管理ツール

では、Ansibleについて詳しく見ていきましょう。Ansibleの中心は構成管理です。構成管理とは、サーバーやミドルウェアの状態を決めた形に保つための考え方です。手作業で作ったサーバーは、時間が経つと少しずつ差分が出ます。誰かが設定ファイルを変更した、パッケージを追加した、サービスの起動設定を変えた、という変更が残るためです。

構成管理を使うと、サーバーの中身をあるべき状態として書けます。対象ホストへPlaybookを実行すると、Ansibleが現在の状態を確認し、packagefiletemplateservice などのモジュールで表せる範囲では、必要な変更だけを反映します。

現場で扱う対象はこのあたりです。

対象
パッケージ nginxpostgresql-clientamazon-cloudwatch-agent
ユーザー app ユーザー、sudo権限、SSH鍵
ファイル /etc/nginx/nginx.conf、アプリ設定、systemd unit
ディレクトリ /opt/app/var/log/app、所有者と権限
サービス 起動、停止、自動起動、再起動
OS設定 sysctl、timezone、locale、ログローテーション

構成管理は、サーバーを一度だけ作るためではなく、作った後も同じ状態を保つために使います。ここが、初回起動スクリプトだけで済ませる場合との大きな違いです。

2-2. エージェントレス

Ansibleは、管理対象ホストに常駐エージェントを入れない構成が基本です。LinuxサーバーではSSHで接続し、Playbookに書かれた処理を実行します。WindowsではWinRMを使う構成もあります。

エージェントレスであることは、導入時に管理対象へ入れるものを減らします。すでにSSH接続できるサーバーであれば、Inventoryに接続先を書き、Playbookを用意すれば始められます。

ただし、エージェントレスでも準備は必要です。接続ユーザー、秘密鍵、sudo権限、Python実行環境、ネットワーク経路などをそろえておきます。クラウド環境では、踏み台サーバーやSession Managerとの組み合わせも設計対象によくなります。

2-3. 宣言的と手順的の中間

CloudFormationやTerraformは、あるべき状態を宣言して、ツールが差分を判断します。Ansibleも状態を指定できますが、PlaybookはTaskを上から順に実行するため、面白いことに手順的な性質も持っています。

たとえば ansible.builtin.packagestate: present と書くと、パッケージがなければインストールし、すでに入っていれば変更しません。これは宣言的です。一方で、Taskの順番はPlaybookに書いた順に意味を持ちます。パッケージを入れてから設定ファイルを置き、設定ファイルが変わったらサービスを再起動する、という流れを作ることができます。

Ansibleが宣言的な状態指定とTask順序実行を組み合わせる図

この中間的な性質があるので、Ansibleは単なる設定ファイル配布よりも踏み込んだ構成管理に向いています。サーバーの中身は、リソースを作れば終わるものではなく、パッケージ、ファイル、権限、サービス、アプリ配置の順番が関係します。Ansibleは、その順番をPlaybookで表しながら、各Taskを冪等(べき等)に書けます。

3. TerraformやCloudFormationとの違い

3-1. TerraformとCloudFormationが得意なこと

TerraformやCloudFormationは、クラウドAPIで管理されるリソースの作成、更新、削除に向いています。VPCを作る、サブネットを作る、セキュリティグループ(SG)を作る、EC2を作る、RDSを作る、IAMロールを作る、といった領域です。

これらのツールは、リソース間の依存関係を扱えます。たとえば、VPCを作ってからサブネットを作る、IAMロールを作ってからインスタンスプロファイルを作る、セキュリティグループを作ってからEC2に付ける、といった関係をテンプレート上の参照から判断できます。

クラウドAPI側で管理する対象です。

領域
ネットワーク VPC、サブネット、ルートテーブル、セキュリティグループ
コンピュート EC2、Auto Scaling Group、ECS、Lambda
データ RDS、DynamoDB、S3
権限 IAMロール、ポリシー、インスタンスプロファイル
監視 CloudWatch Alarm、Log Group
外部公開 ALB、API Gateway、CloudFront、Route 53

この領域は、TerraformやCloudFormationに任せるのが基本です。クラウドリソースの依存関係、差分、削除、置換、状態管理を扱うためです。

3-2. Ansibleが得意なこと

Ansibleは、作成済みサーバーの中身を合わせる用途に強いです。OSに入れるパッケージ、配置する設定ファイル、ディレクトリ権限、サービス起動状態、ミドルウェアごとの細かい設定をPlaybookで表せます。

Ansibleが担当することの多い領域です。

領域
OS設定 timezone、locale、sysctl、ユーザー
パッケージ Nginx、PostgreSQL client、監視エージェント
ファイル配置 設定ファイル、アプリ設定、証明書配置
テンプレート 環境ごとに値が違う設定ファイル
サービス systemdの有効化、起動、再起動
アプリ配置 リリースファイル展開、設定反映、ヘルスチェック

押さえておきたいのは、Ansibleはサーバーの中に入って作業する領域を扱えることです。IaCでもEC2のUserData、CloudFormationの AWS::CloudFormation::Init、Terraformのprovisioner、cloud-initなどでファイル配置やパッケージ導入はできます。ただ、複数ファイル、複数サービス、環境ごとのテンプレート、再実行、差分確認、Role分割まで考えると、Ansibleのほうが構成をPlaybookとRoleに分けてレビューできます。これがAnsibleを敢えて使う理由ですね。サーバーレベルのインフラ案件だと、かなりの頻度で出会います。

3-3. 違いを一枚でまとめる

TerraformやCloudFormationとAnsibleの担当領域比較

違いは、ざっとビジュアルで確認すると上の画像みたいな感じです。
TerraformやCloudFormationとAnsibleの詳細な違いも表にしておきます。

観点 Terraform / CloudFormation Ansible
主な対象 クラウドリソース サーバー内の構成
代表例 VPC、EC2、RDS、IAM パッケージ、ユーザー、ファイル、サービス
実行単位 リソース Task、Playbook、Role
状態管理 state、stack 実行時に対象ホストの状態を確認
得意な変更 リソース作成、削除、依存関係 ファイル配置、権限、サービス再起動
注意点 OS内の細かい設定が肥大化する場合がある クラウドリソース依存関係は専用IaCほど得意ではない

この表からわかる通り、AnsibleはIaCと競合するだけのツールではありません。クラウドリソースを作るIaCの上に重ねて、サーバー内の構成をコード化するツールとして考えると良いと思います。むしろ、相互に助け合うような関係なんです。

4. Ansibleでコード化できるもの

4-1. パッケージとサービス

具体的なコードを見ていきましょう。Ansibleでは、パッケージとサービスを状態として書けます。Nginxを入れる、サービスを起動する、自動起動を有効にする、といった処理です。

- name: Install nginx
  ansible.builtin.package:
    name: nginx
    state: present

- name: Start nginx
  ansible.builtin.service:
    name: nginx
    state: started
    enabled: true

state: present は、パッケージが入っていなければ入れ、すでに入っていれば変更しない、という意味です。state: started はサービスが起動している状態を表し、enabled: true はOS起動時にも自動で立ち上がる設定です。

これを手作業で行う場合、SSHで入ってパッケージを入れ、systemctlを実行し、複数台で同じ作業を繰り返します。Ansibleにすると、同じ内容をPlaybookとして共有できます。

4-2. ユーザーと権限

アプリケーション用ユーザーやディレクトリ権限もコード化できます。たとえば、app ユーザーを作り、/opt/app の所有者を app にし、実行に必要な権限を付ける、といった設定です。

- name: Create app user
  ansible.builtin.user:
    name: app
    shell: /sbin/nologin
    system: true

- name: Create app directory
  ansible.builtin.file:
    path: /opt/app
    state: directory
    owner: app
    group: app
    mode: "0755"

このような設定は、後から人手で確認すると抜け漏れが出ることがあります。Ansibleで書くと、ユーザー、ディレクトリ、権限がPlaybook上で確認できます。YAMLなので非常に可読性が高いです。

4-3. 設定ファイル

Ansibleは、設定ファイルの管理にも向いています。固定ファイルなら copy、環境ごとに値を変えるなら template を使います。

固定ファイルを置く例です。

- name: Copy static config
  ansible.builtin.copy:
    src: files/app.conf
    dest: /etc/app/app.conf
    owner: root
    group: root
    mode: "0644"

テンプレートを置く例です。

- name: Deploy nginx config
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: "0644"
  notify: Restart nginx

notify を使うと、設定ファイルが変わった場合だけHandlerを呼び出せます。設定変更がないのに毎回サービスを再起動する、といった運用を避けられます。

4-4. アプリケーション配置

Ansibleは、アプリケーションの配置にも使えます。ビルド済みファイルを配る、設定ファイルを出し分ける、サービスを再起動する、ヘルスチェックを行う、といった流れです。

ただし、現在のクラウドネイティブな構成では、コンテナイメージやサーバーレスを使う選択も増えています。ECS Fargate、Lambda、Cloud Runのようにサーバーへ入らない構成では、Ansibleの出番は小さくなります。

Ansibleが特に役立つのは、EC2、オンプレミスVM、既存のLinuxサーバー、ミドルウェア設定が残る環境です。サーバー内の構成をGitで管理したい場合に、選択肢としてかなり強いです。

5. Ansibleの基本要素

5-1. Inventory

Inventoryは、Ansibleが接続するホスト一覧です。静的なINI/YAMLファイルでも書けますし、クラウドAPIから取得するDynamic Inventoryも使えます。

[web]
web-01 ansible_host=10.0.1.10
web-02 ansible_host=10.0.1.11

[db]
db-01 ansible_host=10.0.2.10

[all:vars]
ansible_user=ec2-user
ansible_ssh_private_key_file=~/.ssh/app.pem

静的Inventoryは小さな環境で始めるには十分です。AWSのAuto Scaling Groupや頻繁に増減するサーバーを扱う場合は、amazon.aws.aws_ec2 inventory pluginやTerraformの出力からInventoryを生成する構成を検討します。

Inventoryでは、ホスト一覧に加えて webdb のようなグループを作り、グループごとに変数を分けられます。たとえばWebサーバーにはNginx設定、DBサーバーにはバックアップ設定、といった分け方です。

5-2. Playbook

Playbookは、どのホストに何を実行するかを書くYAMLファイルです。hosts で対象を決め、tasks に処理を書きます。

- name: Configure web servers
  hosts: web
  become: true
  tasks:
    - name: Install nginx
      ansible.builtin.package:
        name: nginx
        state: present

    - name: Start nginx
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

become: true は、必要なTaskを管理者権限で実行するための指定です。パッケージ導入や /etc 配下の設定ファイル配置では、root権限が必要になることが多いです。

Playbookでは、同じ作業を何度も実行できるように書くことが重要です。shellcommand に長いコマンドを詰め込むより、packagefiletemplateservice などのモジュールを使うほうが、状態をコードとして表せます。

5-3. Module

Moduleは、Ansibleが実際の処理を行うパーツ(部品)です。ファイルを作る、テンプレートを配置する、サービスを起動する、パッケージを入れる、といった操作ごとにモジュールがあります。

よく使うモジュールを挙げます。

モジュール 用途
ansible.builtin.package OSパッケージ管理
ansible.builtin.file ファイル、ディレクトリ、権限
ansible.builtin.copy 静的ファイル配置
ansible.builtin.template Jinja2テンプレートからファイル生成
ansible.builtin.lineinfile 1行単位の編集
ansible.builtin.blockinfile 複数行ブロックの編集
ansible.builtin.service サービス起動、停止、自動起動

公式ドキュメントでは、copy はファイルやディレクトリをリモートへコピーし、所有者や権限などのメタ情報も設定できると説明されています。file はファイル、ディレクトリ、シンボリックリンクの属性や存在/不在を扱います。

5-4. Handler

Handlerは、Taskの変更があったときだけ実行される処理です。設定ファイルを更新した場合だけNginxを再起動する、といった用途に向いています。

- name: Configure nginx
  hosts: web
  become: true
  tasks:
    - name: Deploy nginx config
      ansible.builtin.template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
      notify: Restart nginx

  handlers:
    - name: Restart nginx
      ansible.builtin.service:
        name: nginx
        state: restarted

この例では、taskshandlers が同じPlayの中にあります。テンプレート反映でファイル内容が変わった場合だけ Restart nginx が呼ばれ、変更がなければ再起動しません。なお、service モジュールの state: restarted は呼ばれたら毎回再起動する指定です。だからこそ、通常のTaskではなくHandlerとして扱うと、不要な再起動を避けられます。

5-5. Role

Roleは、Playbookの中身を役割ごとに分ける仕組みです。Nginx用Role、アプリ用Role、監視エージェント用Roleのように分けると、複数環境で再利用できます。

Roleの代表的な構成です。

roles/
  nginx/
    tasks/
      main.yml
    handlers/
      main.yml
    templates/
      nginx.conf.j2
    files/
      index.html
    defaults/
      main.yml
    vars/
      main.yml

tasks は実行する処理、handlers は変更時だけ呼ぶ処理、templates はJinja2テンプレート、files は固定ファイル、defaults は上書きできる既定値、vars はRole内で使う変数を置く場所です。

変数をRoleへ詰め込みすぎると、運用時の変更範囲が広がります。環境差分は group_varshost_vars に置き、Roleは何を設定するかに集中させます。

Ansible RoleとInventory/Playbook/Moduleの関係

6. ファイル構成まで管理できる理由

6-1. ディレクトリを作る

Ansibleでファイル構成まで管理できる理由は、filecopytemplate などのモジュールが、ファイルそのものだけでなく、所有者、グループ、権限、ディレクトリの存在まで扱えるからです。

- name: Create application directories
  ansible.builtin.file:
    path: "{{ item.path }}"
    state: directory
    owner: app
    group: app
    mode: "{{ item.mode }}"
  loop:
    - { path: "/opt/app", mode: "0755" }
    - { path: "/var/log/app", mode: "0750" }
    - { path: "/etc/app", mode: "0750" }

このPlaybookでは、3つのディレクトリを作り、所有者と権限まで決めています。ディレクトリがすでに存在し、指定どおりの状態なら変更は入りません。手作業で mkdirchownchmod を実行するより、状態を残せます。

6-2. 静的ファイルを配置する

変更しない固定ファイルは copy で配置します。

- name: Copy application unit file
  ansible.builtin.copy:
    src: files/app.service
    dest: /etc/systemd/system/app.service
    owner: root
    group: root
    mode: "0644"
  notify: Reload systemd

この例では、systemd unitファイルを配置しています。ファイル内容が変わった場合だけHandlerを呼び、systemdをreloadする構成にできます。

6-3. テンプレートで環境差分を吸収する

開発環境と本番環境で、接続先やポート番号が違うことはよくあります。AnsibleではJinja2テンプレートを使い、変数を差し込んだ設定ファイルを生成できます。

templates/app.conf.j2 の例です。

server_name = {{ app_server_name }}
listen_port = {{ app_port }}
log_level = {{ app_log_level }}

Playbook側では、テンプレートを配置します。

- name: Deploy app config
  ansible.builtin.template:
    src: app.conf.j2
    dest: /etc/app/app.conf
    owner: root
    group: app
    mode: "0640"
  notify: Restart app

環境ごとの値は group_vars に置きます。

# group_vars/dev.yml
app_server_name: dev.example.internal
app_port: 8080
app_log_level: debug
# group_vars/production.yml
app_server_name: app.example.com
app_port: 8080
app_log_level: info

同じテンプレートを使いながら、環境ごとに違う設定ファイルを出せます。テンプレート本体、環境ごとの変数、配置先の権限をPlaybook上で確認できるため、手順書だけでファイル配置を管理するより変更理由を説明できます。

Jinja2テンプレートと変数で環境別設定ファイルを生成する流れ

6-4. 行単位の編集は慎重に使う

lineinfileblockinfile を使うと、既存ファイルの一部だけを変更できます。便利ですが、使いすぎるとファイル全体の完成形が把握しにくくなります。

設定ファイル全体を管理できるなら template を優先します。OS標準の大きな設定ファイルに一部だけ設定を足す場合は、lineinfileblockinfile が候補になります。

たとえば、/etc/ssh/sshd_config の一部だけ変更する場合は lineinfile が合う場合があります。一方で、アプリケーション設定ファイルを自分たちで管理するなら、テンプレートを丸ごと持ったほうが差分をファイル単位で確認できます。

7. 他のIaCツールとの重ね使い

7-1. Terraformで作り、Ansibleで合わせる

TerraformでEC2やSecurity Groupを作り、出力されたIPアドレスやDNS名をAnsible Inventoryへ渡す構成です。

Terraform側でInventoryファイルを生成する方法もありますし、amazon.aws.aws_ec2 inventory pluginを使って、EC2タグから対象ホストを取得する方法もあります。固定IPに依存しすぎるとサーバー入れ替え時に修正箇所が増えるため、タグやグループ名で対象を決める構成にすると、サーバーを入れ替えた後もPlaybook側の変更を抑えられます。

Terraform公式ドキュメントでは、provisionerは他の手段が使えない場合の最終手段に近い扱いです。リソース作成後のOS内設定が増えるなら、Terraform provisionerに詰め込むより、Ansibleへ分けたほうが、レビュー時にクラウド側の変更とOS内の変更を分けて説明できるかと思います。

7-2. CloudFormationで作り、Ansibleで合わせる

CloudFormationにも AWS::CloudFormation::Init があり、packages、groups、users、sources、files、commands、services などを扱えます。cfn-init はこれらを packages、groups、users、sources、files、commands、services の順に処理します。EC2の初期設定をCloudFormationテンプレート内で扱いたい場合には便利です。

ただし、複数の設定ファイル、複数のRole、環境ごとの差分、反映前の差分確認、再実行まで考えると、CloudFormationテンプレートにOS内設定を入れ続けるほど肥大化します。

そのため、CloudFormationでVPC、EC2、IAM Role、Security Groupなどを作り、AnsibleでOS内の構成を合わせる分担は十分に現実的です。

7-3. 重ね使いの全体像

TerraformやCloudFormationで作ったEC2へ、AnsibleがSSHで入り、OS内の構成を合わせる流れです。

Terraformなどで作ったEC2へAnsibleでOS内設定を反映する流れ

クラウドリソースの作成や削除はTerraformやCloudFormation、サーバー内の構成はAnsibleが担当します。責任範囲を分けると、レビュー時にどの変更がクラウド側で、どの変更がOS内なのかを説明できます。

8. リポジトリ構成例

8-1. 小さく始める構成

小さな構成なら、次のように始められます。

ansible/
  ansible.cfg
  inventories/
    dev.ini
    production.ini
  playbooks/
    site.yml
  roles/
    nginx/
      tasks/
        main.yml
      handlers/
        main.yml
      templates/
        nginx.conf.j2
      defaults/
        main.yml

ansible.cfg の例です。

[defaults]
inventory = inventories/dev.ini
roles_path = roles
host_key_checking = True
retry_files_enabled = False

playbooks/site.yml の例です。

- name: Configure web servers
  hosts: web
  become: true
  roles:
    - nginx

Roleを使うと、Playbook本体はどのホストにどのRoleを適用するかに集中できます。細かいTaskはRole側へ置きます。

8-2. 環境差分の置き場所

環境差分は、Inventoryや group_vars に置きます。

ansible/
  inventories/
    dev/
      hosts.ini
      group_vars/
        web.yml
    production/
      hosts.ini
      group_vars/
        web.yml

開発環境の変数例です。

app_log_level: debug
app_server_name: dev.example.internal

本番環境の変数例です。

app_log_level: info
app_server_name: app.example.com

同じRoleを使い、変数だけ環境ごとに切り替える形です。Roleの中に環境名の条件分岐を増やしすぎると、変更範囲が広がります。環境差分は外へ出すのが良いかと思います。

8-3. IaCリポジトリと分けるか

IaCとAnsibleを同じリポジトリに置くか、分けるかはチームの運用次第です。

構成 向いている場面
同じリポジトリ 小さなシステム、同じチームが両方管理する
別リポジトリ 基盤チームとアプリ運用チームが分かれる
mono repo 変更の流れを1か所で管理したい
repo分割 権限やリリースサイクルを分けたい

最初は同じリポジトリでも問題ありません。ただし、TerraformのstateやCloudFormation stackと、AnsibleのPlaybook実行タイミングは別物です。パイプラインでは、IaC反映、Inventory更新、Ansible実行の順番を明確にします。

9. 最小のPlaybookを書く

9-1. Nginxを入れて設定する

最小構成として、Nginxをインストールし、設定ファイルを置き、サービスを起動するPlaybookを書きます。

- name: Configure nginx
  hosts: web
  become: true
  vars:
    nginx_port: 80
  tasks:
    - name: Install nginx
      ansible.builtin.package:
        name: nginx
        state: present

    - name: Deploy nginx config
      ansible.builtin.template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        owner: root
        group: root
        mode: "0644"
      notify: Restart nginx

    - name: Start nginx
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: Restart nginx
      ansible.builtin.service:
        name: nginx
        state: restarted

テンプレート例です。

events {}

http {
  server {
    listen {{ nginx_port }};
    location / {
      return 200 "Hello from Ansible\n";
    }
  }
}

実行コマンドです。

ansible-playbook -i inventories/dev.ini playbooks/nginx.yml

このPlaybookで、パッケージ、設定ファイル、サービス状態までまとめて扱えます。設定ファイルが変わった場合だけHandlerが呼ばれ、Nginxが再起動されます。

9-2. 反映前に確認する

本番環境へいきなり反映するのではなく、先に変更内容を確認します。

ansible-playbook -i inventories/production.ini playbooks/nginx.yml --check --diff

--check は実際の変更を行わずに、どのTaskが変更対象になるかを確認します。--diff はファイル変更の差分を出します。ただし、check modeではパッケージ導入やディレクトリ作成が実際には行われないため、後続Taskがその前提に依存していると失敗する場合があります。初回構築では検証環境で通常実行し、2回目以降の変更確認で --check --diff を使うとよいです。

構成管理は、コードを書いたら終わりではありません。反映前の差分確認、対象ホストの絞り込み、失敗時の戻し方まで運用に含めておくと、事故を減らせます。

10. 【ハンズオン】AWS上のEC2でAnsibleを試す

ここでは、AWS上にAmazon Linux 2023のEC2を1台作り、手元のPCからAnsibleでNginxを入れてみます。EC2作成はAWS CLI、サーバー内の構成変更はAnsible、という分担にします。ここまで説明してきた重ね使いを小さく体験するためのハンズオンです。

大まかな流れは、AWS CLIでEC2を用意し、AnsibleのInventoryに接続先を書き、PlaybookでNginxを設定し、最後にブラウザや curl で応答を確認する、という順番です。AWS CLIはクラウド側の操作、AnsibleはEC2の中の操作を担当します。

AWS CLIでEC2を作成しAnsibleでNginxを設定するハンズオン全体像

10-1. 前提を確認する

手元のPCにAWS CLIとPython 3が入っている前提で進めます。最初にAWS CLIの接続先を確認します。aws sts get-caller-identity は、いま使っている認証情報のアカウントIDやユーザー、ロールを返します。ここでエラーになる場合は、AWS CLIの認証情報を先に設定してください。

aws sts get-caller-identity
aws configure get region

aws configure get region は、AWS CLIに設定されている既定リージョンを確認するコマンドです。空で返ってきても後続のコマンドで東京リージョンを明示するため問題ありませんが、意図しないリージョンへEC2を作らないように、ここで一度確認しておきます。

AWS CLIで認証情報とリージョンを確認した実行結果

AnsibleはPythonで動くため、このハンズオンでは専用の仮想環境に入れます。PC全体のPython環境へ直接入れず、検証用の .venv-ansible に閉じ込める形です。検証後はディレクトリごと削除できます。

python3 -m venv .venv-ansible
source .venv-ansible/bin/activate
python -m pip install --upgrade pip
python -m pip install ansible

source .venv-ansible/bin/activate の後は、ターミナル上で仮想環境が有効になります。python -m pip install ansible で、この仮想環境の中にAnsibleをインストールします。

Python仮想環境にAnsibleをインストールした実行結果

インストールできたか確認します。バージョン情報が表示されれば、手元のPCから ansible コマンドを実行できる状態です。

ansible --version

ansible --versionでAnsibleのインストールを確認した結果

リージョンを明示しておきます。東京リージョンで試す例です。

export AWS_REGION=ap-northeast-1
aws configure set region "$AWS_REGION"

このハンズオンではDefault VPCを使います。Default VPCには、外部から接続できるデフォルトサブネットが用意されているため、VPC、サブネット、Internet Gateway、ルートテーブルを別々に作る手順を省けます。Default VPCがないアカウントでは、検証用VPCとパブリックサブネットのIDを自分で指定してください。

export VPC_ID=$(aws ec2 describe-vpcs \
  --filters Name=is-default,Values=true \
  --query 'Vpcs[0].VpcId' \
  --output text)

export SUBNET_ID=$(aws ec2 describe-subnets \
  --filters Name=vpc-id,Values="$VPC_ID" Name=default-for-az,Values=true \
  --query 'Subnets[0].SubnetId' \
  --output text)

echo "$VPC_ID"
echo "$SUBNET_ID"

echo "$VPC_ID"echo "$SUBNET_ID"vpc-...subnet-... の値が返れば、この後のEC2作成で使うネットワーク情報を取得できています。None や空文字になる場合は、Default VPCが存在しないか、リージョンが想定と違う可能性があります。

Default VPCとサブネットIDを取得した実行結果

10-2. EC2を作成する

SSH接続用のキーペアを作ります。EC2へAnsibleで接続するときは、裏側ではSSHを使います。そのため、EC2作成時に指定するKeyPairと、手元に保存する秘密鍵ファイルの対応がずれると接続できません。

前回の検証で同じ名前のKeyPairやSecurity Groupが残っていると作成に失敗するため、ここでは時刻を付けた名前にします。秘密鍵は ~/.ssh に保存します。iCloud Driveなど、パスに半角スペースを含む場所に鍵を置くとInventoryの記述で苦戦する場合があるためです。

mkdir -p ansible-handson ~/.ssh

export HANDSON_SUFFIX="$(date +%Y%m%d%H%M%S)"
export KEY_NAME="ansible-handson-key-${HANDSON_SUFFIX}"
export SG_NAME="ansible-handson-sg-${HANDSON_SUFFIX}"
export INSTANCE_NAME="ansible-handson-${HANDSON_SUFFIX}"
export KEY_PATH="${HOME}/.ssh/${KEY_NAME}.pem"

if aws ec2 create-key-pair \
  --key-name "$KEY_NAME" \
  --query 'KeyMaterial' \
  --output text > "${KEY_PATH}.tmp"; then
  mv "${KEY_PATH}.tmp" "$KEY_PATH"
else
  rm -f "${KEY_PATH}.tmp"
  echo "KeyPairの作成に失敗しました。KEY_NAMEを変えるか、残っているKeyPairを削除してください。"
  exit 1
fi

chmod 400 "$KEY_PATH"
test -s "$KEY_PATH"
ssh-keygen -y -f "$KEY_PATH" > /dev/null

create-key-pair の結果はいったん .tmp に保存し、成功した場合だけ正式な鍵ファイルへ移します。こうしておくと、KeyPair作成に失敗したときに0バイトの秘密鍵ファイルだけが残る事故を避けられます。test -s "$KEY_PATH" は鍵ファイルが空でないことを確認し、ssh-keygen -y -f "$KEY_PATH" はSSH鍵として読めることを確認しています。

次に、SSHとHTTPを自分のグローバルIPからだけ許可するSecurity Groupを作ります。22番ポートはAnsibleがSSH接続するため、80番ポートはNginxのHTTP応答を確認するために使います。

export MY_IP="$(curl -s https://checkip.amazonaws.com)/32"

export SG_ID=$(aws ec2 create-security-group \
  --group-name "$SG_NAME" \
  --description "Ansible handson SSH and HTTP" \
  --vpc-id "$VPC_ID" \
  --query 'GroupId' \
  --output text)

aws ec2 authorize-security-group-ingress \
  --group-id "$SG_ID" \
  --protocol tcp \
  --port 22 \
  --cidr "$MY_IP"

aws ec2 authorize-security-group-ingress \
  --group-id "$SG_ID" \
  --protocol tcp \
  --port 80 \
  --cidr "$MY_IP"

MY_IP には、手元PCがインターネットへ出るときのグローバルIPアドレスを入れています。VPNやテザリングを切り替えるとIPアドレスが変わり、SSHやHTTPで接続できなくなる場合があります。そのときはSecurity Groupの許可元CIDRを更新してください。

Security Groupを作成してSSHとHTTPを許可した実行結果

Amazon Linux 2023の最新AMI IDをSSM Parameter Storeから取得し、EC2を1台起動します。AMI IDを直接書くとリージョンや時期で変わるため、AWSが公開しているSSMパラメータから取得します。

export AMI_ID=$(aws ssm get-parameter \
  --name /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 \
  --query 'Parameter.Value' \
  --output text)

export INSTANCE_ID=$(aws ec2 run-instances \
  --image-id "$AMI_ID" \
  --instance-type t3.micro \
  --key-name "$KEY_NAME" \
  --security-group-ids "$SG_ID" \
  --subnet-id "$SUBNET_ID" \
  --associate-public-ip-address \
  --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$INSTANCE_NAME}]" \
  --query 'Instances[0].InstanceId' \
  --output text)

aws ec2 wait instance-status-ok \
  --instance-ids "$INSTANCE_ID"

export PUBLIC_IP=$(aws ec2 describe-instances \
  --instance-ids "$INSTANCE_ID" \
  --query 'Reservations[0].Instances[0].PublicIpAddress' \
  --output text)

echo "$PUBLIC_IP"

aws ec2 wait instance-status-ok は、EC2のステータスチェックが通るまで待つコマンドです。インスタンスが起動直後でも、SSH接続の準備が終わるまで少し時間がかかるため、ここで待機します。最後に PUBLIC_IP を取得し、このIPアドレスをAnsibleの接続先として使います。

EC2のステータス待ちは数分かかる場合があります。

EC2のパブリックIPを取得した実行結果
パブリックIPが返ってきました(黒塗りの部分)。

10-3. AnsibleからEC2へ接続する

Inventoryを作ります。Inventoryは、Ansibleが接続するホスト一覧です。ここでは ansible-target という名前のホストを1台定義し、実際の接続先としてEC2のパブリックIPを指定します。

ansible_user=ec2-user は、Amazon Linux 2023へSSH接続するときのユーザー名です。ansible_ssh_private_key_file には、10-2で作成した秘密鍵のパスを指定します。パスに半角スペースが入っても崩れないよう、値をシングルクォートで囲んでいます。

cat > ansible-handson/inventory.ini <<EOF
[web]
ansible-target ansible_host=${PUBLIC_IP} ansible_user=ec2-user ansible_ssh_private_key_file='${KEY_PATH}'
EOF

初回接続のため、SSHのknown_hostsへEC2のホストキーを追加します。SSHでは、接続先サーバーのホストキーを手元の known_hosts に記録します。これを先に入れておくと、Ansible実行時に対話入力で止まりません。

mkdir -p ~/.ssh
ssh-keyscan -H "$PUBLIC_IP" >> ~/.ssh/known_hosts

Ansibleのpingモジュールで接続を確認します。この ping はICMPのpingではなく、SSHで対象ホストへ入り、AnsibleがPythonを使ってモジュールを実行できるかを確認するためのテストです。

ansible -i ansible-handson/inventory.ini web -m ping

pong が返れば、手元のAnsibleからEC2へ接続できています。ここまで通れば、Security Group、パブリックIP、SSHユーザー、秘密鍵、Inventoryの基本設定がそろっています。

Ansible pingモジュールでEC2接続を確認した結果

10-4. NginxをAnsibleで設定する

Nginxを入れ、デモページを配置し、サービスを起動するPlaybookを作ります。become: true は、対象EC2上で管理者権限を使う指定です。パッケージ導入や /usr/share/nginx/html/ へのファイル配置にはroot権限が必要なので、ここで有効にしています。

このPlaybookは3つのTaskで構成します。1つ目でNginxをインストールし、2つ目でトップページのHTMLを配置し、3つ目でNginxサービスを起動します。HTMLの内容が変わった場合だけHandlerに通知し、Nginxを再起動します。

cat > ansible-handson/nginx.yml <<'EOF'
- name: Configure nginx on EC2
  hosts: web
  become: true
  tasks:
    - name: Install nginx
      ansible.builtin.package:
        name: nginx
        state: present

    - name: Put demo page
      ansible.builtin.copy:
        dest: /usr/share/nginx/html/index.html
        content: |
          Hello from Ansible on EC2
        owner: root
        group: root
        mode: "0644"
      notify: Restart nginx

    - name: Start nginx
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: Restart nginx
      ansible.builtin.service:
        name: nginx
        state: restarted
EOF

初回構築では、まず検証用EC2に通常実行します。check modeではNginxパッケージが実際には入らないため、その後のファイル配置Taskが前提ディレクトリ不足で失敗する場合があります。最初の1回は実際に反映し、EC2の中を目的の状態にします。

ansible-playbook \
  -i ansible-handson/inventory.ini \
  ansible-handson/nginx.yml

Ansible PlaybookでNginxを初回反映した実行結果

実行結果では、最後の PLAY RECAP を確認します。failed=0 であれば、Playbookは失敗していません。changed が付いたTaskは、対象EC2に変更を入れたTaskです。初回はパッケージ導入、HTML配置、サービス起動が入るため、いくつかのTaskが changed になります。

2回目以降の変更確認では、check modeとdiffで変更予定を確認できます。

ansible-playbook \
  -i ansible-handson/inventory.ini \
  ansible-handson/nginx.yml \
  --check \
  --diff

Ansible Playbookをcheck modeとdiffで確認した実行結果

--check は実際には反映せず、変更が必要かどうかを判定します。--diff はファイル内容に差分がある場合に、変更前後の差を表示します。すでに目的の状態になっていれば、2回目以降は changed=0 に近い結果になります。これがAnsibleの冪等性を体感できる部分です。

設定を変えた場合は、差分を確認してから反映します。

ansible-playbook \
  -i ansible-handson/inventory.ini \
  ansible-handson/nginx.yml

Ansible Playbookを再実行して冪等性を確認した結果

Playbookを再実行しても、すでに同じ状態なら不要な変更は入りません。HTMLを変更した場合は、copy Taskが changed になり、Handler経由でNginxの再起動が走ります。設定ファイルやテンプレートを扱う実務でも、この流れはよく使います。

HTTPで応答を確認します。

curl "http://${PUBLIC_IP}"

curlでNginxのHTTP応答を確認した結果

Hello from Ansible on EC2 が返れば、EC2作成はAWS CLI、EC2内のNginx設定はAnsible、という役割分担を体験できています。

10-5. リソースを削除する

検証後は、EC2、Security Group、キーペア、手元の検証ファイルを削除します。EC2がSecurity Groupを使っている間はSecurity Groupを削除できないため、まずインスタンスを終了し、終了完了を待ってからSecurity Groupを削除します。

aws ec2 terminate-instances \
  --instance-ids "$INSTANCE_ID"

aws ec2 wait instance-terminated \
  --instance-ids "$INSTANCE_ID"

aws ec2 delete-security-group \
  --group-id "$SG_ID"

aws ec2 delete-key-pair \
  --key-name "$KEY_NAME"

rm -f "$KEY_PATH" "${KEY_NAME}.pem"
rm -rf ansible-handson .venv-ansible

このハンズオンでは、クラウドリソースの作成と削除をAWS CLIで行い、OS内の構成をAnsibleで扱いました。実務ではAWS CLI部分をTerraformやCloudFormationに置き換えると、この記事で扱っているIaCとの重ね使いに近い構成になります。Cloudformationに関しては、下記記事で詳しくまとめていますので、良ければご参照ください。

11. 運用で気をつけること

11-1. 冪等性を壊さない

Ansibleでは、同じPlaybookを何度実行しても、対象が同じ状態にそろうように書くことが大切です。これを冪等性と呼びます。

冪等性を壊す例です。

- name: Append config line
  ansible.builtin.shell: echo "PermitRootLogin no" >> /etc/ssh/sshd_config

このTaskは、実行するたびに同じ行を追記します。代わりに、lineinfile を使います。

- name: Disable root login
  ansible.builtin.lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '^PermitRootLogin'
    line: 'PermitRootLogin no'
  notify: Restart sshd

shellcommand が悪いわけではありません。ただし、状態を表せるモジュールがあるなら、そちらを優先すると変更結果を具体的に説明できます。

11-2. 秘密情報を平文で置かない

もはや言うまでもない、必須中の必須事項です。
パスワード、秘密鍵、APIトークンをPlaybookやInventoryへ平文で置くのは避けます。Ansible Vault、AWS Secrets Manager、SSM Parameter Store、CI/CDのSecret機能などを使います。

Ansible Vaultで暗号化ファイルを作る例です。

ansible-vault create group_vars/production/vault.yml

実行時にVaultパスワードを指定します。

ansible-playbook -i inventories/production.ini playbooks/site.yml --ask-vault-pass

CI/CDで使う場合は、Vault password fileや外部Secret管理と組み合わせます。秘密情報の扱いは運用ルールまで含めて決めます。

11-3. 対象ホストを絞る

Ansibleは複数台へ一括反映できるため、対象ホストの指定ミスが大きな事故につながります。Inventoryのグループ名、環境名、タグ、limit指定を慎重に扱います。

対象を絞る例です。

ansible-playbook -i inventories/production.ini playbooks/site.yml --limit web-01

複数台へ反映する場合も、いきなり全台ではなく、1台、半分、全台のように段階を分けると、失敗した範囲を切り分けられます。

11-4. IaCとAnsibleの二重管理を避ける

同じ設定をTerraformとAnsibleの両方で管理すると、どちらが正しいのか分からなくなります。たとえば、EC2のSecurity GroupをTerraformで管理しながら、AnsibleでOSのfirewalld設定も変更する場合、責任範囲を明確にします。

二重管理を避けるための目安です。

対象 管理元
クラウド上のSecurity Group Terraform / CloudFormation
OS内のfirewalld Ansible
IAM Role Terraform / CloudFormation
サービス用ユーザー Ansible
Log Group Terraform / CloudFormation
ログ出力設定ファイル Ansible

境界が曖昧なまま進めると、障害時の調査範囲が広がります。どのツールがどの状態を管理するかを、リポジトリのREADMEや運用手順に書いておくのが良いかと思います。

11-5. CI/CDで検証する

Ansibleもコードなので、CIで最低限の検証を行います。構文チェック、対象Playbookの確認、必要に応じたcheck mode実行を組み込みます。

ansible-playbook --syntax-check playbooks/site.yml
ansible-playbook -i inventories/dev.ini playbooks/site.yml --check --diff

GitHub Actionsで書くなら、たとえば次の形です。

name: ansible-check

on:
  pull_request:
    paths:
      - "ansible/**"

jobs:
  syntax-check:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Install Ansible
        run: python -m pip install ansible

      - name: Syntax check
        working-directory: ansible
        run: ansible-playbook --syntax-check playbooks/site.yml

実際にサーバーへ接続するcheck modeをCIで行う場合は、認証情報、接続経路、対象環境の分離を設計します。Pull Requestごとに本番へ接続する構成は避け、検証環境で確認するのが基本ですね。

おわりに

ここまで再三繰り返した通り、Ansibleは、TerraformやCloudFormationの代わりというより、それらが作った土台の上でサーバーの中身を合わせるためのツールです。相互に助け合う存在だとご理解いただければ幸いです。

ではまた、お会いしましょう。

参考リンク

Ansible公式ドキュメント

Ansibleモジュール

Ansible Vault

Red Hat Ansible関連資料

IaCとの使い分け

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?