👺

Ansibleを学びたい。ンゴ

2024/11/25に公開

はじめに

初めまして、自動化大好きなインフラ屋です。

Ansible触ってみたいと思いませんか?私は思います。
なぜならば、TerraformでEC2をデプロイするにあたって、OS設定をユーザデータでするのに限界を感じたから。。(もう無駄に長ったらしいシェルスクリプトを書きたくない。。)
そこで、OS設定をAnsibleに任せられたら素敵やん?と思った次第です。

しかし、私はTerraformはある程度経験があるものの、Ansibleについては「なんかPlaybookとかいうもので設定内容を定義するらしい」ということくらいしか知りません。
という訳で、まずはAnsibleというものの概要について掴んでいきます。

Ansible、どんな感じ?

Terraformとの比較

AnsibleでもAWSやAzureなんかのクラウドのリソースデプロイを自動化することはできるようですが、AnsibleはOS設定とかNW機器とかによく使われてるイメージがあります。ChatGPTと遊んでたら思いのほか分かりやすい要約をしてくれたので載せておきます。ただし、生成AIの回答ですので、どこまで正確かは保証しかねます。ご希望の方は参考程度にご覧ください。

ChatGPTの要約

Ansibleは、システム管理やアプリケーションデプロイ、プロビジョニング、構成管理を自動化するためのオープンソースのツールやフレームワークンゴ。特に「エージェントレス」な設計が特徴で、他の多くのツールと違い、管理対象のノードに専用ソフトウェアをインストールする必要がないンゴ。

Ansibleの特徴

  1. エージェントレス
    • SSHを使って管理対象ホストと通信するため、管理対象に特別なソフトウェアは必要なく、Pythonがインストールされていれば動くンゴ。
  2. 宣言型アプローチ
    • ユーザーは「何を実現したいか」を定義するだけで、Ansibleが状態を達成するために必要なアクションを実行してくれるンゴ(例: パッケージのインストール、ファイルの配置)。
  3. Playbookを使った操作
    • YAML形式で書かれた「Playbook」にタスクや状態を記述するンゴ。これが自動化の基本単位になるンゴ。
  4. モジュールベース
    • 数百種類以上のモジュール(例: ファイル管理、パッケージ管理、クラウド操作)が提供されており、カスタマイズや拡張も可能ンゴ。
  5. 広い適用範囲
    • サーバーの設定、クラウドプロバイダーのリソース操作(AWS、Azureなど)、ネットワーク機器の設定管理、コンテナ操作(Dockerなど)など多用途に使えるンゴ。

Terraformとの違い

  • Terraform:
    • 主に「プロビジョニングツール」。インフラをコード化し、クラウドやオンプレでのリソースを一括管理するンゴ。
    • デプロイ前に状態を計画するプラン(terraform plan)があり、構築や変更を事前にシミュレーションできる。
  • Ansible:
    • 「構成管理ツール」として使われることが多いンゴ。サーバーの状態を整えたり、アプリケーションをデプロイしたりするのが得意ンゴ。
    • 一部プロビジョニングも可能だが、Terraformほど専門的ではない。

実際に使う流れ(ざっくり)

  1. 管理対象ホストのインベントリファイル(INI形式やYAML形式)を準備する。
  2. PlaybookにやりたいタスクをYAMLで記述する。
  3. ansible-playbookコマンドを使ってタスクを実行する。

シンプルな例

以下はApacheをインストールし、起動するPlaybookの例ンゴ:

- name: Install and start Apache
  hosts: webservers
  become: yes

  tasks:
    - name: Install Apache
      apt:
        name: apache2
        state: present

    - name: Start Apache service
      service:
        name: apache2
        state: started

AnsibleはTerraformと組み合わせても活用できるので、両方を理解するとより強力なインフラ管理が可能になるンゴ!

ポイント

