【Ansible】第3回 Ansible Runner編

インフラ・サーバー

おはようございます、わっしょい村です。前回は実際にAnsibleを使ってEC2にワードプレスを構築しました。

しかし前回の普通のAnsibleではAnsibleをインストールした環境によって使えるモジュールなどの環境差分が出てくる可能性があります。今回は環境差分を無くすべく、Ansibleの実行をコンテナ内で行うことができるAnsible Runnerを解説していきます。

Ansible Runnerとは

Ansible Runnerは簡単に言うとAnsibleをいい感じに実行してくれるツールです(雑)。このいい感じというのは何個かメリットがあります。できることは以下です。

  • 実行履歴を記録してくれる。
  • コンテナから実行することができるため環境差分問題がない。
  • 決まったディレクトリ構造があり余計なファイル指定がいらない。

などなど他にもありそうですが僕が受けてる恩恵はこれくらいです。ディレクトリ構造については言葉だけではわかりづらいかと思いますが、例えばインベントリファイルなどです。前回はansible実行時に-iオプションでhostsファイルを指定していました。Ansible Runnerではインベントリファイルの保存場所が決まっていたりとディレクトリ構造にルールが設けられているのでチーム内で設定ファイルの理解がしやすかったり余計なオプションの指定などが不要になったりします。

全体像

前回同様やることはEC2へのワードプレス構築です。ただしディレクトリ構造が少し変わっています。

├── env
│   └── settings
├── inventory
│   └── hosts
└── project
    ├── ansible.cfg
    ├── create.yml
    ├── delete_aws
    │   ├── delete.yml
    │   └── roles
    │       └── delete_aws
    │           ├── tasks
    │           │   └── main.yml
    │           └── vars
    │               └── main.yml
    ├── group_vars
    │   └── all.yml
    ├── roles
    │   ├── aws_vpc
    │   │   ├── files
    │   │   │   ├── config
    │   │   │   └── credentials
    │   │   ├── tasks
    │   │   │   └── main.yml
    │   │   └── vars
    │   │       └── main.yml
    │   ├── web_server
    │   │   ├── tasks
    │   │   │   └── main.yml
    │   │   └── vars
    │   │       └── main.yml
    │   └── wordpress
    │       ├── tasks
    │       │   └── main.yml
    │       └── vars
    │           └── main.yml
    └── ssh_config

基本的なファイル群はprojectディレクトリ配下に移動しています。そしてhostsファイルはinventory配下に移動しています。これがansible-runnerのディレクトリ構造です。そして新しくsettingsファイルが追加されています。こちらは後で解説します。

実行準備(ansible-builder)

今回はDockerコンテナを使用します。Dockerのインストールは以下記事をご確認ください。

Ansible Runnerで実行するためには実行環境をビルドする必要があります。そこで登場するのがAnsible Builderです。大まかな流れとしてAnsible Builderでビルドし、その後にAnsible Runnerで実行するイメージです。まずはAnsible BuilderとRunnerをインストールします。

$pip3 install ansible-builder ansible-runner

それではビルドに移ります。まずはディレクトリを作成します。

$ mkdir ansible-docker
$ cd ansible-docker

execution-environment.ymlを作成。

---
version: 1

dependencies:
  galaxy: requirements.yml
  python: requirements.txt

additional_build_steps:
  prepend: |
    RUN pip3 install --upgrade pip

requirements.txtを作成。これは必要なpythonライブラリを書くことで実行環境にインストールすることができます。AWS周りのライブラリを入れておきましょう。

boto
boto3
botocore
awscli

requirements.ymlにはAnsibleで使用するモジュールを書きます。基本的にansible.builtin以外は全部入れておくといいでしょう。今回は以下です。

collections:
  - amazon.aws
  - community.aws
  - community.mysql

これでOKです。

最後にビルドを実行します。tagにはわかりやすい名前を設定しておきましょう。実行完了までに数分かかります。

$ ansible-builder build --tag aws_wordpress_image

実行(ansible-runner)

ビルドが完了したらansible-runnerの実行に移ります。env/settingsファイルに先ほどビルドしたイメージを指定します。

container_image: aws_wordpress_image
process_isolation_executable: docker
process_isolation: true

