🌏

ReactとAWSでポートフォリオサイト作ってみた

2025/03/06に公開

こんにちは、laughtaoneです。
今回は、ReactAWSを用いてポートフォリオサイトを作成したので、その手順を紹介します。
未来のどなたかに役立てば幸いです。

それでは、手順を追って説明していきます。

(前提)

どんなものを作るのか説明していきます。
人によっては、今回作るものの一部が要らなかったりしますので、ご注意ください。
まず、今回は、次のような構造でAWS環境を作っていきます。(図は間違ってる可能性あります)

簡単に言うと「2つのEC2を立ち上げて、ALBで負荷分散して、独自ドメインでサイトを公開する」というものです。
また、リージョンは東京を使用します。
人によっては「EC2は2つもいらないよ」や「独自ドメインはいらないよ」という方もいらっしゃると思いますが、今回はこの構造で作成しましたので、適宜ステップを飛ばして、自分なりの構造で作成してみてください!

AWSのCloudShellを開く

今回、冒頭からALB作成まではCloudShellを用いて、作業していきます。
CloudShellとは、AWSが提供するブラウザベースのシェル環境で、シェルと同じ操作性でAWSを操作できます。

通常の操作 と CloudShell の違い

例として、キーペアを作る作業で、GUIで操作できるAWSマネジメントコンソールを用いる場合とCloudShellを用いる場合で、次のような関係となっています。
AWSマネジメントコンソールでの作業

aws ec2 create-key-pair \
    --key-name "sugukesu-keypair" \
    --key-type "rsa" \
    --key-format "pem"

よく見てみると、「キーペア名」「キータイプ」「キーフォーマット」として、設定している値がどちらも同じであることがわかると思います。
このように、AWSマネジメントコンソール上での操作を、シェルのようにコマンドで操作できるのがCloudShellです。

CloudShellがよくわからない場合は、CloudShellのコードを解読すれば、AWSマネジメントコンソールでも同様に作業できますので、ご自身の好みに合わせてください!
(解読も難しい場合、CloudShellのコマンドをChatGPTにコピペして、AWSマネジメントコンソールでの操作方法を聞けば答えてくれます)

CloudShellは、まずAWSマネジメントコンソール(つまりAWSにログイン後の最初の画面)を開き、左に下にある「CloudShell」を押すだけで開きます。

$ マークが表示されれば、準備完了です!

変数の設定

AWSの各要素の名前を一括指定するために、変数で管理します。
次のコードは、私の場合の命名なので、ご自身の任意の名前に変更可能です。この命名によって、何か変わるとかはないので、自由に付け直してみてください。
付け直すには、""の中の文字列を変えるだけでおけです。

VPC_NAME="pf-react-vpc"     # VPC名
SUBNET_1_NAME="pf-react_subnet-tokyo1a"     # サブネット名(1つ目)
SUBNET_2_NAME="pf-react_subnet-tokyo1b"     # サブネット名(2つ目)
IGW_NAME="pf-react_igw"     # インターネットゲートウェイ名
KEY_NAME="pf-react_aws-ec2-keypair"     # キーペア名
SECURITY_GROUP_NAME="pf-react_security-group"     # セキュリティグループ名
AMI_ID="ami-072298436ce5cb0c4"     # Amazon Linux 2023 AMIのAMI ID
EC2_1_NAME="pf-react_pub-ec2-tokyo1a"     # EC2名(1つ目)
EC2_2_NAME="pf-react_pub-ec2-tokyo1b"     # EC2名(2つ目)
TARGET_GROUP_NAME="pf-react-target-group"     # ターゲットグループ名
ALB_NAME="pf-react-alb"     # ALB名

各変数が決まったら、CloudShellにコピペして、Enter を1,2回押します。
何も反応がなければ成功です!

VPC作成

次のコマンドで、VPCを作成し、返されるJSONからVpc.VpcIdの値だけを抽出し、変数VPC_IDに代入します。

VPC_ID=$(aws ec2 create-vpc \
    --cidr-block "10.0.0.0/24" \
    --tag-specifications "ResourceType=vpc,Tags=[{Key=Name,Value=$VPC_NAME}]" \
    --query "Vpc.VpcId" \
    --output text)

次のコマンドで、VPC IDが返されることから、正常にVPCが作成されたことを確認する。

echo "VPC_ID: $VPC_ID"
実行結果
$ echo "VPC_ID: $VPC_ID"
VPC_ID: vpc-084d90d1a4afbca39

次のコマンドで、VPCがavailableになったことを確認する。(availableになっていない場合、しばらく待ってから再度実行する)

aws ec2 describe-vpcs --vpc-ids $VPC_ID --query "Vpcs[0].State" --output text
実行結果
$ aws ec2 describe-vpcs --vpc-ids $VPC_ID --query "Vpcs[0].State" --output text
available

サブネット作成

次のコマンドで、1つ目のサブネットを作成し、返されるJSONからSubnet.SubnetIdの値だけを抽出し、変数SUBNET_1_IDに代入する。

SUBNET_1_ID=$(aws ec2 create-subnet \
    --vpc-id "$VPC_ID" \
    --availability-zone-id "apne1-az4" \
    --cidr-block "10.0.0.0/25" \
    --tag-specifications "ResourceType=subnet,Tags=[{Key=Name,Value=$SUBNET_1_NAME}]" \
    --query "Subnet.SubnetId" \
    --output text)

同様に次のコマンドで、2つ目のサブネットを作成し、返されるJSONからSubnet.SubnetIdの値だけを抽出し、変数SUBNET_2_IDに代入する。

SUBNET_2_ID=$(aws ec2 create-subnet \
    --vpc-id "$VPC_ID" \
    --availability-zone-id "apne1-az2" \
    --cidr-block "10.0.0.128/25" \
    --tag-specifications "ResourceType=subnet,Tags=[{Key=Name,Value=$SUBNET_2_NAME}]" \
    --query "Subnet.SubnetId" \
    --output text)

次のコマンドで、サブネットIDが2つ返されることから、正常にサブネットが作成されたことを確認する。

echo "SUBNET_1_ID: $SUBNET_1_ID"
echo "SUBNET_2_ID: $SUBNET_2_ID"
実行結果
$ echo "SUBNET_1_ID: $SUBNET_1_ID"
SUBNET_1_ID: subnet-サブネットID1つ目
$ echo "SUBNET_2_ID: $SUBNET_2_ID"
SUBNET_2_ID: subnet-サブネットID2つ目

インターネットゲートウェイ(IGW)作成

次のコマンドで、インターネットゲートウェイを作成し、返されるJSONからInternetGateway.InternetGatewayIdの値だけを抽出し、変数IGW_IDに代入する。

IGW_ID=$(aws ec2 create-internet-gateway \
    --tag-specifications "ResourceType=internet-gateway,Tags=[{Key=Name,Value=$IGW_NAME}]" \
    --query "InternetGateway.InternetGatewayId" \
    --output text)

次のコマンドで、インターネットゲートウェイIDが返されることから、正常にインターネットゲートウェイが作成されたことを確認する。

echo "IGW_ID: $IGW_ID"
実行結果
$ echo "IGW_ID: $IGW_ID"
IGW_ID: igw-0937a904806d1b425

インターネットゲートウェイをVPCに接続

次のコマンドで、インターネットゲートウェイをVPCに接続(アタッチ)する。

aws ec2 attach-internet-gateway \
    --vpc-id "$VPC_ID" \
    --internet-gateway-id "$IGW_ID"

次のコマンドで、VPCに関連するルートテーブルのIDを取得し、変数ROUTE_TABLE_IDに代入する。

ROUTE_TABLE_ID=$(aws ec2 describe-route-tables \
    --filters "Name=vpc-id,Values=$VPC_ID" \
    --query "RouteTables[0].RouteTableId" \
    --output text)

