AWXでAWS EC2インスタンス作成→処理実行→削除を自動化

# はじめに
自宅のRaspberry Pi上でAWXを使うために[ARM64版のビルド](https://qiita.com/ussvgr/items/f95d950bf39aa67d0189)をしているのですが、
Rapsberry Pidでは性能不足ということもあり、ビルド時にAWS EC2のARM64インスタンスを利用しています。
ビルド時しか使わないので、費用削減のために毎回スポットインスタンス作成→ビルド終わったら削除、とコンソールから手作業でやっていましたが、だんだん面倒になってきたため自動化したいと思います。

# 実現したいこと
AWX 16.0.0を使って、以下のような事を実現させます。

1.AWS上に以下のような環境を構築
![AWS (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/b8245b4e-fa68-2402-63ec-5ebc0b3d6c2c.jpeg)

2.作成したEC2インスタンス上でAWXをビルド、完了したらコンテナレジストリにアップロード

3.アップロード完了後作成した環境を削除

# コンテナレジストリの準備
今回、コンテナレジストリとして[Amazon Elastic Container Registry
](https://aws.amazon.com/jp/ecr/) を利用しようと思うので、事前に作成しておきます。
名前(awx)とLinux・ARM64を指定するぐらいです。
![スクリーンショット 2020-12-13 17.34.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/595f8412-c16c-9f06-1849-9def108fa1f4.png)

EC2インスタンスからコンテナイメージをPush可能にするため、IAMロールを作成しておきます。
トライアンドエラーを繰り返しつつビジュアルエディタでポチポチと作成した結果、以下のようなポリシーであればPush可能なようです。
これを「awx-builder-role」という名前で作成しておきます。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "ecr-public:DescribeImages",
                "ecr-public:InitiateLayerUpload",
                "ecr-public:PutRepositoryCatalogData",
                "ecr-public:UploadLayerPart",
                "ecr-public:PutImage",
                "ecr-public:GetRepositoryCatalogData",
                "ecr-public:GetRegistryCatalogData",
                "ecr-public:CompleteLayerUpload",
                "ecr-public:PutRegistryCatalogData",
                "ecr-public:GetRepositoryPolicy",
                "ecr-public:BatchCheckLayerAvailability"
            ],
            "Resource": [
                "arn:aws:ecr-public::<アカウントID>:repository/*",
                "arn:aws:ecr-public::<アカウントID>:registry/*"
            ]
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "ecr-public:GetAuthorizationToken",
                "sts:GetServiceBearerToken"
            ],
            "Resource": "*"
        }
    ]
}
```

# IAMユーザー
AWSコンソールからAWXが使うIAMユーザーを作成します。
![スクリーンショット 2020-12-13 2.46.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/f8381459-2f67-1245-dfb8-c4fd2f590331.png)

ポリシーは以下のようにしました。

- AmazonEC2FullAccess
- AmazonVPCFullAccess
- 上記で作成した「awx-builder-role」のGetRole,PassRole（EC2インスタンスにロールを付与するために必要となる）
![スクリーンショット 2020-12-13 20.28.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/3d009f13-b18c-7828-50ca-a00988c37feb.png)

作成時に表示されるアクセスキーID・シークレットアクセスキーをAWXのCredentials(認証情報)に登録しましょう。

Credential Typeを「Amazon Web Services」とし、
Access Key・Secret Keyをそれぞれ入力して保存します。
![スクリーンショット 2020-12-13 2.58.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/4dad3fa9-ce36-8d72-2f04-1eaf457e14cb.png)


# インベントリ登録
作成されるEC2インスタンスの情報を取得するため、ダイナミックインベントリを作成します。
まずは名前を付けていったん保存し、
![スクリーンショット 2020-12-13 15.23.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/7805f428-04da-0155-4329-515fee7f0240.png)

SourcesからAddボタンを押下し以下のように入力していきます。

- Source: Amazon EC2
- Credential: <上記で登録したCredentialを選択>
- Overwrite: チェックを付ける（最新の情報でインベントリを上書きする）
- Update on launch: チェックを付ける（このインベントリを利用するジョブ実行時に毎回インベントリの内容を更新します）
- Source variables: 以下のYAMLを入力し、「Ansible」というタグの値でグループを作るようにします。

```yaml
---
keyed_groups:
  - key: tags.Ansible
    separator: ''
