【Ansible】第2回 AWS EC2編

インフラ・サーバー

あけましておめでとうございます、わっしょい村です。前回はAnsibleの導入と簡単な操作をやりました。

今回はAnsibleを使ってEC2の立ち上げからEC2内への操作までやっていきます。EC2内にはワードプレスをセットアップしアクセスできるところをゴールとします。

全体像

まずは全体像を確認します。流れとしては最初にAWS関係の構築を行います。VPCやサブネット、インターネットゲートウェイ、EC2などです。その後EC2内にWebサーバー系のミドルウェアを導入し、ワードプレスを構築します。

EC2内にワードプレスを構築する流れは以下の公式チュートリアルのコマンドをAnsibleに置き換えてやることとします。

Amazon Linux 2 での LAMP のインストール - Amazon Elastic Compute Cloud
Apache ウェブサーバーを PHP と MariaDB のサポートとともに EC2 インスタンスにインストールします。
Amazon Linux 2 での WordPress ブログのホスト - Amazon Elastic Compute Cloud
EC2 インスタンスに WordPress ブログをインストール、構成し、セキュリティを確保するためのチュートリアル。

完成するAnsibleのディレクトリ構造は以下になります。

├── ansible.cfg
├── create.yml
├── delete_aws
│   ├── delete.yml
│   └── roles
│       └── delete_aws
│           ├── tasks
│           │   └── main.yml
│           └── vars
│               └── main.yml
├── group_vars
│   └── all.yml
├── hosts
├── roles
│   ├── aws_vpc
│   │   ├── tasks
│   │   │   └── main.yml
│   │   └── vars
│   │       └── main.yml
│   ├── web_server
│   │   ├── tasks
│   │   │   └── main.yml
│   │   └── vars
│   │       └── main.yml
│   └── wordpress
│       ├── tasks
│       │   └── main.yml
│       └── vars
│           └── main.yml
└── ssh_config

写真の方がわかりやすいかもしれません。

AnsibleではRolesという仕様を使って構成を組むことが多いです。今回はロールを3つに分けています。1つはAWSを立ち上げるロール。2つめはEC2内にWebサーバーミドルウェアやDBを構築するロール。最後がワードプレスを構築し設定するロールです。

そして今回はこの3つのロールとは別でAWS環境を削除するロールも用意しています。今回はCFnを使わないので環境を抹消したい時にコンソールのいろんなとこをぽちぽちしなくちゃいけなかったんですがそれもAnsibleで行ってしまおうというわけです。

設定ファイル

今回roles関連のファイル以外の設定ファイルがいくつかあります。

1つがhostsです。中身は一行だけです。

my_host

これは今回使うEC2が一つだけなのでそれのホスト名としてmy_hostと名付けています。これがもし2台構成など複数台の場合はそれに応じてホスト名を追加してください。今回は1行だけでOKです。

他にもansible.cfgと言う設定ファイルがあります。中身は以下です。

[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -F ./ssh_config -q

これはEC2へssh接続するための設定です。ここでssh接続にはssh_configファイルを使いますよと宣言しています。そのためssh_configファイルも作成しておきましょう。ただし最初はssh_configファイルは空ファイルのままで大丈夫です。EC2起動後に自動で中身を書き加えるようにします。

最後がgroup_vars/all.ymlです。名前の通りグローバル変数的な役割です。ホスト名をどこでも使えるように変数化しています。

ssh_host: my_host

ロール

それではロールファイルに入っていきます。基本的にロールはタスクが書かれたタスクファイルと変数が定義されている変数ファイルに分かれています。

AWSロール

それではroles/aws_vpc/tasks/main.ymlから見ていきます。ここでは以下を行っています。

  • VPC作成
  • サブネットの作成
  • IGW作成
  • ルートテーブルの作成
  • 秘密鍵生成
  • セキュリティグループ作成

またそれぞれ作成後にIDをスタックと呼んでいるymlファイルに書き込んでいます。こちらは後ほど作成します。

- 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 }}"

- 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: 鍵用の空ファイル作成
  ansible.builtin.file:
    path: ~/.ssh/{{ keypair_info.key.name }}.pem
    state: touch
    mode: 0600
  when: keypair_info.key.private_key is defined

- name: 空ファイルに秘密鍵を書き込む
  ansible.builtin.shell: echo "{{ keypair_info.key.private_key }}" > ~/.ssh/"{{ keypair_info.key.name }}".pem
  when: keypair_info.key.private_key is defined

- 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 }}"

- 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' }}"

変数ファイルは以下です。気をつけるポイントはsec_ipです。こちらには自分の使用しているIPアドレスを書き込んでください。ワードプレスへのアクセスやssh接続などは全てここに書かれたIPアドレスのみに制限します。自分のIPアドレスがわからない方はこちらのサイトで確認してください。インスタンスは無料枠を指定しているのでそのままで大丈夫です。

