既存ALBからCloudFormationテンプレート作成

# 設定の背景
### 困っていたこと・やりたいこと
AWS上でEC2/RDS/ALBを含む構成で利用していて、本番環境と同じ構成のステージング環境があります。
ステージング環境は夜間使う人がいないため、利用料金の節約のために日次でEventBridge + Lambdaを動かし、自動で夜間休止をかけてきました。EC2やRDSはこの方法で請求対象となる起動時間を短縮できます。

一方、ALBでは設定が若干複雑で、時間単位でそこそこ料金がかかる割に、一時停止機能が提供されていないため、ステージング環境にも毎月数千円分ほどかかけつつ放置していました。これを使わないときにOff/Onするような感覚で使用できる状況をつくれないか、というのが今回の作業の背景です。

結局Delete/Createするしかないので、CloudFormationでスタック作成することにしました。
（SecurityGroupやTargetGroup設定には料金がかからないので、ALB本体だけDelete/Createするようにします）

# CLIで現在の設定情報を取得
既に存在しているALBリソースの調べるには、まずAWS-CLIを使用して設定内容をJSONで取得してしまうことにしました。

`elasticloadbalancing:Describe*` あたりのポリシーがついているIAMユーザを使用します。

aws cliが使える手頃なAmazonLinux2のEC2インスタンスにて実行。

```sh
aws elbv2 describe-load-balancers
```
<details><summary>取得結果</summary><div>

```json
{
    "LoadBalancers": [
        {
            "IpAddressType": "ipv4", 
            "VpcId": "vpc-***************", 
            "LoadBalancerArn": "arn:aws:elasticloadbalancing:ap-northeast-1:***********", 
            "State": {
                "Code": "active"
            }, 
            "DNSName": "*******************.ap-northeast-1.elb.amazonaws.com", 
            "SecurityGroups": [
                "sg-*****************"
            ], 
            "LoadBalancerName": "********************", 
            "CreatedTime": "0000-00-00T00:00:00.000Z", 
            "Scheme": "internet-facing", 
            "Type": "application", 
            "CanonicalHostedZoneId": "****************", 
            "AvailabilityZones": [
                {
                    "SubnetId": "subnet-**************", 
                    "LoadBalancerAddresses": [], 
                    "ZoneName": "ap-northeast-1d"
                }, 
                {
                    "SubnetId": "subnet-**************", 
                    "LoadBalancerAddresses": [], 
                    "ZoneName": "ap-northeast-1a"
                }, 
                {
                    "SubnetId": "subnet-**************", 
                    "LoadBalancerAddresses": [], 
                    "ZoneName": "ap-northeast-1c"
                }
            ]
        }
    ]
}
```
</div></details>

表示されたARNを使って、続いてattributesも取得。
```sh
aws elbv2 describe-load-balancer-attributes --load-balancer-arn arn:aws:elasticloadbalancing:ap-northeast-1:*********************************************
```
<details><summary>取得結果</summary><div>

```json
{
    "Attributes": [
        {
            "Value": "true", 
            "Key": "access_logs.s3.enabled"
        }, 
        {
            "Value": "****************", 
            "Key": "access_logs.s3.bucket"
        }, 
        {
            "Value": "", 
            "Key": "access_logs.s3.prefix"
        }, 
        {
            "Value": "60", 
            "Key": "idle_timeout.timeout_seconds"
        }, 
        {
            "Value": "false", 
            "Key": "deletion_protection.enabled"
        }, 
        {
            "Value": "false", 
            "Key": "routing.http2.enabled"
        }, 
        …（略）…
    ]
}
```
</div></details>