```
![スクリーンショット 2020-12-13 16.28.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/35acc756-9f82-3b7c-d1eb-b95f2f5ac44c.png)

# 認証情報登録
作成するEC2を操作するためのCredential（認証情報）を作成しておきます。
今回、Ubuntuのインスタンスを作成しますが、AWSにおけるUbuntuインスタンスのデフォルトユーザーは「ubuntu」ですので、ユーザー名ubuntuとしてログイン可能な認証情報を作成しておきます。
![スクリーンショット 2020-12-13 16.38.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/ff8c06b3-6458-db15-4a64-b70bab316873.png)

# プロジェクト作成
以下の3つのPlaybookを含んだプロジェクトを作成します。

### AWS環境作成用Playbook
```create_ec2spot_environment.yaml
---
- name: Create AWS EC2 Spot Instance Environment
  hosts: localhost
  connection: local
  environment:
    AWS_REGION: ap-northeast-1
  tasks:
  - name: Get SSH Authorized key
    uri:
      url: https://gitlab.com/ussvgr.keys
      return_content: yes
    register: ssh_key
  - name: Dump SSH Authorized key
    debug:
      msg: "SSH Public key is {{ ssh_key.content }}"
  - name: Create SSH Key
    ec2_key:
      name: my_keypair
      key_material: "{{ ssh_key.content }}"
  - name: Create a VPC
    ec2_vpc_net:
      name: Spot VPC
      cidr_block: 10.10.0.0/16
    register: vpc_result
  - name: Create Subnet
    ec2_vpc_subnet:
      vpc_id: "{{ vpc_result.vpc.id }}"
      cidr: 10.10.0.0/24
      map_public: yes
      state: present
      az: ap-northeast-1a
      wait: yes
    register: subnet_result
  - name: Create Internet Gateway
    ec2_vpc_igw:
      vpc_id: "{{ vpc_result.vpc.id }}"
      state: present
    register: igw_result
  - name: Create Route table
    ec2_vpc_route_table:
        vpc_id: "{{ vpc_result.vpc.id }}"
        tags:
          Name: Public
        subnets:
          - "{{ subnet_result.subnet.id }}"
        routes:
          - dest: 0.0.0.0/0
            gateway_id: "{{ igw_result.gateway_id }}"
  - name: Create Security Group
    ec2_group:
      name: allow_ssh_from_my_environment
      description: sg with rule descriptions
      vpc_id: "{{ vpc_result.vpc.id }}"
      rules:
        - proto: tcp
          ports:
          - 22
          cidr_ip: 0.0.0.0/0
          rule_desc: allow SSH
    register: sg_result
  - name: Create Spot Instance
    ec2:
      spot_price: "0.05"
      keypair: my_keypair
      vpc_subnet_id: "{{ subnet_result.subnet.id }}"
      group_id: "{{ sg_result.group_id }}"
      instance_type: t4g.large
      image: ami-0d1f7bec0e294ef80 # Ubuntu Server 20.04 LTS
      instance_tags:
        Ansible: awx_builder
      instance_profile_name: awx-builder-role
      zone: ap-northeast-1a
      count_tag:
        Ansible: awx_builder
      exact_count: 1
      volumes:
      - device_name: /dev/sda1
        delete_on_termination: yes
        volume_size: 16
      wait: yes
      instance_initiated_shutdown_behavior: terminate
```
愚直に

- SSH用のキーペア
- VPC
- サブネット
- Internet Gateway
- ルートテーブル
- セキュリティグループ
- EC2インスタンス

を作成するだけのPlaybookです。
後に削除する事を考えて、各リソースに「Ansible: awx_builder」というタグを付けています。
また、EC2インスタンスには上記で作成した「awx-builder-role」を付与しています。
上記ではセキュリティグループはInternet向けに22番全開放になっているので、実際使う環境に合わせ適宜変更すること。

###AWXコンテナイメージビルド用Playbook

```build_awx.yaml
---
- name: Create AWS EC2 Spot Instance 
  hosts: awx_builder
  become: yes
  tasks:
  - name: Install requirement packages
    apt:
      update_cache: yes
      name:
        - docker.io
        - python3-docker
        - ansible
        - unzip
  - name: Download awscli v2
    unarchive:
      src: https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip
      dest: /tmp
      remote_src: yes
  - name: Install awscli v2
    shell:
      cmd: /tmp/aws/install
  - name: Start Docker service
    systemd:
      name: docker
      state: started
  - name: Login to ECR
    shell:
      cmd: /usr/local/bin/aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws
  - name: Get latest AWX release tag
    uri:
      url: https://api.github.com/repos/ansible/awx/tags
      return_content : yes
    register: uri_response
  - name: (Info) AWX version
    debug:
      msg: "latest AWX version is {{ uri_response.json[0].name }}"
  - name: Clone AWX repo
    git:
      repo: https://github.com/ansible/awx.git
      dest: /src/awx
      version: "{{ uri_response.json[0].name }}"
  - name: add line to Inventory for Container registory
    blockinfile:
      path: /src/awx/installer/inventory
      block: |
        docker_registry=public.ecr.aws
        docker_registry_repository=v9x8p9g7
  - name: Build AWX image
    shell:
      cmd: ansible-playbook -i inventory build.yml
      chdir: /src/awx/installer/
