2015年11月29日日曜日

[CentOS6][Ansible] EC2インスタンスで Webサーバを作成する


Ansible で EC2インスタンスのWebサーバ(CentOS6+Apache)を1台構築してみました。
自宅からAWSに接続することを前提にしています。

Ansible では以下のタスクを行います。
  • CentOS公式AMIからEC2インスタンスの作成
  • ルートパーティションの拡張(8GB ⇒ 10GB)
  • CentOS6の基本設定
    • SElinux無効化
    • ipv6無効化
    • ファイヤーウォール無効化
    • 日本語(ja_JP.UTF-8)設定
    • タイムゾーン(Asia/Tokyo)設定
  • Apache導入

AWS側は、事前に以下のリソースを用意します。
  • サブネット(パブリック)
  • セキュリティグループ(SSH、HTTPを許可)
  • キーペア


Ansibleのインストール


Ansible のインストールは以下のとおり。 
libselinux-python は、SELinuxの設定で使用します。
python-boto は EC2 の作成で使用します。
[root@node01 ~]# yum install ansible libselinux-python python-boto

Ansible のバージョンは以下のとおり
[root@node01 ~]# ansible --version
ansible 1.9.4
  configured module search path = None

Ansible プレイブックのディレクトリ構成


今回、ansible は 一般ユーザの infraユーザで実行することにしました。
Ansible のプレイブックは、ベストプラクティスを参考に以下のように配置しました。
[infra@node01 ansible]$ tree .
.
├── aws.yml
├── group_vars
├── host_vars
├── key.pem
├── production
├── roles
│   ├── apache
│   │   └── tasks
│   │       └── main.yml
│   ├── common
│   │   ├── defaults
│   │   ├── files
│   │   │   └── disable-ipv6.conf
│   │   ├── handlers
│   │   ├── meta
│   │   ├── tasks
│   │   │   └── main.yml
│   │   ├── templates
│   │   └── vars
│   │       └── main.yml
│   ├── ec2
│   │   ├── tasks
│   │   │   └── main.yml
│   │   └── vars
│   │       └── main.yml
│   └── growpart
│       ├── tasks
│       │   └── main.yml
│       └── vars
│           └── main.yml
├── site.yml
└── webservers.yml

19 directories, 13 files


Ansible プレイブックの内容


各ファイルの内容は以下のとおり。
key.pem はAWSで作成したキーペアの秘密鍵です。
CentOS6の公式AMで作成したEC2インスタンスには、この秘密鍵を使用して centos ユーザでログインします。
centosユーザはパスワードなしの sudo が使用できるようになっています。
[infra@node01 ansible]$ more * | cat -
::::::::::::::
aws.yml
::::::::::::::
- hosts: localhost
  connection: local
  gather_facts: False
  roles:
    - ec2
- hosts: webservers
  gather_facts: True
  sudo: yes
  roles:
    - growpart
::::::::::::::
key.pem
::::::::::::::
-----BEGIN RSA PRIVATE KEY-----
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-----END RSA PRIVATE KEY-----
::::::::::::::
production
::::::::::::::
[local]
localhost

[webservers:vars]
ansible_ssh_user=centos
ansible_ssh_private_key_file=/home/infra/ansible/key.pem

::::::::::::::
site.yml
::::::::::::::
- include: aws.yml
- include: webservers.yml
::::::::::::::
webservers.yml
::::::::::::::
- hosts: webservers
  gather_facts: True
  sudo: yes
  roles:
    - common
    - apache
[infra@node01 ansible]$ more */*/*/* | cat -
::::::::::::::
roles/apache/tasks/main.yml
::::::::::::::
- name: Apache のインストール
  yum: name=httpd state=latest
  tags:
    - apache

- name: Apache の自動起動ON
  service: name=httpd enabled=yes
  tags:
    - apache

- name: Apache の起動
  service: name=httpd state=started
  tags:
    - apache
::::::::::::::
roles/common/files/disable-ipv6.conf
::::::::::::::
options ipv6 disable=1
::::::::::::::
roles/common/tasks/main.yml
::::::::::::::
#-----------------------
# OS 基本設定
#-----------------------

# SElinux off
# yum install libselinux-python
- name: SELinux を無効にする
  selinux: state=disabled
  tags:
    - os_basic
    - selinux