これだけで準備完了です。roles配下やその他のファイル内容は基本的には前回と一緒です。ただしAWS周りのmain.ymlのみ少々変更があるのでそれだけ以下に示します。実行コンテナ内には.sshディレクトリや.awsディレクトリがないのでそれを作成しています。またアクセスキーなどの情報もないのでファイルとして用意する必要があります。

- name: ".awsディレクトリ作成"
  ansible.builtin.file:
    dest: ~/.aws/
    state: directory

- name: "AWS用のconfigファイル作成"
  ansible.builtin.copy: 
    src: ../files/config
    dest: ~/.aws/config

- name: "AWS用のcredentialsファイル作成"
  ansible.builtin.copy: 
    src: ../files/credentials
    dest: ~/.aws/credentials

# VPC作成
- name: VPC作成
  amazon.aws.ec2_vpc_net:
    name: "{{ vpc_name }}"
    cidr_block: "{{ vpc_cidr }}"
    region: "{{ region }}"
  register: vpc_info

#スタックにVPCidを入力。スタックに別の値が書き込まれている場合は書き換える
- name: スタックにVPC idを追加
  ansible.builtin.lineinfile:
    path: ./delete_aws/roles/delete_aws/vars/main.yml
    regexp: '^vpc_id'
    line: "{{ 'vpc_id: '+vpc_info.vpc.id }}"

# サブネットの作成
- name: サブネット作成
  amazon.aws.ec2_vpc_subnet:
    vpc_id: "{{ vpc_info.vpc.id }}"
    cidr: "{{ item.subnet_cidr }}"
    az: "{{ item.subnet_az }}"
    region: "{{ region }}"
    resource_tags: {"Name": "{{ item.subnet_name }}"}
  register: sub_info
  with_items:
    - "{{ subnet }}"

#スタックに1個目のsubnetidを入力。スタックに別の値が書き込まれている場合は書き換える
- name: スタックに1個目のsubnetidを追加
  ansible.builtin.lineinfile:
    path: ./delete_aws/roles/delete_aws/vars/main.yml
    regexp: '^subnet1_id'
    line: "{{ 'subnet1_id: '+sub_info.results[0].subnet.id }}"

#スタックに2個目のsubnetidを入力。スタックに別の値が書き込まれている場合は書き換える
- name: スタックに2個目のsubnetidを追加
  ansible.builtin.lineinfile:
    path: ./delete_aws/roles/delete_aws/vars/main.yml
    regexp: '^subnet2_id'
    line: "{{ 'subnet2_id: '+sub_info.results[1].subnet.id }}"

# IGWの作成
- name: IGW作成
  amazon.aws.ec2_vpc_igw:
    vpc_id: "{{ vpc_info.vpc.id }}"
    region: "{{ region }}"
    tags: { "Name": "{{ igw_name }}" }
  register: igw_info

#スタックにIGWidを入力。スタックに別の値が書き込まれている場合は書き換える
- name: スタックにIGWidを追加
  ansible.builtin.lineinfile:
    path: ./delete_aws/roles/delete_aws/vars/main.yml
    regexp: '^IGW_id'
    line: "{{ 'IGW_id: '+igw_info.gateway_id }}"
 
# ルートテーブルの作成
- name: ルートテーブルの作成
  amazon.aws.ec2_vpc_route_table:
    vpc_id: "{{ vpc_info.vpc.id }}"
    subnets: "{{ attache_igw_subnet }}"
    routes:
      - dest: 0.0.0.0/0
        gateway_id: "{{ igw_info.gateway_id }}"
    region: "{{ region }}"
    resource_tags: { "Name": "{{ routetable_name }}" }
  register: RT_info

#スタックにルートテーブルidを入力。スタックに別の値が書き込まれている場合は書き換える
- name: スタックにルートテーブルidを追加
  ansible.builtin.lineinfile:
    path: ./delete_aws/roles/delete_aws/vars/main.yml
    regexp: '^route_table_id'
    line: "{{ 'route_table_id: '+RT_info.route_table.id }}"

#キーペア作成
- name: キーペア作成
  amazon.aws.ec2_key:
    name: "{{ keypair_name }}"
    region: "{{ region }}"
  register: keypair_info

#スタックにキーペア名を入力。スタックに別の値が書き込まれている場合は書き換える
- name: スタックにキーペア名を追加
  ansible.builtin.lineinfile:
    path: ./delete_aws/roles/delete_aws/vars/main.yml
    regexp: '^ec2_key_pair_name'
    line: "{{ 'ec2_key_pair_name: '+keypair_info.key.name }}"