自分でも色々と調べた上でのポイントになります。主にこちらの本や公式ドキュメントを参考にしています。
https://zenn.dev/y_mrok/books/ansible-no-tsukaikata/viewer/chapter2

  1. Ansibleをインストールし、実行するサーバを「コントロールノード」と呼ぶ
  2. Ansibleで設定・管理されるサーバ(NW機器だったりもする)を「マネージドノード」と呼ぶ
  3. マネージドノードへのAnsibleやエージェントのインストールは不要
  4. 代わりに、コントロールノードからSSHで接続できる必要がある
  5. マネージドノードには予めPythonがインストールされている必要がある
  6. マネージドノードの一覧を、インベントリ(YAML形式)で定義する
  7. インベントリで定義されたマネージドノードへに対して「どのような順番で何をするか」を、プレイブック(YAML形式)で定義する
  8. Ansibleの実行結果には、冪等性がある

冪等性がある、という点がミソでしょうか。1回実行しても2回実行しても結果が変わらないよ、ということです。EC2のユーザーデータでこれを実現しようとすると中々骨が折れますからね。

実行環境を作る

概要が分かったところで、実際に使ってみましょう。今回は最小構成で「とりあえずAnsibleで設定できた!」というところまでを目標とします。
実行環境ですが、AWS上にEC2を起動してから、CloudShell上でAnsibleを実行してEC2のOSを設定してみようと思います。以下のイメージ。

実行環境イメージ図

Terraformをインストール

EC2はTerraformで構築したいため、まずはCloudShellにTerraformをインストールしていきます。

$ sudo yum install -y yum-utils
$ sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo
$ sudo yum -y install terraform

インストールできました

[cloudshell-user@ip-10-140-120-54 ~]$ terraform -v
Terraform v1.9.8
on linux_amd64
[cloudshell-user@ip-10-140-120-54 ~]$ 

Terraform実行(EC2起動)

続いて、ソースコードをCloudShell上に輸送してterraformコマンドを実行、EC2を構築します。CloudShellはコンソールから直接ファイルをやり取りできるので便利ですね。ソースコードは以下の通りです。EC2と一緒に、CloudShellからのSSHを許可するSecurity Groupなども作成します。

main.tf
# Load subnet
data "aws_subnet" "subnet" {
  filter {
    name   = "tag:Name"
    values = [var.subnet_name]
  }
}

# Load latest AMI of Amazon Linux 2023
data "aws_ami" "al2023" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "architecture"
    values = ["x86_64"]
  }
  filter {
    name   = "name"
    values = ["al2023-ami-2023*"]
  }
}

# Create security group for EC2 Instances
resource "aws_security_group" "sg" {
  name        = var.security_group_name
  vpc_id      = data.aws_subnet.subnet.vpc_id
  
  tags = {
    Name = var.security_group_name
  }
}

# Allow SSH inbound
resource "aws_vpc_security_group_ingress_rule" "ingress" {
  security_group_id = aws_security_group.sg.id
  cidr_ipv4         = var.control_node_ip
  from_port         = 22
  to_port           = 22
  ip_protocol       = "tcp"
}

# Allow HTTPS outbound
resource "aws_vpc_security_group_egress_rule" "egress" {
  security_group_id = aws_security_group.sg.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 443
  to_port           = 443
  ip_protocol       = "tcp"
}

# Create key_pair for EC2 Instances
resource "tls_private_key" "key_pair" {
  algorithm = "RSA"
  rsa_bits  = 4096
}

locals {
  private_key_file = "./ec2-user.pem"
}

resource "local_file" "private_key_pem" {
  filename = local.private_key_file
  content  = tls_private_key.key_pair.private_key_pem
  provisioner "local-exec" {
    command = "chmod 600 ${local.private_key_file}"
  }
}

resource "aws_key_pair" "key_pair" {
  key_name   = "ec2-user"
  public_key = tls_private_key.key_pair.public_key_openssh
}