# ipv6 off
- name: ipv6 を無効にする(/etc/sysconfig/network)
  lineinfile: >
    dest=/etc/sysconfig/network
    state=present
    backup=yes
    backrefs=no
    insertafter=EOF
    regexp='^NETWORKING_IPV6='
    line='NETWORKING_IPV6=no'
  tags:
    - os_basic
    - ipv6

- name: ipv6 を無効にする(/etc/modprobe.d/)
  copy: >
    src=disable-ipv6.conf
    dest=/etc/modprobe.d/disable-ipv6.conf
    group=root
    owner=root
    mode=0644
    backup=yes
  tags:
    - os_basic
    - ipv6

# Firewall off
- name: Firewall を無効にする
  service: name={{ item }} enabled=no
  with_items: firewall_srv
  tags:
    - os_basic
    - firewall

# LANG=ja_JP.UTF-8
- name: 日本語(ja_JP.UTF-8) にする
  lineinfile: >
    dest=/etc/sysconfig/i18n
    state=present
    backup=yes
    backrefs=no
    insertafter=EOF
    regexp='^LANG='
    line='LANG=ja_JP.UTF-8'
  tags:
    - os_basic
    - lang

# TIMEZONE=Asia/Tokyo
- name: タイムゾーンを Asia/Tokyo にする
  lineinfile: >
    dest=/etc/sysconfig/clock
    state=present
    backup=yes
    backrefs=no
    insertafter=EOF
    regexp='^ZONE='
    line='ZONE=Asia/Tokyo'
  tags:
    - os_basic
    - zone

- name: ローカルタイムを Asia/Tokyo にする
  file: >
    src=/usr/share/zoneinfo/Asia/Tokyo
    dest=/etc/localtime
    state=link
    force=yes
    owner=root
    group=root
  tags:
    - os_basic
    - zone

#-----------------------
# OS パッケージの最新化
#-----------------------

- name: yum アップデート
  yum: name=* enablerepo=epel state=latest
  tags:
    - yum_update

- name: reboot!
  command: shutdown -r now
  tags:
    - yum_update

- name: wait for SSH port down
  local_action: wait_for host={{ inventory_hostname }} port=22 state=stopped
  sudo: no
  tags:
    - yum_update

- name: wait for SSH port up
  local_action: wait_for host={{ inventory_hostname }} port=22 state=started timeout=300
  sudo: no
  tags:
    - yum_update

::::::::::::::
roles/common/vars/main.yml
::::::::::::::

firewall_srv:
 - iptables
 - ip6tables
::::::::::::::
roles/ec2/tasks/main.yml
::::::::::::::
# yum install python-boto

- name: EC2インスタンスを作成
  ec2:
    aws_access_key: "{{ aws_access_key }}"
    aws_secret_key: "{{ aws_secret_key }}"
    assign_public_ip: "{{ assign_public_ip }}"
    region: "{{region}}"
    zone: "{{zone}}"
    image: "{{ ami_image }}"
    instance_type: "{{ instance_type }}"
    key_name: "{{ key_name }}"
    group_id: "{{ sg_id }}"
    private_ip: "{{ ip }}"
    vpc_subnet_id: "{{ vpc_subnet_id }}"
    instance_tags:
      Name: "{{ hostname }}"
      System: "Test"
    wait: yes
    wait_timeout: 300
    volumes:
      - device_name: "{{ device_name }}"
        volume_size: "{{ volume_size }}"
        delete_on_termination: yes
  register: ec2
  tags:
    - ec2
#- debug: var=ec2

- name: SSHで接続できるようになるまで待機
  local_action: wait_for port=22 host={{ item.public_dns_name }} timeout=300 state=started
  with_items: ec2.instances
  when: ec2.changed == true
  tags:
    - ec2

- name: 作成したEC2インスタンスをインベントリに追加
  local_action: add_host hostname="{{ item.public_ip }}" groupname="webservers"
  with_items: ec2.instances
  when: ec2.changed == true
  tags:
    - ec2

::::::::::::::
roles/ec2/vars/main.yml
::::::::::::::

