ansibleでAWSリソースを作りきる

# ansibleにおけるAWSリソース構築

ansibleでは多くのAWS用のモジュールが提供されており、主要なAWSサービスであれば、大抵の構築作業を自動化することができます。
しかし、実際に構築してみるとansibleモジュールがAWSの新しい設定に対応できていなかったり、適用したい設定がモジュールにない場合があります。

このようなモジュールが対応できていない部分については、以下２つの手段がとられるかと思います。

- 手動でリソース構築を行う
- AWSコマンドを部分的に適用する

リソースの手動構築は、手順の作成の手間、操作ミスの増加も考えると、できるだけ行いたくありません。

そのため、最後までプログラムでAWSリソースを作りきるには、２つ目のAWSコマンドの実行しか選択肢はありません。

しかしながら、AWSコマンドを使う場合、ansibleにおける冪等性の確保が新たな課題となります。
ansibleを何回実行しても結果を同じ状態に保つには、単純にcommandモジュールでAWSコマンドを実行するだけだと冪等性が確保できないことがあります。
例えば、すでに構築済みのリソースに対して、あらためて構築を要求するようなコマンドを発行した場合、作成済みの結果が返ってきてエラーとなる場合があります。
この場合１回目のコマンドの実行は成功しますが、２回目以降のコマンドは失敗となってしまいます。

２回目以降も結果を同じ状態に保つには、すでに設定がAWSに適用済みかどうかAWSの設定情報の確認が必要となります。

# ansibleで冪等性を確保しつつ、AWSコマンドベースでリソース構築

以下の方針で、冪等性を保ちつつAWSリソースの構築を行ってみます。

-   対象リソースがすでに構築されているのであれば何もせず、成功として終了。
-   対象リソースが構築されていないのであれば対象リソースを作成し、成功として終了。
-   例外はすべて失敗として終了させる。

ここではpythonのboto3ライブラリを利用し、API GatewayのAPI Keyを構築する例を用います。
シェルスクリプトでも同じことが可能です。

## プレイブック側の記述

プレイブック側では、API keyの名前とAWSプロファイルを引数にpythonスクリプトを呼び出します。
結果が毎回Changedになるのを防ぐため、changed_whenにはFalseを設定します。

    - hosts: localhost
      tasks:
        - name: Create API Key only if it does not exist.
          script: add_api_key.py {{api_key_name}} {{aws_profile}}
          args:
            executable: python3.6
          changed_when: False

## スクリプト側の記述

こちらはプレイブックで呼び出しているadd_api_key.pyの中身です。
スクリプト側では、引数で取得したAWSプロファイルからオブジェクトを生成します。
あとは方針の通り、指定の名前でAPI Keyがなければ、API keyを作成します。

    import sys
    import boto3

    try:
        expected_api_key_name = sys.argv[1]
        profile = sys.argv[2]

        session = boto3.Session(profile_name=profile)
        api_client = session.client('apigateway')

        # 想定するAPIキー名が存在するか確認する
        api_key_name = ""
        api_keys = api_client.get_api_keys(includeValues=True)
        for api_key in api_keys['items']:
            if api_key['name'] == expected_api_key_name:
                api_key_name = api_key['name']
                print("{0} is already created".format(api_key_name), "")
                sys.exit(0)

        # 想定するAPIキー名が取得できない場合はAPIキーを作成
        if api_key_name == "":
            api_key_desc = "created from ansible"
            api_key_res = api_client.create_api_key(
                name=expected_api_key_name,
                description=api_key_desc,
                enabled=True)
            api_key_name = api_key_res['name']

        #例外がなかった場合は、成功として終了
        print("Created {0}".format(api_key_name), "")
        sys.exit(0)

    except Exception as e:
        #例外発生した場合は、エラーとして終了
        print(e, "")
        sys.exit(1)

## プレイブック実行結果

### 初回
初回はリソースが存在しなかったため、リソースが構築されます。

    TASK [Create API Key only if it does not exist.] ok: [localhost] => {
        "changed": false,
        "rc": 0,
        "stderr": "",
        "stderr_lines": [],
        "stdout": "Created test_api \n",
        "stdout_lines": [
            "Created test_api "
        ]
    }

### ２回目以降
2回目以降はリソースがすでに存在したため、リソースは構築せず、すでに存在する旨のメッセージを表示します。

    TASK [Create API Key only if it does not exist.] ok: [localhost] => {
        "changed": false,
        "rc": 0,
        "stderr": "",
        "stderr_lines": [],
        "stdout": "test_api is already created \n",
        "stdout_lines": [
            "test_api is already created "
        ]
    }

### 失敗した場合

失敗すると、rcが１となり、FAILEDとなります。

    TASK [Create API Key only if it does not exist.] fatal: [localhost]: FAILED! => {
        "changed": false,
        "msg": "non-zero return code",
        "rc": 1,
        "stderr": "",
        "stderr_lines": [],
        "stdout": "list index out of range \n",
        "stdout_lines": [
            "list index out of range "
        ]
    }

以上のようにすることで、冪等性を保ちつつ、ansibleモジュールがカバーできていない部分を補うことができます。
足りない部分をこのように補っていくことで、AWSコマンドが許す限り、ansibleでもAWSリソースを作りきることができます。

まだCloud Formationは使ってないだとか、Ansibleでメンテナンスが必要というときには、検討余地のある方法かと思います。