# Create EC2 Instances
resource "aws_instance" "ec2" {
  for_each      = toset(var.ec2_names)
  ami           = var.ami_id == null ? data.aws_ami.al2023.id : var.ami_id
  instance_type = "t2.micro"
  key_name      = aws_key_pair.key_pair.key_name
  subnet_id     = data.aws_subnet.subnet.id

  vpc_security_group_ids      = [aws_security_group.sg.id]
  associate_public_ip_address = var.associate_public_ip_address

  tags = {
    Name = each.value
  }
}

output "ec2_public_ip" {
  value = {for k, v in aws_instance.ec2 : k => v.public_ip}
}

output "ec2_private_ip" {
  value = {for k, v in aws_instance.ec2 : k => v.private_ip}
}
variables.tf
variable "control_node_ip" {
  description = "Security group allows SSH from this IP address"
  type        = string
  validation {
    condition     = can(regex("^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\/[1-3]?[0-9]$", var.control_node_ip))
    error_message = "IP address must be a valid IPv4 CIDR that represents a network address like \"10.0.0.1/32\"."
  }
}

variable "ec2_names" {
  description = "If you want multiple instances at once, specify more than one value."
  type        = list(string)
}

variable "ami_id" {
  description = "Latest al2023(x86_64) is the default"
  type        = string
  default     = null
}

variable "subnet_name" {
  type = string
}

variable "security_group_name" {
  type = string
}

variable "associate_public_ip_address" {
  type    = bool
  default = false
}

変数はterraform.tfvarsファイルに書き込み、terraform init、applyを実行。アウトプットとして作成したEC2のIPを表示してくれます。

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

Outputs:

ec2_private_ip = {
  "ansible-node1" = "10.0.0.65"
}
ec2_public_ip = {
  "ansible-node1" = "x.x.x.x"
}
[cloudshell-user@ip-10-140-120-54 tf_ec2]$ 

Ansibleをインストール

いよいよ本題です。コントロールノードとなるCloudShellにAnsibleをインストールしましょう。(EPELリポジトリを追加する必要があるかと思っていましたが、CloudShellが稼働しているAmazonLinux2023のリポジトリに元々入っているようです。)

