AWS CLIで Web サイトを構築、管理、運用する(17日目)

17日目！

昨日立ち上げた EC2 インスタンス。
皆様もお気づきの通り、1台構成で、何かあったときに可用性が失われます。
また、セキュリティグループのインバウンドルールをCloudFrontからのアクセスのみとしていますが、パブリックサブネットに配置されていて、もしものときを考えると少し心もとないです。

なので、今日から2日に分けてネットワーク構成の変更と可用性向上のために冗長化します。


# 17日目の要約
ネットワークの構成を変更するよ！


# AWS CLI の準備
このあたりをみて、好きなバージョンとお使いのOSにあった環境設定をしてくださいね。
なんなら、 AWS CloudShell で実行するのも楽でよいと思います。
この記事シリーズは、AWS CloudShell で実行し、実行例を載せています。

バージョン１
https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/install-cliv1.html

バージョン２
https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/install-cliv2.html

# 概要
パブリックサブネットに加えて、プライベートサブネットを作成する。そして、ALBを構築する。

# さあ、やってみよう！
## 現在の構成を確認する
EC2 インスタンスやVPC、サブネットなどのネットワーク周りの構成を確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344161/f75f8142-7b7b-e184-d30a-cf28c6a5404a.png)

これを、ひとまず、以下の様にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344161/2599e4f6-b537-add3-4ed2-2c588c1097f7.png)
※Instance くんにはちょっときえてもらっています。