続いてタグ情報も。
```sh
aws elbv2 describe-tags --resource-arns arn:aws:elasticloadbalancing:ap-northeast-1:*****************************```
```
<details><summary>取得結果</summary><div>

```json
{
    "TagDescriptions": [
        {
            "ResourceArn": "arn:aws:elasticloadbalancing:ap-northeast-1:********************", 
            "Tags": [
                {
                    "Value": "staging", 
                    "Key": "env"
                }
            ]
        }
    ]
}
```
</div></details>

リスナーの情報も取得。
```sh
aws elbv2 describe-listeners --load-balancer-arn ********
```
<details><summary>取得結果</summary><div>

```json
{
    "Listeners": [
        {
            "Protocol": "HTTPS", 
            "DefaultActions": [
                {
                    "Type": "fixed-response", 
                    "Order": 1, 
                    "FixedResponseConfig": {
                        "ContentType": "text/plain", 
                        "MessageBody": "Bad Request", 
                        "StatusCode": "400"
                    }
                }
            ], 
            "SslPolicy": "ELBSecurityPolicy-FS-1-2-2019-08", 
            "Certificates": [
                {
                    "CertificateArn": "arn:aws:acm:ap-northeast-1:***************"
                }
            ], 
            "LoadBalancerArn": "arn:aws:elasticloadbalancing:ap-northeast-1:************", 
            "Port": 443, 
            "ListenerArn": "arn:aws:elasticloadbalancing:ap-northeast-1:**************"
        }, 
        {
            "Protocol": "HTTP", 
            "DefaultActions": [
                {
                    "RedirectConfig": {
                        "Protocol": "HTTPS", 
                        "Host": "#{host}", 
                        "Query": "#{query}", 
                        "Path": "/#{path}", 
                        "Port": "443", 
                        "StatusCode": "HTTP_302"
                    }, 
                    "Type": "redirect", 
                    "Order": 1
                }
            ], 
            "LoadBalancerArn": "arn:aws:elasticloadbalancing:ap-northeast-1:***************", 
            "Port": 80, 
            "ListenerArn": "arn:aws:elasticloadbalancing:ap-northeast-1:***************"
        }
    ]
}
```
</div></details>

リスナールールの情報も取得。デフォルトアクション以外のルール情報があれば取っておきます。

```sh
 aws elbv2 describe-rules --listener-arn *****
