EC2サーバ構築をIaC化:CloudFormationでEC2 MySQLのDBサーバを自動デプロイする手順【SG参照 / コンソール版との比較付き】

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

はじめに

「コンソールで手動構築したSG参照の構成を、コードで再現したい」——それを実現するのが AWS CloudFormation です。

この記事では、template.yaml 1ファイルにリソース構成を定義し、コマンド1本でEC2 MySQL(MariaDB)環境を一括構築するハンズオンを紹介します。コンソール版(AWSコンソール版ハンズオン)と全く同じ構成を、CloudFormationで自動化します。

ローカル環境(VSCode)
  └── template.yaml + AWS CLI
        ↓ スタック作成(コマンド1本)
AWS環境
  ├── IAMロール(SSM接続用)
  ├── ClientSecurityGroup(クライアントSG)
  │     └── 適用先: クライアントEC2
  ├── DBSecurityGroup(DBサーバSG)
  │     ├── MySQL(3306) ← ClientSecurityGroup を参照  ★SG参照
  │     └── 適用先: DBサーバEC2
  ├── DBサーバEC2(MariaDB)
  └── クライアントEC2(mysqlコマンド)

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

  • SG参照(SG-to-SG)を SourceSecurityGroupId でtemplate.yamlに記述する方法
  • 2台のEC2・2つのSGを template.yaml 1ファイルで一括管理
  • aws cloudformation create-stack コマンド1本での全リソース一括デプロイ
  • delete-stack によるSGの依存関係を自動解決した一括削除

このハンズオンの特徴:

  • コンソール版では順番を意識しながら手動で作成・削除していたSGの依存関係を、CloudFormationが自動管理
  • 2台分のEC2を並列で起動するため、コンソール版より短時間で構築完了

-->

スポンサーリンク

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

比較項目CloudFormationコンソール(手動)
SG×2台の作成template.yamlに定義(順序自動管理)クライアントSG → DBサーバSGの順で手動作成
EC2×2台の起動コマンド1本(並列作成)2回インスタンス起動操作
SG削除(依存関係あり)delete-stack 1本(自動解決)DBサーバSG → クライアントSGの順で手動管理
全リソースのデプロイコマンド1本(5〜8分)複数画面を行き来(20〜30分)
再現性高い(同じ構成を何度でも再現可能)低い(手順ミスのリスク大)
バージョン管理Gitで管理可能不可

スポンサーリンク

キーワード解説

用語意味
CloudFormationAWSが提供するIaC(Infrastructure as Code)サービス。YAMLテンプレートでリソースをコード化する
スタックCloudFormationが管理するリソースのまとまり。今回はEC2×2・SG×2・IAMロールを1スタックで管理
SG参照(SG-to-SG)セキュリティグループのルールで、アクセス元として別のSGを指定する設定
SourceSecurityGroupIdCloudFormationでSG参照を設定するプロパティ。!GetAtt ClientSecurityGroup.GroupId でSGのIDを取得して指定する
!GetAttCloudFormation組み込み関数。リソースの属性値を取得する
プライベートIPVPC内でのみ使用できるIPアドレス。EC2間の通信に使う

前提条件

ツール確認コマンド最低バージョン目安
AWS CLI v2aws --version2.x
Gitgit --version2.x
VSCode--

AWS認証確認:

aws sts get-caller-identity

アカウントIDが表示されれば認証設定済みです。


template.yaml の全文

以下が今回使用する template.yaml の全文です。プロジェクトフォルダ(ec2-mysql-db/)直下に配置します。

⚠️ コピー前に確認: Default: '123.456.78.901/32' の箇所はダミーIPです。このまま使うとスタック作成は成功しますが、SSHもMySQLもアクセスできません。--parameters でIPを上書きするか(推奨)、Defaultを自分のIPに書き換えてから使ってください。

AWSTemplateFormatVersion: '2010-09-09'
Description: 'EC2 + MySQL (MariaDB) DB Server Hands-on (Phase 1-3) - SG-to-SG inter-EC2 communication control'

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

  MyIP:
    Type: String
    Default: '123.456.78.901/32'
    Description: Your IP address to allow SSH and MySQL access (CIDR format e.g. 203.0.113.1/32)