## サブネットを追加する
とりあえず、サブネットを追加していきます。
14日目の[サブネットの作成](https://qiita.com/hirosys/items/0311a01cf50f0a7f3ef8#%E3%82%B5%E3%83%96%E3%83%8D%E3%83%83%E3%83%88-%E3%81%AE%E4%BD%9C%E6%88%90)の手順と同じですが、復習を兼ねてみていきます。

実行前に、 VPC の ID を確認しておいてくださいね。

### パブリックサブネットを追加する
#### パブリックサブネットを作成する

まずはパブリックサブネットから作っていきます。
cidr-block の値と availability-zone の値に注意してくださいね。

```sh
aws ec2 create-subnet --cidr-block 10.0.1.0/24 --availability-zone ap-northeast-1c \
--vpc-id <VPC ID>
```

サブネットの作成に成功すると、以下のような json が返ります。
SubnetId の値を確認しておきます。

```json
{
    "Subnet": {
        "AvailabilityZone": "ap-northeast-1c",
        "AvailabilityZoneId": "apne1-az1",
        "AvailableIpAddressCount": 251,
        "CidrBlock": "10.0.1.0/24",
        "DefaultForAz": false,
        "MapPublicIpOnLaunch": false,
        "State": "available",
        "SubnetId": "subnet-*****************",
        "VpcId": "vpc-*****************",
        "OwnerId": "************",
        "AssignIpv6AddressOnCreation": false,
        "Ipv6CidrBlockAssociationSet": [],
        "SubnetArn": "arn:aws:ec2:ap-northeast-1:************:subnet/subnet-*****************"
    }
}
```

#### パブリックサブネットの設定を変更する
続いて、EC2 起動時にパブリック IP アドレスが自動で割り当たるように、ec2 modify-subnet-attribute コマンドで設定を変更します。

```sh
aws ec2 modify-subnet-attribute --subnet-id <サブネット ID> --map-public-ip-on-launch
```

こちらは、正常終了しても特に json は返ってきません。

#### パブリックサブネットにルートテーブルを関連づける
作ったサブネットはパブリックサブネットなので、 Internet Gateway(IGW)に向かうルートがあるルートテーブルを関連づけるために ec2 associate-route-table コマンドを実行します。

```sh
aws ec2 associate-route-table --route-table-id <ルートテーブルの ID> \
--subnet-id <サブネットの ID>
```

関連付けが正常に行えたら、以下のような json が返ります。

```json
{
    "AssociationId": "rtbassoc-*****************",
    "AssociationState": {
        "State": "associated"
    }
}
```

パブリックサブネットの作成は以上です。

### プライベートサブネットを追加する
続いて、プライベートサブネットを作成していきます。

#### １つめ

```sh
aws ec2 create-subnet --cidr-block 10.0.10.0/24 --availability-zone ap-northeast-1a \
--vpc-id <VPC ID>
```

サブネットの作成に成功すると、以下のような json が返ります。
SubnetId の値を確認しておきます。

```json
{
    "Subnet": {
        "AvailabilityZone": "ap-northeast-1a",
        "AvailabilityZoneId": "apne1-az4",
        "AvailableIpAddressCount": 251,
        "CidrBlock": "10.0.10.0/24",
        "DefaultForAz": false,
        "MapPublicIpOnLaunch": false,
        "State": "available",
        "SubnetId": "subnet-*****************",
        "VpcId": "vpc-*****************",
        "OwnerId": "************",
        "AssignIpv6AddressOnCreation": false,
        "Ipv6CidrBlockAssociationSet": [],
        "SubnetArn": "arn:aws:ec2:ap-northeast-1:************:subnet/subnet-*****************"
    }
}
```

#### メインのルートテーブルを関連づける
作ったサブネットはプライベートサブネットなので、 メインのルートテーブルを関連づけるために ec2 associate-route-table コマンドを実行します。

```sh
aws ec2 associate-route-table --route-table-id <メインのルートテーブルの ID> \
--subnet-id <1つ目のプライベートサブネットの ID>
```

関連付けが正常に行えたら、以下のような json が返ります。

```json
{
    "AssociationId": "rtbassoc-*****************",
    "AssociationState": {
        "State": "associated"
    }
}
```


#### 2つめ
```sh
aws ec2 create-subnet --cidr-block 10.0.11.0/24 --availability-zone ap-northeast-1c \
--vpc-id <VPC ID>
```

サブネットの作成に成功すると、以下のような json が返ります。
SubnetId の値を確認しておきます。

```json
{
    "Subnet": {
        "AvailabilityZone": "ap-northeast-1c",
        "AvailabilityZoneId": "apne1-az1",
        "AvailableIpAddressCount": 251,
        "CidrBlock": "10.0.11.0/24",
        "DefaultForAz": false,
        "MapPublicIpOnLaunch": false,
        "State": "available",
        "SubnetId": "subnet-*****************",
        "VpcId": "vpc-*****************",
        "OwnerId": "************",
        "AssignIpv6AddressOnCreation": false,
        "Ipv6CidrBlockAssociationSet": [],
        "SubnetArn": "arn:aws:ec2:ap-northeast-1:************:subnet/subnet-*****************"
    }
}
```

#### メインのルートテーブルを関連づける
作ったサブネットはプライベートサブネットなので、 メインのルートテーブルを関連づけるために ec2 associate-route-table コマンドを実行します。

```sh
aws ec2 associate-route-table --route-table-id <メインのルートテーブルの ID> \
--subnet-id <2つ目のプライベートサブネットの ID>
```

関連付けが正常に行えたら、以下のような json が返ります。

```json
{
    "AssociationId": "rtbassoc-*****************",
    "AssociationState": {
        "State": "associated"
    }
}
```


## ALB を起動する
サブネットの設定が完了したので、 可用性を高め、冗長構成にするために使う ALB の設定をしていきます。

### セキュリティグループを作成する
最初に、セキュリティグループを作成します。
作り方は、[14日目](https://qiita.com/hirosys/items/0311a01cf50f0a7f3ef8#:~:text=%E3%81%AE%E4%BD%9C%E6%88%90%E3%80%81%E8%A8%AD%E5%AE%9A-,%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%97%E3%81%AE%E4%BD%9C%E6%88%90,-%E3%81%BE%E3%81%A8%E3%82%81)の手順と同じように、 CloudFront からのアクセスのみを受けるようにします。

#### ALB 用のセキュリティグループを作成する

ALB 用のセキュリティグループを作成します。
```sh
aws ec2 create-security-group --description "ALBs SG" \
--group-name "sg_alb" --vpc-id <VPC ID>
```

出力される セキュリティグループのIDを確認しておきます。

```json
{
    "GroupId": "sg-*****************"
}
```

次に、インバウンドルールを作成します。
IPRANGE 変数に、CloudFront の IP アドレス範囲一覧を取得、格納します。
そして、その値を用いて、インバウンドルールを作成します。

```sh
IPRANGE=`curl https://ip-ranges.amazonaws.com/ip-ranges.json | jq -c '.prefixes[] | select(.service=="CLOUDFRONT") | .ip_prefix'`

for ip in ${IPRANGE}; do iplist_tmp=$iplist_tmp,\{CidrIp=$ip\}; done
iplist=`echo ${iplist_tmp} | sed "s/^,//g"`

aws ec2 authorize-security-group-ingress --group-name sg_alb \
--ip-permissions IpProtocol=tcp,FromPort=80,ToPort=80,IpRanges=[${iplist}]
```

インバウンドルールが正常に作成できると、以下のような json が返ってきます。
指定したIPアドレス範囲の個数だけルールが列挙されます。

```json
{
    "Return": true,
    "SecurityGroupRules": [
        {
            "SecurityGroupRuleId": "sgr-*****************",
            "GroupId": "sg-*****************",
            "GroupOwnerId": "************",
            "IsEgress": false,
            "IpProtocol": "tcp",
            "FromPort": 80,
            "ToPort": 80,
            "CidrIpv4": "***.***.***.***/**"
        },"(以下略)"
    ]
}
```

#### EC2 用のセキュリティグループを再作成する
14日に作ったセキュリティグループのままだと、ALBからの通信が受け付けられないのと、CloudFrontからのインバウンドルールを消していくのは面倒なので、再作成してしまおうということです。

```sh
aws ec2 create-security-group --description "Web Servers SG" \
--group-name "sg_web2" --vpc-id <VPC ID>
```

出力される セキュリティグループのIDを確認しておきます。

```json
{
    "GroupId": "sg-*****************"
}
```

次に、インバウンドルールを作成します。
ALBからの通信が受け付けられればよいので、ALBのセキュリティグループIDを用いてルールを作成します。

```sh
aws ec2 authorize-security-group-ingress --group-name sg_web2 \
    --protocol tcp \
    --port 80 \
    --source-group sg_alb
```

以下のような json が返ってきます。

```json
{
    "Return": true,
    "SecurityGroupRules": [
        {
            "SecurityGroupRuleId": "sgr-*****************",
            "GroupId": "sg-*****************",
            "GroupOwnerId": "************",
            "IsEgress": false,
            "IpProtocol": "tcp",
            "FromPort": 80,
            "ToPort": 80,
            "ReferencedGroupInfo": {
                "GroupId": "sg-*****************"
            }
        }
    ]
}
```


#### ALB 用のセキュリティグループのアウトバウンドルールを設定する

次に、ALB側のアウトバウンドルールに、EC2に対する通信のみを認めるようにします。

一旦、フルオープンのルールを削除します。```revoke-security-group-egress``` コマンドを使います。

```sh
aws ec2 revoke-security-group-egress --group-id <確認しておいた ALB のセキュリティグループID> --protocol -1 --port -1 --cidr 0.0.0.0/0
```

実行に成功すると、以下のような json が返ってきます。

```json
{
    "Return": true
}
```

そして、EC2に向かうアウトバウンドルールを追加するために、```authorize-security-group-egress``` コマンドを実行します。

```sh
aws ec2 authorize-security-group-egress --group-id <確認しておいた ALB のセキュリティグループID> --protocol tcp --port 80 --source-group <確認しておいた EC2用の新しいセキュリティグループID>
```

以下、余談ですが・・・
インバウンドルールとアウトバウンドルールで必須オプションが異なるのはなんでなんでしょうね・・・？
アウトバウンド側でも --group-name で登録できてもいいのになと。
また、--source-group に セキュリティグループ名で指定すると、エラーになります。なぜ。。。

正常に実行できると、以下のような json が返ってきます。

```json
{
    "Return": true,
    "SecurityGroupRules": [
        {
            "SecurityGroupRuleId": "sgr-*****************",
            "GroupId": "sg-*****************",
            "GroupOwnerId": "************",
            "IsEgress": true,
            "IpProtocol": "tcp",
            "FromPort": 80,
            "ToPort": 80,
            "ReferencedGroupInfo": {
                "GroupId": "sg-*****************"
            }
        }
    ]
}
```

### ALB を作成する
セキュリティグループが作成できたら、 ALB を作っていきます。
elbv2 create-load-balancer コマンドを実行します。

```sh
aws elbv2 create-load-balancer --name webserver-alb --subnets <1つ目のパブリックサブネットID> <2つ目のパブリックサブネットID> --security-groups <ALB用のセキュリティグループID>
```

ALB が作成できると、以下のような json が返ります。

```json
{
    "LoadBalancers": [
        {
            "LoadBalancerArn": "arn:aws:elasticloadbalancing:ap-northeast-1:************:loadbalancer/app/webserver-alb/*****************",
            "DNSName": "webserver-alb-*********.ap-northeast-1.elb.amazonaws.com",
            "CanonicalHostedZoneId": "*****************",
            "CreatedTime": "2021-12-16T15:33:06.830000+00:00",
            "LoadBalancerName": "webserver-alb",
            "Scheme": "internet-facing",
            "VpcId": "vpc-*****************",
            "State": {
                "Code": "provisioning"
            },
            "Type": "application",
            "AvailabilityZones": [
                {
                    "ZoneName": "ap-northeast-1c",
                    "SubnetId": "subnet-*****************",
                    "LoadBalancerAddresses": []
                },
                {
                    "ZoneName": "ap-northeast-1a",
                    "SubnetId": "subnet-*****************",
                    "LoadBalancerAddresses": []
                }
            ],
            "SecurityGroups": [
                "sg-*****************"
            ],
            "IpAddressType": "ipv4"
        }
    ]
}
```

LoadBalancerArn と DNSName　の値を確認しておいてください。


### ターゲットグループを作成する
次にターゲットグループを作っていきます。

```sh
aws elbv2 create-target-group --name Web-Server-Target --protocol HTTP --port 80 \
--vpc-id <VPC ID> --health-check-path "/ec2/index.html"
```

```json
{
    "TargetGroups": [
        {
            "TargetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:************:targetgroup/Web-Server-Target/*****************",
            "TargetGroupName": "Web-Server-Target",
            "Protocol": "HTTP",
            "Port": 80,
            "VpcId": "vpc-*****************",
            "HealthCheckProtocol": "HTTP",
            "HealthCheckPort": "traffic-port",
            "HealthCheckEnabled": true,
            "HealthCheckIntervalSeconds": 30,
            "HealthCheckTimeoutSeconds": 5,
            "HealthyThresholdCount": 5,
            "UnhealthyThresholdCount": 2,
            "HealthCheckPath": "/ec2/index.html",
            "Matcher": {
                "HttpCode": "200"
            },
            "TargetType": "instance",
            "ProtocolVersion": "HTTP1",
            "IpAddressType": "ipv4"
        }
    ]
}
```

### リスナー
そして、ALBとターゲットグループをつなぐ、リスナーの設定を行います。
elbv2 create-listener　を実行します。

```sh
aws elbv2 create-listener --load-balancer-arn <ALBのARN> \
--protocol HTTP --port 80  \
--default-actions Type=forward,TargetGroupArn=<ターゲットグループのARN>
```

HTTPで８０番ポートへの着信を指定のターゲットグループに転送するよという設定です。

正常に実行できると、以下のような json が返ります。

```json
{
    "Listeners": [
        {
            "ListenerArn": "arn:aws:elasticloadbalancing:ap-northeast-1:************:listener/app/webserver-alb/*****************/*****************",
            "LoadBalancerArn": "arn:aws:elasticloadbalancing:ap-northeast-1:************:loadbalancer/app/webserver-alb/*****************",
            "Port": 80,
            "Protocol": "HTTP",
            "DefaultActions": [
                {
                    "Type": "forward",
                    "TargetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:************:targetgroup/Web-Server-Target/*****************",
                    "ForwardConfig": {
                        "TargetGroups": [
                            {
                                "TargetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:************:targetgroup/Web-Server-Target/*****************",
                                "Weight": 1
                            }
                        ],
                        "TargetGroupStickinessConfig": {
                            "Enabled": false
                        }
                    }
                }
            ]
        }
    ]
}
```

### Route53 にレコードを追加する
ALB はデフォルト設定だと、インターネット向け通信ができるように DNSName を持っています。この名前をCloudFront のディストリビューションに設定すればよいのですが、管理しやすいように、Route53で CNAME を割り当てます。

```sh
ALB_CNAME=web-alb.<ドメイン名>
ALB_NAME=<ALBのDNSName>
HOSTED_ZONE_ID=<ホスティッドゾーンID>

aws route53 change-resource-record-sets --hosted-zone-id ${HOSTED_ZONE_ID} \
--change-batch "{\"Changes\": [ { \"Action\": \"UPSERT\", \"ResourceRecordSet\": { \"Name\": \"${ALB_CNAME}.\", \"Type\": \"CNAME\", \"TTL\": 300, \"ResourceRecords\": [{\"Value\": \"${ALB_NAME}\"}]}}]}"
```

レコード追加に成功すると、以下の json が返ります。

```json
{
    "ChangeInfo": {
        "Id": "/change/C0*******************",
        "Status": "PENDING",
        "SubmittedAt": "2021-12-16T16:31:24.215000+00:00"
    }
}
```

# まとめ
今回は、可用性向上と冗長化のための下準備としてサブネットの追加や ALB の作成を行いました。
明日は、EC2 インスタンスの可用性を高め、伸縮性を得るために、AutoScaling を設定します。

* 今回使ったコマンド
 * ec2 create-subnet
 * ec2 modify-subnet-attribute
 * ec2 associate-route-table
 * ec2 create-security-group
 * ec2 authorize-security-group-ingress
 * ec2 revoke-security-group-egress
 * ec2 authorize-security-group-egress
 * elbv2 create-load-balancer
 * elbv2 create-target-group
 * elbv2 create-listener
 * route53 change-resource-record-sets