CloudFormationでALB + EC2(Tomcat) + RDS 3層構成を自動構築する手順【27リソース一括デプロイ / コンソール版との比較付き】

AWS Basic
スポンサーリンク
スポンサーリンク

はじめに

「コンソールで40〜50分かかったALB + EC2(Tomcat) + RDS環境をコードで再現したい」——それを実現するのが AWS CloudFormation です。

この記事では、template.yaml 1ファイルにVPC・サブネット・セキュリティグループ・IAMロール・SSM Parameter Store・RDS・ALB・ターゲットグループ・EC2(約27リソース)を定義し、コマンド1本で3層構成を一括構築するハンズオンを紹介します。コンソール版(AWSコンソール版ハンズオン)と全く同じ構成をCloudFormationで自動化します。

ローカル環境(VSCode)
  ├── template.yaml(約27リソースを定義)
  └── AWS CLI コマンド
        ↓ スタック作成(コマンド1本)
AWS環境
  └── VPC(10.0.0.0/16)
        ├── パブリックサブネット1(10.0.1.0/24 / AZ-a)← ALB + EC2
        ├── パブリックサブネット2(10.0.2.0/24 / AZ-c)← ALB(2AZ必須)
        ├── プライベートサブネット1(10.0.3.0/24 / AZ-a)← RDS配置
        ├── プライベートサブネット2(10.0.4.0/24 / AZ-c)← DBサブネットグループ用
        ├── ALB SG → EC2 SG → RDS SG(3段階のSG-to-SG制御)
        ├── SSM Parameter Store: /my/alb/db-password
        ├── RDS MySQL 8.0(db.t3.micro)
        ├── ALB(HTTP:80 → ターゲットグループ → EC2:8080)
        └── EC2(t2.micro / Tomcat / UserDataで自動セットアップ)

このハンズオンで体験できること:

  • ALB・ターゲットグループ・リスナーを template.yaml で一括定義する方法
  • TomcatVersion パラメータでTomcatのバージョンを切り替える設計
  • ALBの2AZ要件をCloudFormationでどう表現するか
  • delete-stack 1本でALB・RDS・IAMなど27リソースを自動削除

このハンズオンの特徴:

  • コンソール版では40〜50分・13ステップかかった作業が、コマンド1本(RDS待ち15〜20分込み)で完了
  • delete-stack 1本でALBのリスナー含む全リソースの依存関係を自動解決して削除

この記事は AWSコンソール版ハンズオン の比較記事です。
コンソール版でALB・ターゲットグループ・ヘルスチェックを視覚的に学んだ後に読むと理解が深まります。


-->

スポンサーリンク

CloudFormation vs コンソール:どれだけ違うか

比較項目CloudFormationコンソール(手動)
VPC + サブネット4つ + IGW + ルートテーブルtemplate.yaml に定義済み約20〜30分・複数画面
SG 3つ(ALB/EC2/RDS)の作成template.yaml に定義済み3画面で個別設定
ALB + TG + Listener の作成template.yaml に定義済み3つの画面で設定
Tomcat UserDataの貼り付けtemplate.yaml に組み込み済み手動コピペ
全体のデプロイコマンド1本(RDS待ち15〜20分)約40〜50分
削除delete-stack 1本(自動解決)13ステップ手動
再現性高い(何度でも同じ構成を再現可能)低い(手順漏れのリスク大)
バージョン管理Gitで管理可能不可

スポンサーリンク

キーワード解説

用語意味
CloudFormationAWSが提供するIaC(Infrastructure as Code)サービス
スタックCloudFormationが管理するリソースのまとまり。今回は約27リソースを1スタックで管理
AWS::ElasticLoadBalancingV2::LoadBalancerALB本体を定義するリソース型
AWS::ElasticLoadBalancingV2::TargetGroupターゲットグループを定義するリソース型
AWS::ElasticLoadBalancingV2::ListenerALBのリスナー(受付ルール)を定義するリソース型
DeletionPolicy: Deleteスタック削除時にRDSも確実に削除する設定
CAPABILITY_NAMED_IAM名前付きIAMリソースを含むスタックの作成に必要なフラグ

前提条件

ツール確認コマンド
AWS CLI v2aws --version

AWS認証確認:

aws sts get-caller-identity

構築されるリソース(約27個)

