はじめに
「コンソールで手動構築した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.yaml1ファイルで一括管理 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で管理可能 | 不可 |
キーワード解説
| 用語 | 意味 |
|---|---|
| CloudFormation | AWSが提供するIaC(Infrastructure as Code)サービス。YAMLテンプレートでリソースをコード化する |
| スタック | CloudFormationが管理するリソースのまとまり。今回はEC2×2・SG×2・IAMロールを1スタックで管理 |
| SG参照(SG-to-SG) | セキュリティグループのルールで、アクセス元として別のSGを指定する設定 |
SourceSecurityGroupId | CloudFormationでSG参照を設定するプロパティ。!GetAtt ClientSecurityGroup.GroupId でSGのIDを取得して指定する |
!GetAtt | CloudFormation組み込み関数。リソースの属性値を取得する |
| プライベートIP | VPC内でのみ使用できるIPアドレス。EC2間の通信に使う |
前提条件
| ツール | 確認コマンド | 最低バージョン目安 |
|---|---|---|
| AWS CLI v2 | aws --version | 2.x |
| Git | git --version | 2.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)| 方法 | 指定するもの | 特徴 |
|---|---|---|
CidrIp | IPアドレス(/32 など) | IPが変わると設定変更が必要 |
SourceSecurityGroupId | セキュリティグループID | そのSGに属するEC2全体を対象にできる |
!Refvs!GetAtt ... .GroupIdの違い:VpcIdを明示していないSGに対して!RefするとSGの名前が返るためSourceSecurityGroupId(IDを期待)でエラーになります。!GetAtt ClientSecurityGroup.GroupIdを使うことで常にSGのID(sg-xxx)を確実に取得できます。
構築されるリソースと論理ID:
| リソース | template.yaml上の論理ID |
|---|---|
| IAMロール | EC2Role |
| インスタンスプロファイル | EC2InstanceProfile |
| クライアントSG | ClientSecurityGroup |
| DBサーバSG | DBSecurityGroup |
| DBサーバEC2 | DBInstance |
| クライアントEC2 | ClientInstance |
作業順序
⓪ 自分の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-dbtemplate.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 rootMariaDBのプロンプト(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;exit5-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;exit5-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-1An 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-stack | DBサーバ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 decoded | template.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サーバのプライベートIP(DBPrivateIP)を指定する |
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サーバを構築する手順
関連記事
-->
コメント