Ansible AWXでOracle Cloud Infrastructure操作（カスタム認証情報タイプ利用）

#はじめに
この記事は[Adventar Ansible Advent Calendar 2020](https://adventar.org/calendars/5719)の17日目の記事です。

AWXで[Azure](https://qiita.com/ussvgr/items/cb91a5a2f5b0d2ab1d6b)・[AWS](https://qiita.com/ussvgr/items/230b1436f604ae16dddf)と触ってきたので、次はOracle Cloud Infrastructure(OCI)をやります。

OCI公式で[Ansible利用に関するドキュメント](https://docs.cloud.oracle.com/ja-jp/iaas/Content/API/SDKDocs/ansible.htm)が公開されています。
が、AWXの認証情報の一覧にはOCIが存在しません。
![スクリーンショット 2020-12-17 0.41.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/638060bf-b391-4dc3-c69f-b4d0b2e70068.png)
そこで「[カスタム認証情報タイプ](https://docs.ansible.com/ansible-tower/3.8.0/html_ja/userguide/credential_types.html)」を作成する事で、標準にはないOCIに対応させてみます。

#環境
AWX 16.0.0 (ARM64自家ビルド版)

#AWX venv環境の準備
[OCI公式ドキュメントのスタートガイド](https://docs.cloud.oracle.com/ja-jp/iaas/Content/API/SDKDocs/ansiblegetstarted.htm#Getting_Started_with_Oracle_Cloud_Infrastructure_and_Ansible)を読むと、[Python用のOracle Cloud Infrastructure SDKのインストール](https://oracle-cloud-infrastructure-python-sdk.readthedocs.io/en/latest/installation.html#downloading-and-installing-the-sdk)が必要との事でした。
pipでインストールすることができるようなので、以前紹介した[venv環境の作成](https://qiita.com/ussvgr/items/367438c70c9768fe39f3)を行います。

venv環境は以下のように

- Python 3.6
- Ansible 2.9.15
- OCI SDKのインストール

を指定します。


```venv_vars.yaml
---
custom_venvs:
  - name: oci
    python: python3.6
    python_ansible_version: 2.9.15
    python_modules:
      - oci
```
また、ARM64版特有の問題と思いますが、そのままですとvenv作成がエラーとなってしまいますので、以下のようにテンプレートファイルの修正を行います。
(initコンテナ内でlibffi-develのインストールとpipのアップグレードを実行)

```diff
--- awx-16.0.0/installer/roles/kubernetes/templates/deployment.yml.j2.bak	2020-12-17 00:53:36.000000000 +0900
+++ awx-16.0.0/installer/roles/kubernetes/templates/deployment.yml.j2	2020-12-16 01:59:25.000000000 +0900
@@ -144,12 +144,13 @@ spec:
             - >-
               yum install -y ansible curl python-setuptools epel-release \
                 openssl openssl-devel gcc python-devel &&
-              yum install -y python-virtualenv python36 python36-devel &&
+              yum install -y python-virtualenv python36 python36-devel libffi-devel &&
               mkdir -p {{ custom_venvs_path }} &&
 {% for custom_venv in custom_venvs %}
               virtualenv -p {{ custom_venv.python | default(custom_venvs_python) }} \
                 {{ custom_venvs_path }}/{{ custom_venv.name }} &&
               source {{ custom_venvs_path }}/{{ custom_venv.name }}/bin/activate &&
+              pip install --upgrade pip &&
               {{ custom_venvs_path }}/{{ custom_venv.name }}/bin/pip install {{ trusted_hosts }} -U psutil \
                 "ansible=={{ custom_venv.python_ansible_version }}" &&
 {% if custom_venv.python_modules is defined %}
```

準備ができたら以下のコマンドでAWXのインストールPlaybookを流します。

```bash
$ ansible-playbook -i inventory install.yml --extra-vars "@venv_vars.yaml"
```
しばらく待って、AWXコンテナがRunning状態になったら準備完了です。

```bash
$ kubectl get pod -n awx   
NAME                          READY   STATUS    RESTARTS   AGE
awx-postgresql-postgresql-0   1/1     Running   2          38d
awx-7bc79c69f5-rsn2x          3/3     Running   0          9m
```

この後、AWXのメニューから Settings -> Miscellaneous System でカスタムvenvのパスを記述する必要があるのですが、16.0.0ではうまく保存ができないようです。（保存ボタンが機能しない？）
ですので、APIで追加しました。

```bash
$ curl -X PATCH 'http://<ユーザー名>:<パスワード>@<AWXのIP>/api/v2/settings/system/' \                                                          
    -d '{"CUSTOM_VENV_PATHS": ["/opt/custom-venvs/"]}'  -H 'Content-Type:application/json' 
```

curl実行後、設定画面で以下のようにパスが保存されていればOKです。
![スクリーンショット 2020-12-17 1.50.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/ce7a597d-0199-0195-70ca-7e10b4f01f03.png)



#AWX カスタム認証情報タイプの準備
[Ansible Towerのドキュメント](https://docs.ansible.com/ansible-tower/3.8.0/html_ja/userguide/credential_types.html#create-a-new-credential-type)を参照しつつ進めていきます。
カスタム認証情報タイプとは、入力した値を

- 環境変数
- Ansibleの変数
- 設定ファイル

といった形でPlaybookから参照できるようにする、という仕組みのようです。

[ociモジュールのコード](https://github.com/oracle/oci-ansible-modules/blob/master/module_utils/oracle/oci_config_utils.py)を見た結果、必要な値としては、

- OCI_CONFIG_FILE: 認証情報を記述した設定ファイルのパス
- OCI_USER_KEY_FILE: ユーザー認証用の秘密鍵ファイルのパス

の2つの環境変数があればよさそうです。


AWXの左メニュー、 Administration -> Credential Types を開きAddボタンをクリックし登録します。
![スクリーンショット 2020-12-17 1.22.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/88e94ab5-ca7a-e2e1-b6f5-eb545e9c7873.png)

各項目に以下のように入力していきます。

- Name: Oracle Cloud Infrastracture
- Input configuration: 下記YAML

```input_config.yaml
---
fields:
  - id: config_file
    type: string
    label: OCI SDK Config file
    multiline: true
  - id: api_key
    type: string
    label: API Private key
    format: ssh_private_key
    secret: true
    multiline: true
required:
  - config_file
  - api_key
```
- Injector configuration: 下記YAML

```injector_config.yaml
---
env:
  OCI_CONFIG_FILE: '{{ tower.filename.config_file }}'
  OCI_USER_KEY_FILE: '{{ tower.filename.api_key }}'
file:
  template.config_file: '{{ config_file }}'
  template.api_key: '{{ api_key }}'
```

以上で設定がカスタム認証情報の設定が完了しました。

# OCI API実行用ユーザー作成
続いてOCI上でAWX用のユーザーを作成していきます。
コンソールにログインし、ガバナンスと管理 -> アイデンティティ -> ユーザー を選択します。
![スクリーンショット 2020-12-17 1.55.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/4f42be10-5f5d-87fe-f848-4ec23542c8d9.png)

ユーザーの名前と説明を記入し「作成」ボタンをクリックします。
![スクリーンショット 2020-12-17 2.00.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/171e362c-145a-0aa1-24fc-b172623349f3.png)
ユーザーが作成されたら、「ユーザー機能の編集」ボタンをクリックし、APIキー以外のチェックを外し保存します。
![スクリーンショット 2020-12-17 2.02.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/6f67d236-647a-4e6e-5bdd-ade4ba232964.png)

続いてユーザーのページの左下のリソース -> グループ から「ユーザーをグループの追加」をクリック、リソース作成に必要な権限を持ったグループに紐付けます。
ここでは、Administratorsを追加しました。
![スクリーンショット 2020-12-17 2.47.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/c5d0b6dd-266f-1613-cb1f-34bb240808d9.png)
次に左下のリソース -> APIキーを選択し、「APIキーの追加」ボタンをクリックします。
![スクリーンショット 2020-12-17 2.03.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/7a1bbba4-6a15-7ab2-12d9-eb413265ead9.png)

APIキー・ペアの生成 を選択し、「秘密キーのダウンロード」をクリックしキーをダウンロードします。
![スクリーンショット 2020-12-17 2.05.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/85695cab-e487-dff9-d53b-4eec93cc9867.png)
「追加」クリックすると下記の画面が表示されるので、「CONFIGURATION FILE PREVIEW」の中のテキストをコピーして保存、「閉じる」ボタンで完了させます。
![スクリーンショット 2020-12-17 2.09.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/4f98b3ae-7885-c3d0-c254-be03c93a53b1.png)

# 認証情報登録
作成したOCI用ユーザーの情報を、AWXの認証情報として登録していきます。
先ほどカスタム認証情報タイプを作成したため、Credential Typeに「Oracle Cloud Infrastructure」という選択肢が増えています。
保存したコンフィグ情報と秘密キーをファイル選択もしくはテキスト貼り付けで入力します。
![スクリーンショット 2020-12-17 2.14.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/7e873611-41f9-ea3c-90ee-01e1a41961e7.png)
保存したコンフィグ情報の中に以下の行があります。

```ini
key_file=<path to your private keyfile> # TODO
```
秘密キーのファイルパスに置き換えるよう指示がありますが、処理の過程で環境変数OCI_USER_KEY_FILEの値に置き換わっていくため、そのままで問題ありません。

入力が完了したら「Save」ボタンで保存します。

#プロジェクト作成
OCI公式のサンプルPlaybookから[Always Freeのインスタンス作成](https://github.com/oracle/oci-ansible-modules/tree/master/samples/compute/always_free_launch_compute_instance)をベースに、以下のプロジェクトを作成しました。

```
├── collections
│   └── requirements.yaml
└── sample.yaml
```

```collections/requirements.yaml
---
collections:
  - oracle.oci
```

```sample.yaml
---
- name: Create OCI Always free Environment
  hosts: localhost
  connection: local
  collections:
    - oracle.oci
  vars:
    # please chenge before run
    instance_compartment: <コンパートメントOCID>
    instance_ad: "EWPC:AP-TOKYO-1-AD-1" #Availavility Domainの値は環境によって変わると思われる。

    # Ubuntu 20.04
    instance_image: "ocid1.image.oc1.ap-tokyo-1.aaaaaaaamvx4wblavkrmeyrzrsm6gzg3tc4mx22o4c5gv5nq3azwe2kyps5q"

    # common networking definitions
    quad_zero_route: "0.0.0.0/0"
    TCP_protocol: "6"
    SSH_port: "22"

    vcn_name: "myalwaysfreetestvcn"
    vcn_cidr_block: "10.0.0.0/16"
    vcn_dns_label: "alwaysfreevcn"

    ig_name: "myalwaysfreeinternetgatewayformytestvcn"

    route_table_name: "myalwaysfreeroutetable"
    # route all internet access to our Internet Gateway
    route_table_rules:
        - cidr_block: "{{ quad_zero_route }}"
          network_entity_id: "{{ ig_id }}"

    subnet_cidr: "10.0.0.48/28"
    subnet_name: "myalwaysfreetestsubnet"
    subnet_dns_label: "myfreesubnet"

    securitylist_name: "myalwaysfreesecuritylist"

    # always free shape
    instance_shape: "VM.Standard.E2.1.Micro"
    instance_hostname: "myalwaysfreetestinstance"
  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 a VCN
      oci_network_vcn:
        compartment_id: "{{ instance_compartment }}"
        display_name: "{{ vcn_name }}"
        cidr_block: "{{ vcn_cidr_block }}"
        dns_label: "{{ vcn_dns_label }}"
      register: result
    - set_fact:
        vcn_id: "{{ result.vcn.id }}"
  
    - name: Create a new Internet Gateway
      oci_network_internet_gateway:
        compartment_id: "{{ instance_compartment }}"
        vcn_id: "{{ vcn_id }}"
        name: "{{ ig_name }}"
        is_enabled: 'yes'
        state: 'present'
      register: result
    - set_fact:
        ig_id: "{{ result.internet_gateway.id }}"
  
    - name: Create route table to connect internet gateway to the VCN
      oci_network_route_table:
        compartment_id: "{{ instance_compartment }}"
        vcn_id: "{{ vcn_id }}"
        name: "{{ route_table_name }}"
        route_rules: "{{ route_table_rules }}"
        state: 'present'
      register: result
    - set_fact:
        rt_id: "{{ result.route_table.id }}"

    - name: Create a security list for allowing access to public instance
      oci_network_security_list:
        name: "{{ securitylist_name }}"
        compartment_id: "{{ instance_compartment }}"
        vcn_id: '{{ vcn_id }}'
        ingress_security_rules:
          - source: "{{ quad_zero_route }}"
            protocol: "{{ TCP_protocol }}"
            tcp_options:
              destination_port_range:
                min: {{ SSH_port }}
                max: {{ SSH_port }}
        egress_security_rules: 
          - destination: "{{ quad_zero_route }}"
            protocol: "{{ TCP_protocol }}"
            tcp_options:
              destination_port_range:
                min: {{ SSH_port }}
                max: {{ SSH_port }}
      register: result
    - set_fact:
        instance_security_list_ocid: "{{ result.security_list.id }}"

    - name: Create a subnet to host the public instance. Link security_list and route_table.
      oci_network_subnet:
        availability_domain: "{{ instance_ad }}"
        cidr_block: "{{ subnet_cidr }}"
        compartment_id: "{{ instance_compartment }}"
        display_name: "{{ subnet_name }}"
        prohibit_public_ip_on_vnic: false
        route_table_id: "{{ rt_id }}"
        security_list_ids: [ "{{ instance_security_list_ocid }}" ]
        vcn_id: '{{ vcn_id }}'
        dns_label: "{{ subnet_dns_label }}"
      register: result
    - set_fact:
        instance_subnet_id: "{{ result.subnet.id }}"
  
    - name: Launch an instance
      oci_compute_instance:
        availability_domain: "{{ instance_ad }}"
        compartment_id: "{{ instance_compartment }}"
        name: "my_always_free_test_instance"
        source_details:
          source_type: image
          image_id: "{{ instance_image }}"
        shape: "{{ instance_shape }}"
        create_vnic_details:
            assign_public_ip: True
            hostname_label: "{{ instance_hostname }}"
            subnet_id: "{{ instance_subnet_id }}"
        metadata:
            ssh_authorized_keys: "{{ ssh_key.content }}"
      register: result

    - name: Print instance details
      debug:
        msg: "Launched a new instance {{ result }}"
    - set_fact:
        instance_id: "{{result.instance.id }}"

    - name: Get the VNIC attachment details of instance
      oci_compute_vnic_attachment_facts:
        compartment_id: "{{ instance_compartment }}"
        instance_id: "{{ instance_id }}"
      register: result

    - name: Get details of the VNIC
      oci_network_vnic_facts:
        id: "{{ result.vnic_attachments[0].vnic_id }}"
      register: result
    - set_fact:
        instance_public_ip: "{{result.vnic.public_ip}}"

    - name: Print the public ip of the newly launched instance
      debug:
        msg: "Public IP of launched instance {{ instance_public_ip }}"

    - name: Wait (upto 5 minutes) for port 22 to become open
      wait_for:
        port: 22
        host: '{{ instance_public_ip }}'
        state: started
        delay: 10
      vars:
        ansible_connection: local
```

コンパートメントOCIDは、OCIコンソールの左メニュー、アイデンティティ -> コンパートメント と辿っていくと確認できます。
![スクリーンショット 2020-12-17 3.10.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/1ad85a09-1d0f-f1bc-3edd-f8d99af39dee.png)
Availavility DomainはCloudShellなどからOCI CLIツールを利用して確認可能です。

```bash
$ oci iam availability-domain list --query 'data[*].name'
[
  "EWPC:AP-TOKYO-1-AD-1"
]
```

また、プロジェクトには冒頭で作成したOCI用のvenvを指定する必要がありますが、AWX 16.0.0ですと環境を選択するリストボックスが表示されませんでした。
なので、こちらもAPIで設定を追加します。
プロジェクトをいったん保管し、ブラウザのURLバーを確認しプロジェクトのIDを確認します。（下記例だと「28」です）
![スクリーンショット 2020-12-17 3.18.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/d3023cc3-9071-ac0b-46a1-7c7b4c01204a.png)
以下のcurlコマンドでAPIを実行します。

```bash
$ curl -X PATCH 'http://<ユーザー名>:<パスワード>@<AWXのIP>/api/v2/projects/28/' \                                                                
    -d '{"custom_virtualenv": "/opt/custom-venvs/oci/"}'  -H 'Content-Type:application/json'
```

プロジェクトを再度ブラウザで確認し、設定が以下のように反映されていればOKです。
![スクリーンショット 2020-12-17 3.23.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/f75bd288-c407-ed3c-e34f-9da9de080bcb.png)

#ジョブテンプレート作成
以下のようにジョブテンプレートを作成します。

- Inventory: localhostが含まれるインベントリを作成して指定
- Project: 上記で作成したプロジェクトを指定
- Playbook: sample.yaml
- Credentials: Categoryから「Oracle Cloud Infrastructure」を選び、登録したOCIの認証情報を指定

![スクリーンショット 2020-12-17 3.24.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/13583981-6ebe-59eb-45a9-091634a1464f.png)

#実行
ジョブテンプレートを実行してしばらく待つと、以下のように仮想サーバのパブリックIPがログに表示されます。
![スクリーンショット 2020-12-17 3.29.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274613/2bf9cf24-d5b2-e473-2e12-4f01b56c2942.png)

Playbook内で指定した鍵ペアでSSHログインして、仮想サーバが起動している事を確認してみましょう！

#おわりに
OCIはAlwaysFree枠が他クラウドよりもスペックが良く、個人的な検証用途には重宝しています。
AWXでボタン１つでVMデプロイ可能になったので、作っては壊しのサイクルがより捗りそうです。

#参考資料
[ANSIBLE TOWERでVIRTUALENVを使ってみる | 日常系エンジニアのTech Blog](https://sky-joker.tech/2019/07/13/ansible-tower%E3%81%A6%E3%82%99virtualenv%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B/)
APIでのvenv設定について参考にさせていただきました。

[OCI Ansibleモジュールの公式リポジトリ](https://github.com/oracle/oci-ansible-modules)

[Ansible Galaxyのoracle.ociコレクション](https://galaxy.ansible.com/oracle/oci)

[ociモジュールのドキュメント](https://oci-ansible-collection.readthedocs.io/en/latest/collections/oracle/oci/index.html)
めちゃくちゃ大量にあります。Oracle様の本気度合いを感じる。