Resources:
  EC2Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: 'my-ec2-mysql-role'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: 'sts:AssumeRole'
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
      Tags:
        - Key: Name
          Value: 'my-ec2-mysql-role'

  # Instance Profile: wrapper that attaches the IAM role to EC2
  EC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: 'my-ec2-mysql-profile'
      Roles:
        - !Ref EC2Role

  # Client Security Group: attached to the AP server (client side)
  # The DB server SG references this SG to allow MySQL access only from this group
  ClientSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: 'my MySQL client SG (AP server role)'
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref MyIP
          Description: SSH from my IP
      Tags:
        - Key: Name
          Value: 'my-ec2-mysql-client-sg'

  # DB Server Security Group: allows MySQL ONLY from the client SG (not from the internet)
  # This is the key concept of SG-to-SG inter-EC2 communication control
  DBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: 'my MySQL DB server SG'
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref MyIP
          Description: SSH from my IP (admin only)
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          SourceSecurityGroupId: !GetAtt ClientSecurityGroup.GroupId
          Description: MySQL from client SG only (EC2-to-EC2 control - NOT open to internet)
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          CidrIp: !Ref MyIP
          Description: MySQL from my IP (for initial testing from local)
      Tags:
        - Key: Name
          Value: 'my-ec2-mysql-db-sg'

  # DB Server EC2: MariaDB (MySQL compatible) installed via UserData
  DBInstance:
    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
      SecurityGroupIds:
        - !Ref DBSecurityGroup
      IamInstanceProfile: !Ref EC2InstanceProfile
      # UserData: install MariaDB and configure initial users and database
      UserData:
        Fn::Base64: |
          #!/bin/bash
          dnf update -y
          dnf install -y mariadb105-server mariadb105

          systemctl start mariadb
          systemctl enable mariadb

          sudo mysql -u root << 'SQLEOF'
          SET PASSWORD FOR root@localhost = PASSWORD('Admin1234!');
          CREATE USER IF NOT EXISTS 'handson'@'%' IDENTIFIED BY 'Handson1234!';
          GRANT ALL PRIVILEGES ON *.* TO 'handson'@'%' WITH GRANT OPTION;
          CREATE DATABASE IF NOT EXISTS sampledb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
          FLUSH PRIVILEGES;
          SQLEOF
      Tags:
        - Key: Name
          Value: 'my-ec2-mysql-db-instance'

  # Client EC2: represents the AP server - has mysql client installed to test DB connection
  ClientInstance:
    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
      SecurityGroupIds:
        - !Ref ClientSecurityGroup
      IamInstanceProfile: !Ref EC2InstanceProfile
      # UserData: install mysql client only (no server)
      UserData:
        Fn::Base64: |
          #!/bin/bash
          dnf update -y
          dnf install -y mariadb105
      Tags:
        - Key: Name
          Value: 'my-ec2-mysql-client-instance'

Outputs:
  DBInstanceId:
    Description: DB server EC2 Instance ID
    Value: !Ref DBInstance

  DBPublicIP:
    Description: DB server public IP (SSH access for admin)
    Value: !GetAtt DBInstance.PublicIp

  DBPrivateIP:
    Description: DB server private IP (use this to connect from client EC2)
    Value: !GetAtt DBInstance.PrivateIp

  ClientInstanceId:
    Description: Client EC2 Instance ID
    Value: !Ref ClientInstance

  ClientPublicIP:
    Description: Client EC2 public IP (SSH to test DB connection)
    Value: !GetAtt ClientInstance.PublicIp

  DBSSHCommand:
    Description: SSH command to connect to DB server
    Value: !Sub 'ssh -i C:\Users\username\.ssh\${KeyName}.pem ec2-user@${DBInstance.PublicIp}'

  ClientSSHCommand:
    Description: SSH command to connect to client EC2
    Value: !Sub 'ssh -i C:\Users\username\.ssh\${KeyName}.pem ec2-user@${ClientInstance.PublicIp}'

  MySQLConnectCommand:
    Description: MySQL connect command (run this from client EC2)
    Value: !Sub 'mysql -h ${DBInstance.PrivateIp} -u handson -pHandson1234! sampledb'

SG参照の書き方:IPアドレス指定との違い:

SecurityGroupIngress:

  # 通常のIPアドレス指定(前回までのやり方)
  - IpProtocol: tcp
    FromPort: 22
    ToPort: 22
    CidrIp: !Ref MyIP           # ← IPアドレスを指定

  # SG参照(このハンズオンの核心)
  - IpProtocol: tcp
    FromPort: 3306
    ToPort: 3306
    SourceSecurityGroupId: !GetAtt ClientSecurityGroup.GroupId  # ← SGのIDを取得
    Description: MySQL from client SG only (EC2-to-EC2 control)
方法指定するもの特徴
CidrIpIPアドレス(/32 など)IPが変わると設定変更が必要
SourceSecurityGroupIdセキュリティグループIDそのSGに属するEC2全体を対象にできる

!Ref vs !GetAtt ... .GroupId の違い: VpcId を明示していないSGに対して !Ref するとSGの名前が返るため SourceSecurityGroupId(IDを期待)でエラーになります。!GetAtt ClientSecurityGroup.GroupId を使うことで常にSGのIDsg-xxx)を確実に取得できます。