- name: "sshディレクトリ作成"
  ansible.builtin.file:
    dest: ~/.ssh/
    state: directory

#鍵用の空ファイル作成
- name: 鍵用の空ファイル作成
  ansible.builtin.file:
    path: ~/.ssh/{{ keypair_info.key.name }}.pem
    state: touch
    mode: 0600

#作成された空ファイルに秘密鍵を書き込む
- name: 空ファイルに秘密鍵を書き込む
  ansible.builtin.shell: echo "{{ keypair_info.key.private_key }}" > ~/.ssh/"{{ keypair_info.key.name }}".pem

#セキュリティグループ作成
- name: セキュリティグループ作成
  amazon.aws.ec2_group:
    name: "{{ secgrp_name }}"
    description: "{{ description }}"
    vpc_id: "{{ vpc_info.vpc.id }}"
    #vpc_id: vpc-083c83aba2a42c56e 既存のVPCに入れる場合は上記コメントアウトしてここに指定
    region: "{{ region }}"
    rules: 
        - proto: "tcp"
          ports: "{{ sec_tcp_ports }}"
          cidr_ip: "{{ sec_ip }}"
  register: secgrp_info

#スタックにSGidを入力。スタックに別の値が書き込まれている場合は書き換える
- name: スタックにセキュリティグループidを入力。
  ansible.builtin.lineinfile:
    path: ./delete_aws/roles/delete_aws/vars/main.yml
    regexp: '^SG_id'
    line: "{{ 'SG_id: '+secgrp_info.group_id }}"

#ec2作成
- name: EC2インスタンスの生成
  amazon.aws.ec2_instance:
    name: "{{ ec2_name }}"        # インスタンスの名前を指定
    key_name: "{{ keypair_name }}"  # インスタンスにログインするための認証鍵を指定
    instance_type: "{{ instance_type }}"         # インスタンスタイプを指定
    image_id: "{{ image }}"
    region: "{{ region }}"          # 東京リージョンを指定
    vpc_subnet_id: "{{ sub_info.results[0].subnet.id }}"   # サブネットを指定(ここは適宜変える)
    security_group: "{{ secgrp_name }}"          # セキュリティグループ名を指定(ここは適宜変える)
    network:
      assign_public_ip: yes
    state: started
    wait: yes 
  register: ec2_info

# ec2ステータスチェック(上記ec2作成でもステータスチェックオプションを入れているが念のためこちらでもステータスチェック)
- name: ec2がstartedになるまで待機
  ansible.builtin.shell: aws ec2 describe-instances --instance-ids "{{ ec2_info.instance_ids[0] }}" --query "Reservations[].Instances[].State.Name | [0]" | tr -d "\""
  register: state
  until: state.stdout == "running"
  retries: 12
  delay: 10

#スタックにec2のidを入力。スタックに別の値が書き込まれている場合は書き換える
- name: スタックにec2idを追加
  ansible.builtin.lineinfile:
    path: ./delete_aws/roles/delete_aws/vars/main.yml
    regexp: '^ec2_id'
    line: "{{ 'ec2_id: '+ec2_info.instance_ids[0] }}"

#ssh_configファイルにec2情報を書き込み
- name: ssh_configに書き込み
  ansible.builtin.lineinfile:
    dest: ./ssh_config
    line: "{{ item }}"
  with_items:
    - "{{ 'Host '+ssh_host }}"
    - "{{ '  HostName '+ec2_info.instances[0].network_interfaces[0].association.public_ip }}"
    - "{{ '  User '+ec2_user_name }}"
    - "{{ '  IdentityFile ~/.ssh/'+keypair_name+'.pem' }}"

アクセスキーやシークレットキーは既に設定済みの環境でcat ~/.aws/credentialsなどで確認するのが早いと思います。シークレットキーは暗号化されているので注意が必要です。

[default]
region = ap-northeast-1
output = json
[default]
aws_access_key_id = アクセスキー
aws_secret_access_key = シークレットキー

実際に実行してみましょう。コマンドは以下です。

$ ansible-runner run . -p create.yml

project/create.ymlの指定が必要では?となるかもしれませんがその指定は不要です。ansible-runnerはインベントリファイルはinventoryディレクトリ配下、他のファイルはprojectディレクトリ配下を勝手に参照するようになっています。