# vars file for aws

#リージョン
region: "ap-northeast-1"

#VPC名
vpc_name: "wordpress-vpc"
#VPC CIDR
vpc_cidr: "10.0.0.0/16"

#サブネット
subnet:
    - { subnet_cidr: "10.0.1.0/24" ,subnet_az: "ap-northeast-1a" ,subnet_name: "wordpress-sabnet1" }
    - { subnet_cidr: "10.0.2.0/24" ,subnet_az: "ap-northeast-1c" ,subnet_name: "wordpress-sabnet2" }

#IGW名
igw_name: "wordpress-igw"

#IGWアタッチ先サブネットCIDR
attache_igw_subnet:
    - "10.0.1.0/24"
    - "10.0.2.0/24"

#ルートテーブル名
routetable_name: "wordpress-rt"

#ec2キーペア名
keypair_name: "wordpress-ec2-key"

#セキュリティグループ名
secgrp_name: "wordpress-secgrp"

#セキュリティグループ定義
description: "wordpress Security Group"

#インバウンドルール ポート番号(TCP)
sec_tcp_ports:
    - "443"
    - "53"
    - "80"
    - "22"

#インバウンドルール 許可IPアドレス
sec_ip:
    - "x.x.x.x/32"

#ec2名
ec2_name: wordpress-ec2
#ec2インスタンスタイプ
instance_type: t2.micro
#ec2 イメージid AmazonLinux2(ami-0bba69335379e17f8)推奨
image: ami-0bba69335379e17f8

#ec2ユーザー名
ec2_user_name: ec2-user

Webサーバロール

ここからはEC2内への操作がメインとなります。変数ファイルは使いません。変数化したいものがあれば適宜追加してください。今回は分かりやすく変数ファイルは使っていません。

さてここで一つ問題になってくるのがansible.builtin.shellモジュールです。Ansibleではshellモジュールはなるべく使わないという風潮があります。なぜなら冪等性が担保されていないからです。AWSロールの時のように状態の確認などで使う分には問題ないのですが実際に操作を加える時には好まれないのが実際のところです。それでも今回インストールにはamazon-linux-extrasが必要です。しかしこのコマンドのモジュールがAnsibleにはありません。どうやって冪等性を担保するかを他のモジュールを駆使して実装するわけですが、、、今回はshellモジュールのままでOKです。なぜならamazon-linux-extrasコマンド自体に冪等性があるからです。そのため何度amazon-linux-extrasコマンドを実行してもインストールが2回行われることはありません。のでshellモジュールを使っちゃいまーーーす!

# # webserver構築

- name: LAMPとPHP7.2のインストール
  ansible.builtin.shell: amazon-linux-extras install -y lamp-mariadb10.2-php7.2 php7.2

- name: ApacheとMariaDBインストール
  ansible.builtin.yum: 
    name: 
      - httpd
      - mariadb-server
      - MySQL-python
    state: latest

- name: PyMySQLインストール
  ansible.builtin.pip: 
    name: PyMySQL

- name: Apache起動
  ansible.builtin.systemd:
    name: httpd
    state: started
    enabled: yes

- name: ec2-userをapacheグループに追加
  ansible.builtin.user:
    name: ec2-user
    groups: apache
    append: yes

- name: /var/www配下のユーザーとグループを変更する。
  ansible.builtin.file:
    path: /var/www
    owner: ec2-user
    group: apache
    recurse: yes

- name: /var/wwwとその配下のディレクトリの権限変更
  ansible.builtin.file:
    path: /var/www/
    state: directory
    recurse: yes
    mode: '2755'

ワードプレスロール

次はワードプレスの構築です。パスワード等はAWSチュートリアルのままにしちゃってます。適宜変えてください。これこそ変数ファイルに入れてもよさそうです。

# # wordpress構築

- name: ワードプレスダウンロード
  ansible.builtin.get_url:
    url: https://wordpress.org/latest.tar.gz
    dest: /home/ec2-user/wordpress.tar.gz

- name: ワードプレス解凍
  ansible.builtin.unarchive:
    src: /home/ec2-user/wordpress.tar.gz
    dest: /home/ec2-user
    remote_src: yes

- name: MariaDB起動
  ansible.builtin.systemd:
    name: mariadb
    state: started
    enabled: yes

- name: DB作成
  community.mysql.mysql_db:
    name: wordpress-db

- name: ユーザー作成
  community.mysql.mysql_user:
    name: wordpress-user
    host: localhost
    password: your_strong_password
    priv: 'wordpress-db.*:ALL,GRANT'