# EC2
region: us-east-1
zone: us-east-1a
aws_access_key: xxxxxxxxxxxxx
aws_secret_key: xxxxxxxxxxxxx
assign_public_ip: yes
ami_image: ami-57cd8732
key_name: keypaire
hostname: apache01
ip: 10.0.0.11
vpc_subnet_id: "subnet-xxxx"
sg_id: [ "sg-xxxx", "sg-xxxx" ]
instance_type: "t2.micro"
device_name: "/dev/sda1"
volume_size: 10

::::::::::::::
roles/growpart/tasks/main.yml
::::::::::::::
# ルートパーティション拡張
- name: epel レポジトリのインストール
  yum: name=epel-release state=latest
  tags:
    - growpart

- name: yum パッケージのインストール
  yum: name={{ item }} enablerepo=epel state=latest
  with_items: grow_part_pkg
  tags:
    - growpart

- name: パーティション拡張の指示
  shell: "dracut --force --add growroot /boot/initramfs-$(uname -r).img"
  register: dracut
  ignore_errors: true
  changed_when: false
  tags:
    - growpart

- name: reboot!
  command: shutdown -r now
  tags:
    - growpart

- name: wait for SSH port down
  local_action: wait_for host={{ inventory_hostname }} port=22 state=stopped
  sudo: no
  tags:
    - growpart

- name: wait for SSH port up
  local_action: wait_for host={{ inventory_hostname }} port=22 state=started timeout=300
  sudo: no
  tags:
    - growpart

::::::::::::::
roles/growpart/vars/main.yml
::::::::::::::
grow_part_pkg:
 - cloud-utils-growpart
 - dracut-modules-growroot
[infra@node01 ansible]$

イベントリ(puroduction)の webservers グループには、ホスト(IPアドレス)を指定していませんが、EC2作成時に自動的にIPアドレスが追加されるようにしています。
プレイブック実行時に、作成したEC2インスタンスのパブリックIPがwebsrversグループに追加され、後続の処理では、追加されたIPアドレスに対してタスクを実行します。

Ansibleプレイブックのタスク一覧


このプレイブックが実施するタスクは以下のとおり。
上から順番に実行されます。
[infra@node01 ansible]$ ansible-playbook site.yml -i production --list-tasks

playbook: site.yml

  play #1 (localhost):  TAGS: []
    EC2インスタンスを作成       TAGS: [ec2]
    SSHで接続できるようになるまで待機   TAGS: [ec2]
    作成したEC2インスタンスをインベントリに追加 TAGS: [ec2]

  play #2 (webservers): TAGS: []
    epel レポジトリのインストール       TAGS: [growpart]
    yum パッケージのインストール        TAGS: [growpart]
    パーティション拡張の指示    TAGS: [growpart]
    reboot!     TAGS: [growpart]
    wait for SSH port down      TAGS: [growpart]
    wait for SSH port up        TAGS: [growpart]

  play #3 (webservers): TAGS: []
    SELinux を無効にする        TAGS: [os_basic, selinux]
    ipv6 を無効にする(/etc/sysconfig/network)   TAGS: [ipv6, os_basic]
    ipv6 を無効にする(/etc/modprobe.d/) TAGS: [ipv6, os_basic]
    Firewall を無効にする       TAGS: [firewall, os_basic]
    日本語(ja_JP.UTF-8) にする  TAGS: [lang, os_basic]
    タイムゾーンを Asia/Tokyo にする    TAGS: [os_basic, zone]
    ローカルタイムを Asia/Tokyo にする  TAGS: [os_basic, zone]
    yum アップデート    TAGS: [yum_update]
    reboot!     TAGS: [yum_update]
    wait for SSH port down      TAGS: [yum_update]
    wait for SSH port up        TAGS: [yum_update]
    Apache のインストール       TAGS: [apache]
    Apache の自動起動ON TAGS: [apache]
    Apache の起動       TAGS: [apache]



Ansible実行(checkモード)


実際に変更は行わないようにして実行して、動きを確認してみます。
[infra@node01 ansible]$ ansible-playbook site.yml -i production --check

PLAY [localhost] **************************************************************

TASK: [ec2 | EC2インスタンスを作成] **********************************
skipping: [localhost]
ok: [localhost]

TASK: [ec2 | SSHで接続できるようになるまで待機] ****************
skipping: [localhost] => (item=ec2.instances)