次のコマンドで、ルートテーブルIDが変数ROUTE_TABLE_IDに代入されたか確認する。

echo "ROUTE_TABLE_ID: $ROUTE_TABLE_ID"
実行結果
$ echo "ROUTE_TABLE_ID: $ROUTE_TABLE_ID"
ROUTE_TABLE_ID: rtb-0b3ff27d625ab6ad5

次のコマンドで、そのルートテーブルの内容を更新する。

aws ec2 create-route \
    --route-table-id "$ROUTE_TABLE_ID" \
    --destination-cidr-block "0.0.0.0/0" \
    --gateway-id "$IGW_ID"
実行結果
$ aws ec2 create-route \
>     --route-table-id "$ROUTE_TABLE_ID" \
>     --destination-cidr-block "0.0.0.0/0" \
>     --gateway-id "$IGW_ID"
{
    "Return": true
}

キーペア作成

次のコマンドで、キーペアを作成する。

aws ec2 create-key-pair \
    --key-name "$KEY_NAME" \
    --key-type "rsa" \
    --key-format "pem" \
    --query 'KeyMaterial' \
    --output text \
    > "${KEY_NAME}.pem"

次のコマンドで、権限を確認する。

ls -l "${KEY_NAME}.pem"

このコマンドを実行すると、次の結果が返される。

$ ls -l "${KEY_NAME}.pem"
-rw-r--r--. 1 cloudshell-user cloudshell-user 1675 Mar  2 04:35 pf-react_aws-ec2-keypair.pem

この結果は、以下の権限状況であることを示しています:

  • rw- (所有者: 読み書き可能)
  • r-- (グループ: 読み取りのみ)
  • r-- (その他のユーザー: 読み取りのみ)

次のコマンドで、キーペアの権限を、ファイルの所有者だけが読み取ることができ、他のユーザーはアクセスできなくなるよう変更する。

chmod 400 "${KEY_NAME}.pem"

実行しても、何も返されないのが正常です。
次のコマンドで、もう一度権限を確認します。

ls -l "${KEY_NAME}.pem"

すると、次の結果が返されます。

$  ls -l "${KEY_NAME}.pem"
-r--------. 1 cloudshell-user cloudshell-user 1675 Mar  2 04:35 pf-react_aws-ec2-keypair.pem

この結果は、以下の権限状況であることを示しています:

  • 所有者: r-- (読み取りのみ)
  • グループ: -- (アクセス不可)
  • その他のユーザー: -- (アクセス不可)

この権限状況から、無事、キーペアの権限が変更できたことがわかります。
このように、権限設定をしないと、SSH 接続時に「Permissions are too open」というエラーが出ることがあります。

次に、このキーペアをPC上にも保存しておきます。
次のコマンドを実行します。

ls

実行結果として、次のようにキーペアがあるのが確認できると思うので、このキーペア名を .pemを含めて、全て選択してコピーします。

実行結果
~ $ ls
pf-react_aws-ec2-keypair.pem

CloudShellの右上の「アクション」から「ファイルのダウンロード」を押します。

ファイルパスとして、さっきコピーしたキーペア名をペーストして、右下の「ダウンロード」を押します。

正常にダウンロードされたことを確認します。

なお、このキーペアとIPアドレスに加え、偶然許可されたセキュリティグループに設定されていたら誰でも簡単にEC2にアクセスできてしまうので、取り扱いや管理には細心の注意を払ってください!(PC上の適切な場所に移動しておくことをおすすめします)

セキュリティグループの作成

次のコマンドで、セキュリティグループを作成し、返されるJSONからGroupIdの値だけを抽出変数SECURITY_GROUP_IDに代入します。

SECURITY_GROUP_ID=$(aws ec2 create-security-group \
    --group-name "$SECURITY_GROUP_NAME" \
    --description "security group" \
    --vpc-id "$VPC_ID" \
    --query 'GroupId' \
    --output text)

次のコマンドで、セキュリティグループIDが返されることから、正常にセキュリティグループが作成されたことを確認する。

echo "SECURITY_GROUP_ID: $SECURITY_GROUP_ID"
実行結果
$ echo "SECURITY_GROUP_ID: $SECURITY_GROUP_ID"
SECURITY_GROUP_ID: sg-0494790bd9d9bcbf8

次のコマンドで、作成したセキュリティグループにインバウンドルールを追加します。

aws ec2 authorize-security-group-ingress \
    --group-id "$SECURITY_GROUP_ID" \
    --ip-permissions '[{
        "IpProtocol": "tcp",
        "FromPort": 22,
        "ToPort": 22,
        "IpRanges": [
        {"CidrIp": "0.0.0.0/0"}
        ]
    },
    {
        "IpProtocol": "tcp",
        "FromPort": 80,
        "ToPort": 80,
        "IpRanges": [
        {"CidrIp": "0.0.0.0/0"}
        ]
    }]'

実行結果として、"Return": trueセキュリティグループルール表示されればおけです。

{
    "Return": true,
    "SecurityGroupRules": [
        {
            "SecurityGroupRuleId": "sgr-00ed63ae3d1e8a85a",
            "GroupId": "sg-0494790bd9d9bcbf8",
            "GroupOwnerId": "390403882691",
            "IsEgress": false,
            "IpProtocol": "tcp",
            "FromPort": 22,
            "ToPort": 22,
            "CidrIpv4": "0.0.0.0/0",
            "SecurityGroupRuleArn": "arn:aws:ec2:ap-northeast-1:390403882691:security-group-rule/sgr-00ed63ae3d1e8a85a"
        },
        {
            "SecurityGroupRuleId": "sgr-0cb14bdcf911e46b1",
            "GroupId": "sg-0494790bd9d9bcbf8",
            "GroupOwnerId": "390403882691",
            "IsEgress": false,
            "IpProtocol": "tcp",
            "FromPort": 80,
            "ToPort": 80,
            "CidrIpv4": "0.0.0.0/0",
            "SecurityGroupRuleArn": "arn:aws:ec2:ap-northeast-1:390403882691:security-group-rule/sgr-0cb14bdcf911e46b1"
        }
    ]
}
(END)

(END) から抜け出すには、q を押すだけでおけです。

EC2作成

EC2を作成する前に、次のコマンドで、今回使いたい Amazon Linux 2023 AMI が存在することを確認します。

aws ec2 describe-images --image-ids $AMI_ID \
    --query 'Images[0].[ImageId, Name, State, CreationDate, Architecture]' \
    --output table

実行結果として、available が確認できればおけです。

$ aws ec2 describe-images --image-ids $AMI_ID \
>     --query 'Images[0].[ImageId, Name, State, CreationDate, Architecture]' \
>     --output table
----------------------------------------------------
|                  DescribeImages                  |
+--------------------------------------------------+
|  ami-072298436ce5cb0c4                           |
|  al2023-ami-2023.6.20250218.2-kernel-6.1-x86_64  |
|  available                                       |
|  2025-02-20T21:31:58.000Z                        |
|  x86_64                                          |
+--------------------------------------------------+

次のコマンドで、2つのEC2を作成し、返されるJSONからInstances[0].InstanceIdの値だけを抽出し、変数EC2_1_IDとEC2_2_IDに代入します。

# EC2作成(1つ目)
EC2_1_ID=$(aws ec2 run-instances \
    --image-id $AMI_ID \
    --instance-type "t2.micro" \
    --key-name "$KEY_NAME" \
    --network-interfaces "SubnetId=$SUBNET_1_ID,AssociatePublicIpAddress=true,DeviceIndex=0,Groups=$SECURITY_GROUP_ID" \
    --count "1" \
    --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$EC2_1_NAME}]" \
    --query 'Instances[0].InstanceId' \
    --output text)