```
EC2上でAWXをビルドするためのPlaybookです。
[過去のバージョンでビルドしたとき](https://qiita.com/ussvgr/items/f95d950bf39aa67d0189)はいろいろと手を入れてましたが、最近のバージョンでは何も手を加えずにそのままARM64でビルド可能です。
最新のリリースバージョンを取得するための方法に迷ったのですが、https://api.github.com/repos/ansible/awx/tags で取得できる最初の要素からタグ名を取得しています。（パッと見ちゃんと動いてそうなので、いったんコレで…）
また、ハマったポイントとして、パブリック公開版のECRのパスワードを取得する際は、ECRのリージョンがap-northeast-1だったとしてもawscliでap-northeast-1を指定するとエラーになります。
us-east-1を指定したら動いたようなのでそのままにしています。そのうち改善されるかな？

### AWS環境削除用Playbook

```delete_ec2spot_environment.yaml
---
- name: Delete AWS EC2 Spot Instance Environment
  hosts: localhost
  connection: local
  environment:
    AWS_REGION: ap-northeast-1
  vars:
    tag_ansible: awx_builder
  tasks:
  - name: get EC2 Instance ID 
    ec2_instance_info:
      filters:
        "tag:Ansible": "{{ tag_ansible }}"
    register: instance_info_result
  - name: get Security Group ID 
    ec2_group_info:
      filters:
        "tag:Ansible": "{{ tag_ansible }}"
    register: sg_info_result
  - name: get Subnet ID 
    ec2_vpc_subnet_info:
      filters:
        "tag:Ansible": "{{ tag_ansible }}"
    register: subnet_info_result
  - name: get InternetGateway ID 
    ec2_vpc_igw_info:
      filters:
        "tag:Ansible": "{{ tag_ansible }}"
    register: igw_info_result
  - name: get Route table ID
    ec2_vpc_route_table_info:
      filters:
        "tag:Ansible": "{{ tag_ansible }}"
    register: rt_info_result
  - name: Terminate EC2 Instance
    ec2:
      state: absent
      instance_ids: "{{ item.instance_id }}"
      wait: yes
    loop: "{{ instance_info_result.instances }}"
  - name: Delete Security group
    ec2_group:
      state: absent
      group_id: "{{ item.group_id }}"
    loop: "{{ sg_info_result.security_groups }}"
  - name: Delete Route table
    ec2_vpc_route_table:
      state: absent
      lookup: id
      vpc_id: "{{ item.vpc_id }}"
      route_table_id: "{{ item.id }}"
    loop: "{{ rt_info_result.route_tables }}"
  - name: Delete Internet Gateway
    ec2_vpc_igw:
      state: absent
      vpc_id: "{{ item.attachments[0].vpc_id }}"
    loop: "{{ igw_info_result.internet_gateways }}"
  - name: Delete Subnet
    ec2_vpc_subnet:
      state: absent
      vpc_id: "{{ item.vpc_id }}"
      cidr: "{{ item.cidr_block }}"
    loop: "{{ subnet_info_result.subnets }}"
  - name: Delete VPC
    ec2_vpc_net:
      name: Spot VPC
      cidr_block: 10.10.0.0/16
      state: absent
  - name: Delete SSH Keys
    ec2_key:
      name: my_keypair
      state: absent
```
作成時に付けたタグを基にリソースのIDを取得し、作成時と逆の順番で各リソースを削除しています。

# AWX上のテンプレート作成
各操作ごとにジョブテンプレートを作成していきます。
###AWS環境作成用ジョブテンプレート
- Inventory: localhostのみ登録したインベントリを作成して指定
- Project/Playbook: 上記のYAMLを含んだProjectを作成し指定
- Credentials： 上記でAWSのIAMを登録した認証情報を指定
![スクリーンショット 2020-12-14 0.40.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/430a0962-9729-0c53-cd66-8423ddb9d0d4.png)

###AWXコンテナイメージビルド用ジョブテンプレート
- Inventory: 作成したダイナミックインベントリを指定
- Project/Playbook: 上記のYAMLを含んだProjectを作成し指定
- Credentials: 作成したubuntuユーザーでログインする用の認証情報を指定
![スクリーンショット 2020-12-14 0.45.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/c0215646-9754-cba2-6119-1fe1c0895c43.png)

###AWS環境削除用ジョブテンプレート
作成用とほぼ同じで、Playbookのみ差し替えます。
![スクリーンショット 2020-12-14 0.48.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/c287272d-8b17-c4f1-dc03-1bfc082cb558.png)

###ワークフローテンプレート
上記の3つのジョブテンプレートを、成功時に順々に実行するよう繋いだワークフローを作成します。
![スクリーンショット 2020-12-14 0.49.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/95b80763-419f-95fc-10ad-d48591d75366.png)

#実行
完成したらワークフローを実行します。
30〜40分ほどして問題なく完了していれば、ECR上にコンテナイメージがアップロードされています。
![スクリーンショット 2020-12-14 0.54.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/0a84938e-67d9-e7c0-8f68-7065bd0efbd3.png)
また、ビルドに使ったAWS上の環境もキレイサッパリ消えているはずです。

#おわりに
ビルドしたARM64版のAWXコンテナイメージは[ココ](https://gallery.ecr.aws/v9x8p9g7/awx)に置いています。