$ sudo yum install -y ansible
[cloudshell-user@ip-10-140-120-54 ~]$ ansible --version
ansible [core 2.15.3]
  config file = None
  configured module search path = ['/home/cloudshell-user/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.9/site-packages/ansible
  ansible collection location = /home/cloudshell-user/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/bin/ansible
  python version = 3.9.16 (main, Jul  5 2024, 00:00:00) [GCC 11.4.1 20230605 (Red Hat 11.4.1-2)] (/usr/bin/python3.9)
  jinja version = 3.1.4
  libyaml = True
[cloudshell-user@ip-10-140-120-54 ~]$ 

インベントリ、プレイブックを作成

続いて、インベントリとプレイブックを作成します。

インベントリ作成

まずはインベントリから作成します。
「hosts」の中に各ホストの論理名とIPや接続ユーザ名などの設定を入れるようです。そして、すべてのホストが「all」グループに含まれる。必要に応じて子グループを作って、グループごとに管理することもできるようです。夢が広がる。
ansible_hostには作成したEC2のグローバルIPを指定します。SSH秘密鍵は、先ほどTerraformで一緒に作っておいたファイルを指定します。

inventory.yaml
---
all:
  hosts:
    ansible-node1:
      ansible_host: x.x.x.x
      ansible_user: ec2-user
      ansible_ssh_private_key_file: /home/cloudshell-user/my_first_ansible-master/tf_ec2/ec2-user.pem

1台だけなので簡潔ですね。もっと数が多くなるとファイルを分割したり、ホストごとに変数を共有したりなど色々あるようですが、それはまたの機会にします。

プレイブック作成

次に肝心のプレイブックです。以下をAnsibleで設定することを目標としてみます。

  • パッケージのインストール(nginx)
  • サービスの起動、有効化(nginx)
  • ユーザ、グループの作成

ChatGPTに上記条件でプレイブック作成を依頼したところ、以下を返してくれました。直感的に何をするのかが分かりやすくて大変良きですね。tasksセクションで実行するタスクを定義するようで、合計4つのタスクを実行するプレイブックになっています。4行目の「become」がナニコレ?でしたが、rootユーザーになれ、という指令だそうです。OS設定をいじくるときなんかはよく使いそうですね。
なんとなく動きそうな感じがするので、これで動かしてみましょう。

playbook.yaml
---
- name: Setup nginx and user configuration
  hosts: all
  become: yes

  tasks:
    - name: Ensure nginx is installed
      ansible.builtin.yum:
        name: nginx
        state: present

    - name: Ensure nginx service is started and enabled
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: yes

    - name: Create group
      ansible.builtin.group:
        name: test-group
        state: present

    - name: Create user
      ansible.builtin.user:
        name: test-user
        group: test-group
        state: present

動け、Ansible!

プレイブックを実行

いざ、実行。プレイブックを実行するには、ansible-playbookコマンドを使用するようです。インベントリファイルは-iオプションで、プレイブックはコマンド引数で指定します。

[cloudshell-user@ip-10-140-120-54 ansible]$ ls -l
total 8
-rw-r--r--. 1 cloudshell-user cloudshell-user 201 Nov 24 14:18 inventory.yaml
-rw-r--r--. 1 cloudshell-user cloudshell-user 573 Nov 24 14:15 playbook.yaml
[cloudshell-user@ip-10-140-120-54 ansible]$ ansible-playbook -i inventory.yaml playbook.yaml 

PLAY [Setup nginx and user configuration] *********************************************************************************************************************************************************************************************************************************************

TASK [Gathering Facts] ****************************************************************************************************************************************************************************************************************************************************************
The authenticity of host 'x.x.x.x (x.x.x.x)' can't be established.
ED25519 key fingerprint is SHA256:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
[WARNING]: Platform linux on host ansible-node1 is using the discovered Python interpreter at /usr/bin/python3.9, but future installation of another Python interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-
core/2.15/reference_appendices/interpreter_discovery.html for more information.
ok: [ansible-node1]

TASK [Ensure nginx is installed] ******************************************************************************************************************************************************************************************************************************************************
changed: [ansible-node1]

TASK [Ensure nginx service is started and enabled] ************************************************************************************************************************************************************************************************************************************
changed: [ansible-node1]

TASK [Create group] *******************************************************************************************************************************************************************************************************************************************************************
changed: [ansible-node1]

TASK [Create user] ********************************************************************************************************************************************************************************************************************************************************************
changed: [ansible-node1]

PLAY RECAP ****************************************************************************************************************************************************************************************************************************************************************************
ansible-node1              : ok=5    changed=4    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

[cloudshell-user@ip-10-140-120-54 ansible]$ 

一発でうまくいったようです!初回のSSH接続なのでホスト鍵登録していいか聞かれてますね。
サマリを見るとok=5change=4と表示されています。
はて?タスク数は4つしかなかったはず。ok=5とは・・・?

まず、サマリのokchangeですが、以下の違いがあるようです。

  • ok: タスク実行成功(changeを含む合計数)
  • change: タスク実行成功(指示通りにマネージドノードを変更)

変更ありのタスク実行数は当初の想定通りですが、変更なしのタスクが1件実行されているようです。実行ログを見てみると見覚えのないタスクTASK [Gathering Facts]が実行されていました。どうやらこのタスクは自動でAnsibleが実行するらしく、ファクト変数というものを収集しているようです。なんじゃそりゃ?という感じですが、難しいことはなく、単にマネージドノードのIPとかOS情報を集めてきているようです。このファクト変数の収集タスクもサマリに含まれていたわけですね。

結果確認

実行はうまくいったようなので、実際にEC2にログインして中身を見てみましょう。

[cloudshell-user@ip-10-140-120-54 ansible]$ ssh -i /home/cloudshell-user/my_first_ansible-master/tf_ec2/ec2-user.pem ec2-user@x.x.x.x
   ,     #_
   ~\_  ####_        Amazon Linux 2023
  ~~  \_#####\
  ~~     \###|
  ~~       \#/ ___   https://aws.amazon.com/linux/amazon-linux-2023
   ~~       V~' '->
    ~~~         /
      ~~._.   _/
         _/ _/
       _/m/'
Last login: Sun Nov 24 14:30:54 2024 from 3.89.149.161
[ec2-user@ip-10-0-0-65 ~]$ 

ログインできました。設定状況を見てみましょう。

[ec2-user@ip-10-0-0-65 ~]$ nginx -v
nginx version: nginx/1.26.2
[ec2-user@ip-10-0-0-65 ~]$ systemctl is-active nginx
active
[ec2-user@ip-10-0-0-65 ~]$ systemctl is-enabled nginx
enabled
[ec2-user@ip-10-0-0-65 ~]$ getent passwd | grep test-user
test-user:x:1001:1001::/home/test-user:/bin/bash
[ec2-user@ip-10-0-0-65 ~]$ groups test-user
test-user : test-group
[ec2-user@ip-10-0-0-65 ~]$ 

nginxがインストール、起動、有効化され、test-groupに所属するtest-userが作成されていますね。計画通りです。

冪等性の確認

Ansibleのポイントの一つとして冪等性を挙げていたことですし、そのままもう一回プレイブックを実行してみましょう。本当に冪等性があるのであれば、実行結果は変わらないはずです。

[cloudshell-user@ip-10-140-120-54 ansible]$ ansible-playbook -i inventory.yaml playbook.yaml 

PLAY [Setup nginx and user configuration] *********************************************************************************************************************************************************************************************************************************************

TASK [Gathering Facts] ****************************************************************************************************************************************************************************************************************************************************************
[WARNING]: Platform linux on host ansible-node1 is using the discovered Python interpreter at /usr/bin/python3.9, but future installation of another Python interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-
core/2.15/reference_appendices/interpreter_discovery.html for more information.
ok: [ansible-node1]

TASK [Ensure nginx is installed] ******************************************************************************************************************************************************************************************************************************************************
ok: [ansible-node1]

TASK [Ensure nginx service is started and enabled] ************************************************************************************************************************************************************************************************************************************
ok: [ansible-node1]

TASK [Create group] *******************************************************************************************************************************************************************************************************************************************************************
ok: [ansible-node1]

TASK [Create user] ********************************************************************************************************************************************************************************************************************************************************************
ok: [ansible-node1]

PLAY RECAP ****************************************************************************************************************************************************************************************************************************************************************************
ansible-node1              : ok=5    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

[cloudshell-user@ip-10-140-120-54 ansible]$

ok=5で結果は同じですね。ただし1回目の実行で必要な変更は実施済なので、change=0になっています。望ましい状態になっている、という意味で結果は同じですので、冪等性アリと言えるでしょう。これが単純なシェルスクリプトだったりすると、エラーの雨あられとなるところです。うーむ有難い。

まとめ

非常に単純な処理ではありますが、Ansibleを使ってOS設定を入れてみました。使ってみた所感ですが、特に難しいと感じる箇所もなく、すんなりと使えた印象です。プレイブックなども構造がシンプルなので、かなりとっつきやすいと感じました。その割に色々とできそうなことは多く、大規模な自動化にも対応できそうです。次はもう少し複雑な処理をしたり、深いところまで突っ込んでいきたいですね。
最後に、今回の検証で使用したTerraformソースやプレイブックなどは以下にまとめています。よろしければお使いください。お読みいただきありがとうございました。

https://github.com/tarai709/my_first_ansible/tree/master

Discussion