```
<details><summary>取得結果</summary><div>

```json
{
    "Rules": [
        {
            "Priority": "1", 
            "Conditions": [
                {
                    "Field": "host-header", 
                    "HostHeaderConfig": {
                        "Values": [
                            "*********"
                        ]
                    }, 
                    "Values": [
                        "**********"
                    ]
                }
            ], 
            "RuleArn": "arn:aws:elasticloadbalancing:ap-northeast-1:*************", 
            "IsDefault": false, 
            "Actions": [
                {
                    "Type": "fixed-response", 
                    "Order": 1, 
                    "FixedResponseConfig": {
                        "ContentType": "text/html", 
                        "MessageBody": "<!doctype html>\n<html lang=\"ja\">******</html>", 
                        "StatusCode": "503"
                    }
                }
            ]
        }, 
        {
            "Priority": "2", 
            "Conditions": [
                {
                    "Field": "host-header", 
                    "HostHeaderConfig": {
                        "Values": [
                            "**********"
                        ]
                    }, 
                    "Values": [
                        "**********"
                    ]
                }
            ], 
            "RuleArn": "arn:aws:elasticloadbalancing:ap-northeast-1:*************", 
            "IsDefault": false, 
            "Actions": [
                {
                    "ForwardConfig": {
                        "TargetGroupStickinessConfig": {
                            "Enabled": false
                        }, 
                        "TargetGroups": [
                            {
                                "TargetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:************", 
                                "Weight": 1
                            }
                        ]
                    }, 
                    "TargetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:*******", 
                    "Type": "forward", 
                    "Order": 1
                }
            ]
        }, 
        （…略…）
    ]
}
```
</div></details>


# CloudFormationの設定

YAMLの方がコメントを書けるメリットがありますが、今回はCLIの結果をそのまま使ってJSONでファイルを作成してみました。
AWSコンソールのCloudFormationデザイナーでポチポチリソースを追加しながら、テンプレートのPropertiesの部分にCLIの取得結果を加工しながら入れて行きます。

### ALB（[AWS::ElasticLoadBalancingV2::LoadBalancer](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-loadbalancer.html)）
CLIで取得したALBの情報のうち、CloudFormationの設定では不要なものや項目名が異なるものがあるので加工する必要があります。
- `VpcId` → 不要
- `LoadBalancerArn` → 不要
- `State` → 不要
- `DNSName` → 不要
- `LoadBalancerName` → ※`Name`に変更
- `CreatedTime` → 不要
- `CanonicalHostedZoneId` → 不要
- `AvailabilityZones` → 不要だけど、Subnetsの項目で使う
- `LoadBalancerAttributes` → 項目を追加して、Attributesの中身の配列を指定
- `SubnetMappings` → （SunetMappingsとSubnetsはどちらか片方で指定）
- `Subnets` → 追加（AvailabilityZonesのSubnetIdを配列で指定）
- `Tags` → 項目を追加して、Tagsの中身を追加

### Listener（[AWS::ElasticLoadBalancingV2::Listener](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-listener.html)）
リスナーも同様に。HTTP/HTTPSで2つリスナーを付けていたのでそれぞれ。
- `LoadBalancerArn` → `{"Ref": "CloudFormationで指定したLBの論理名"}`
- `ListenerArn` → 不要

### ListenerRule（[AWS::ElasticLoadBalancingV2::ListenerRule](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-listenerrule.html)）
デフォルトアクションはリスナーで設定するので、それ以外のアクションがある場合はリスナールールで指定。
- `IsDefault` → 不要
- `RuleArn` → 不要
- `ListenerArn` → `{"Ref": "上で指定したListenerの論理名"}`
- `Conditions` → HostHeaderConfigを消して、Field+Valuesだけにする（どちらか片方しか指定できない）

### スタック作成
コンソールの画面に従ってスタックを作成してみてうまくいけばテンプレートは完成です。


## CloudFormationのスタック作成時に失敗して困ったこと
「Invalid request provided: aws::elasticloadbalancingv2::listenerrule validation exception」が出てしまい、CloudFormation Linterなども使用してみたものの、原因が分からず困りました。
CloudTrailの画面からCreateRuleの失敗メッセージの詳細を確認すると、詳細なエラーメッセージも確認でき、hostHeaderConfigとField+Valuesの競合が原因だと判明しました。

# ついでにRoute53の指定もCloudFormationにて指定
ALBを毎度Create/Deleteする形にすると、リソースが変わるのでRoute53の向き先を変更してあげる必要があります。そこでテンプレートにRoute53の設定も追加しておきます。（スタックの削除時に消えてしまうのでそれでよければ。上書きはしてくれなさそうなので既存のものは一度削除しないとダメかも）
```json
        "DnsRecord": {
            "Type": "AWS::Route53::RecordSetGroup",
            "Properties": {
                "HostedZoneId": "************",
                "RecordSets": [
                    {
                        "Name": "**********",
                        "Type": "A",
                        "AliasTarget": {
                            "HostedZoneId": {
                                "Fn::GetAtt": ["【CloudFormationテンプレートでのLBの論理名】",
                                    "CanonicalHostedZoneID"
                                ]
                            },
                            "DNSName": {
                                "Fn::GetAtt": [
                                    "【CloudFormationテンプレートでのLBの論理名】",
                                    "DNSName"
                                ]
                            }
                        }
                    },
                    （…略…）
                ]
            }
        }
```

# 便利な使い方
StepFunctionsから、EC2/RDSを起動するLambda(or EventBridge)と、今回のCloudFormationのスタック作成を一連で実行できるようにしておき、手動で必要なときにステージング環境を上げるようなステートマシンを作成しておくと何かと便利かもしれません。


# 【参考】
- [AWSドキュメント＞AWS CloudFormation＞User Guide＞Elastic Load Balancing V2](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/AWS_ElasticLoadBalancingV2.html)
- [AWS CLI ELBv2](https://docs.aws.amazon.com/cli/latest/reference/elbv2/index.html#cli-aws-elbv2)