🌀

【AWS】CloudFormationで爆速環境構築🌀

2023/08/30に公開

概要

会社で AWS を触ることになり、基本から学んでいこうと思ったため備忘録として記事を書き始めました。
今回は AWS の CloudFormation を使用して 3 種類の異なるシステムをさくっと構築してみようと思います。
もし理解が違うよというところ等ありましたら優しく教えて頂けると幸いです 🙇‍♀️

今回作る物

よくある長々とした手順書を手動で順々に実行して環境構築するのではなく、環境構築用の設定ファイル設置とクリックのみで環境構築を行います。
例えば、EC2 上に React アプリケーションを立ち上げるために下の様なながーい手順書を実行して環境構築しなければいけないところ…。

以下設定ファイル設置と環境構築実行のボタンクリックのみで爆速で環境構築を行える様になります。

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/23
      Tags:
        - Key: Name
          Value: react-vpc
  Subnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId:
        Ref: VPC
      CidrBlock: 10.0.0.0/24
      Tags:
        - Key: Name
          Value: public-react-subnet
      AvailabilityZone: ap-northeast-1a
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: react-internet-gateway
  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId:
        Ref: InternetGateway
      VpcId:
        Ref: VPC
  RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: Name
          Value: public-react-route-table
      VpcId:
        Ref: VPC
  InternetRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: InternetGateway
  SubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId:
        Ref: Subnet
      RouteTableId:
        Ref: RouteTable
  KeyPair:
    Type: AWS::EC2::KeyPair
    Properties:
      KeyName: react-server-key
  SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: AllowSSHAndHTTPSecurityGroup
      VpcId:
        Ref: VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: "22"
          ToPort: "22"
          CidrIp: [自分のIPアドレス]
        - IpProtocol: tcp
          FromPort: "80"
          ToPort: "80"
          CidrIp: [自分のIPアドレス]
  ElasticIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
  EIPAssociation:
    Type: AWS::EC2::EIPAssociation
    Properties:
      InstanceId:
        Ref: EC2Instance
      EIP:
        Ref: ElasticIP
  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      Tags:
        - Key: Name
          Value: react-server
      InstanceType: t2.micro
      ImageId: ami-04beabd6a4fb6ab6f
      KeyName:
        Ref: KeyPair
      SecurityGroupIds:
        - Fn::GetAtt:
            - SecurityGroup
            - GroupId
      SubnetId:
        Ref: Subnet
      UserData:
        Fn::Base64: |
          # nginxをインストールするスクリプト
          sudo yum install -y nginx
          sudo systemctl start nginx
          # reactアプリケーションを作成するスクリプト
          sudo yum install -y nodejs npm
          npx -y create-react-app /home/ec2-user/sample-app
          cd /home/ec2-user/sample-app
          npm run build
          # nginxにreactアプリケーションを載せるスクリプト
          sudo rm -rf /usr/share/nginx/html/*
          sudo cp -r /home/ec2-user/sample-app/build/* /usr/share/nginx/html/

※上記手順書の例として挙げているのは私が前に書いた記事です 😗
手動で環境構築してみるのも結構勉強になったのでよかったらご覧ください。
https://zenn.dev/alichan/articles/c40b793253f5db

CloudFormation とは

以下 CloudFormation の説明です。
少し長くなるので、CloudFormation での環境構築方法だけ知りたい方は CloudFormation で環境構築省略してみるの章のみご覧ください 😌

CloudFormation とは

公式ドキュメント引用。
CloudFormation とは AWS のサービスによる環境構築を設定ファイルとボタン押下のみで行なってくれるサービスです。
設定ファイルはテンプレートと呼ばれ、上記の様に記述されます。
例えば、上記のテンプレートは EC2 上に React アプリケーションを立ち上げるためのテンプレートですがもし CloudFormation を使用しなかった場合 AWS のマネジメントコンソールで私が前に書いた記事の様に順々に手動でサービスを立ち上げなければなりません。
この様なことを毎回実行していると、下記の様なことが起こりえます。

  • 一つ手順書の手順を飛ばしてしまっただけでサービスが立ち上げられなくなる
  • 手動で実行しているので、同じ環境を 10 個作りたいといった要望が出てきた時に 10 回手動でサービスを立ち上げなくてはならなくなる
  • 1 ヶ月前の環境に戻したくなった時再環境構築が記憶頼りになる

これらの問題の解決策となるのが CloudFormation です。
テンプレート一つ用意しておけば全く同じ環境がボタン押下のみで立ち上げられるため手順をとばすといったことがあり得ません。
また、複数回の環境構築における手間も軽減することができます。
更に、テンプレートを保存しておけば過去の環境構築方法を覚えていなくてもボタン押下のみで環境構築をすることができます。

テンプレートは上記の様に yml ファイルで記述することができます。
上記テンプレートを記載し CloudFormation に設置してボタンを押下すれば、一発で React アプリケーションが立ち上がる様になります。

CloudFormation で環境構築省略してみる

今回は以下三つのタイプの異なるシステムの基盤環境を CloudFormation でさくっと構築してみます。

  • EC2 + Nginx + React で作成する Web アプリ
  • React + Express + MySQL で作成する 3 層 Web システム
  • Amplify + Nextjs + WebSocket API で作成するリアルタイムチャット

EC2 + Nginx + React で作成する Web アプリ

  1. システムの説明

上記がこれから作成する Web システムのアーキテクチャ図です。
EC2 上に Web サーバー提供用ソフトの Nginx をのせその中に React アプリケーションを格納し、Web 上に React アプリケーションを公開します。
手動でこのアプリを作成した記事はこちらです。
https://zenn.dev/alichan/articles/c40b793253f5db

  1. 環境構築に使用する CloudFormation の yml ファイル

以下が上記システムの基盤環境構築を行うための yml ファイルです。
こちらを CloudFormation に設置して基盤環境の構築を行います。
一枚目のテンプレートのため、コメントで各プロパティの簡単な説明を記載しておきます。

react-server.yml

# Resources:配下に立ち上げるリソース(AWS上のサービス)を指定。
# VPC、Subnet、EC2インスタンス…等Reactアプリケーションを立ち上げる際に必要になるリソースを指定している。
Resources:
  # このリソースに設定する名前を指定。
  # VPCと指定しているが、どんな名前でも指定することが可能
  VPC:
    # Type:立ち上げるAWSのリソースを指定。
    # ここではVPCを指定している。
    Type: AWS::EC2::VPC
    # Properties:リソースに対しての設定を指定。
    # ここではVPCに紐づけるCidrBlockや名前タグ等を指定する。
    Properties:
      CidrBlock: 10.0.0.0/23
      Tags:
        - Key: Name
          Value: react-vpc
  Subnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId:
        Ref: VPC
      CidrBlock: 10.0.0.0/24
      Tags:
        - Key: Name
          Value: public-react-subnet
      AvailabilityZone: ap-northeast-1a
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: react-internet-gateway
  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId:
        Ref: InternetGateway
      VpcId:
        # Ref関数を使用することでテンプレート内の他リソースを引用して指定することが可能
        Ref: VPC
  RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: Name
          Value: public-react-route-table
      VpcId:
        Ref: VPC
  InternetRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: InternetGateway
  SubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId:
        Ref: Subnet
      RouteTableId:
        Ref: RouteTable
  KeyPair:
    Type: AWS::EC2::KeyPair
    Properties:
      KeyName: react-server-key
  SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: AllowSSHAndHTTPSecurityGroup
      VpcId:
        Ref: VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: "22"
          ToPort: "22"
          CidrIp: [自分のIPアドレス]
        - IpProtocol: tcp
          FromPort: "80"
          ToPort: "80"
          CidrIp: [自分のIPアドレス]
  ElasticIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
  EIPAssociation:
    Type: AWS::EC2::EIPAssociation
    Properties:
      InstanceId:
        Ref: EC2Instance
      EIP:
        Ref: ElasticIP
  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      Tags:
        - Key: Name
          Value: react-server
      InstanceType: t2.micro
      ImageId: ami-04beabd6a4fb6ab6f
      KeyName:
        Ref: KeyPair
      SecurityGroupIds:
        # GetAtt関数を使用することでテンプレート内の他リソース配下の変数を指定することが可能
        - Fn::GetAtt:
            - SecurityGroup
            - GroupId
      SubnetId:
        Ref: Subnet
      # UserDataプロパティーでEC2インスタンス立ち上げ時に実行するスクリプトを指定
      UserData:
        Fn::Base64: |
          # nginxをインストールするスクリプト
          sudo yum install -y nginx
          sudo systemctl start nginx
          # reactアプリケーションを作成するスクリプト
          sudo yum install -y nodejs npm
          npx -y create-react-app /home/ec2-user/sample-app
          cd /home/ec2-user/sample-app
          npm run build
          # nginxにreactアプリケーションを載せるスクリプト
          sudo rm -rf /usr/share/nginx/html/*
          sudo cp -r /home/ec2-user/sample-app/build/* /usr/share/nginx/html/
  1. 環境構築してみる

上記 yml ファイルを使用してシステムの環境構築を行います。

AWS マネジメントコンソールで CloudFormation を開いてスタックの作成を押下してください。

テンプレートの指定 > テンプレートファイルのアップロード > ファイルの選択で上記で作成した yml ファイルを指定し、次へを押下してください。

スタック名に yml ファイル名などを設定し次へを押下してください。
この後も引き続き次へを押下し、最後は送信を押下してスタックを作成してください。

下記イベントタブで順次リソースが作成されているのがわかると思います。
最終的に作成されたスタックの下に CREATE_COMPLETE の文字がでたら環境構築終了です。
ほぼ設定ファイルの設置とボタン押下のみで環境構築ができました。

下記リソースタブで論理 ID が ElasticIP の物理 ID として指定されている IP アドレスをブラウザの検索バーに入力してください。

下記の様に React アプリケーションが表示されるはずです。

React + Express + MySQL で作成する 3 層 Web システム

  1. システムの説明

上記がこれから作成する 3 層 Web システムのアーキテクチャ図です。
ざっくり説明すると

  • VPC 上にパブリックサブネットとプライベートサブネットを作成
  • 各サブネットに Web サーバー、API サーバー、DB サーバーの役割を担う EC2 インスタンスを配置
  • Web サーバーに Nginx と React アプリケーション、API サーバーに Node.js と Express、DB サーバーに MySQL を格納
  • NAT ゲートウェイとインターネットゲートウェイを配置することで システムを Web 上からアクセス可能にする

という流れで作成されたシステムになっています。

手動でこのシステムを作成した記事はこちらです。
https://zenn.dev/alichan/articles/03dc627e490f4d

  1. 環境構築に使用する CloudFormation の yml ファイル

以下が上記システムの基盤環境構築を行うための yml ファイルです。
こちらを CloudFormation に設置して基盤環境の構築を行います。

web3-server.yml

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/23
      Tags:
        - Key: Name
          Value: web3-vpc
  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId:
        Ref: VPC
      CidrBlock: 10.0.0.0/24
      Tags:
        - Key: Name
          Value: public-web3-subnet
      AvailabilityZone: ap-northeast-1a
  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId:
        Ref: VPC
      CidrBlock: 10.0.1.0/24
      Tags:
        - Key: Name
          Value: private-web3-subnet
      AvailabilityZone: ap-northeast-1a
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: web3-internet-gateway
  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId:
        Ref: InternetGateway
      VpcId:
        Ref: VPC
  NatGatewayEIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
  NatGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId:
        Fn::GetAtt:
          - NatGatewayEIP
          - AllocationId
      SubnetId:
        Ref: PublicSubnet
      Tags:
        - Key: Name
          Value: web3-nat-gateway
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: Name
          Value: public-web3-route-table
      VpcId:
        Ref: VPC
  PublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: InternetGateway
  PublicSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId:
        Ref: PublicSubnet
      RouteTableId:
        Ref: PublicRouteTable
  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: Name
          Value: private-web3-route-table
      VpcId:
        Ref: VPC
  PrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: PrivateRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId:
        Ref: NatGateway
  PrivateSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId:
        Ref: PrivateSubnet
      RouteTableId:
        Ref: PrivateRouteTable
  WebKeyPair:
    Type: AWS::EC2::KeyPair
    Properties:
      KeyName: web3-webserver-key
  WebSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: AllowSSHAndHTTPSecurityGroup
      VpcId:
        Ref: VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: "22"
          ToPort: "22"
          CidrIp: [自分のIPアドレス]
        - IpProtocol: tcp
          FromPort: "80"
          ToPort: "80"
          CidrIp: [自分のIPアドレス]
      Tags:
        - Key: Name
          Value: web3-webserver-sg
  WebElasticIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
  WebEIPAssociation:
    Type: AWS::EC2::EIPAssociation
    Properties:
      InstanceId:
        Ref: WebEC2Instance
      EIP:
        Ref: WebElasticIP
  WebEC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      Tags:
        - Key: Name
          Value: web3-webserver
      InstanceType: t2.micro
      ImageId: ami-04beabd6a4fb6ab6f
      KeyName:
        Ref: WebKeyPair
      SecurityGroupIds:
        - Fn::GetAtt:
            - WebSecurityGroup
            - GroupId
      SubnetId:
        Ref: PublicSubnet
      UserData:
        Fn::Base64: |
          # nginxとnodejsをインストールするスクリプト
          sudo yum install -y nginx
          sudo systemctl start nginx
          sudo yum install -y nodejs npm
          npx -y create-react-app /home/ec2-user/sample-app
          cd /home/ec2-user/sample-app
          npm install axios
  APIKeyPair:
    Type: AWS::EC2::KeyPair
    Properties:
      KeyName: web3-apiserver-key
  APISecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: AllowSSHAndHTTPSecurityGroup
      VpcId:
        Ref: VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: "22"
          ToPort: "22"
          CidrIp:
            Fn::Join:
              - ""
              - - Fn::GetAtt:
                    - WebEC2Instance
                    - PrivateIp
                - "/32"
        - IpProtocol: tcp
          FromPort: "8000"
          ToPort: "8000"
          CidrIp:
            Fn::Join:
              - ""
              - - Fn::GetAtt:
                    - WebEC2Instance
                    - PrivateIp
                - "/32"
      Tags:
        - Key: Name
          Value: web3-apiserver-sg
  APIEC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      Tags:
        - Key: Name
          Value: web3-apiserver
      InstanceType: t2.micro
      ImageId: ami-04beabd6a4fb6ab6f
      KeyName:
        Ref: APIKeyPair
      SecurityGroupIds:
        - Fn::GetAtt:
            - APISecurityGroup
            - GroupId
      SubnetId:
        Ref: PrivateSubnet
      UserData:
        Fn::Base64: |
          # nodejsをインストールするスクリプト
          sudo yum install -y nodejs npm
          mkdir /home/ec2-user/sample-api
          cd /home/ec2-user/sample-api
          npm init
          npm install express mysql2
  DBKeyPair:
    Type: AWS::EC2::KeyPair
    Properties:
      KeyName: web3-dbserver-key
  DBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: AllowSSHAndMySQLSecurityGroup
      VpcId:
        Ref: VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: "22"
          ToPort: "22"
          CidrIp:
            Fn::Join:
              - ""
              - - Fn::GetAtt:
                    - APIEC2Instance
                    - PrivateIp
                - "/32"
        - IpProtocol: tcp
          FromPort: "3306"
          ToPort: "3306"
          CidrIp:
            Fn::Join:
              - ""
              - - Fn::GetAtt:
                    - APIEC2Instance
                    - PrivateIp
                - "/32"
      Tags:
        - Key: Name
          Value: web3-dbserver-sg
  DBEC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      Tags:
        - Key: Name
          Value: web3-dbserver
      InstanceType: t2.micro
      ImageId: ami-04beabd6a4fb6ab6f
      KeyName:
        Ref: DBKeyPair
      SecurityGroupIds:
        - Fn::GetAtt:
            - DBSecurityGroup
            - GroupId
      SubnetId:
        Ref: PrivateSubnet
      UserData:
        Fn::Base64: |
          # mysqlをインストールするスクリプト
          sudo yum install -y https://dev.mysql.com/get/mysql80-community-release-el9-3.noarch.rpm
          sudo yum install -y mysql-community-server
          sudo systemctl start mysqld
  1. 環境構築してみる

上記 EC2 + Nginx + React で作成する Web アプリ3. 環境構築してみるで行なった手順で環境構築を行なってください。
設定するスタック名と yml ファイルのみ上記の物に変更してください。
こちらの yml ファイルで行っているのは EC2 インスタンスの立ち上げと必要なソフトウェア(Node.js や MySQL)のインストールのみなので、もし実際にシステムを完成させたい場合以下記事VPC を使用して 3 層アーキテクチャの Web システムを作成してみよう

    1. Web サーバを立ち上げて、React アプリケーションを載せる
    1. データベースサーバを立ち上げて、データベースを載せる
    1. API サーバを立ち上げて、WebAPI を載せる

で React アプリケーションの立ち上げ、データベースへのデータ投入、API アプリケーションの作成等を行ってみてください 😌

https://zenn.dev/alichan/articles/03dc627e490f4d

Amplify + Next + WebSocket API で作成するリアルタイムチャット

  1. システムの説明

上記がこれから作成する Web システムのアーキテクチャ図です。
ざっくり説明すると

  • チャット画面を Next アプリケーションで作成し Ampify でホスティング
  • WebSocket API + Lambda で DB とユーザーのやり取り部分のロジックを作成
  • DynamoDB でメッセージや通信上情報を格納

というシステムになっています。

手動でこのシステムを作成した記事はこちらです。
https://zenn.dev/alichan/articles/f1b3aabeb96158

  1. 環境構築に使用する CloudFormation の yml ファイル

以下が上記システムの基盤環境構築を行うための yml ファイルです。
こちらを CloudFormation に設置して基盤環境の構築を行います。

chat-app.yml

Resources:
  DynamoDBTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: message_rooms
      AttributeDefinitions:
        - AttributeName: room_id
          AttributeType: N
      KeySchema:
        - AttributeName: room_id
          KeyType: HASH
      ProvisionedThroughput:
        ReadCapacityUnits: 5
        WriteCapacityUnits: 5

  RestApi:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: chat-rest-api

  ApiGatewayMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      AuthorizationType: NONE
      HttpMethod: GET
      ResourceId:
        Fn::GetAtt:
          - RestApi
          - RootResourceId
      RestApiId:
        Ref: RestApi
      Integration:
        IntegrationHttpMethod: POST
        Type: AWS_PROXY
        Uri:
          Fn::Sub: arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetAllMessagesLambdaFunction.Arn}/invocations

  GetAllMessagesLambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: getAllMessages
      Runtime: nodejs18.x
      Handler: index.handler
      Role:
        Fn::GetAtt:
          - LambdaExecutionRole
          - Arn
      Code:
        ZipFile: |
          const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
          const { DynamoDBDocumentClient, GetCommand } = require("@aws-sdk/lib-dynamodb");

          exports.handler = async (event) => {
            const client = new DynamoDBClient({});
            const docClient = DynamoDBDocumentClient.from(client);

            const getCommand = new GetCommand({
              TableName: "message_rooms",
              Key: {
                room_id: 1,
              },
            });

            const docResponse = await docClient.send(getCommand);

            const response = {
              statusCode: 200,
              body: JSON.stringify({
                messages: docResponse.Item.messages,
              }),
            };
            return response;
          };

  GetAllMessagesLambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName:
        Ref: GetAllMessagesLambdaFunction
      Principal: apigateway.amazonaws.com

  ApiGatewayDeployment:
    Type: AWS::ApiGateway::Deployment
    Properties:
      RestApiId:
        Ref: RestApi
    DependsOn:
      - ApiGatewayMethod

  ApiGatewayStage:
    Type: AWS::ApiGateway::Stage
    Properties:
      RestApiId:
        Ref: RestApi
      DeploymentId:
        Ref: ApiGatewayDeployment
      StageName: production

  WebSocketApi:
    Type: AWS::ApiGatewayV2::Api
    Properties:
      Name: chat-websocket
      ProtocolType: WEBSOCKET
      RouteSelectionExpression: $request.body.action

  ConnectWebSocketRoute:
    Type: AWS::ApiGatewayV2::Route
    Properties:
      ApiId:
        Ref: WebSocketApi
      RouteKey: $connect
      Target:
        Fn::Sub: integrations/${ConnectLambdaIntegration}

  SendMessageWebSocketRoute:
    Type: AWS::ApiGatewayV2::Route
    Properties:
      ApiId:
        Ref: WebSocketApi
      RouteKey: sendMessage
      Target:
        Fn::Sub: integrations/${SendMessageLambdaIntegration}

  DisconnectWebSocketRoute:
    Type: AWS::ApiGatewayV2::Route
    Properties:
      ApiId:
        Ref: WebSocketApi
      RouteKey: $disconnect
      Target:
        Fn::Sub: integrations/${DisconnectLambdaIntegration}

  WebSocketStage:
    Type: AWS::ApiGatewayV2::Stage
    Properties:
      ApiId:
        Ref: WebSocketApi
      StageName: production
      AutoDeploy: true

  ConnectLambdaIntegration:
    Type: AWS::ApiGatewayV2::Integration
    Properties:
      ApiId:
        Ref: WebSocketApi
      IntegrationType: AWS_PROXY
      IntegrationUri:
        Fn::Sub: arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ConnectLambdaFunction.Arn}/invocations

  SendMessageLambdaIntegration:
    Type: AWS::ApiGatewayV2::Integration
    Properties:
      ApiId:
        Ref: WebSocketApi
      IntegrationType: AWS_PROXY
      IntegrationUri:
        Fn::Sub: arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SendMessageLambdaFunction.Arn}/invocations

  DisconnectLambdaIntegration:
    Type: AWS::ApiGatewayV2::Integration
    Properties:
      ApiId:
        Ref: WebSocketApi
      IntegrationType: AWS_PROXY
      IntegrationUri:
        Fn::Sub: arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${DisconnectLambdaFunction.Arn}/invocations

  ConnectLambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: connect
      Runtime: nodejs18.x
      Handler: index.handler
      Role:
        Fn::GetAtt:
          - LambdaExecutionRole
          - Arn
      Code:
        ZipFile: |
          const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
          const {
            PutCommand,
            DynamoDBDocumentClient,
            GetCommand,
          } = require("@aws-sdk/lib-dynamodb");

          exports.handler = async (event) => {
            const client = new DynamoDBClient({});
            const docClient = DynamoDBDocumentClient.from(client);

            const getCommand = new GetCommand({
              TableName: "message_rooms",
              Key: {
                room_id: 1,
              },
            });

            const response = await docClient.send(getCommand);
            const connectionId = event.requestContext.connectionId;

            let connectionIds = response.Item.connection_ids;

            connectionIds.push(connectionId);

            const putCommand = new PutCommand({
              TableName: "message_rooms",
              Item: {
                ...response.Item,
                connection_ids: connectionIds,
              },
            });

            await docClient.send(putCommand);

            return {
              statusCode: 200,
            };
          };

  SendMessageLambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: sendMessage2
      Runtime: nodejs18.x
      Handler: index.handler
      Role:
        Fn::GetAtt:
          - LambdaExecutionRole
          - Arn
      Code:
        ZipFile: |
          import {
            ApiGatewayManagementApiClient,
            PostToConnectionCommand,
          } from "@aws-sdk/client-apigatewaymanagementapi";
          import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
          import {
            PutCommand,
            DynamoDBDocumentClient,
            GetCommand,
          } from "@aws-sdk/lib-dynamodb";

          export const handler = async (event) => {
            const client = new DynamoDBClient({});
            const docClient = DynamoDBDocumentClient.from(client);

            const getCommand = new GetCommand({
              TableName: "message_rooms",
              Key: {
                room_id: 1,
              },
            });

            const response = await docClient.send(getCommand);
            const body = JSON.parse(event.body);

            const connectionIds = response.Item.connection_ids;
            let messages = response.Item.messages;

            messages.push({
              send_user: body.data.send_user,
              message: body.data.message,
            });

            const putCommand = new PutCommand({
              TableName: "message_rooms",
              Item: {
                ...response.Item,
                messages,
              },
            });

            await docClient.send(putCommand);

            const apigClient = new ApiGatewayManagementApiClient({
              endpoint:
                "https://" +
                event.requestContext.domainName +
                "/" +
                event.requestContext.stage,
            });

            const sendMessages = connectionIds.map(async (connectionId) => {
              const apigCommand = new PostToConnectionCommand({
                ConnectionId: connectionId,
                Data: JSON.stringify({ messages }),
              });

              await apigClient.send(apigCommand);
            });

            await Promise.all(sendMessages);

            return {
              statusCode: 200,
            };
          };

  DisconnectLambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: disconnect
      Runtime: nodejs18.x
      Handler: index.handler
      Role:
        Fn::GetAtt:
          - LambdaExecutionRole
          - Arn
      Code:
        ZipFile: |
          import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
          import {
            PutCommand,
            DynamoDBDocumentClient,
            GetCommand,
          } from "@aws-sdk/lib-dynamodb";

          export const handler = async (event) => {
            const client = new DynamoDBClient({});
            const docClient = DynamoDBDocumentClient.from(client);

            const getCommand = new GetCommand({
              TableName: "message_rooms",
              Key: {
                room_id: 1,
              },
            });

            const response = await docClient.send(getCommand);
            const connectionId = event.requestContext.connectionId;

            let connectionIds = response.Item.connection_ids;

            const index = connectionIds.indexOf(connectionId);
            connectionIds.splice(index, 1);

            const putCommand = new PutCommand({
              TableName: "message_rooms",
              Item: {
                ...response.Item,
                connection_ids: connectionIds,
              },
            });

            await docClient.send(putCommand);

            return {
              statusCode: 200,
            };
          };

  ConnectLambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName:
        Ref: ConnectLambdaFunction
      Principal: apigateway.amazonaws.com

  SendMessageLambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName:
        Ref: SendMessageLambdaFunction
      Principal: apigateway.amazonaws.com

  DisconnectLambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName:
        Ref: DisconnectLambdaFunction
      Principal: apigateway.amazonaws.com

  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: LambdaExecutionRole
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: LambdaPolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: arn:aws:logs:*:*:*
              - Effect: Allow
                Action:
                  - "dynamodb:*"
                Resource: "*"
              - Effect: Allow
                Action:
                  - "execute-api:*"
                Resource: "*"

  CodeCommitRepository:
    Type: AWS::CodeCommit::Repository
    Properties:
      RepositoryName: chat-front
  AmplifyApp:
    Type: AWS::Amplify::App
    Properties:
      Name: chat-front
      Repository:
        Fn::GetAtt:
          - CodeCommitRepository
          - CloneUrlHttp
      BuildSpec: |
        version: 1
        frontend:
          phases:
            preBuild:
              commands:
                - npm ci
            build:
              commands:
                - npm run build
          artifacts:
            baseDirectory: .next
            files:
              - '**/*'
          cache:
            paths:
              - node_modules/**/*
  AmplifyBranch:
    Type: AWS::Amplify::Branch
    Properties:
      AppId:
        Fn::GetAtt:
          - AmplifyApp
          - AppId
      BranchName: main
      EnableAutoBuild: true
  1. 環境構築してみる

上記 EC2 + Nginx + React で作成する Web アプリ3. 環境構築してみるで行なった手順で環境構築を行なってください。
設定するスタック名と yml ファイルのみ上記の物に変更してください。
こちらの yml ファイルで行っているのは Amplify でのホスティング環境構築、API の構築、DynamoDB の立ち上げのみなので、もし実際にシステムを完成させたい場合以下記事WebSocket APIでリアルタイムチャット作ってみた(Amplify+Next+WebSocket API使用)

    1. チャットのデータを保管する DynamoDB を構築する
    1. Next+Amplify+CodeCommit でチャット画面を作成して web 上に公開する

で Next.js アプリケーションの CodCommit への push、データベースへのデータ投入等を行ってみてください 😌

https://zenn.dev/alichan/articles/f1b3aabeb96158

終わりに

以上、CloudFormation で異なる三つのタイプのシステムの環境構築を行なってみました。
お役に立つことができていたら幸いです。
ここまで読んでいただき本当にありがとうございます 🙇‍♀️

参照

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/Welcome.html

Discussion