また実行履歴はartifactsディレクトリが勝手に作られその中に保存されます。

AWS環境の削除は以下コマンドでできます。

$ ansible-runner run . -p delete_aws/delete.yml

delete時のmain.ymlも前回と異なる部分があるので以下を使ってください。ローカルの秘密鍵の削除が不要になったり、.aws/configファイルの追加などがあります。

- name: ".awsディレクトリ作成"
  ansible.builtin.file:
    dest: ~/.aws/
    state: directory

- name: "AWS用のconfigファイル作成"
  ansible.builtin.copy: 
    src: ../../../../roles/aws_vpc/files/config
    dest: ~/.aws/config

- name: "AWS用のcredentialsファイル作成"
  ansible.builtin.copy: 
    src: ../../../../roles/aws_vpc/files/credentials
    dest: ~/.aws/credentials

- name: EC2インスタンス削除
  ansible.builtin.shell: aws ec2 terminate-instances --instance-ids "{{ ec2_id }}"
  register: ec2_delete_info

- name: ec2削除確認
  debug:
    msg: "{{ ec2_delete_info.stdout }}"

- name: ec2が終了済みになるまで待機
  ansible.builtin.shell: aws ec2 describe-instances --instance-ids "{{ ec2_id }}" --query "Reservations[].Instances[].State.Name | [0]" | tr -d "\""
  register: state
  until: state.stdout == "terminated"
  retries: 18
  delay: 10

- name: キーペア削除
  ansible.builtin.shell: aws ec2 delete-key-pair --key-name "{{ ec2_key_pair_name }}"
  register: key_pair_delete_info

- name: キーペア削除確認
  debug:
    msg: "{{ key_pair_delete_info.stdout }}"

- name: IGWデタッチ
  ansible.builtin.shell: aws ec2 detach-internet-gateway --internet-gateway-id "{{ IGW_id }}" --vpc-id "{{ vpc_id }}"
  register: IGW_detach_info

- name: IGWデタッチ確認
  debug:
    msg: "{{ IGW_detach_info.stdout }}"

- name: IGW削除
  ansible.builtin.shell: aws ec2 delete-internet-gateway --internet-gateway-id "{{ IGW_id }}"
  register: IGW_delete_info

- name: IGW削除確認
  debug:
    msg: "{{ IGW_delete_info.stdout }}"

- name: サブネット1削除
  ansible.builtin.shell: aws ec2 delete-subnet --subnet-id "{{ subnet1_id }}"
  register: subnet1_delete_info

- name: サブネット1削除確認
  debug:
    msg: "{{ subnet1_delete_info.stdout }}"

- name: サブネット2削除
  ansible.builtin.shell: aws ec2 delete-subnet --subnet-id "{{ subnet2_id }}"
  register: subnet2_delete_info

- name: サブネット2削除確認
  debug:
    msg: "{{ subnet2_delete_info.stdout }}"

- name: ルートテーブル削除
  ansible.builtin.shell: aws ec2 delete-route-table --route-table-id "{{ route_table_id }}"
  register: RT_delete_info

- name: ルートテーブル削除確認
  debug:
    msg: "{{ RT_delete_info.stdout }}"

- name: セキュリティグループ削除
  ansible.builtin.shell: aws ec2 delete-security-group --group-id "{{ SG_id }}"
  register: SG_delete_info

- name: セキュリティグループ削除確認
  debug:
    msg: "{{ SG_delete_info.stdout }}"

- name: VPC削除
  ansible.builtin.shell: aws ec2 delete-vpc --vpc-id "{{ vpc_id }}"
  register: vpc_delete_info

- name: VPC削除確認
  debug:
    msg: "{{ vpc_delete_info.stdout }}"

- name: ssh_config削除
  ansible.builtin.file: 
    path: ../ssh_config
    state: absent

- name: ssh_config空ファイル生成
  ansible.builtin.file:
    path: ../ssh_config
    state: touch

次回 Docker Compose編

今回はEC2への操作でしたがもっと気軽に検証環境を作成するにはローカルにコンテナを立てるのが手っ取り早いです。次回はローカルPCにDocker Composeでコンテナをたて、そこにAnsibleでワードプレス環境を構築していくやり方を解説します。