- name: ワードプレス設定ファイルコピー
  ansible.builtin.copy:
    src: /home/ec2-user/wordpress/wp-config-sample.php
    dest: /home/ec2-user/wordpress/wp-config.php
    remote_src: yes

- name: ワードプレス設定ファイル編集
  ansible.builtin.replace:
    path: /home/ec2-user/wordpress/wp-config.php
    regexp: "{{ item.before }}"
    replace: "{{ item.after }}"
  with_items: 
    - { before: 'database_name_here', after: 'wordpress-db'}
    - { before: 'username_here', after: 'wordpress-user'}
    - { before: 'password_here', after: 'your_strong_password'}

- name: ワードプレスファイル移動
  ansible.builtin.copy:
    src: /home/ec2-user/wordpress
    dest: /var/www/html/
    remote_src: yes

AWS削除ロール

最後はAWS環境を削除するロールです。間違って実行しないようにディレクトリを別に分けています。そこだけ注意してください。やってることは簡単でAWS構築時に変数ファイルに書かれたIDをCLIを使って削除しているだけです。削除するだけなので冪等性とかは考えていません。

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

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

- name: ec2が終了済みになるまで待機
  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: キーペア削除
  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: ローカルのキーペア削除
  shell: rm {{ ec2_key_pair_path+ec2_key_pair_name+'.pem' }}

- name: IGWデタッチ
  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削除
  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削除
  shell: aws ec2 delete-subnet --subnet-id "{{ subnet1_id }}"
  register: subnet1_delete_info

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

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

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

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

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

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

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

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

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

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

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

僕が擬似スタックと呼んでる変数ファイルはこんな感じです。AWS構築時に随時ここにIDが追加されていく仕様にしています。

vpc_id: 
IGW_id: 
route_table_id: 
subnet1_id: 
subnet2_id: 
SG_id: 
ec2_key_pair_name: 
ec2_key_pair_path: ~/.ssh/
ec2_id: 

構築後は以下みたいな感じになります。この値を取り出してCLIで一括削除すると言うわけですね。

vpc_id: vpc-08f19dc7eafd47b2e
IGW_id: igw-09223a23fce6a0892
route_table_id: rtb-0d548ddf8d868fc5a
subnet1_id: subnet-0ebdea41fde8585cb
subnet2_id: subnet-0390a316b377e37d1
SG_id: sg-00fa631c83d56555c
ec2_key_pair_name: wordpress-ec2-key
ec2_key_pair_path: ~/.ssh/
ec2_id: i-07f640f45bd5c9a1e

Ansible実行

さて準備は整いましたので実行していきたいと思います。の前にAWS CLIだけローカルで使えるようにしておいてください。こちらがわかりやすいかなと思います。

それでは最後に実行していきます。以下のPlaybookファイルを作成します。ここでは先ほど作成したロールを順番に実行していくものです。

#AWS環境を立ち上げるロール
- name: create vpc subnet igw routetable
  hosts: localhost
  connection: local
  roles:
    - aws_vpc

#ec2内にwebサーバーを立ち上げるロール
- name: create web server
  vars_files: group_vars/all.yml
  hosts: "{{ ssh_host }}"
  become: yes
  become_user: root
  roles:
    - web_server

#ec2内にワードプレスを立ち上げるロール
- name: create editor
  vars_files: group_vars/all.yml
  hosts: "{{ ssh_host }}"
  become: yes
  become_user: root
  roles:
    - wordpress

そして実行は以下で行ってください。

ansible-playbook -i hosts create.yml

問題なければ最後までエラーなく実行が終わるかと思います。ワードプレスへのアクセスはコンソールからパブリックDNSを確認し、ブラウザで「パブリックDNS/wordpress」で検索すると制限したIPアドレスでのみアクセスできます。

最後に消す場合です。delete_aws配下にdelete.ymlを書きます。

- name: AWS環境削除
  hosts: localhost
  connection: local
  roles:
    - delete_aws

そして実行します。

ansible-playbook -i hosts delete_aws/delete.yml

これでAWS環境が一括削除されます。

その他

Ansible実行時に以下の警告が出る場合があります。これはコントロール先のEC2のどのpythonを使うか指定されてないからわかんないよと言う警告です。

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

別に無視でも問題ないのですが気になる方はansible.cfgに以下の設定を足してください。

[defaults]
interpreter_python=/usr/bin/python3

最後に

今回はEC2にワードプレス環境を構築しました。しかし今回のやり方に課題もあります。それはAnsibleがMacに依存していると言うことです。同じことを別のPCやOSで実行すると使えていたモジュールが使えなかったり、ansibleのバージョン差分でエラーが起きたりします。

次回はこれを解決するべくAnsibleをコンテナ内で実行することにより環境差分を無くすansible-runnerを使ってワードプレス構築をやっていきたいと思います。