AWS: S3 を origin とする CloudFront を設定する Ansible Role

色々とハマったのでメモ。

## 参考文献

- [cloudfront_distribution - create, update and delete aws cloudfront distributions. — Ansible Documentation](https://docs.ansible.com/ansible/latest/modules/cloudfront_distribution_module.html)
- [s3_bucket - Manage S3 buckets in AWS, Ceph, Walrus and FakeS3 — Ansible Documentation](https://docs.ansible.com/ansible/latest/modules/s3_bucket_module.html)

## 当初やろうとしたこと

1. cloudfront_distribution モジュールで以下のパラメーターを指定, CloudFront Distribution を作成する。
   - default_origin_domain_name
   - default_root_object
   - origins の domain_name と s3_origin_access_identity_enabled
1. 返り値を変数に格納する。今回は distribution_result とする。
1. distribution_result の origins の items **リスト** の s3_origin_config の origin_access_identity から `origin-access-identity/cloudfront` 文字列を削除した末尾の ID を取り出す。
1. s3_bucket モジュールで先ほどの ID を S3 のポリシーに設定する。

## ハマったこと

### 1. S3 のポリシーって動的に設定できるの?

結論は Yes.
それまで, templates の j2 ファイルは vars や defaults で設定した変数しか扱えないと思い込んでいたけども, 実は register した変数も扱える。

### 2. え, リストってどうやって取り出すの?

こいつのせいで 2 日掛かったorz
C に慣れてるせいで, 当初は `distribution_result.origins.items[0]` と書いてもうまく取り出せない。
一応 `distribution_result.origins` までなら取り出せることは確認。他に試した内容は以下の通り。

|変数                                   |値                                                                                     |
|:--------------------------------------|:--------------------------------------------------------------------------------------|
|distribution_result.origins.items      |`<built-in method items of dict object at 0x7fb8b8f65280`>                             |
|distribution_result.origins.items.0    |VARIABLE IS NOT DEFINED!: builtin_function_or_method object has no element 0           |
|distribution_result.origins.items[0]   |VARIABLE IS NOT DEFINED!: builtin_function_or_method object has no element 0           |
|distribution_result.origins.items['0'] |VARIABLE IS NOT DEFINED!: 'builtin_function_or_method object' has no attribute '0'     |


で, 調べてみると, **リストなんだから with_items で取り出せばいいんじゃね?** という記事が。(元記事は失念orz)
そこで, 一旦以下のデバッグ出力で試してみることに。

```yaml
- debug:
    var: "{{ item }}"
  with_items: distribution_result.origins.items
```

結果。

```json
{
    "distribution_result.origins.items": "<built-in method items of dict object at 0x7f3cfbb75c58>",
    "item": "distribution_result.origins.items"
}
```

…え? built-in method items of dict object? ですか?
仕方ないので `built-in method items of dict object` で調べてみると, 同じような内容で悩んでいる方はおられるものです。

- [ansible - can't read custom facts with list (array) of items - Stack Overflow](https://stackoverflow.com/questions/40281706/cant-read-custom-facts-with-list-array-of-items)

しかも丁寧に,

> This is happening because the name **items** is clashing with the name of some built-in method.

とのこと。
じゃーどーするのよ? と読み進めると, 優しい方もおられるもので, `['items']` という書き方で試せよ, と。
なるほど。やってみましょう。

```yaml
- debug:
    var: "{{ item }}"
  with_items: distribution_result.origins['items']
```

結果。

```json
{
    "distribution_result.origins['items']": [
        {
            "custom_headers": {
                "quantity": 0
            },
            "domain_name": "test-s3-bucket991.s3.amazonaws.com",
            "id": "2018-07-20T15:05:00.375176",
            "origin_path": "",
            "s3_origin_config": {
                "origin_access_identity": "origin-access-identity/cloudfront/E3QU476GYR1RJB"
            }
        }
    ],
    "item": "distribution_result.origins['items']"
}
```

お! だいぶ近い! けど要素が 2 つ出来てる! orz いいや, with_items 側も括ってしまえ。

```yaml
- debug:
    var: "{{ item }}"
  with_items: "{{ distribution_result.origins['items'] }}"
```

結果。

```json
{
    "<type 'dict'>": "VARIABLE IS NOT DEFINED!: ",
    "item": {
        "custom_headers": {
            "quantity": 0
        },
        "domain_name": "test-s3-bucket991.s3.amazonaws.com",
        "id": "2018-07-20T15:05:00.375176",
        "origin_path": "",
        "s3_origin_config": {
            "origin_access_identity": "origin-access-identity/cloudfront/E3QU476GYR1RJB"
        }
    }
}
```

お, item が辞書からリストになりましたな。変数を連結してみましょう。

```yaml
- debug:
    var: "{{ item.s3_origin_config.origin_access_identity }}"
  with_items: "{{ distribution_result.origins['items'] }}"
```

結果。

```json
{
    "item": {
        "custom_headers": {
            "quantity": 0
        },
        "domain_name": "test-s3-bucket991.s3.amazonaws.com",
        "id": "2018-07-20T15:05:00.375176",
        "origin_path": "",
        "s3_origin_config": {
            "origin_access_identity": "origin-access-identity/cloudfront/E3QU476GYR1RJB"
        }
    },
    "origin-access-identity/cloudfront/E3QU476GYR1RJB": "VARIABLE IS NOT DEFINED!: 'origin' is undefined"
}
```

あれ? 一番下がそれっぽくなってる。ってことは, これにフィルターを掛ければ良さそう?

```yaml
- debug:
    var: "{{ item.s3_origin_config.origin_access_identity | regex_replace('origin-access-identity/cloudfront/') }}"
  with_items: "{{ distribution_result.origins['items'] }}"
```

結果。

```json
{
    "E3QU476GYR1RJB": "VARIABLE IS NOT DEFINED!: 'E3QU476GYR1RJB' is undefined",
    "item": {
        "custom_headers": {
            "quantity": 0
        },
        "domain_name": "test-s3-bucket991.s3.amazonaws.com",
        "id": "2018-07-20T15:05:00.375176",
        "origin_path": "",
        "s3_origin_config": {
            "origin_access_identity": "origin-access-identity/cloudfront/E3QU476GYR1RJB"
        }
    }
}
```

おぉ! 一番上で取り出せてますな。
ということで, 現在のソースコードの通りになりましたとさ。

### 3. cloudfront_distribution して直ぐに s3_bucket を実行するとエラーになるんだけどorz

**Invalid principal in policy** と出力される。
調査すると, cloudfront_distribution モジュール実行直後は確かに S3 ポリシー向けの ID を取得できるんだけど, 実際にはまだ有効化されてないっぽい。
苦肉の策として, pause モジュールで 10 秒待機させることにしました。

## サンプルソース

- [sb3f-ryo-sato/ansible-role-s3-cloudfront: An ansible role for AWS CloudFront to connect S3.](https://github.com/sb3f-ryo-sato/ansible-role-s3-cloudfront)

と言っても, 今回作ったファイルは以下の 3 つ。

- [tasks/main.yml](https://github.com/sb3f-ryo-sato/ansible-role-s3-cloudfront/blob/master/tasks/main.yml)
- [defaults/main.yml](https://github.com/sb3f-ryo-sato/ansible-role-s3-cloudfront/blob/master/defaults/main.yml)
- [templates/s3_bucket_policy.json.j2](https://github.com/sb3f-ryo-sato/ansible-role-s3-cloudfront/blob/master/templates/s3_bucket_policy.json.j2)

## ansible-playbook -vvv 実行時の出力 JSON 抜粋

具体的なコマンド: `$ ansible-playbook -vvv s3-cloudfront.yml -i '127.0.0.1,'`

※1 そういえば今月の 13 日 (金) は JSON の日でした。

※2 一応, アカウント ID は伏せています。

### cloudfront_distribution

```json
{
    "active_trusted_signers": {
        "enabled": false,
        "quantity": 0
    },
    "aliases": {
        "quantity": 0
    },
    "arn": "arn:aws:cloudfront::************:distribution/E3J3Z681FITN9S",
    "cache_behaviors": {
        "quantity": 0
    },
    "caller_reference": "2018-07-20T15:05:00.375176",
    "changed": true,
    "comment": "Distribution created by Ansible with datetime stamp 2018-07-20T15:05:00.375176",
    "custom_error_responses": {
        "quantity": 0
    },
    "default_cache_behavior": {
        "allowed_methods": {
            "cached_methods": {
                "items": [
                    "HEAD",
                    "GET"
                ],
                "quantity": 2
            },
            "items": [
                "HEAD",
                "GET"
            ],
            "quantity": 2
        },
        "compress": false,
        "default_ttl": 86400,
        "forwarded_values": {
            "cookies": {
                "forward": "none"
            },
            "headers": {
                "quantity": 0
            },
            "query_string": true,
            "query_string_cache_keys": {
                "quantity": 0
            }
        },
        "lambda_function_associations": {
            "quantity": 0
        },
        "max_ttl": 31536000,
        "min_ttl": 0,
        "smooth_streaming": false,
        "target_origin_id": "2018-07-20T15:05:00.375176",
        "trusted_signers": {
            "enabled": false,
            "quantity": 0
        },
        "viewer_protocol_policy": "allow-all"
    },
    "default_root_object": "index.html",
    "domain_name": "d2q6fsy0du38oe.cloudfront.net",
    "enabled": true,
    "http_version": "http2",
    "id": "E3J3Z681FITN9S",
    "in_progress_invalidation_batches": 0,
    "invocation": {
        "module_args": {
            "alias": null,
            "aliases": [],
            "aws_access_key": null,
            "aws_secret_key": null,
            "cache_behaviors": null,
            "caller_reference": null,
            "comment": null,
            "custom_error_responses": null,
            "default_cache_behavior": null,
            "default_origin_domain_name": "test-s3-bucket991.s3-website-ap-northeast-1.amazonaws.com",
            "default_origin_path": null,
            "default_root_object": "index.html",
            "distribution_id": null,
            "e_tag": null,
            "ec2_url": null,
            "enabled": null,
            "http_version": null,
            "ipv6_enabled": null,
            "logging": null,
            "origins": [
                {
                    "custom_headers": {
                        "quantity": 0
                    },
                    "domain_name": "test-s3-bucket991.s3.amazonaws.com",
                    "id": "2018-07-20T15:05:00.375176",
                    "origin_path": "",
                    "s3_origin_config": {
                        "origin_access_identity": "origin-access-identity/cloudfront/E3QU476GYR1RJB"
                    }
                }
            ],
            "price_class": null,
            "profile": null,
            "purge_aliases": false,
            "purge_cache_behaviors": false,
            "purge_custom_error_responses": false,
            "purge_origins": false,
            "purge_tags": false,
            "region": null,
            "restrictions": null,
            "security_token": null,
            "state": "present",
            "tags": {},
            "validate_certs": true,
            "viewer_certificate": null,
            "wait": false,
            "wait_timeout": 1800,
            "web_acl_id": null
        }
    },
    "is_ipv6_enabled": false,
    "last_modified_time": "2018-07-20T06:05:02.012000+00:00",
    "logging": {
        "bucket": "",
        "enabled": false,
        "include_cookies": false,
        "prefix": ""
    },
    "origins": {
        "items": [
            {
                "custom_headers": {
                    "quantity": 0
                },
                "domain_name": "test-s3-bucket991.s3.amazonaws.com",
                "id": "2018-07-20T15:05:00.375176",
                "origin_path": "",
                "s3_origin_config": {
                    "origin_access_identity": "origin-access-identity/cloudfront/E3QU476GYR1RJB"
                }
            }
        ],
        "quantity": 1
    },
    "price_class": "PriceClass_All",
    "restrictions": {
        "geo_restriction": {
            "quantity": 0,
            "restriction_type": "none"
        }
    },
    "status": "InProgress",
    "tags": {},
    "viewer_certificate": {
        "certificate_source": "cloudfront",
        "cloud_front_default_certificate": true,
        "minimum_protocol_version": "TLSv1"
    },
    "web_acl_id": ""
}
```

### pause

```json
{
    "changed": false,
    "delta": 10,
    "echo": true,
    "rc": 0,
    "start": "2018-07-20 15:05:03.088325",
    "stderr": "",
    "stdout": "Paused for 10.0 seconds",
    "stop": "2018-07-20 15:05:13.088542",
    "user_input": ""
}
```

### s3_bucket

```json
{
    "changed": true,
    "invocation": {
        "module_args": {
            "aws_access_key": null,
            "aws_secret_key": null,
            "ceph": false,
            "ec2_url": null,
            "force": false,
            "name": "test-s3-bucket991",
            "policy": "{\"Version\": \"2008-10-17\", \"Id\": \"PolicyForCloudFrontPrivateContent\", \"Statement\": [{\"Action\": \"s3:GetObject\", \"Principal\": {\"AWS\": \"arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E3QU476GYR1RJB\"}, \"Resource\": \"arn:aws:s3:::test-s3-bucket991/*\", \"Effect\": \"Allow\", \"Sid\": \"1\"}]}",
            "profile": null,
            "region": null,
            "requester_pays": false,
            "s3_url": null,
            "security_token": null,
            "state": "present",
            "tags": null,
            "validate_certs": true,
            "versioning": null
        }
    },
    "item": {
        "custom_headers": {
            "quantity": 0
        },
        "domain_name": "test-s3-bucket991.s3.amazonaws.com",
        "id": "2018-07-20T15:05:00.375176",
        "origin_path": "",
        "s3_origin_config": {
            "origin_access_identity": "origin-access-identity/cloudfront/E3QU476GYR1RJB"
        }
    },
    "name": "test-s3-bucket991",
    "policy": {
        "Id": "PolicyForCloudFrontPrivateContent",
        "Statement": [
            {
                "Action": "s3:GetObject",
                "Effect": "Allow",
                "Principal": {
                    "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E3QU476GYR1RJB"
                },
                "Resource": "arn:aws:s3:::test-s3-bucket991/*",
                "Sid": "1"
            }
        ],
        "Version": "2008-10-17"
    },
    "requester_pays": false,
    "tags": {},
    "versioning": {
        "MfaDelete": "Disabled",
        "Versioning": "Disabled"
    }
}
```

どっとはらい。