# EC2作成(2つ目)
EC2_2_ID=$(aws ec2 run-instances \
    --image-id $AMI_ID \
    --instance-type "t2.micro" \
    --key-name "$KEY_NAME" \
    --network-interfaces "SubnetId=$SUBNET_2_ID,AssociatePublicIpAddress=true,DeviceIndex=0,Groups=$SECURITY_GROUP_ID" \
    --count "1" \
    --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$EC2_2_NAME}]" \
    --query 'Instances[0].InstanceId' \
    --output text)

実行結果として、何も表示されないのが正常です。
次のコマンドで、EC2 IDが代入されたか確認します。

echo "EC2_1_ID: $EC2_1_ID"
echo "EC2_2_ID: $EC2_2_ID"
実行結果
$ echo "EC2_1_ID: $EC2_1_ID"
EC2_1_ID: i-01f75e680f4a4c6dd
$ echo "EC2_2_ID: $EC2_2_ID"
EC2_2_ID: i-0f8006f9b6de33d22

次のコマンドで、各EC2インスタンスのパブリックIPv4アドレスを取得し、変数EC2_1_IPとEC2_2_IPに代入します。

# EC2インスタンスのパブリックIPv4アドレスを取得(1つ目)
EC2_1_IP=$(aws ec2 describe-instances \
    --instance-ids "$EC2_1_ID" \
    --query 'Reservations[0].Instances[0].PublicIpAddress' \
    --output text)

# EC2インスタンスのパブリックIPv4アドレスを取得(2つ目)
EC2_2_IP=$(aws ec2 describe-instances \
    --instance-ids "$EC2_2_ID" \
    --query 'Reservations[0].Instances[0].PublicIpAddress' \
    --output text)

実行結果として、何も表示されないのが正常です。
次のコマンドで、各EC2インスタンスのパブリックIPv4アドレスが変数に代入されたか確認します。

echo "EC2_1_IP: $EC2_1_IP"
echo "EC2_2_IP: $EC2_2_IP"
実行結果
$ echo "EC2_1_IP: $EC2_1_IP"
EC2_1_IP: 54.65.195.77
$ echo "EC2_2_IP: $EC2_2_IP"
EC2_2_IP: 35.77.69.27

EC2の設定

1つ目のEC2インスタンス

次のコマンドで、1つ目のEC2インスタンスにSSH接続します。

ssh -i $KEY_NAME.pem ec2-user@$EC2_1_IP

接続しようとすると、このサーバーを信頼して接続しますかどうか聞いてくるので、yes と入力してEnter を押します

$ ssh -i $KEY_NAME.pem ec2-user@$EC2_1_IP
The authenticity of host '54.65.195.77 (54.65.195.77)' can't be established.
ED25519 key fingerprint is SHA256:biv+ZNa6xF87YE08bnphXeg8wHUPVHVaAE1zCJ/bZps.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? 

すると、SSH接続に成功し、鳥さんが表示されます。

$ ssh -i $KEY_NAME.pem ec2-user@$EC2_1_IP
The authenticity of host '54.65.195.77 (54.65.195.77)' can't be established.
ED25519 key fingerprint is SHA256:biv+ZNa6xF87YE08bnphXeg8wHUPVHVaAE1zCJ/bZps.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '54.65.195.77' (ED25519) to the list of known hosts.
   ,     #_
   ~\_  ####_        Amazon Linux 2023
  ~~  \_#####\
  ~~     \###|
  ~~       \#/ ___   https://aws.amazon.com/linux/amazon-linux-2023
   ~~       V~' '->
    ~~~         /
      ~~._.   _/
         _/ _/
       _/m/'
[ec2-user@ip-10-0-0-100 ~]$ 

次のコマンドで、EC2インスタンスがインターネットに接続されているか確認するために、Googleにpingを3回とばします

ping www.google.com -c 3

実行結果から、きちんと飛んでいることを確認します。

$ ping www.google.com -c 3
PING www.google.com (172.217.26.228) 56(84) bytes of data.
64 bytes from nrt12s51-in-f4.1e100.net (172.217.26.228): icmp_seq=1 ttl=117 time=2.28 ms
64 bytes from nrt12s51-in-f4.1e100.net (172.217.26.228): icmp_seq=2 ttl=117 time=2.50 ms
64 bytes from nrt12s51-in-f4.1e100.net (172.217.26.228): icmp_seq=3 ttl=117 time=2.94 ms

--- www.google.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 2.282/2.572/2.937/0.272 ms

次のコマンドで、EC2インスタンスのパッケージをアップデートします。

sudo yum update
実行結果

僕自身、何回か同じようにEC2インスタンスを立ち上げてアプデを行ったことがありますが、全てアップデがあったことがないです。でもこれで成功です。

$ sudo yum update
Amazon Linux 2023 Kernel Livepatch repository                                                                  118 kB/s |  14 kB     00:00    
Dependencies resolved.
Nothing to do.
Complete!

次のコマンドで、EC2インスタンスにApacheをインストールします。

sudo yum install httpd

次のコマンドで、EC2インスタンスでApacheを起動し、自動起動するようにも設定する。(途中で y/N で聞いてくるが、y を押して続行する)

sudo systemctl start httpd
sudo systemctl enable httpd

長い実行結果の中で「Complete!」と表示されればおけです。
次のコマンドで、EC2インスタンスのHTMLを設定します。なお、今回設定するHTMLは、僕の場合のもので、任意のコードに変更可能です。

sudo echo '
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>ポートフォリオサイト | tokyo-1</title>
</head>
<body>
    <main>
        <h3 style="padding: 8px 16px; background-color: #ecffec; border-radius: 12px; display: inline-block; color: #0dab76;">✅ 東京リージョン(1つ目のAZ) から表⽰</h3>
        <p>ポートフォリオサイトは、現在作業中です。</p>
</body>
</html>
' | sudo tee /var/www/html/index.html > /dev/null

実行しても何も表示されないが、それが正常です。
次のコマンドで、EC2インスタンスへのSSH接続を切断します。

exit

2つ目のEC2インスタンス

2つ目のEC2も、HTMLコードを除いて、1つ目と同じ手順で行います。
そのため、ここではコードだけを紹介します。
次のコマンドで、2つ目のEC2インスタンスにSSH接続します。
接続しようとすると、このサーバーを信頼して接続するかどうか聞いてくるので、yes と入力してEnter を押すと、SSH接続に成功します。

ssh -i $KEY_NAME.pem ec2-user@$EC2_2_IP

次のコマンドで、EC2インスタンスがインターネットに接続されているか確認するために、Googleにpingを3回とばし、実行結果からきちんと飛ばされていることを確認します。

ping www.google.com -c 3

次のコマンドで、EC2インスタンスのパッケージをアップデートします。

sudo yum update

次のコマンドで、EC2インスタンスにApacheをインストールします。

sudo yum install httpd

EC2インスタンスでApacheを起動し、自動起動するようにも設定し、長い実行結果の中で「Complete!」と表示されればおけです。(途中で y/N で聞いてきますが、yを押して続行します)

sudo systemctl start httpd
sudo systemctl enable httpd

次のコマンドで、EC2インスタンスのHTMLを設定します。なお、今回設定するHTMLは、僕の場合のもので、任意のコードに変更可能です。

sudo echo '
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>ポートフォリオサイト | tokyo-2</title>
</head>
<body>
    <main>
        <h3 style="padding: 8px 16px; background-color: #ecf7ff; border-radius: 12px; display: inline-block; color: #0d68ab;">✅ 東京リージョン(2つ目のAZ) から表⽰</h3>
        <p>ポートフォリオサイトは、現在作業中です。</p>
</body>
</html>
' | sudo tee /var/www/html/index.html > /dev/null

アクセス確認

次のコマンドで、再度、EC2インスタンスのそれぞれのパブリックIPv4アドレスを確認します。

echo "$EC2_1_IP"
echo "$EC2_2_IP"
実行結果
$ echo "$EC2_1_IP"
54.65.195.77
$ echo "$EC2_2_IP"
35.77.69.27