カテゴリリソース論理ID
ネットワークVPCVPC
ネットワークインターネットゲートウェイInternetGateway + IGWAttachment
ネットワークパブリックサブネット × 2PublicSubnet1 + PublicSubnet2
ネットワークプライベートサブネット × 2PrivateSubnet1 + PrivateSubnet2
ネットワークパブリックルートテーブルPublicRouteTable + PublicRoute + PublicSubnet1RTA + PublicSubnet2RTA
ネットワークプライベートルートテーブルPrivateRouteTable + PrivateSubnet1RTA + PrivateSubnet2RTA
ネットワークS3 VPC Gateway EndpointS3VPCEndpoint
セキュリティALBセキュリティグループALBSecurityGroup
セキュリティEC2セキュリティグループEC2SecurityGroup
セキュリティRDSセキュリティグループRDSSecurityGroup
IAMEC2ロール + インスタンスプロファイルEC2Role + EC2InstanceProfile
パラメータSSM Parameter StoreDBPasswordParam
データベースRDS DBサブネットグループDBSubnetGroup
データベースRDS MySQLインスタンスRDSInstance
ロードバランサーターゲットグループTargetGroup
ロードバランサーALB本体ALB
ロードバランサーリスナーALBListener
コンピュートEC2インスタンスEC2Instance

template.yaml(完全版)

このファイルをそのまま使ってハンズオンを実施できます。C:\my-aws\aws-learning-projects\alb-ec2-rds\template.yaml として保存してください。

AWSTemplateFormatVersion: '2010-09-09'
Description: 'ALB + EC2(Tomcat) + RDS 3-tier architecture hands-on (Phase 2-2)'

