【AWS】AnsibleでLightsailインスタンスを作る【Lightsail】

# はじめに
先日、AWSでLightsailインスタンスを作った際、CloudFormationが未対応だったのでAnsibleでインフラ構成を定義・構築しました。
[lightsail - Create or delete a virtual machine instance in AWS Lightsail — Ansible Documentation](https://docs.ansible.com/ansible/2.7/modules/lightsail_module.html)

2018/11/24現在、まだCloudFormationはLightsailに対応していないようです。

AnsibleでLightsailインスタンスを構築した際、完全に自動化できず幾つか手動で対応が必要だったこと、Lightsail特有のハマりポイントがあったことから、記録として残します。


# TL;DR
- Lightsail利用時の注意
    - SSH用の鍵ペアはEC2とは別管理
    - 固定グローバルIPアドレスは「Static IP」というものが用意されている
    - Static IP≒Elastic IPであるが、それぞれ別管理
- AnsibleのLightsailモジュール利用時の注意
    - Static IPの取得・割り当て等は未対応
- Ansibleのroute53モジュール利用時の注意
    - AssumeRoleするprofileを利用しているとtaskが失敗するケースがある

# 目的・概要
Ansible Playbookで下記の作業を全て実施できれば理想ですが、lightsailモジュールではStatic IPの操作は未対応のようです。したがって、この作業は手動で行う必要がありました。
Ansible Playbook化出来たのは、以下の項番2～4までとなります。

1. Static IPの確保
2. Lightsailインスタンスの作成
3. Static IPのLightsailインスタンスへの割り当て
4. 当該Static IPを指すRoute53 CNAMEレコードの作成

ちなみにStatic IPとは、EC2で言うところのElastic IPのようなものです。ただし、EC2用のそれとは別管理であるため、Lightsailサービス内で新たにStatic IPを取得する必要があります。

[Static IP addresses in Amazon Lightsail | Lightsail Documentation](https://lightsail.aws.amazon.com/ls/docs/en/articles/understanding-static-ip-addresses-in-amazon-lightsail)

# 作成したPlaybook
作成したPlaybookはGitHub上に公開しました。
https://github.com/tmiki/server-config/tree/master/ansible

上記リポジトリには他のPlaybookもたくさん入っていますので、本稿に関するもののみを抜粋します。


ansible-playbookコマンドから指定して実行するファイルは下記となります。「aws_lightsail_wp」というRoleを別途定義し、これを読み込んでいます。

```yaml:aws_lightsail_wp.yml
#
# aws_lightsail_wp.yml
#
---
- hosts: localhost
  connection: local
  gather_facts: False
  roles:
   - aws_lightsail_wp

```

「aws_lightsail_wp」Roleの実体です。

```yaml:roles/aws_lightsail_wp/tasks/main.yml
#
# aws_lightsail_wp
#
---

- name: Create a Lightsail instance
  lightsail:
    state: present
    name: "{{ lightsail_wp.instance_name }}"
    profile: "{{ aws_profile }}"
    region: "{{ region }}"
    zone: "{{ az_primary }}"
    blueprint_id: "{{ lightsail_wp.blueprint_id }}"
    bundle_id: "{{ lightsail_wp.bundle_id }}"
    key_pair_name: "{{ lightsail_wp.key_pair_name }}"
    user_data: " echo 'hello world' > /home/ubuntu/test.txt"
    wait_timeout: 500
  register: _lightsail_instance
- debug:
    var: _lightsail_instance
- debug:
    msg:
     - "Name is {{  _lightsail_instance.instance.name  }}"
     - "Arn is {{  _lightsail_instance.instance.arn  }}"
  when: not ansible_check_mode

- name: Retrieve a global IP address from the Static IP 1/2
  command: "aws lightsail --profile {{aws_profile}} get-static-ip --static-ip-name {{ lightsail_wp.static_ip_name }}"
  register: _result
  when: not ansible_check_mode

- name: Retrieve a global IP address from the Static IP 2/2
  set_fact:
    _lightsail_static_ip: "{{ _result.stdout | from_json }}"
  when: not ansible_check_mode
- debug:
    var: _lightsail_static_ip
- debug:
    msg: "Current static IP is {{ _lightsail_static_ip.staticIp.ipAddress }}"
  when: not ansible_check_mode

- name: Attach the Static IP with the Lightsail instance
  command: "aws lightsail --profile {{aws_profile}} attach-static-ip --static-ip-name {{ lightsail_wp.static_ip_name }} --instance-name {{ lightsail_wp.instance_name }}"
  when:
   - not ansible_check_mode
   - _lightsail_instance.instance.is_static_ip != true

- name: Create a Route53 CNAME record points to the static IP address of the Lightsail instance
  route53:
    state: present
    profile: "{{ aws_profile }}"
    zone: "{{ route53zone }}"
    record: "example-site.{{ route53zone }}"
    type: A
    ttl: 300
    value: "{{ _lightsail_static_ip.staticIp.ipAddress }}"
  when: not ansible_check_mode

```

上記のtaskで利用している変数定義です。

```yaml:hosts/dev1/group_vars/all.yml
---
env: dev1
Env: "{{ env.capitalize() }}"

aws_profile: dev1

route53zone: "dev1.your-domain.com"

region: ap-northeast-1
az_primary: ap-northeast-1a
az_secondary: ap-northeast-1c

lightsail_wp:
  blueprint_id: "wordpress_4_9_6"
  bundle_id: "nano_2_0"
  instance_name: "{{env}}-example-site"
  static_ip: "{{env}}-example-site-ip"
  key_pair_name: "{{env}}-lightsail-keypair"

```

# Lightsailインスタンス構築手順
必要な手順は下記のREADME.mdにまとめてあります。
https://github.com/tmiki/server-config/blob/master/ansible/README.md

各ステップについてそれぞれ説明します。

## 1. Create a key pair for Lightsail
まず最初にSSH用の鍵ペアを作ります。EC2用のものとLightsail用のものは独立して管理されているため、初めてLightsailインスタンスを作る際にはこちらも併せて作る必要があります。
鍵ペアの作成には「aws lightsail create-key-pair」コマンドを実行します。当該鍵ペアは「--key-pair-name」で指定した名前で保存されます。Lightsailインスタンスの作成時には、この名前で鍵ペアを指定することができます。

実行結果に秘密鍵が含まれるため、リダイレクトして保存しておくと良いでしょう。言うまでもないことですが、秘密鍵の取り扱いには十分注意します。

```
$ aws lightsail create-key-pair --key-pair-name dev1-lightsail-keypair > dev1-lightsail-keypair.json
```

## 2. Allocate a new Static IP
次に、Static IPを取得します。Elastic IPとの大きな違いは、任意の名前を付与して管理できることです。ここでは「dev1-example-site-ip」という名称でStatic IPを取得しています。

```
$ aws lightsail allocate-static-ip --static-ip-name dev1-example-site-ip
{
    "operations": [
        {
            "status": "Succeeded",
            "resourceType": "StaticIp",
            "isTerminal": true,
            "statusChangedAt": 1542990973.062,
            "location": {
                "availabilityZone": "all",
                "regionName": "ap-northeast-1"
            },
            "operationType": "AllocateStaticIp",
            "resourceName": "dev1-example-site-ip",
            "id": "47fb700f-ffff-ffff-ffff-ffffffffffff",
            "createdAt": 1542990972.707
        }
    ]
}

```

「aws lightsail get-static-ips」コマンドで、取得済みのStatic IPを確認することが可能です。

```
$ aws lightsail get-static-ips
{
    "staticIps": [
        {
            "name": "dev1-example-site-ip",
            "resourceType": "StaticIp",
            "supportCode": "0123456789012/xx.xx.xx.xx",
            "arn": "arn:aws:lightsail:ap-northeast-1:123456789012:StaticIp/9283dbfd-ffff-ffff-ffff-ffffffffffff",
            "isAttached": false,
            "ipAddress": "xx.xx.xx.xx",
            "createdAt": 1542990972.707,
            "location": {
                "availabilityZone": "all",
                "regionName": "ap-northeast-1"
            }
        }
    ]
}
```

## 3. Modify variable definitions
自分の環境に合わせて、Ansible Playbook用の変数定義を編集します。当該Playbookの実行に必要な変数は、「hosts/*/group_vars/all.yml」に定義する想定です。

```
$ vi hosts/dev1/group_vars/all.yml
```

## 4. Run the Playbook
Playbookを実行します。

```
$ ansible-playbook -vv -i hosts/dev1 aws_lightsail_wp.yml
```

## 5.Create a Route53 record manually, if the Playbook fails.
このPlaybookは、取得したStatic IPを指すCNAMEレコードを自動的に作成します。
原因は不明（というより詳細は未調査）ですが、AssumeRoleするProfileを利用しているとレコードの作成に失敗します。最新のAnsible／AWS CLI／Boto3の環境で発生するかどうかは不明です。

このような場合、Route53レコードは手動で作る必要があります。

# Playbookの解説
## lightsailモジュール
varsで定義したパラメータ等を元に、Lightsailインスタンスを作成します。Lightsailはblueprintとして、WordpressやLAMP、Node.jsなど予め準備しており、これをもとに新たなLightsailインスタンスを構築することができます。
bundle_idは、簡単に言うとインスタンスサイズです。CPUやメモリ容量、転送可能なデータ量などのサーバリソースがセットになったものです。

blueprint_id、bundle_idの取得は下記記事を参照してください。

[【小ネタ】AWS LightsailでblueprintIdやbundleIdを確認する【Lightsail】](https://qiita.com/tmiki/items/cfdf5970204ff877cd57)

key_pair_nameとして、先ほど手動で作った鍵ペアが指定されるよう、変数定義しておきます。

```yaml:lightsailモジュール
- name: Create a Lightsail instance
  lightsail:
    state: present
    name: "{{ lightsail_wp.instance_name }}"
    profile: "{{ aws_profile }}"
    region: "{{ region }}"
    zone: "{{ az_primary }}"
    blueprint_id: "{{ lightsail_wp.blueprint_id }}"
    bundle_id: "{{ lightsail_wp.bundle_id }}"
    key_pair_name: "{{ lightsail_wp.key_pair_name }}"
    user_data: " echo 'hello world' > /home/ubuntu/test.txt"
    wait_timeout: 500
  register: _lightsail_instance
- debug:
    var: _lightsail_instance
- debug:
    msg:
     - "Name is {{  _lightsail_instance.instance.name  }}"
     - "Arn is {{  _lightsail_instance.instance.arn  }}"
  when: not ansible_check_mode
```

## Static IPをLightsailインスタンスに紐づける
Static IPは先ほどのlightsailモジュールでは操作できません。そのため、「command」モジュールを用いて、AWS CLIを実行しています。
「aws lightsail attach-static-ip」コマンドで、先ほど生成したLightsailインスタンスに紐づけています。

なおこれに先立ち、「aws lightsail get-static-ip」コマンドで、先ほど手動で取得したStatic IPの実際のIPアドレスを取りだしていますが、これはこの後、Route53レコード作成に必要な情報となります。

```yaml:StaticIP
- name: Retrieve a global IP address from the Static IP 1/2
  command: "aws lightsail --profile {{aws_profile}} get-static-ip --static-ip-name {{ lightsail_wp.static_ip_name }}"
  register: _result
  when: not ansible_check_mode

- name: Retrieve a global IP address from the Static IP 2/2
  set_fact:
    _lightsail_static_ip: "{{ _result.stdout | from_json }}"
  when: not ansible_check_mode
- debug:
    var: _lightsail_static_ip
- debug:
    msg: "Current static IP is {{ _lightsail_static_ip.staticIp.ipAddress }}"
  when: not ansible_check_mode

- name: Attach the Static IP with the Lightsail instance
  command: "aws lightsail --profile {{aws_profile}} attach-static-ip --static-ip-name {{ lightsail_wp.static_ip_name }} --instance-name {{ lightsail_wp.instance_name }}"
  when:
   - not ansible_check_mode
   - _lightsail_instance.instance.is_static_ip != true

```

## Route53 CNAMEレコードの作成
Ansibleのroute53モジュールを利用して、CNAMEレコードを作成します。
ゾーン名は「route53zone」という変数で定義されます。当該Lightsailインスタンスに対応するドメイン名は、「lightsail_wp.site_hostname」変数及び「route53zone」変数を組み合わせたものとなります。

なお先日試した際には、AssumeRoleするProfileを利用したところ、レコードの作成に失敗しました。原因も詳しく調査していないので詳細は不明ですが、このような場合は手動でRoute53レコードを作る必要があります。

```yaml:Route53
- name: Create a Route53 CNAME record points to the static IP address of the Lightsail instance
  route53:
    state: present
    profile: "{{ aws_profile }}"
    zone: "{{ route53zone }}"
    record: "{{ lightsail_wp.site_hostname }}.{{ route53zone }}"
    type: A
    ttl: 300
    value: "{{ _lightsail_static_ip.staticIp.ipAddress }}"
  when: not ansible_check_mode

```


# おわりに
Lightsailのインフラ構成を定義・管理するという需要は恐らく小さいと思われるので、CloudFormationもAnsibleも完全に対応するのはまだ先のことでしょう。
それでも、出来る限り構成管理は進めていった方が、後々の管理が楽になりますので、可能な限りやっておいた方が良いでしょう。