ブラウザから、返された2つのパブリックIPv4アドレスにそれぞれアクセスし、さっき設定したHTMLのページが表示されることを確認します。

※表示されない場合

該当のEC2インスタンスにCloudShellからSSH接続した状態で、下のコマンドで Apache を起動してください。僕は、最初にこのコマンドをし忘れて、なかなかページが表示されず、セキュリティグループなどの関係ない設定を見てしまい、30分くらい時間を浪費しました。

sudo systemctl start httpd

このコマンドを実行しても表示されない場合は、GPT様に聞いてみてください。

ここまでで、ALBを用いた負荷分散をさせずにEC2インスタンスの設定が完了しました。

ターゲットグループの設定

次のコマンドで、ターゲットグループを作成し、ターゲットグループのARNを返すようにして変数TARGET_GROUP_ARNに代入します。

TARGET_GROUP_ARN=$(aws elbv2 create-target-group \
    --target-type "instance" \
    --name "$TARGET_GROUP_NAME" \
    --protocol "HTTP" \
    --port "80" \
    --ip-address-type "ipv4" \
    --vpc-id "$VPC_ID" \
    --protocol-version "HTTP1" \
    --query 'TargetGroups[0].TargetGroupArn' \
    --output text)

次のコマンドで、ターゲットグループのARNが代入されたか確認します。

echo "TARGET_GROUP_ARN: $TARGET_GROUP_ARN"
実行結果
$ echo "TARGET_GROUP_ARN: $TARGET_GROUP_ARN"
TARGET_GROUP_ARN: arn:aws:elasticloadbalancing:ap-northeast-1:390403882691:targetgroup/pf-react-target-group/2d2575afc0a86779

次のコマンドで、ターゲットグループを設定します。

aws elbv2 register-targets \
    --target-group-arn "$TARGET_GROUP_ARN" \
    --targets "Id=$EC2_1_ID,Port=80" "Id=$EC2_2_ID,Port=80"

ALBの作成

次のコマンドで、ALBを作成し、出力されるALBの情報を変数 output に代入します。