Parameters:
  EmployeeId:
    Type: String
    Default: '123456'
    Description: Employee ID used as a prefix for resource names

  KeyName:
    Type: String
    Default: 'my-alb-key'
    Description: Name of an existing EC2 KeyPair

  MyIP:
    Type: String
    Default: '203.0.113.1/32'    # [IMPORTANT] Replace with your own IP (check: curl https://checkip.amazonaws.com)
    Description: Your IP address to allow SSH access (CIDR format)

  DBPassword:
    Type: String
    Default: 'Handson1234!'
    NoEcho: true
    Description: Master password for RDS MySQL (also stored in SSM Parameter Store)

  TomcatVersion:
    Type: String
    Default: '10.1.28'
    Description: Apache Tomcat version (check latest at https://archive.apache.org/dist/tomcat/tomcat-10/)

Resources:

  # VPC and Network
  # ============================================================

  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-alb-vpc'
        - Key: Cost
          Value: !Ref EmployeeId

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-alb-igw'
        - Key: Cost
          Value: !Ref EmployeeId

  IGWAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  # Public Subnet 1 (AZ-a): ALB + EC2 Tomcat
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: !Select [0, !GetAZs '']
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-alb-public-subnet-1'
        - Key: Cost
          Value: !Ref EmployeeId

  # Public Subnet 2 (AZ-b): ALB requires subnets in at least 2 AZs
  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.2.0/24
      AvailabilityZone: !Select [1, !GetAZs '']
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-alb-public-subnet-2'
        - Key: Cost
          Value: !Ref EmployeeId

  # Private Subnet 1 (AZ-a): RDS instance is placed here
  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.3.0/24
      AvailabilityZone: !Select [0, !GetAZs '']
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-alb-private-subnet-1'
        - Key: Cost
          Value: !Ref EmployeeId

  # Private Subnet 2 (AZ-b): required for RDS Subnet Group (minimum 2 AZs)
  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.4.0/24
      AvailabilityZone: !Select [1, !GetAZs '']
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-alb-private-subnet-2'
        - Key: Cost
          Value: !Ref EmployeeId

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-alb-public-rt'
        - Key: Cost
          Value: !Ref EmployeeId

  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: IGWAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable

  PublicSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRouteTable

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-alb-private-rt'
        - Key: Cost
          Value: !Ref EmployeeId

  PrivateSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateRouteTable

  PrivateSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet2
      RouteTableId: !Ref PrivateRouteTable

  # S3 VPC Gateway Endpoint: free, allows EC2 to reach S3 for dnf package downloads
  S3VPCEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref VPC
      ServiceName: !Sub 'com.amazonaws.${AWS::Region}.s3'
      VpcEndpointType: Gateway
      RouteTableIds:
        - !Ref PublicRouteTable
        - !Ref PrivateRouteTable

  # ============================================================
  # IAM
  # ============================================================

  EC2Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub 'n${EmployeeId}-alb-ec2-role'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: 'sts:AssumeRole'
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
        - arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-alb-ec2-role'
        - Key: Cost
          Value: !Ref EmployeeId

  EC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: !Sub 'n${EmployeeId}-alb-ec2-profile'
      Roles:
        - !Ref EC2Role

  # ============================================================
  # Security Groups
  # ============================================================

  # ALB Security Group: accept HTTP:80 from the internet
  ALBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: !Sub 'n${EmployeeId} ALB SG - HTTP:80 from internet'
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
          Description: HTTP from internet
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-alb-sg'
        - Key: Cost
          Value: !Ref EmployeeId

  # EC2 Security Group: Tomcat:8080 from ALB SG only, SSH from MyIP
  EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: !Sub 'n${EmployeeId} EC2 Tomcat SG'
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 8080
          ToPort: 8080
          SourceSecurityGroupId: !GetAtt ALBSecurityGroup.GroupId
          Description: Tomcat from ALB SG only (SG-to-SG control)
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref MyIP
          Description: SSH from my IP
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-alb-ec2-sg'
        - Key: Cost
          Value: !Ref EmployeeId

  # RDS Security Group: MySQL:3306 from EC2 SG only
  RDSSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: !Sub 'n${EmployeeId} RDS MySQL SG (EC2-to-RDS only)'
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          SourceSecurityGroupId: !GetAtt EC2SecurityGroup.GroupId
          Description: MySQL from EC2 SG only (SG-to-SG control)
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-alb-rds-sg'
        - Key: Cost
          Value: !Ref EmployeeId

  # ============================================================
  # SSM Parameter Store
  # NOTE: CloudFormation can only create String type, not SecureString.
  # ============================================================
  DBPasswordParam:
    Type: AWS::SSM::Parameter
    Properties:
      Name: !Sub '/n${EmployeeId}/alb/db-password'
      Type: String
      Value: !Ref DBPassword
      Description: !Sub 'RDS MySQL master password for n${EmployeeId} ALB hands-on'
      Tags:
        Cost: !Ref EmployeeId

  # ============================================================
  # RDS
  # ============================================================

  DBSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: !Sub 'n${EmployeeId} RDS subnet group (private subnets in 2 AZs)'
      SubnetIds:
        - !Ref PrivateSubnet1
        - !Ref PrivateSubnet2
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-alb-rds-subnet-group'
        - Key: Cost
          Value: !Ref EmployeeId

  RDSInstance:
    Type: AWS::RDS::DBInstance
    DeletionPolicy: Delete
    Properties:
      DBInstanceIdentifier: !Sub 'n${EmployeeId}-alb-rds-mysql'
      DBInstanceClass: db.t3.micro
      Engine: mysql
      EngineVersion: '8.0'
      MasterUsername: admin
      MasterUserPassword: !Ref DBPassword
      AllocatedStorage: '20'
      StorageType: gp2
      DBName: sampledb
      DBSubnetGroupName: !Ref DBSubnetGroup
      VPCSecurityGroups:
        - !GetAtt RDSSecurityGroup.GroupId
      MultiAZ: false
      PubliclyAccessible: false
      BackupRetentionPeriod: 0
      DeleteAutomatedBackups: true
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-alb-rds-mysql'
        - Key: Cost
          Value: !Ref EmployeeId

  # ============================================================
  # ALB (Application Load Balancer)
  # ============================================================

  # Target Group: routes traffic to EC2 Tomcat on port 8080
  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: !Sub 'n${EmployeeId}-alb-tg'
      Protocol: HTTP
      Port: 8080
      VpcId: !Ref VPC
      TargetType: instance
      HealthCheckProtocol: HTTP
      HealthCheckPort: '8080'
      HealthCheckPath: /
      HealthyThresholdCount: 2
      UnhealthyThresholdCount: 3
      HealthCheckIntervalSeconds: 30
      HealthCheckTimeoutSeconds: 5
      Targets:
        - Id: !Ref EC2Instance
          Port: 8080
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-alb-tg'
        - Key: Cost
          Value: !Ref EmployeeId

  # ALB: internet-facing, spans 2 public subnets
  ALB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: !Sub 'n${EmployeeId}-alb-alb'
      Type: application
      Scheme: internet-facing
      IpAddressType: ipv4
      Subnets:
        - !Ref PublicSubnet1
        - !Ref PublicSubnet2
      SecurityGroups:
        - !GetAtt ALBSecurityGroup.GroupId
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-alb-alb'
        - Key: Cost
          Value: !Ref EmployeeId

  # Listener: HTTP:80 -> forward to TargetGroup
  ALBListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref ALB
      Protocol: HTTP
      Port: 80
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroup

  # ============================================================
  # EC2 (AP Server with Tomcat)
  # ============================================================

  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64}}'
      InstanceType: t2.micro
      KeyName: !Ref KeyName
      SubnetId: !Ref PublicSubnet1
      SecurityGroupIds:
        - !GetAtt EC2SecurityGroup.GroupId
      IamInstanceProfile: !Ref EC2InstanceProfile
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          set -xe
          exec > >(tee /var/log/user-data.log) 2>&1

          dnf update -y
          dnf install -y java-17-amazon-corretto wget

          # Install Tomcat
          # Check latest version at: https://archive.apache.org/dist/tomcat/tomcat-10/
          TOMCAT_VERSION="${TomcatVersion}"
          cd /opt
          wget -q https://archive.apache.org/dist/tomcat/tomcat-10/v${!TOMCAT_VERSION}/bin/apache-tomcat-${!TOMCAT_VERSION}.tar.gz
          tar xzf apache-tomcat-${!TOMCAT_VERSION}.tar.gz
          mv apache-tomcat-${!TOMCAT_VERSION} tomcat
          chmod +x /opt/tomcat/bin/*.sh

          # Deploy simple web application to ROOT context
          cat > /opt/tomcat/webapps/ROOT/index.html << 'HTMLEOF'
          <!DOCTYPE html>
          <html>
          <body>
          <h1>AP Server - Phase 2-2 ALB + Tomcat + RDS</h1>
          <p>This server is load balanced by ALB and connects to RDS MySQL in the private subnet.</p>
          </body>
          </html>
          HTMLEOF

          # Start Tomcat
          /opt/tomcat/bin/startup.sh
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-alb-ap-instance'
        - Key: Cost
          Value: !Ref EmployeeId

Outputs:
  ALBEndpoint:
    Description: ALB DNS name - access via browser
    Value: !Sub 'http://${ALB.DNSName}'

  EC2PublicIP:
    Description: EC2 Tomcat server public IP (for SSH)
    Value: !GetAtt EC2Instance.PublicIp

  RDSEndpoint:
    Description: RDS MySQL endpoint hostname
    Value: !GetAtt RDSInstance.Endpoint.Address

  DBPasswordParamName:
    Description: SSM Parameter Store path for RDS password
    Value: !Ref DBPasswordParam

  SSHCommand:
    Description: SSH command to EC2 AP server
    Value: !Sub 'ssh -i C:\Users\username\.ssh\${KeyName}.pem ec2-user@${EC2Instance.PublicIp}'

  MySQLConnectCommand:
    Description: MySQL connect command (run FROM the EC2 AP server)
    Value: !Sub 'mysql -h ${RDSInstance.Endpoint.Address} -u admin -pHandson1234! sampledb'

作業順序

⓪ 自分のIPアドレスを確認する
      ↓
① キーペアを作成する(コンソールで実施)
      ↓
② プロジェクトフォルダに移動する
      ↓
③ スタックを作成する(aws cloudformation create-stack)
      ↓
④ 作成完了・Outputsを確認する(RDSは15〜20分かかる)
      ↓
⑤ 動作確認(ALBアクセス・SSH・RDS接続)
      ↓
⑥ スタックを削除する(aws cloudformation delete-stack)

【重要】⓪ 自分のIPアドレスを確認する

curl https://checkip.amazonaws.com

控えておく情報: 自分のIPアドレス(例: 203.0.113.1


① キーペアの作成(コンソールで実施)

CloudFormationではキーペアのダウンロードができないため、コンソールで先に作成します。

AWSコンソール → EC2 → キーペア → 「キーペアを作成」

設定項目
名前my-alb-key
キーペアのタイプRSA
プライベートキーファイル形式.pem

ダウンロードされた my-alb-key.pem を保存します。

C:\Users\ユーザー名\.ssh\my-alb-key.pem

② プロジェクトフォルダに移動する

VSCodeのターミナル(CMD)を開き、プロジェクトフォルダに移動します。

cd C:\my-aws\aws-learning-projects\alb-ec2-rds

template.yaml があることを確認します。

dir template.yaml

③ スタックの作成

以下のコマンドを実行します。203.0.113.1 は⓪で確認した自分のIPアドレスに置き換えます。

aws cloudformation create-stack ^
  --stack-name my-alb-stack ^
  --template-body file://template.yaml ^
  --region ap-northeast-1 ^
  --capabilities CAPABILITY_NAMED_IAM ^
  --parameters ^
    ParameterKey=EmployeeId,ParameterValue=123456 ^
    ParameterKey=KeyName,ParameterValue=my-alb-key ^
    ParameterKey=MyIP,ParameterValue=203.0.113.1/32 ^
    ParameterKey=DBPassword,ParameterValue=Handson1234!

TomcatVersion パラメータについて: デフォルト値は 10.1.28。変更する場合は ParameterKey=TomcatVersion,ParameterValue=10.1.xx を追加します。利用可能なバージョンは https://archive.apache.org/dist/tomcat/tomcat-10/ で確認します。

CAPABILITY_NAMED_IAM について: EC2Role のように名前を指定したIAMリソースを作成する場合に必要なフラグです。

成功すると以下のような StackId が表示されます。

{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:123456789012:stack/my-alb-stack/xxxxx"
}

④ 作成完了・Outputsの確認

注意: RDSの作成には約15〜20分かかります。CREATE_IN_PROGRESS が続く間は正常です。

作成状況の確認

aws cloudformation describe-stacks ^
  --stack-name my-alb-stack ^
  --query "Stacks[0].StackStatus" ^
  --output text
ステータス意味
CREATE_IN_PROGRESS作成中(待つ)
CREATE_COMPLETE作成完了
CREATE_FAILED作成失敗(後述のトラブルシューティングを参照)
ROLLBACK_COMPLETE失敗してロールバック完了

Outputs(接続情報)の確認

CREATE_COMPLETE になったら以下を実行します。

aws cloudformation describe-stacks ^
  --stack-name my-alb-stack ^
  --query "Stacks[0].Outputs" ^
  --output table
+----------------------+------------------------------------------------------------------+
|  OutputKey           |  OutputValue                                                     |
+----------------------+------------------------------------------------------------------+
|  ALBEndpoint         |  http://my-alb-alb-1234567890.ap-northeast-1.elb.amazonaws.com  |
|  EC2PublicIP         |  x.x.x.x                                                        |
|  RDSEndpoint         |  my-alb-rds-mysql.xxxxx.ap-northeast-1.rds.amazonaws.com        |
|  DBPasswordParamName |  /my/alb/db-password                                             |
|  SSHCommand          |  ssh -i C:\Users\...\.ssh\my-alb-key.pem ec2-user@x.x.x.x       |
+----------------------+------------------------------------------------------------------+

控えておく情報: ALBEndpointEC2PublicIPRDSEndpoint


⑤ 動作確認

ALBのヘルスチェック確認

EC2 → ターゲットグループ → my-alb-tg → 「ターゲット」タブ

EC2のステータスが 「healthy」 になるまで待ちます(EC2起動後5〜10分)。

Tomcatの起動はEC2が「実行中」になってからさらに数分かかります。initialunhealthy が続く場合はしばらく待ちます。

ALB経由でWebアクセス確認

Outputsに表示された ALBEndpoint をブラウザで開きます。

http://(ALBEndpointのDNS名)

「AP Server - Phase 2-2 ALB + Tomcat + RDS」と表示されれば正常です。

EC2にSSH接続する

Outputsの SSHCommand を実行します(パスは実際のパスに修正します)。

ssh -i C:\Users\ユーザー名\.ssh\my-alb-key.pem ec2-user@(EC2PublicIP)

Parameter StoreからDBパスワードを取得する

EC2に接続した状態で実行します。

aws ssm get-parameter \
  --name "/my/alb/db-password" \
  --query "Parameter.Value" \
  --output text \
  --region ap-northeast-1

Handson1234! が表示されれば成功です。

--with-decryption が不要な理由: CloudFormationで作成したパラメータは Type: String(平文)のため、フラグなしでそのまま値が返ります。コンソールで SecureString として作成した場合は --with-decryption が必要です(コンソール版参照)。

RDSへのMySQL接続テスト

# RDSエンドポイントをOutputsの値に置き換える
RDS_ENDPOINT="my-alb-rds-mysql.xxxxxxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com"

# Parameter StoreからDBパスワードを取得して接続(String型のため--with-decryption不要)
DB_PASSWORD=$(aws ssm get-parameter \
  --name "/my/alb/db-password" \
  --query "Parameter.Value" \
  --output text \
  --region ap-northeast-1)

mysql -h $RDS_ENDPOINT -u admin -p"$DB_PASSWORD" sampledb

接続成功後、SQL操作を確認します。

-- テーブル作成
CREATE TABLE items (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(100),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- データ挿入
INSERT INTO items (name) VALUES ('Apple'), ('Banana');

-- データ確認
SELECT * FROM items;

-- 後片付け
DROP TABLE items;

EXIT;

EC2からSSH接続を切断します。

exit

⑥ スタックの削除

課金を止めるために、ハンズオン完了後は必ず削除します。

aws cloudformation delete-stack ^
  --stack-name my-alb-stack ^
  --region ap-northeast-1

注意: RDS削除には約10〜15分かかります。コマンド実行後もしばらく待つ必要があります。

削除状況の確認

aws cloudformation describe-stacks ^
  --stack-name my-alb-stack ^
  --query "Stacks[0].StackStatus" ^
  --output text
ステータス意味
DELETE_IN_PROGRESS削除中(待つ)
DELETE_FAILED削除失敗(トラブルシューティングを参照)

削除完了の確認(以下のエラーが表示されれば削除完了):

aws cloudformation describe-stacks ^
  --stack-name my-alb-stack ^
  --region ap-northeast-1
An error occurred (ValidationError) when calling the DescribeStacks operation:
Stack with id my-alb-stack does not exist

キーペアの手動削除

キーペアはCloudFormationで管理していないため手動で削除します。

EC2 → キーペア → my-alb-key を選択 → 「アクション」→「削除」


コンソール版との比較

作業コンソール(手動)CloudFormation
VPC + サブネット4つ + IGW + ルートテーブル約20〜30分template.yaml に定義済み
SG 3つ(ALB/EC2/RDS)の作成3画面で個別設定template.yaml に定義済み
ALB + TG + Listener の作成3つの画面で設定template.yaml に定義済み
Tomcat UserDataの貼り付け手動コピペtemplate.yaml に組み込み済み
全体のデプロイ約40〜50分コマンド1本(RDS待ち15〜20分)
削除13ステップ手動delete-stack 1本

トラブルシューティング

症状原因対処
CREATE_FAILED → ROLLBACKになるパラメータ不正・リソース上限などコンソールのCloudFormationイベントタブでエラー詳細を確認
ALBに繋がらないEC2のTomcatがまだ起動中TGのヘルスチェックがHealthyになるまで待つ(5〜10分)
ALBに繋がるが502エラーTomcat起動失敗SSH接続して sudo cat /var/log/user-data.log 確認
ERROR 2003: MySQL接続できないRDS SGのソース設定ミスRDS SGのインバウンドルールを確認
Tomcatバージョンエラー指定バージョンがアーカイブに存在しないhttps://archive.apache.org/dist/tomcat/tomcat-10/ で確認して TomcatVersion パラメータを更新
DELETE_FAILEDリソースの依存関係が残っているコンソールからALBを手動削除してから delete-stack を再実行
スタック作成後しばらく CREATE_IN_PROGRESS が続くRDSの作成に時間がかかっている正常。約15〜20分待つ

失敗時の詳細確認コマンド

aws cloudformation describe-stack-events ^
  --stack-name my-alb-stack ^
  --query "StackEvents[?ResourceStatus=='CREATE_FAILED'].[ResourceType,ResourceStatusReason]" ^
  --output table

まとめ

ステップ内容
キーペアをコンソールで作成
②③alb-ec2-rds/ フォルダに移動し create-stack を実行
CREATE_COMPLETE になったらOutputsでALBエンドポイント・EC2IP・RDSエンドポイントを確認
ヘルスチェックがHealthyになったらALB → EC2 → RDS MySQL への接続テスト
delete-stack 1本で27リソースを一括削除

CloudFormation版の最大のメリットは再現性削除の簡単さです。コンソールでは40〜50分・13ステップかかった作業が、コマンド1本(待ち時間込みで約20分)で完了します。

コンソール操作でALB・ターゲットグループ・ヘルスチェックを視覚的に確認したい場合は、AWSコンソール版ハンズオンを参照してください。

コメント