構築されるリソースと論理ID:

リソースtemplate.yaml上の論理ID
IAMロールEC2Role
インスタンスプロファイルEC2InstanceProfile
クライアントSGClientSecurityGroup
DBサーバSGDBSecurityGroup
DBサーバEC2DBInstance
クライアントEC2ClientInstance

作業順序

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

⓪ 自分のIPアドレスを確認する

curl https://checkip.amazonaws.com

またはルーター管理画面(http://192.168.0.1 など)の「WAN IPアドレス」で確認します。

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


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

キーペアのみコンソールで作成します。CloudFormationではキーペアのダウンロードが行えないためです。

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

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

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

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

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

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

cd C:\my-aws\aws-learning-projects\ec2-mysql-db

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

dir template.yaml

③ スタックの作成

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

aws cloudformation create-stack ^
  --stack-name my-ec2-mysql-stack ^
  --template-body file://template.yaml ^
  --region ap-northeast-1 ^
  --capabilities CAPABILITY_NAMED_IAM ^
  --parameters ^
    ParameterKey=KeyName,ParameterValue=my-ec2-mysql-key ^
    ParameterKey=MyIP,ParameterValue=203.0.113.1/32

各オプションの説明:

オプション意味
--stack-name my-ec2-mysql-stackスタック名(英字始まり)
--template-body file://template.yaml使用するテンプレートファイル
--region ap-northeast-1デプロイ先リージョン(東京)
--capabilities CAPABILITY_NAMED_IAM名前付きIAMロール作成の明示的な許可
--parameters ...テンプレートのParametersに渡す値

^ について: Windowsのコマンドプロンプトで長いコマンドを複数行に分けるための改行エスケープ文字です。

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

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


④ 作成完了・Outputsの確認

スタック作成は完了まで約5〜8分かかります(EC2×2台の起動 + MariaDBインストールの時間)。

作成状況の確認

aws cloudformation describe-stacks ^
  --stack-name my-ec2-mysql-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-ec2-mysql-stack ^
  --query "Stacks[0].Outputs" ^
  --output table

以下のような出力が表示されます。

---------------------------------------------------------------------------------------
|                                   DescribeStacks                                    |
+---------------------+---------------------------------------------------------------+
|  OutputKey          |  OutputValue                                                  |
+---------------------+---------------------------------------------------------------+
|  DBInstanceId       |  i-0xxxxxxxxxxxxxxx1                                         |
|  DBPublicIP         |  x.x.x.x                                                     |
|  DBPrivateIP        |  172.31.x.x      ← クライアントEC2からの接続に使用            |
|  ClientInstanceId   |  i-0xxxxxxxxxxxxxxx2                                         |
|  ClientPublicIP     |  y.y.y.y                                                     |
|  DBSSHCommand       |  ssh -i ...pem ec2-user@x.x.x.x                             |
|  ClientSSHCommand   |  ssh -i ...pem ec2-user@y.y.y.y                             |
|  MySQLConnectCommand|  mysql -h 172.31.x.x -u handson -pHandson1234! sampledb     |
+---------------------+---------------------------------------------------------------+

控えておく情報:

  • DBPrivateIP: DBサーバのプライベートIP(クライアントEC2からの接続に使う)
  • DBSSHCommand: DBサーバへのSSHコマンド
  • ClientSSHCommand: クライアントEC2へのSSHコマンド
  • MySQLConnectCommand: クライアントEC2から実行するMySQL接続コマンド


⑤ 動作確認

5-1. DBサーバの動作確認

Outputsの DBSSHCommand を実行してDBサーバに接続します。

ssh -i C:\Users\ユーザー名\.ssh\my-ec2-mysql-key.pem ec2-user@(DBPublicIP)
# MariaDBのサービス状態を確認(active (running) と表示されれば正常)
sudo systemctl status mariadb

# rootでログイン
sudo mysql -u root

MariaDBのプロンプト(MariaDB [(none)]>)が表示されたら以下を実行します。

-- ユーザー一覧(handsonユーザーが作成済みか確認)
SELECT User, Host FROM mysql.user;

-- データベース一覧(sampledbが存在するか確認)
SHOW DATABASES;

-- テスト用テーブルとデータを作成
USE sampledb;
CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(50) NOT NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO users (name) VALUES ('Alice'), ('Bob'), ('Charlie');
SELECT * FROM users;

EXIT;
exit

5-2. SG参照の確認:クライアントEC2からMySQL接続

Outputsの ClientSSHCommand を実行してクライアントEC2に接続します。

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

Outputsに表示された MySQLConnectCommand をそのまま実行します。

mysql -h (DBPrivateIP) -u handson -pHandson1234! sampledb

接続成功すると MariaDB [sampledb]> が表示されます。

-- DBサーバで作成したデータが見えることを確認
SELECT * FROM users;

EXIT;
exit

5-3. SG参照の効果を確認する(任意・スキップ可)

前提: ローカルPCに mysql クライアントがインストールされている場合のみ実施します。5-2でクライアントEC2からの接続が成功していれば、このハンズオンの学習目的(SG参照によるEC2間通信制御)は達成済みです。

ローカルPCに mysql コマンドがある場合、ローカルターミナルで以下を実行します。

mysql -h (DBPublicIP) -u handson -pHandson1234! sampledb

本番環境でのベストプラクティス: DBサーバSGからMyIP指定のルールを削除し、SG参照のみにすることで、DBサーバをインターネットから完全に遮断できます。DBサーバにパブリックIPを付与しないことも有効です。


⑥ スタックの削除

課金を止めるために、ハンズオン完了後は必ず削除してください。

CloudFormationはスタック削除で全リソースを一括削除できます。SGの依存関係(DBサーバSG → クライアントSGの削除順序)も自動的に解決されます。

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

削除の進行状況を確認します(完了まで約5〜8分)。

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

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

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

このメッセージが表示されれば削除完了です。キーペアはCloudFormationで管理していないため、手動で削除します。

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

ローカルの .pem ファイルも削除します。


コンソール版との比較

コンソール版でSGを手動で依存関係順に作成・削除していた手順が、CloudFormationではすべて自動管理されます。

CFnの記述コンソールでやること
ClientSecurityGroup(クライアントSG定義)EC2 → SGを作成(SSH:22ルール)
DBSecurityGroup(DB SG定義・SG参照)EC2 → SGを作成(SSH:22 + MySQL:3306のSG参照ルール)
DBInstance(DBサーバEC2・UserData)EC2 → インスタンス起動(UserData貼り付け・DB SGを選択)
ClientInstance(クライアントEC2)EC2 → インスタンス起動(クライアント SGを選択)
delete-stackDBサーバSG → クライアントSG → IAMロール(順番管理が必要)

CloudFormationのSG削除順序の自動管理: コンソール版ではDBサーバSG → クライアントSGの順で手動削除が必要でしたが、CloudFormationはリソース間の依存関係を自動的に解決して正しい順序で削除します。


トラブルシューティング

症状原因対処
CREATE_FAILED になるキーペアが存在しないコンソールでキーペアを作成し、KeyName パラメータを確認
CREATE_FAILED になる同名のIAMロールが既に存在するコンソールでロールを削除してから再実行
stackName failed to satisfy regular expression patternスタック名が数字始まりCloudFormationのスタック名は英字で始まる必要がある
Unable to load paramfile, text contents could not be decodedtemplate.yaml に日本語が含まれているtemplate.yaml のコメントは英語のみで記述する(Windows環境の既知の問題)
クライアントEC2からMySQLに接続できないMariaDBのセットアップがまだ完了していないCREATE_COMPLETE 後2〜3分待ってから再試行。DBサーバで sudo systemctl status mariadb を確認
ERROR 2003: Can't connect to MySQL serverパブリックIPを指定しているDBサーバのプライベートIPDBPrivateIP)を指定する
DELETE_FAILED になる手動でリソースを変更したためCFnが管理できないコンソールで該当リソースを確認して手動削除後、delete-stack を再実行

まとめ

今回のハンズオンで実現できたこと:

確認項目内容
SG参照のIaC化SourceSecurityGroupId: !GetAtt ClientSecurityGroup.GroupId でSG参照をコードで表現
複数リソースの一括管理EC2×2・SG×2・IAMロールを1ファイルで定義・一括デプロイ
Outputs活用DBPrivateIP・MySQLConnectCommandを describe-stacks で自動取得
依存関係の自動解決SGの削除順序をCloudFormationが自動管理(コンソール版では手動)

CloudFormationのメリットを実感できたポイント

  • コンソール版では複数リソースの作成・削除に順番の意識が必要だったが、CloudFormationが自動管理
  • MySQLConnectCommand がOutputsとして自動生成されるため、手動でコマンドを組み立てる必要がない
  • template.yaml をGitで管理することで、SG参照を含む複雑な構成の変更履歴が残せる

コンソール版と比較してみる

CloudFormationが裏で何をやっているか、同じ構成をAWSコンソールのみで構築する手順をまとめました。特にSGの依存関係管理とSG参照の設定手順を視覚的に確認できます。

EC2サーバ構築ハンズオン:AWSコンソールでEC2 MySQLのDBサーバを構築する手順


関連記事

-->

コメント