TASK: [ec2 | 作成したEC2インスタンスをインベントリに追加] ***
skipping: [localhost] => (item=ec2.instances)

PLAY [webservers] *************************************************************
skipping: no hosts matched

PLAY [webservers] *************************************************************
skipping: no hosts matched

PLAY RECAP ********************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0

[infra@node01 ansible]$

実際にEC2インスタンスを作成しないとイベントリにIPアドレスが追加されないので、webservers グループのタスクは実行されません。

Ansible実行


今度は、実際に変更を行います。
途中、EC2インスタンスに初めてSSHログインするタイミングで、接続確認入力を求められるので "yes" を入力します。
[infra@node01 ansible]$ ansible-playbook site.yml -i production

PLAY [localhost] **************************************************************

TASK: [ec2 | EC2インスタンスを作成] **********************************
changed: [localhost]

TASK: [ec2 | SSHで接続できるようになるまで待機] ****************
ok: [localhost -> 127.0.0.1] => (item={u'ramdisk': None, u'kernel': None, u'root_device_type': u'ebs', u'private_dns_name': u'ip-10-0-0-11.ec2.internal', u'tags': {}, u'key_name': u'virginia_key', u'public_ip': u'52.23.158.7', u'image_id': u'ami-57cd8732', u'tenancy': u'default', u'private_ip': u'10.0.0.11', u'groups': {u'sg-bbf176df': u'default', u'sg-01237c67': u'webgroup'}, u'public_dns_name': u'ec2-52-23-158-7.compute-1.amazonaws.com', u'state_code': 16, u'id': u'i-a6f60810', u'placement': u'us-east-1a', u'ami_launch_index': u'0', u'dns_name': u'ec2-52-23-158-7.compute-1.amazonaws.com', u'region': u'us-east-1', u'ebs_optimized': False, u'launch_time': u'2015-11-29T04:24:40.000Z', u'instance_type': u't2.micro', u'state': u'running', u'root_device_name': u'/dev/sda1', u'hypervisor': u'xen', u'virtualization_type': u'hvm', u'architecture': u'x86_64'})

TASK: [ec2 | 作成したEC2インスタンスをインベントリに追加] ***
ok: [localhost -> 127.0.0.1] => (item={u'ramdisk': None, u'kernel': None, u'root_device_type': u'ebs', u'private_dns_name': u'ip-10-0-0-11.ec2.internal', u'tags': {}, u'key_name': u'virginia_key', u'public_ip': u'52.23.158.7', u'image_id': u'ami-57cd8732', u'tenancy': u'default', u'private_ip': u'10.0.0.11', u'groups': {u'sg-bbf176df': u'default', u'sg-01237c67': u'webgroup'}, u'public_dns_name': u'ec2-52-23-158-7.compute-1.amazonaws.com', u'state_code': 16, u'id': u'i-a6f60810', u'placement': u'us-east-1a', u'ami_launch_index': u'0', u'dns_name': u'ec2-52-23-158-7.compute-1.amazonaws.com', u'region': u'us-east-1', u'ebs_optimized': False, u'launch_time': u'2015-11-29T04:24:40.000Z', u'instance_type': u't2.micro', u'state': u'running', u'root_device_name': u'/dev/sda1', u'hypervisor': u'xen', u'virtualization_type': u'hvm', u'architecture': u'x86_64'})

PLAY [webservers] *************************************************************

GATHERING FACTS ***************************************************************
The authenticity of host '52.23.158.7 (52.23.158.7)' can't be established.
RSA key fingerprint is d8:8c:b2:3d:dd:d8:c3:7b:29:ec:41:72:29:ca:da:bf.
Are you sure you want to continue connecting (yes/no)? yes
ok: [52.23.158.7]

TASK: [growpart | epel レポジトリのインストール] ******************
changed: [52.23.158.7]

TASK: [growpart | yum パッケージのインストール] *******************
changed: [52.23.158.7] => (item=cloud-utils-growpart,dracut-modules-growroot)

TASK: [growpart | パーティション拡張の指示] ***********************
ok: [52.23.158.7]

TASK: [growpart | reboot!] ****************************************************
changed: [52.23.158.7]

TASK: [growpart | wait for SSH port down] *************************************
ok: [52.23.158.7 -> 127.0.0.1]