output=$(aws elbv2 create-load-balancer \
    --type "application" \
    --name $ALB_NAME \
    --scheme "internet-facing" \
    --ip-address-type "ipv4" \
    --subnet-mappings "[
        {\"SubnetId\": \"$SUBNET_1_ID\"},
        {\"SubnetId\": \"$SUBNET_2_ID\"}
    ]" \
    --security-groups "$SECURITY_GROUP_ID")

実行しても何も返されないが、それが正常です。
次のコマンドで、ALBのLoadBalancerArnとDNSNameを取得して、変数LOAD_BALANCER_ARNとDNS_NAMEに代入します。

LOAD_BALANCER_ARN=$(echo $output | jq -r '.LoadBalancers[0].LoadBalancerArn')
DNS_NAME=$(echo $output | jq -r '.LoadBalancers[0].DNSName')

次のコマンドで、取得したALBのLoadBalancerArnとDNSNameが代入されたか確認します。

echo "LOAD_BALANCER_ARN: $LOAD_BALANCER_ARN"
echo "DNS_NAME: $DNS_NAME"

次のコマンドで、ALBにリスナーを追加します。

AWS_PAGER="" aws elbv2 create-listener \
    --protocol "HTTP" \
    --port 80 \
    --default-actions Type=forward,TargetGroupArn="$TARGET_GROUP_ARN" \
    --load-balancer-arn "$LOAD_BALANCER_ARN"
実行結果

リスナー情報が出力されればおけです。

{
    "Listeners": [
        {
            "ListenerArn": "arn:aws:elasticloadbalancing:ap-northeast-1:390403882691:listener/app/pf-react-alb/7133bbc299b41b10/ea03ad1a80750c5c",
            "LoadBalancerArn": "arn:aws:elasticloadbalancing:ap-northeast-1:390403882691:loadbalancer/app/pf-react-alb/7133bbc299b41b10",
            "Port": 80,
            "Protocol": "HTTP",
            "DefaultActions": [
                {
                    "Type": "forward",
                    "TargetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:390403882691:targetgroup/pf-react-target-group/2d2575afc0a86779",
                    "ForwardConfig": {
                        "TargetGroups": [
                            {
                                "TargetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:390403882691:targetgroup/pf-react-target-group/2d2575afc0a86779",
                                "Weight": 1
                            }
                        ],
                        "TargetGroupStickinessConfig": {
                            "Enabled": false
                        }
                    }
                }
            ]
        }
    ]
}

次のコマンドで、再度ALBのDNS名を確認します。

echo "$DNS_NAME"

ブラウザから、出力されたDNS名にアクセスし、再読み込みを繰り返すと、接続するEC2先が切り替わり、正常にALBを用いて負荷分散を実現できたことを確認します。

ドメイン取得

今回は、冒頭の「前提」でも述べたように、独自ドメインを設定します。
ドメイン設定には、次の2つの方法があります。

  1. Amazon Route 53でドメインを購入し、割り当てる
  2. ドメイン取得サイトから購入し、AWS側に割り当てる

そして、ChatGPTに聞いてみたところ、

総合的なコスト比較(1年間)

  • AWS Route 53:
    • ドメイン取得・更新: $14.00(.com ドメイン)
    • ホストゾーン管理費用: $6.00
    • 合計: $20.00(約2,800円)
  • お名前.com:
    • ドメイン取得: 0円(初回1個目)
    • ドメイン更新: 1,408円(2年目以降)
    • 合計: 1,408円(初年度)

ただし、AWSでインフラを統合的に管理したい場合や、Route 53の高度なDNS機能を活用したい場合は、AWS Route 53でのドメイン取得が便利です。一方、コストを最優先する場合や、既にお名前.comで他のドメインを管理している場合は、お名前.comでの取得が適しているでしょう。

と回答され、お名前.comというサイトでドメインを購入し、AWSに割り当てた方初年度の比較としては安いことがわかります。
さらに、AWS公式サイトやドメイン取得サイトを調べてみると、Amazon Route 53とドメイン取得サイトのどちらの方法でも、結局はAWSでホストゾーンとDNSクエリの設定をしなければならず、この設定にかかる料金は、どちらの方法でも変わらないため、純粋にドメイン取得・維持にかかる料金や管理方法で比較すればいいことがわかりました。(間違ってたらすみません🥺)

そのため、今回私は1年間0円で取得ができる お名前.com で、ドメイン「thinleaf.net」を取得しました。
お名前.comでドメインを取得する場合の手順は、そのままサイトの画面に沿って進めば大丈夫です。(変なオプションに入らされないように注意してください)

ホストゾーンの作成

お名前.comドメインから取得ができたら、AWSのRoute 53の「ホストゾーン」に飛び、右上の「ホストゾーンの作成」を押します。

取得したドメインを「ドメイン名」に入力、タイプを「パブリックホストゾーン」に選択し、右下の「ホストゾーンの作成」を押します。

作成が成功したら、「レコード」内にタイプが「NS」の「値/トラフィックのルーティング先」があることを確認します。

確認したら、このブラウザのタブを閉じずにそのままの状態にしておきます。

お名前.com上での設定

ブラウザで新しいタブを開き、お名前.comのマイページにアクセスします。
メニューの「ネームサーバー」から「ネームサーバー設定」を押します。

1. ドメインの選択」内の取得したドメイン名に、チェックマークを入れます。

下の「2.ネームサーバーの選択」で「その他のサービス」を開き、「その他のネームサーバーを使う」を選びます。

先ほどのAWSのタブに戻り、さっき確認した「レコード」内にタイプが「NS」の「値/トラフィックのルーティング先」に表示されているドメインを全て、お名前.com内のネームサーバー欄にコピペします。(欄が足りなかったら、+ボタンを押す)

右下の「確認」を押し、「OK」を押します。

完了したことを確認します。

これで、お名前.com側の設定は完了です!

レコードの作成

AWSのRoute 53ホストゾーンに飛び、該当のドメインを押し、右上の「レコードの作成」を押します。

次のように設定します。

  • レコード名:空欄(何も入力しない)
  • レコードタイプ:A
  • エイリアス:オン
  • トラフィックのルーティング先:
    • Application Load Balancer と Classic Load Balancer へのエイリアス」を選択
    • アジアパシフィック (東京)」を選択
    • ALBのDNS名を選択
  • ルーティングポリシー:シンプルルーティング
  • ターゲットのヘルスを評価:オン(「はい」)

    上側の表示から、正常に作成されたことを確認します。

    作成後、ある程度時間を置き、ブラウザから取得したドメインにアクセスし、サイトが表示されることを確認します。(この時、ALBによって負荷分散がされているため、EC2の接続先がアクセスにより異なります)

証明書の発行

このままでは、証明書がないことでブラウザの警告が表示されてしまうので、証明書を発行します。
上の検索バーから「Certificate Manager」と入力し、AWS Certificate Manager のページに行きます。
右上の「リクエスト」を押します。(下の画像では、既に2つ作ってしまっていますが気にしないでください)

証明書タイプとして「パブリック証明書をリクエスト」を選択し、右下の「次へ」を押します。

下記のように項目を設定し、右下の「リクエスト」を押します。

  • ドメイン名:ドメイン名に取得したドメイン
  • 検証方法:DNS検証
  • キーアルゴリズム:RSA 2048

    作成が完了し、該当の証明書のステータスが「保留中の検証」と表示されていることを確認します。

    「保留中の検証」の状態のまま、待たずにドメインの「Route 53でレコードを作成」を押します。

    先ほど入力したドメインを選択し、右下の「レコードを作成」を押します。

    正常に作成されたことを確認します。

    ステータスが「保留中の検証」から「発行済み」に変わるまで待ちます。

セキュリティルールの変更

今のセキュリティルールの状態だと、セキュリティ的にあまりよろしくない状況だったり、HTTPS通信に対応していなかったりするので、修正します。

今の状況

まず、AWSのEC2のページから「セキュリティグループ」を開き、作成したセキュリティルールを開きます。

インバウンドルール」内の右上にある「インバウンドの編集」を押します。

すると、現在のインバウンドルールが表示されます。

そもそも「インバウンドルール」とは、EC2インスタンスに対して外部からアクセスする際に、どんなトラフィックを許可するかのルールのことです。
現在の状態は、

  • HTTP:0.0.0.0/0
  • SSH:0.0.0.0/0

となっており、これは、HTTPとSSHの全てのアクセスを許可していることになります。
つまり、EC2インスタンスに対して、HTTPとSSHなら誰でもアクセスできる状態となっています。
特にSSHは、EC2インスタンスへのログインに使い、キーペアを持っていないとログインできないものの、自分以外の人がログインの試行どこからでもアクセスできるようになってしまっています。
これはあまりよろしくない設定なので、変更していきます。
EC2だけはSSHで直接アクセスしたいから、EC2とそれ以外の要素で、セキュリティルールを分けます
ついでにHTTPS通信にも対応させます。

現在のセキュリティグループを修正

さっき開いた「インバウンドルール」の画面のまま、次のように修正します:
変更前

リソースタイプ ソース
HTTP カスタム 0.0.0.0/0
SSH カスタム 0.0.0.0/0

\downarrow

変更後

リソースタイプ ソース
HTTP カスタム 0.0.0.0/0
SSH カスタム 0.0.0.0/0
HTTPS Anywhere-IPv4 0.0.0.0/0

ルールの保存」を押して、正常に保存されたことを確認します。

EC2用のセキュリティグループを作成

セキュリティ一覧画面に戻り、右上の「セキュリティグループを作成」を押します。

次のように設定します:

基本的な詳細

設定項目 設定値
セキュリティグループ名 pf-react_security-group-ec2
説明(自由に変更可能) security-group_for-ec2
VPC pf-react-vpc

インバウンドルール

ルール 設定項目 設定値
ルール1 タイプ SSH
リソースタイプ マイIP
ルール2 タイプ HTTP
リソースタイプ カスタム
ソース 選択画面中の「セキュリティグループ」から「pf-react_security-group」を選択
ルール3 タイプ HTTPS
リソースタイプ カスタム
ソース 選択画面中の「セキュリティグループ」から「pf-react_security-group」を選択

アウトバウンドルール

そのままにします。(すべてのトラフィックカスタム0.0.0.0.0の状態)

右下の「セキュリティグループを作成」を押し、正常に作成されたことを確認します。

EC2インスタンス一覧画面にとび、1つ目のEC2インスタンスを押します。

アクション」から「セキュリティ」、「セキュリティグループの変更」を押します。

元のセキリティグループの右側にある「削除」を押します。

上の検索バーから、 EC2用にさっき作った「pf-react_security-group-ec2」を選び、右にある「セキュリティグループを追加」を押し、右下の「保存」を押します。

EC2の「セキュリティ」タブから、セキュリティグループが「pf-react_security-group-ec2」となっていることを確認します。

もう片方のEC2にも全く同じ操作をして、セキュリティグループが「pf-react_security-group-ec2」となっていることを確認します。

セキュリティグループの設定による効果の検証

このセキュリティグループの設定による効果を試します。
EC2インスタンスのパブリックIPをコピーして、ブラウザからアクセスしようとすると到達できなくなります。


そして、今度は、自分のPCから、つまり設定したマイIPからEC2インスタンスにssh接続してみます:

成功しました!
そして、今度はスマホからテザリングして、マイIPとは異なるIPアドレスからアクセスしてみます:


期待通り、エラーとなり、正常にセキュリティグループが設定されていることがわかります
このように、SSH接続するIPアドレスを制限することによって、セキュリティ性が向上します(家族とか同じIPアドレスを使用している人がやるリスクは、一応ありますが、、、)

ロードバランサーの設定

このままでは、ロードバランサーにHTTPS通信を適用していないため、httpsでアクセスしてもページは表示されません。

EC2から「ロードバランサー」のページを開きます。

pf-react-alb」を開き、「リスナーとルール**」にある「リスナーの追加」を押します。
次のように設定します:

リスナーの詳細

項目 設定項目 設定値
リスナーの設定 プロトコル HTTPS
ポート 443
デフォルトアクション アクションのルーティング ターゲットグループへ転送
ターゲットグループ pf-react-target-group

セキュアリスナーの設定

項目 設定項目 設定値
セキュリティポリシー セキュリティカテゴリ すべてのセキュリティポリシー
ポリシー名 ELBSecurityPolicy-TLS13-1-2-2021-06
デフォルトSSL/TLSサーバー証明書 証明書の取得先 ACMから
証明書 (ACMから) さっき証明書発行で入力したドメインを選択(今回はthinleaf.net)

右下の「追加」を押し、正常に追加されたことを確認します。
そして、httpsでドメインにアクセスして、正常に表示されることを確認します。

しかし、このままだと、httpでドメインにアクセスした場合、セキュリティ保護なしと表示されたサイトがまだ表示されてしまうため、httpでアクセスしたら自動でhttpsのページに飛ぶように設定します。

AWSの「ロードバランサー」ページから「pf-react-alb」のリスナー一覧ページへ行き、「HTTP:80」を押します。

デフォルト」と書かれたリスナーを選択し、右上の「アクション」から「ルールの編集」を押します。

デフォルトアクション」の「アクションのルーティング」だけを次のように変更します:

変更前


\downarrow

変更後

設定項目 設定値
アクションのルーティング URLにリダイレクト
URLにリダイレクト URI部分
プロトコル HTTPS
ポート 443
ステータスコード 301 - 完全に移動されました


右下の「変更内容の保存」を押し、正常にリスナーが変更されたことを確認します。
httpでドメインにアクセスして、自動でhttpsに飛ばされることを確認します。

ここまでで、AWS側の環境構築は完了です!

Reactの環境構築

ここからは、EC2にReactの環境を構築していきます。
まず、EC2のt2.micro上でReactのビルドをしてみたところ、時間がかかったり、ビルドに失敗してしまい再試行しなくてはならなくなったり、割り当てるメモリ量を上げなくてはならなかったりといった手間が発生してしまいました。

そこで、今回構築する仕組みとしては、自分のPC上でビルドして、そのビルドしたデータEC2に渡して運用する形にしました。

では、進めていきます。

まず、最初に自分のローカルのPCReactサイトを作成してください。
これ以降は、Reactでサイトを作成済みという前提で進めます。
なお、Reactのサイト作成方法は、他の方の記事やChatGPTなどを参照ください。

私は、接続先がわかるように、ヘッダーに接続先を書きました。

EC2 AMIの作成

作成にあたり、失敗してしまうリスクがあるため、現在のEC2の状態バックアップしておきます。(あまりEC2の設定をしていないので簡単に戻せるかもしれませんが、一応やっておきます)

EC2インスタンス一覧ページから、1つ目のEC2インスタンスを選択し、右上の「アクション」から「イメージとテンプレート」の「イメージを作成」を押します。

次のように設定し、右下の「イメージを作成」を押します。

設定項目 設定値
イメージ名(任意の名前に変更できます) Backup-before-React (tokyo1a)
インスタンスを再起動 オフ
インスタンスボリューム デフォルト値(今回の場合は8, 現在のEC2のEBSボリュームと同じサイズにする)


もう一方のEC2インスタンスも、イメージ名だけ「Backup-before-React (tokyo1b)」に変更して同様に作成します。
その後、AMIタブで正常に作成されたことを確認します。

Reactのビルド

ビルド前の準備

ビルドの前に.htaccessというファイルを準備します。これは、ApacheでWebサーバーの設定をフォルダごとに変更できる特別なファイルです。
VSCodeじゃなくてVimでもできますが、VSCodeで進めていきます。
まず、VSCodeでReactプロジェクトのルートディレクトリを開いていることを確認してください。

次に「public」というディレクトリに「.htaccess」というファイルを作ります。

作った.htaccessに、次のコードをコピペして保存します。

RewriteEngine On
RewriteBase /

# index.htmlをそのまま返さないようにする
RewriteRule ^index\.html$ - [L]

# 存在しないファイルまたはディレクトリに対して、index.htmlを返す
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ /index.html [L]

さらに、このまま公開すると、日本語サイトなのにブラウザが「翻訳しますか?」と表示してくるので、public/index.htmlの次のコードを修正します。

変更前

<html lang="en">
\downarrow

変更後

<html lang="ja">

これで、ビルド前の準備は完了です!

ビルド作業

ターミナルを開きます。
VSCodeを使っている方は control @ で開けます。
まず、Reactプロジェクトのルートディレクトリにいること、ls コマンドでsrcpublicディレクトリがあることを確認します。

ls
実行結果

(ディレクトリbuildは、あってもなくてもいいので気にしないでください。)

tooon@MacBookAir portfolio % ls
README.md		package-lock.json	src
build			package.json
node_modules		public

次のコマンドで、ビルドを実行します。

npm run build

そして、サイズなどの情報が表示されれば成功です!

File sizes after gzip:

  132.86 kB  build/static/js/main.2276603b.js
  2.69 kB    build/static/js/488.80ecea7c.chunk.js
  814 B      build/static/css/main.f1fc41c2.css

The project was built assuming it is hosted at /.
You can control this with the homepage field in your package.json.

The build folder is ready to be deployed.
You may serve it with a static server:

  npm install -g serve
  serve -s build

Find out more about deployment here:

  https://cra.link/deployment

EC2でのReactビルドデータの設定

Reactのビルドデータをアップロード

1つ目のEC2

PC上でキーペアのパスをメモする。
僕の場合はこれです。

/Users/ユーザー名/Desktop/portfolio_aws/pf-react_aws-ec2-keypair.pem

AWSから、1つ目のEC2インスタンスのIPアドレスをメモします。

Reactプロジェクトのルートディレクトリにいることを確認し、次のコマンドのキーペア名IPアドレス部分を変えて、build/ フォルダの中身をEC2のApache公開ディレクトリに転送しようとします。

scp -i キーペア名.pem -r build/* ec2-user@IPアドレス:/var/www/html/

しかし、エラーが出ると思います。

% scp -i /Users/tooon/Desktop/portfolio_aws/pf-react_aws-ec2-keypair.pem -r build ec2-user@54.65.195.77:/var/www/html/
scp: stat remote: No such file or directory
scp: failed to upload directory build to /var/www/html/

原因は、ec2ユーザーに書き込み権限がないことです。
試しに、EC2インスタンスにssh接続して、/var/www/html 下にファイルを作ろうとしてみると権限がないことがわかります。

[ec2-user@ip-10-0-0-100 ~]$ touch /var/www/html/test.txt
touch: cannot touch '/var/www/html/test.txt': Permission denied

そこで、EC2インスタンスにssh接続した状態で書き込み権限を変更します。

sudo chown -R ec2-user:ec2-user /var/www/html
sudo chmod -R 755 /var/www/html

何も返されませんが、それが正常です。
再度、PC上のReactプロジェクトのルートディレクトリに戻り、次のコマンドのキーペア名IPアドレス部分を変えて、build/ フォルダの中身をEC2のApache公開ディレクトリに転送します。

scp -i キーペア名.pem -r build/* ec2-user@IPアドレス:/var/www/html/
実行結果
tooon@MacBookAir portfolio % scp -i /Users/tooon/Desktop/portfolio_aws/pf-react_aws-ec2-keypair.pem -r build/* ec2-user@54.65.195.77:/var/www/html/
asset-manifest.json                           100% 1347   135.9KB/s   00:00
deep-purple_leaf.png                          100%   66KB   2.6MB/s   00:00
favicon.ico                                   100% 3870   440.0KB/s   00:00
index.html                                    100%  689    84.4KB/s   00:00
logo192.png                                   100% 5347   700.3KB/s   00:00
logo512.png                                   100% 9664     1.1MB/s   00:00
manifest.json                                 100%  492    43.6KB/s   00:00
robots.txt                                    100%   67     8.5KB/s   00:00
main.f1fc41c2.css.map                         100% 3026   377.6KB/s   00:00
main.f1fc41c2.css                             100% 1715   222.4KB/s   00:00
488.80ecea7c.chunk.js                         100% 7389   766.1KB/s   00:00
main.d59f5b6e.js.LICENSE.txt                  100% 2075   243.6KB/s   00:00
main.d59f5b6e.js.map                          100% 2469KB  15.6MB/s   00:00
488.80ecea7c.chunk.js.map                     100%   17KB   1.7MB/s   00:00
main.d59f5b6e.js                              100%  422KB   7.6MB/s   00:00
deep-purple_leaf.b1636e5a1ff5f4ab9410.png     100%   66KB   4.5MB/s   00:00
green_leaf.101aa3ed9b0a317bf51d.png           100%   65KB   5.3MB/s   00:00
berehearsal.bbcea83f2eb98a491448.png          100%   52KB   5.3MB/s   00:00
thinleaf_icon.003d09e88433098ba0cf.jpg        100% 1281KB  16.3MB/s   00:00
okiben.63a5a154bfeb8be23444.png               100%  163KB   8.7MB/s   00:00
purple_leaf.4f186fd042836b8fdc08.png          100%   99KB   6.2MB/s   00:00
challenge-app.43b87beb23d6f38974d8.png        100%  200KB  10.4MB/s   00:00
shinagawa-runners.fafde8226ba0f583400e.png    100%   63KB   4.9MB/s   00:00  
officedx-app.b53b21d96e00f9af92e8.png         100%  131KB   9.3MB/s   00:00 

アップロードされたことを確認し、EC2インスタンスにSSH接続します。

ssh -i /Users/tooon/Downloads/pf-react_aws-ec2-keypair.pem  ec2-user@54.65.195.77

その後、ls コマンドでアップロードされたことを確認します。

ls /var/www/html/
実行結果

(この ls 内容はあくまで一例です。作成したReactサイトによって異なります。)

$ ls /var/www/html/
asset-manifest.json   favicon.ico  logo192.png  manifest.json  static
deep-purple_leaf.png  index.html   logo512.png  robots.txt

httpd.conf を設定

EC2インスタンスにSSH接続されたそのままの状態で、次のコマンドを実行します。

sudo vi /etc/httpd/conf/httpd.conf

その後、上下キーを使って次の部分を探し、i を押して入力モードにした後、次のようにコードを変更します。

変更前

<Directory "/var/www/html">
    #
    # Possible values for the Options directive are "None", "All",
    # or any combination of:
    #   Indexes Includes FollowSymLinks SymLinksifOwnerMatch ExecCGI MultiViews
    #
    # Note that "MultiViews" must be named *explicitly* --- "Options All"
    # doesn't give it to you.
    #
    # The Options directive is both complicated and important.  Please see
    # http://httpd.apache.org/docs/2.4/mod/core.html#options
    # for more information.
    #
    Options Indexes FollowSymLinks

    #
    # AllowOverride controls what directives may be placed in .htaccess files.
    # It can be "All", "None", or any combination of the keywords:
    #   Options FileInfo AuthConfig Limit
    #
    AllowOverride None

    #
    # Controls who can get stuff from this server.
    #
    Require all granted
</Directory>

\downarrow AllowOverrideNoneAll にする

<Directory "/var/www/html">
    #
    # Possible values for the Options directive are "None", "All",
    # or any combination of:
    #   Indexes Includes FollowSymLinks SymLinksifOwnerMatch ExecCGI MultiViews
    #
    # Note that "MultiViews" must be named *explicitly* --- "Options All"
    # doesn't give it to you.
    #
    # The Options directive is both complicated and important.  Please see
    # http://httpd.apache.org/docs/2.4/mod/core.html#options
    # for more information.
    #
    Options Indexes FollowSymLinks

    #
    # AllowOverride controls what directives may be placed in .htaccess files.
    # It can be "All", "None", or any combination of the keywords:
    #   Options FileInfo AuthConfig Limit
    #
    AllowOverride All

    #
    # Controls who can get stuff from this server.
    #
    Require all granted
</Directory>

All に変更したら esc キーを押し、最後に「:wq」と入力後、Enter キーを押します。
そして、次のコマンドでApacheを再起動させます。

sudo systemctl restart httpd

ブラウザからドメインにアクセスして、1つ目のEC2のサイトにアクセスできるまで再読み込みを繰り返し、正常にReactサイトが表示されることを確認します。

2つ目のEC2

2つ目のEC2でも、IPアドレスを変更して、1つ目のEC2と同様の操作をします。
同様に、ブラウザからドメインにアクセスして、再読み込みを繰り返し、正常にReactサイトが表示されることを確認します。

(たまに白紙ページが表示される場合)

ページを遷移していると、白紙ページが表示されてしまった方がいるかもしれません。

僕の場合は、EC2ごとフッターの一部の文字を変えて、ビルドし直していたため、この症状が見られました。


ビルドがEC2ごとに異なっている状態で、ALBを通じてリクエストが分散されると、どちらかのインスタンスで正しく処理されず、HTMLファイル(index.html)が返される という現象が起こることが原因です。

EC2ごとに一部の文字を変えたい場合は、ある作業をする必要があります。
ここからは、僕の場合の作業を紹介していきます。

EC2上の設定

次のコードで、PHPをインストールします。

sudo yum install -y php

最後に「Complete!」と表示されれば、PHPのインストールの成功です。
一応成功したか確認するために、次のコマンドで、phpのバージョンが出力されることを確認します。

php -v
実行結果
$ php -v
PHP 8.3.10 (cli) (built: Jul 30 2024 13:44:37) (NTS gcc x86_64)
Copyright (c) The PHP Group
Zend Engine v4.3.10, Copyright (c) Zend Technologies
    with Zend OPcache v8.3.10, Copyright (c), by Zend Technologies

次のコマンドで、phpファイルを新規作成する。

sudo vi /var/www/html/metadata.php

次のコードをコピペします。

<?php
header('Content-Type: application/json');

$token_url = 'http://169.254.169.254/latest/api/token';
$metadata_url = 'http://169.254.169.254/latest/meta-data/placement/availability-zone';

// IMDSv2トークンを取得
$ch = curl_init($token_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-aws-ec2-metadata-token-ttl-seconds: 21600']);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
$token = curl_exec($ch);
curl_close($ch);

// メタデータを取得
$ch = curl_init($metadata_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-aws-ec2-metadata-token: $token"]);
$az = curl_exec($ch);
curl_close($ch);

// JSONで返す
echo json_encode(["az" => trim($az)]);
?>

esc キーを押し、最後に「:wq」と入力後、Enter キーを押します。
そして、次のコマンドでApacheを再起動させる。

sudo systemctl restart httpd

両方のEC2インスタンスでこの設定をしてください。
これで、EC2上の設定は完了です!

Reactコードの変更

僕の場合、どのEC2にアクセスしているのか表示させるべく、「?個目」の?をEC2ごとに変える作業をします。
Reactコードにおける変更点は、次の3つです:

1つ目

次のimport文を、冒頭に書きます。

import { useState, useEffect } from "react";

2つ目

ページコードを return する前の部分に、次のコードを書きます。

const [az, setAz] = useState("");
    useEffect(() => {
        fetch("/metadata.php")
            .then((res) => res.json())
            .then((data) => {
                setAz(data.az);
            })
            .catch((err) => console.error("EC2メタデータ取得エラー:", err));
    }, []);

3つ目

変えたい部分のコードを見つけ、次のようにします。
変更前

<p className='footer-aws-list-p' style={{fontSize: '90%'}}>
    <del>( AZ 1個目/2個中 )</del>
</p>
\downarrow

変更後

<p className='footer-aws-list-p' style={{fontSize: '90%'}}>
    ( AZ {(instanceId!=null) ? `${instanceId ?? ''}個目/2個中` :'取得できませんでした'} )
</p>

ここまで終わったら、次のコマンドでReactをビルドします。

npm run build

ターミナルから、PC上のReactプロジェクトのルートディレクトリ下から、次のコマンドのキーペア名IPアドレス部分を変えて、build/ フォルダの中身を、2つのEC2のApache公開ディレクトリに転送します。

scp -i キーペア名.pem -r build/* ec2-user@1つ目のEC2のIPアドレス:/var/www/html/
scp public/.htaccess ec2-user@your-ec2-instance:/var/www/html/
scp -i キーペア名.pem -r build/* ec2-user@2つ目のEC2のIPアドレス:/var/www/html/
scp public/.htaccess ec2-user@your-ec2-instance:/var/www/html/

これで、ブラウザからアクセスし、再読み込みを繰り返したり、ページ遷移をしたりすると白紙ページが表示されず、正常にアクセス先が切り替わることがわかります。

まとめ

これで、Reactで作成したサイトを、AWSを用いてALBで負荷分散させたサイトを独自ドメインで公開することができました。
GitHub Pagesなどを用いればもっと簡単に公開できますが、AWSを使ってみたかったので、今回はこのような形で作成しました。

ちなみに、5日間稼働して、AWS料金は2.24ドルとなっていました。

状況により異なると思いますが、内訳料金発生構造は次のようになっています:

  • VPC:ALBのパブリックIPv4アドレスに対して料金が発生
  • Route 53:ホストゾーンに対して0.5ドル/月クエリに対して0.0000004ドル/クエリ(100万件未満の場合)料金が発生


(補足:パブリックIPv4アドレスからIPv6アドレスへの変更)

補足として、EC2インスタンスに割り当てるアドレスを、パブリックIPv4アドレスからIPv6アドレスへ変更する方法を紹介します。

現在、AWSでパブリックIPv4アドレスを使用すると、無料枠や特定の条件下の場合を除いて、料金が発生してしまいます。

https://business.ntt-east.co.jp/content/cloudsolution/column-481.html
この記事の通り、従来は無料でパブリックIPv4アドレスが使えたのですが、現在は仮に使っていなくても、パブリックIPv4アドレスを持っているだけで、使用状況によっては料金が発生してしまいます。
そして、今回使ったAWSの様々なサービスにおけて割り当てられたパブリックIPv4アドレスにも、状況によっては料金が発生しています。
今回の場合、次のような料金構造となっています。

  • EC2インスタンスに割り当てられたパブリックIPv4アドレス
    • EC2の無料枠に含まれるため、最初の12ヶ月間は無料
    • 無料枠のうち、更に1ヶ月に割り当てられる分の範囲を超えると、最初の12ヶ月間内でも超えた分に対して料金が発生
  • インターネット向けに設定したALBに割り当てられたパブリックIPv4アドレス
    • 無料枠はなく、ALBを動かしている限り料金が発生し続ける
    • IPv6アドレスを使う方法なら無料だが、IPv4ユーザーがサイトにアクセスできなくなることを避けるため、IPv4アドレスを使わなくてはならず、この料金は絶対に必要

そして、この2つのアドレスのうち「EC2インスタンスに割り当てられたパブリックIPv4アドレス」は、無料枠の12ヶ月を超えた13ヶ月目以降は、EC2が起動中かどうかに関わらず料金が発生する上、SSH接続するために使用するとき以外、絶対に必要なものではありません。

解決策

解決策は、2通りあります:

  1. 割り当てるアドレスをIPv4ではなくIPv6にする
    • 自宅のインターネット回線がIPv6に対応していれば、このEC2インスタンスにIPv6を割り当てれば、13ヶ月目以降も無料にできます!
    • ゆえに、自宅のインターネット回線がIPv6に対応している場合、IPv6アドレスを割り当てるべきだと思います。
    • ちなみに、僕が使用している自宅固定回線の楽天ひかり、モバイル回線の楽天モバイルは、恐らくIPv6に対応していました。(IPv6対応の有無は、ググると出てくる対応サイトで確認できます)
    • なお、IPv6に回線が対応していても、学校や出先などのWi-Fiやテザリングの回線がIPv6に対応していない場合、セキュリティグループを変更してもアクセスできなくなるので注意が必要です。
  2. Elastic IPを割り当てる
    • 自宅の回線がIPv4しか対応していない場合、EC2に割り当てるElastic IPが、EC2インスタンスが起動中かつ割り当て状態なら1つ目は無料なので、この方法が有用です。
    • EC2を停止し、EC2にアタッチされていない状態だと課金が発生するので注意が必要です。
    • 1か月に100回以上Elastic IPを再割り当てすると追加課金なので注意が必要です。
    • AWSの LambdaEventBridge を使えば、EC2の起動時にElastic IPを割り当て、停止時にElastic IPを解放することがLambdaの無料枠内に限り無料で可能です。

そして今回は 1. の方法で解決してみます。

IPv6への変更方法

VPCとサブネットのIPv6有効化

まず、AWSのVPC一覧ページを開き、pf-react-vpcを選択後、右上の「アクション」から「CIDRの編集」を押します。

IPv6の部分の左下にある「新しいIPv6 CIDR」を押し、「IPv6 CIDR ブロック」は「Amazon 提供の IPv6 CIDR ブロック」を選択、「ネットワークボーダーグループ」は「ap-northeast-1」を選択し、右下の「CIDRを選択」を押します。

成功したことを確認します。

この表示の中で、CIDRの末尾が「/56」となっていることを確認します。

次に、VPCの「サブネット」から対象のサブネットを選択し、右上の「アクション」から「IPv6 CIDRの編集」を押します。

「サブネットの CIDR ブロック」内の「IPv6 CIDRの追加」を押します。

デフォルトで設定された値だと、「サブネットの CIDR ブロック」の値の末尾が「/56」となっているかもしれません。

/56」となっている場合、下にある下方法に向いている矢印ボタンを押して「/64」にします。(上の「VPC CIDR ブロック」は「/56」のままで大丈夫です)

その後、右下の「保存」を押し、正常に編集されたことを確認します。

2つ目のサブネットについても、同様の作業をしますが、「/64」にしつつ下の「」ボタンで、例えばアドレスを1ずらして、1つ目のサブネットと異なるアドレスを付けます。

その後、右下の「保存」を押し、正常に編集されたことを確認します。

EC2のIPv6アドレス設定

EC2ページに行き、該当のEC2の1つを選択し、右上の「アクション」の「ネットワーキング」から「IPアドレスの管理」を押します。

ここを押して、トグルを開きます。

「IPv6アドレス」の部分の「新しいIPアドレスの割り当て」を押します。

次のように設定し、右下の「保存」を押します。

設定項目 設定値
IPv6アドレス 空欄(自動割り当て)
プライマリ IPv6 IP を割り当てる 有効
パブリック IP の自動割り当て オフ
セカンダリプライベート IPv4 アドレスの再割り当てを許可する オフ


リクエストが正常に完了したことを確認します。

2つ目のEC2インスタンスについても、同様の操作をします。
そして、EC2一覧ページで、両方のEC2において、パブリックIPv4アドレスの割り当てが無くなり、新たにIPv6アドレスが割り当てられていることを確認します。

VPCのルートテーブルのページを開き、VPC「pf-react-vpc」に割り当てられているルートテーブルを選択し、右上の「アクション」から「ルートを編集」を押します。

左下の「ルートを追加」を押し、次のように設定し、右下の「変更を保存」を押します。

設定項目 設定値
送信先 ::/0
ターゲット インターネットゲートウェイ(pf-react_igw)


ルートが正常に更新されたことを確認する。

EC2へのSSHをIPv6専用にする

EC2への、IPv4でのSSH接続を無効にし、IPv6のみに限定します。
EC2のセキュリティグループ一覧ページから、「**
pf-react_security-group-ec2**」を開き、「インバウンドのルールを編集」を押します。
( ⚠️「pf-react_security-group」ではなく末尾に「-ec2」がついている方です)

すでに、SSHで自分の回線のIPv4アドレスを許可したルールがある場合は、これを削除します。

自分のPC上から次のコマンドを実行し、自分の回線の現在のIPv6アドレスを確認し、コピーします。

Mac/Linuxの場合
curl -6 ifconfig.io
Windowsの場合
nslookup myip.opendns.com resolver1.opendns.com

一番下にある「ルールを追加」を押し、次のように設定し、右下の「ルールを保存」を押します。

設定項目 設定値
タイプ SSH
リソースタイプ カスタム
ソース さっきコピーした自分の回線の現在のIPv6アドレス + 「/128
説明 (自分で任意の名前を設定できます)

正常に変更されたことを確認します。

接続確認

EC2一覧画面より、各EC2のIPv6アドレスを確認します。
次のコマンドで、自分のPC上から各EC2にSSH接続します。

ssh -i キーペアのパス ec2-user@EC2のIPv6アドレス

鳥さんが1匹ずつ、合計2匹現れれば完了です!


これで、1ヶ月分の無料枠超過後や12ヶ月の無料枠終了後も、無料でEC2のIPアドレスを使い続けることができます!
なお、自分のIPv6アドレスは、事業者によっては数日で変わる可能性があるので注意してください。その場合は、またセキュリティグループの値をその度に変える必要があります。

以上、ご覧いただきありがとうございました。
間違っている部分があれば、教えていただければ幸いです。

Discussion