TASK: [growpart | wait for SSH port up] ***************************************
ok: [52.23.158.7 -> 127.0.0.1]

PLAY [webservers] *************************************************************

GATHERING FACTS ***************************************************************
ok: [52.23.158.7]

TASK: [common | SELinux を無効にする] ***********************************
changed: [52.23.158.7]

TASK: [common | ipv6 を無効にする(/etc/sysconfig/network)] **************
changed: [52.23.158.7]

TASK: [common | ipv6 を無効にする(/etc/modprobe.d/)] ********************
changed: [52.23.158.7]

TASK: [common | Firewall を無効にする] **********************************
changed: [52.23.158.7] => (item=iptables)
changed: [52.23.158.7] => (item=ip6tables)

TASK: [common | 日本語(ja_JP.UTF-8) にする] *****************************
changed: [52.23.158.7]

TASK: [common | タイムゾーンを Asia/Tokyo にする] *******************
changed: [52.23.158.7]

TASK: [common | ローカルタイムを Asia/Tokyo にする] ****************
changed: [52.23.158.7]

TASK: [common | yum アップデート] ***************************************
changed: [52.23.158.7]

TASK: [common | reboot!] ******************************************************
changed: [52.23.158.7]

TASK: [common | wait for SSH port down] ***************************************
ok: [52.23.158.7 -> 127.0.0.1]

TASK: [common | wait for SSH port up] *****************************************
ok: [52.23.158.7 -> 127.0.0.1]

TASK: [apache | Apache のインストール] *********************************
changed: [52.23.158.7]

TASK: [apache | Apache の自動起動ON] *************************************
changed: [52.23.158.7]

TASK: [apache | Apache の起動] *********************************************
changed: [52.23.158.7]

PLAY RECAP ********************************************************************
52.23.158.7                : ok=22   changed=15   unreachable=0    failed=0
localhost                  : ok=3    changed=1    unreachable=0    failed=0

[infra@node01 ansible]$

動作確認


EC2インスタンスにログインして、パーティションの拡張や、OS設定が成功しているか確認します。
[centos@ip-10-0-0-11 ~]$ getenforce
Disabled
[centos@ip-10-0-0-11 ~]$ lsblk
NAME    MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
xvda    202:0    0  10G  0 disk
└─xvda1 202:1    0  10G  0 part /
[centos@ip-10-0-0-11 ~]$ df -Th
Filesystem     Type   Size  Used Avail Use% Mounted on
/dev/xvda1     ext4   9.8G  867M  8.4G  10% /
tmpfs          tmpfs  498M     0  498M   0% /dev/shm
[centos@ip-10-0-0-11 ~]$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc pfifo_fast state UP qlen 1000
    link/ether 0a:7f:41:05:7d:b5 brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.11/24 brd 10.0.0.255 scope global eth0
[centos@ip-10-0-0-11 ~]$ date
2015年 11月 29日 日曜日 13:40:06 JST
[centos@ip-10-0-0-11 ~]$ chkconfig | grep iptables
iptables        0:off   1:off   2:off   3:off   4:off   5:off   6:off
[centos@ip-10-0-0-11 ~]$ chkconfig | grep ip6tables
ip6tables       0:off   1:off   2:off   3:off   4:off   5:off   6:off
[centos@ip-10-0-0-11 ~]$ chkconfig | grep httpd
httpd           0:off   1:off   2:on    3:on    4:on    5:on    6:off
[centos@ip-10-0-0-11 ~]$ curl -I http://localhost
HTTP/1.1 403 Forbidden
Date: Sun, 29 Nov 2015 04:40:39 GMT
Server: Apache/2.2.15 (CentOS)
Accept-Ranges: bytes
Content-Length: 4961
Connection: close
Content-Type: text/html; charset=UTF-8

[centos@ip-10-0-0-11 ~]$

パーティションも10GBに拡張され、OS設定も成功しています。


◆ 補足

あとで、Webサーバの変更をしたい場合は、イベントリ(production)の webserversグループにEC2インスタンスのIPアドレスを記載します。
以下のように webservers グループだけ指定したり、タグを指定したりして一部のタスクのみ実行できます。

[infra@node01 ansible]$ ansible-playbook site.yml -i production -l webservers
[infra@node01 ansible]$ ansible-playbook site.yml -i production --tags os_basic