<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>AWSCLI - CayTech Lab</title>
	<atom:link href="https://caymezon.com/tag/awscli/feed/" rel="self" type="application/rss+xml" />
	<link>https://caymezon.com</link>
	<description></description>
	<lastBuildDate>Mon, 13 Apr 2026 00:09:05 +0000</lastBuildDate>
	<language>ja</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0</generator>

<image>
	<url>https://caymezon.com/wp-content/uploads/2026/01/cropped-CayTechLab-32x32.jpg</url>
	<title>AWSCLI - CayTech Lab</title>
	<link>https://caymezon.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<atom:link rel='hub' href='https://caymezon.com/?pushpress=hub'/>
	<item>
		<title>CloudFormationでALB + EC2(Tomcat) + RDS 3層構成を自動構築する手順【27リソース一括デプロイ / コンソール版との比較付き】</title>
		<link>https://caymezon.com/aws-handson-cloudformation-alb-ec2-rds/</link>
					<comments>https://caymezon.com/aws-handson-cloudformation-alb-ec2-rds/#respond</comments>
		
		<dc:creator><![CDATA[caymezon]]></dc:creator>
		<pubDate>Mon, 13 Apr 2026 00:09:05 +0000</pubDate>
				<category><![CDATA[AWS Basic]]></category>
		<category><![CDATA[Cloud & Infra]]></category>
		<category><![CDATA[3層構成]]></category>
		<category><![CDATA[ALB]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[AWSCLI]]></category>
		<category><![CDATA[CloudFormation]]></category>
		<category><![CDATA[EC2]]></category>
		<category><![CDATA[IaC]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Parameter Store]]></category>
		<category><![CDATA[RDS]]></category>
		<category><![CDATA[SSM]]></category>
		<category><![CDATA[Tomcat]]></category>
		<category><![CDATA[VPC]]></category>
		<category><![CDATA[ターゲットグループ]]></category>
		<category><![CDATA[ハンズオン]]></category>
		<category><![CDATA[ヘルスチェック]]></category>
		<category><![CDATA[ロードバランサー]]></category>
		<category><![CDATA[初心者]]></category>
		<guid isPermaLink="false">https://caymezon.com/?p=20356</guid>

					<description><![CDATA[<p>目次 はじめにCloudFormation vs コンソール：どれだけ違うかキーワード解説前提条件構築されるリソース（約27個）template.yaml（完全版）作業順序【重要】⓪ 自分のIPアドレスを確認する① キー [&#8230;]</p>
<p>The post <a href="https://caymezon.com/aws-handson-cloudformation-alb-ec2-rds/">CloudFormationでALB + EC2(Tomcat) + RDS 3層構成を自動構築する手順【27リソース一括デプロイ / コンソール版との比較付き】</a> first appeared on <a href="https://caymezon.com">CayTech Lab</a>.</p>]]></description>
										<content:encoded><![CDATA[<div id="toc" class="toc tnt-number toc-center tnt-number border-element"><input type="checkbox" class="toc-checkbox" id="toc-checkbox-2" checked><label class="toc-title" for="toc-checkbox-2">目次</label>
    <div class="toc-content">
    <ol class="toc-list open"><li><a href="#toc1" tabindex="0">はじめに</a></li><li><a href="#toc2" tabindex="0">CloudFormation vs コンソール：どれだけ違うか</a></li><li><a href="#toc3" tabindex="0">キーワード解説</a></li><li><a href="#toc4" tabindex="0">前提条件</a></li><li><a href="#toc5" tabindex="0">構築されるリソース（約27個）</a></li><li><a href="#toc6" tabindex="0">template.yaml（完全版）</a></li><li><a href="#toc7" tabindex="0">作業順序</a></li><li><a href="#toc8" tabindex="0">【重要】⓪ 自分のIPアドレスを確認する</a></li><li><a href="#toc9" tabindex="0">① キーペアの作成（コンソールで実施）</a></li><li><a href="#toc10" tabindex="0">② プロジェクトフォルダに移動する</a></li><li><a href="#toc11" tabindex="0">③ スタックの作成</a></li><li><a href="#toc12" tabindex="0">④ 作成完了・Outputsの確認</a><ol><li><a href="#toc13" tabindex="0">作成状況の確認</a></li><li><a href="#toc14" tabindex="0">Outputs（接続情報）の確認</a></li></ol></li><li><a href="#toc15" tabindex="0">⑤ 動作確認</a><ol><li><a href="#toc16" tabindex="0">ALBのヘルスチェック確認</a></li><li><a href="#toc17" tabindex="0">ALB経由でWebアクセス確認</a></li><li><a href="#toc18" tabindex="0">EC2にSSH接続する</a></li><li><a href="#toc19" tabindex="0">Parameter StoreからDBパスワードを取得する</a></li><li><a href="#toc20" tabindex="0">RDSへのMySQL接続テスト</a></li></ol></li><li><a href="#toc21" tabindex="0">⑥ スタックの削除</a><ol><li><a href="#toc22" tabindex="0">削除状況の確認</a></li><li><a href="#toc23" tabindex="0">キーペアの手動削除</a></li></ol></li><li><a href="#toc24" tabindex="0">コンソール版との比較</a></li><li><a href="#toc25" tabindex="0">トラブルシューティング</a><ol><li><a href="#toc26" tabindex="0">失敗時の詳細確認コマンド</a></li></ol></li><li><a href="#toc27" tabindex="0">まとめ</a></li></ol>
    </div>
  </div>

<h2><span id="toc1">はじめに</span></h2>
<p>「コンソールで40〜50分かかったALB + EC2(Tomcat) + RDS環境をコードで再現したい」——それを実現するのが <strong>AWS CloudFormation</strong> です。</p>
<p>この記事では、<code>template.yaml</code> 1ファイルにVPC・サブネット・セキュリティグループ・IAMロール・SSM Parameter Store・RDS・ALB・ターゲットグループ・EC2（約27リソース）を定義し、<strong>コマンド1本で3層構成を一括構築するハンズオン</strong>を紹介します。コンソール版（<a href="https://caymezon.com/aws-handson-console-alb-ec2-rds/">AWSコンソール版ハンズオン</a>）と全く同じ構成をCloudFormationで自動化します。</p>
<pre><code class="language-plaintext">ローカル環境（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で自動セットアップ）</code></pre>
<p><strong>このハンズオンで体験できること：</strong></p>
<ul>
<li>ALB・ターゲットグループ・リスナーを <code>template.yaml</code> で一括定義する方法</li>
<li><code>TomcatVersion</code> パラメータでTomcatのバージョンを切り替える設計</li>
<li>ALBの2AZ要件をCloudFormationでどう表現するか</li>
<li><code>delete-stack</code> 1本でALB・RDS・IAMなど27リソースを自動削除</li>
</ul>
<p><strong>このハンズオンの特徴：</strong></p>
<ul>
<li>コンソール版では40〜50分・13ステップかかった作業が、コマンド1本（RDS待ち15〜20分込み）で完了</li>
<li><code>delete-stack</code> 1本でALBのリスナー含む全リソースの依存関係を自動解決して削除</li>
</ul>
<hr>
<blockquote>
<p><strong>この記事は <a href="https://caymezon.com/aws-handson-console-alb-ec2-rds/">AWSコンソール版ハンズオン</a> の比較記事です。</strong><br />コンソール版でALB・ターゲットグループ・ヘルスチェックを視覚的に学んだ後に読むと理解が深まります。</p>
</blockquote>
<hr>
<p><!-- ![CloudFormationスタック作成完了・Outputs確認画面](images/cfn-create-complete.jpg) --></p>
<p><!-- <a rel="nofollow" href="//af.moshimo.com/af/c/click?a_id=1384942&p_id=170&pc_id=185&pl_id=4062&url=https%3A%2F%2Fwww.amazon.co.jp%2Fs%3Fk%3D%25E6%259C%25AC%2BAWS%2B%25E9%2596%258B%25E7%2599%25BA%26__mk_ja_JP%3D%25E3%2582%25AB%25E3%2582%25BF%25E3%2582%25AB%25E3%2583%258A%26crid%3D1DE63UBHFOR4K%26sprefix%3D%25E6%259C%25AC%2Baws%2B%25E9%2596%258B%25E7%2599%25BA%252Caps%252C167%26ref%3Dnb_sb_noss" referrerpolicy="no-referrer-when-downgrade" attributionsrc>Amazon検索[本 AWS 開発]</a><img decoding="async" src="//i.moshimo.com/af/i/impression?a_id=1384942&p_id=170&pc_id=185&pl_id=4062" width="1" height="1" style="border:none;" alt="" loading="lazy"> --></p>
<p><!-- <!-- START MoshimoAffiliateEasyLink --><script type="text/javascript">(function(b,c,f,g,a,d,e){b.MoshimoAffiliateObject=a;b[a]=b[a]||function(){arguments.currentScript=c.currentScript||c.scripts[c.scripts.length-2];(b[a].q=b[a].q||[]).push(arguments)};c.getElementById(a)||(d=c.createElement(f),d.src=g,d.id=a,e=c.getElementsByTagName("body")[0],e.appendChild(d))})(window,document,"script","//dn.msmstatic.com/site/cardlink/bundle.js?20220329","msmaflink");msmaflink({"n":"AWSの基本・仕組み・重要用語が全部わかる教科書 (見るだけ図解)","b":"SBクリエイティブ","t":"","d":"https:\/\/m.media-amazon.com","c_p":"\/images\/I","p":["\/51DEDQXj6oL._SL500_.jpg","\/41F589smNwL._SL500_.jpg","\/41R6f9yyCWL._SL500_.jpg","\/41HqWQ9BvmL._SL500_.jpg","\/41p8p0ZU79L._SL500_.jpg","\/41qLC-fndBL._SL500_.jpg","\/41fcLv9VT5L._SL500_.jpg","\/51lRvCsvHqL._SL500_.jpg"],"u":{"u":"https:\/\/www.amazon.co.jp\/dp\/4815607850","t":"amazon","r_v":""},"v":"2.1","b_l":[{"id":1,"u_tx":"Amazonで見る","u_bc":"#f79256","u_url":"https:\/\/www.amazon.co.jp\/dp\/4815607850","a_id":1384942,"p_id":170,"pl_id":27060,"pc_id":185,"s_n":"amazon","u_so":1},{"id":2,"u_tx":"楽天市場で見る","u_bc":"#f76956","u_url":"https:\/\/search.rakuten.co.jp\/search\/mall\/AWS%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%83%BB%E4%BB%95%E7%B5%84%E3%81%BF%E3%83%BB%E9%87%8D%E8%A6%81%E7%94%A8%E8%AA%9E%E3%81%8C%E5%85%A8%E9%83%A8%E3%82%8F%E3%81%8B%E3%82%8B%E6%95%99%E7%A7%91%E6%9B%B8%20(%E8%A6%8B%E3%82%8B%E3%81%A0%E3%81%91%E5%9B%B3%E8%A7%A3)\/","a_id":1384917,"p_id":54,"pl_id":27059,"pc_id":54,"s_n":"rakuten","u_so":2},{"id":3,"u_tx":"Yahoo!ショッピングで見る","u_bc":"#66a7ff","u_url":"https:\/\/shopping.yahoo.co.jp\/search?first=1\u0026p=AWS%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%83%BB%E4%BB%95%E7%B5%84%E3%81%BF%E3%83%BB%E9%87%8D%E8%A6%81%E7%94%A8%E8%AA%9E%E3%81%8C%E5%85%A8%E9%83%A8%E3%82%8F%E3%81%8B%E3%82%8B%E6%95%99%E7%A7%91%E6%9B%B8%20(%E8%A6%8B%E3%82%8B%E3%81%A0%E3%81%91%E5%9B%B3%E8%A7%A3)","a_id":1466950,"p_id":1225,"pl_id":27061,"pc_id":1925,"s_n":"yahoo","u_so":3}],"eid":"eaCUB","s":"s"});</script></p>
<div id="msmaflink-eaCUB">リンク</div>
<p><!-- MoshimoAffiliateEasyLink END --> --></p>
<h2><span id="toc2">CloudFormation vs コンソール：どれだけ違うか</span></h2>
<table>
<thead>
<tr>
<th>比較項目</th>
<th>CloudFormation</th>
<th>コンソール（手動）</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>VPC + サブネット4つ + IGW + ルートテーブル</strong></td>
<td>template.yaml に定義済み</td>
<td>約20〜30分・複数画面</td>
</tr>
<tr>
<td><strong>SG 3つ（ALB/EC2/RDS）の作成</strong></td>
<td>template.yaml に定義済み</td>
<td>3画面で個別設定</td>
</tr>
<tr>
<td><strong>ALB + TG + Listener の作成</strong></td>
<td>template.yaml に定義済み</td>
<td>3つの画面で設定</td>
</tr>
<tr>
<td><strong>Tomcat UserDataの貼り付け</strong></td>
<td>template.yaml に組み込み済み</td>
<td>手動コピペ</td>
</tr>
<tr>
<td><strong>全体のデプロイ</strong></td>
<td>コマンド1本（RDS待ち15〜20分）</td>
<td><strong>約40〜50分</strong></td>
</tr>
<tr>
<td><strong>削除</strong></td>
<td><code>delete-stack</code> 1本（自動解決）</td>
<td><strong>13ステップ手動</strong></td>
</tr>
<tr>
<td><strong>再現性</strong></td>
<td>高い（何度でも同じ構成を再現可能）</td>
<td>低い（手順漏れのリスク大）</td>
</tr>
<tr>
<td><strong>バージョン管理</strong></td>
<td>Gitで管理可能</td>
<td>不可</td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc3">キーワード解説</span></h2>
<table>
<thead>
<tr>
<th>用語</th>
<th>意味</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>CloudFormation</strong></td>
<td>AWSが提供するIaC（Infrastructure as Code）サービス</td>
</tr>
<tr>
<td><strong>スタック</strong></td>
<td>CloudFormationが管理するリソースのまとまり。今回は約27リソースを1スタックで管理</td>
</tr>
<tr>
<td><strong>AWS::ElasticLoadBalancingV2::LoadBalancer</strong></td>
<td>ALB本体を定義するリソース型</td>
</tr>
<tr>
<td><strong>AWS::ElasticLoadBalancingV2::TargetGroup</strong></td>
<td>ターゲットグループを定義するリソース型</td>
</tr>
<tr>
<td><strong>AWS::ElasticLoadBalancingV2::Listener</strong></td>
<td>ALBのリスナー（受付ルール）を定義するリソース型</td>
</tr>
<tr>
<td><strong>DeletionPolicy: Delete</strong></td>
<td>スタック削除時にRDSも確実に削除する設定</td>
</tr>
<tr>
<td><strong>CAPABILITY_NAMED_IAM</strong></td>
<td>名前付きIAMリソースを含むスタックの作成に必要なフラグ</td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc4">前提条件</span></h2>
<table>
<thead>
<tr>
<th>ツール</th>
<th>確認コマンド</th>
</tr>
</thead>
<tbody>
<tr>
<td>AWS CLI v2</td>
<td><code>aws --version</code></td>
</tr>
</tbody>
</table>
<p>AWS認証確認:</p>
<pre><code class="language-cmd">aws sts get-caller-identity</code></pre>
<hr>
<h2><span id="toc5">構築されるリソース（約27個）</span></h2>
<table>
<thead>
<tr>
<th>カテゴリ</th>
<th>リソース</th>
<th>論理ID</th>
</tr>
</thead>
<tbody>
<tr>
<td>ネットワーク</td>
<td>VPC</td>
<td><code>VPC</code></td>
</tr>
<tr>
<td>ネットワーク</td>
<td>インターネットゲートウェイ</td>
<td><code>InternetGateway</code> + <code>IGWAttachment</code></td>
</tr>
<tr>
<td>ネットワーク</td>
<td>パブリックサブネット × 2</td>
<td><code>PublicSubnet1</code> + <code>PublicSubnet2</code></td>
</tr>
<tr>
<td>ネットワーク</td>
<td>プライベートサブネット × 2</td>
<td><code>PrivateSubnet1</code> + <code>PrivateSubnet2</code></td>
</tr>
<tr>
<td>ネットワーク</td>
<td>パブリックルートテーブル</td>
<td><code>PublicRouteTable</code> + <code>PublicRoute</code> + <code>PublicSubnet1RTA</code> + <code>PublicSubnet2RTA</code></td>
</tr>
<tr>
<td>ネットワーク</td>
<td>プライベートルートテーブル</td>
<td><code>PrivateRouteTable</code> + <code>PrivateSubnet1RTA</code> + <code>PrivateSubnet2RTA</code></td>
</tr>
<tr>
<td>ネットワーク</td>
<td>S3 VPC Gateway Endpoint</td>
<td><code>S3VPCEndpoint</code></td>
</tr>
<tr>
<td>セキュリティ</td>
<td>ALBセキュリティグループ</td>
<td><code>ALBSecurityGroup</code></td>
</tr>
<tr>
<td>セキュリティ</td>
<td>EC2セキュリティグループ</td>
<td><code>EC2SecurityGroup</code></td>
</tr>
<tr>
<td>セキュリティ</td>
<td>RDSセキュリティグループ</td>
<td><code>RDSSecurityGroup</code></td>
</tr>
<tr>
<td>IAM</td>
<td>EC2ロール + インスタンスプロファイル</td>
<td><code>EC2Role</code> + <code>EC2InstanceProfile</code></td>
</tr>
<tr>
<td>パラメータ</td>
<td>SSM Parameter Store</td>
<td><code>DBPasswordParam</code></td>
</tr>
<tr>
<td>データベース</td>
<td>RDS DBサブネットグループ</td>
<td><code>DBSubnetGroup</code></td>
</tr>
<tr>
<td>データベース</td>
<td>RDS MySQLインスタンス</td>
<td><code>RDSInstance</code></td>
</tr>
<tr>
<td>ロードバランサー</td>
<td>ターゲットグループ</td>
<td><code>TargetGroup</code></td>
</tr>
<tr>
<td>ロードバランサー</td>
<td>ALB本体</td>
<td><code>ALB</code></td>
</tr>
<tr>
<td>ロードバランサー</td>
<td>リスナー</td>
<td><code>ALBListener</code></td>
</tr>
<tr>
<td>コンピュート</td>
<td>EC2インスタンス</td>
<td><code>EC2Instance</code></td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc6">template.yaml（完全版）</span></h2>
<p>このファイルをそのまま使ってハンズオンを実施できます。<code>C:\my-aws\aws-learning-projects\alb-ec2-rds\template.yaml</code> として保存してください。</p>
<pre><code class="language-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 -&gt; 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 &gt; &gt;(tee /var/log/user-data.log) 2&gt;&amp;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 &gt; /opt/tomcat/webapps/ROOT/index.html &lt;&lt; 'HTMLEOF'
          &lt;!DOCTYPE html&gt;
          &lt;html&gt;
          &lt;body&gt;
          &lt;h1&gt;AP Server - Phase 2-2 ALB + Tomcat + RDS&lt;/h1&gt;
          &lt;p&gt;This server is load balanced by ALB and connects to RDS MySQL in the private subnet.&lt;/p&gt;
          &lt;/body&gt;
          &lt;/html&gt;
          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'</code></pre>
<hr>
<h2><span id="toc7">作業順序</span></h2>
<pre><code class="language-plaintext">⓪ 自分のIPアドレスを確認する
      ↓
① キーペアを作成する（コンソールで実施）
      ↓
② プロジェクトフォルダに移動する
      ↓
③ スタックを作成する（aws cloudformation create-stack）
      ↓
④ 作成完了・Outputsを確認する（RDSは15〜20分かかる）
      ↓
⑤ 動作確認（ALBアクセス・SSH・RDS接続）
      ↓
⑥ スタックを削除する（aws cloudformation delete-stack）</code></pre>
<hr>
<h2><span id="toc8">【重要】⓪ 自分のIPアドレスを確認する</span></h2>
<pre><code class="language-cmd">curl https://checkip.amazonaws.com</code></pre>
<p><strong>控えておく情報</strong>: 自分のIPアドレス（例: <code>203.0.113.1</code>）</p>
<hr>
<h2><span id="toc9">① キーペアの作成（コンソールで実施）</span></h2>
<p>CloudFormationではキーペアのダウンロードができないため、コンソールで先に作成します。</p>
<p><strong>AWSコンソール → EC2 → キーペア → 「キーペアを作成」</strong></p>
<table>
<thead>
<tr>
<th>設定項目</th>
<th>値</th>
</tr>
</thead>
<tbody>
<tr>
<td>名前</td>
<td><code>my-alb-key</code></td>
</tr>
<tr>
<td>キーペアのタイプ</td>
<td><strong>RSA</strong></td>
</tr>
<tr>
<td>プライベートキーファイル形式</td>
<td><strong>.pem</strong></td>
</tr>
</tbody>
</table>
<p>ダウンロードされた <code>my-alb-key.pem</code> を保存します。</p>
<pre><code class="language-plaintext">C:\Users\ユーザー名\.ssh\my-alb-key.pem</code></pre>
<hr>
<h2><span id="toc10">② プロジェクトフォルダに移動する</span></h2>
<p>VSCodeのターミナル（CMD）を開き、プロジェクトフォルダに移動します。</p>
<pre><code class="language-cmd">cd C:\my-aws\aws-learning-projects\alb-ec2-rds</code></pre>
<p><code>template.yaml</code> があることを確認します。</p>
<pre><code class="language-cmd">dir template.yaml</code></pre>
<hr>
<h2><span id="toc11">③ スタックの作成</span></h2>
<p>以下のコマンドを実行します。<code>203.0.113.1</code> は⓪で確認した自分のIPアドレスに置き換えます。</p>
<pre><code class="language-cmd">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!</code></pre>
<blockquote>
<p><strong><code>TomcatVersion</code> パラメータについて</strong>: デフォルト値は <code>10.1.28</code>。変更する場合は <code>ParameterKey=TomcatVersion,ParameterValue=10.1.xx</code> を追加します。利用可能なバージョンは <a href="https://archive.apache.org/dist/tomcat/tomcat-10/">https://archive.apache.org/dist/tomcat/tomcat-10/</a> で確認します。</p>
</blockquote>
<blockquote>
<p><strong><code>CAPABILITY_NAMED_IAM</code> について</strong>: <code>EC2Role</code> のように名前を指定したIAMリソースを作成する場合に必要なフラグです。</p>
</blockquote>
<p>成功すると以下のような StackId が表示されます。</p>
<pre><code class="language-json">{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:123456789012:stack/my-alb-stack/xxxxx"
}</code></pre>
<hr>
<h2><span id="toc12">④ 作成完了・Outputsの確認</span></h2>
<blockquote>
<p><strong>注意</strong>: RDSの作成には<strong>約15〜20分</strong>かかります。<code>CREATE_IN_PROGRESS</code> が続く間は正常です。</p>
</blockquote>
<h3><span id="toc13">作成状況の確認</span></h3>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-alb-stack ^
  --query "Stacks[0].StackStatus" ^
  --output text</code></pre>
<table>
<thead>
<tr>
<th>ステータス</th>
<th>意味</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>CREATE_IN_PROGRESS</code></td>
<td>作成中（待つ）</td>
</tr>
<tr>
<td><code>CREATE_COMPLETE</code></td>
<td>作成完了</td>
</tr>
<tr>
<td><code>CREATE_FAILED</code></td>
<td>作成失敗（後述のトラブルシューティングを参照）</td>
</tr>
<tr>
<td><code>ROLLBACK_COMPLETE</code></td>
<td>失敗してロールバック完了</td>
</tr>
</tbody>
</table>
<h3><span id="toc14">Outputs（接続情報）の確認</span></h3>
<p><code>CREATE_COMPLETE</code> になったら以下を実行します。</p>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-alb-stack ^
  --query "Stacks[0].Outputs" ^
  --output table</code></pre>
<pre><code class="language-plaintext">+----------------------+------------------------------------------------------------------+
|  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       |
+----------------------+------------------------------------------------------------------+</code></pre>
<p><strong>控えておく情報</strong>: <code>ALBEndpoint</code>、<code>EC2PublicIP</code>、<code>RDSEndpoint</code></p>
<p><!-- ![CloudFormation Outputs確認コマンドの実行結果](images/cfn-outputs.jpg) --></p>
<hr>
<h2><span id="toc15">⑤ 動作確認</span></h2>
<h3><span id="toc16">ALBのヘルスチェック確認</span></h3>
<p><strong>EC2 → ターゲットグループ → <code>my-alb-tg</code> → 「ターゲット」タブ</strong></p>
<p>EC2のステータスが <strong>「healthy」</strong> になるまで待ちます（EC2起動後5〜10分）。</p>
<blockquote>
<p>Tomcatの起動はEC2が「実行中」になってからさらに数分かかります。<code>initial</code> や <code>unhealthy</code> が続く場合はしばらく待ちます。</p>
</blockquote>
<p><!-- ![ターゲットグループのHealthy確認画面](images/target-healthy.jpg) --></p>
<h3><span id="toc17">ALB経由でWebアクセス確認</span></h3>
<p>Outputsに表示された <code>ALBEndpoint</code> をブラウザで開きます。</p>
<pre><code class="language-plaintext">http://（ALBEndpointのDNS名）</code></pre>
<p>「AP Server - Phase 2-2 ALB + Tomcat + RDS」と表示されれば正常です。</p>
<h3><span id="toc18">EC2にSSH接続する</span></h3>
<p>Outputsの <code>SSHCommand</code> を実行します（パスは実際のパスに修正します）。</p>
<pre><code class="language-cmd">ssh -i C:\Users\ユーザー名\.ssh\my-alb-key.pem ec2-user@（EC2PublicIP）</code></pre>
<h3><span id="toc19">Parameter StoreからDBパスワードを取得する</span></h3>
<p>EC2に接続した状態で実行します。</p>
<pre><code class="language-bash">aws ssm get-parameter \
  --name "/my/alb/db-password" \
  --query "Parameter.Value" \
  --output text \
  --region ap-northeast-1</code></pre>
<p><code>Handson1234!</code> が表示されれば成功です。</p>
<blockquote>
<p><strong><code>--with-decryption</code> が不要な理由</strong>: CloudFormationで作成したパラメータは <code>Type: String</code>（平文）のため、フラグなしでそのまま値が返ります。コンソールで <strong>SecureString</strong> として作成した場合は <code>--with-decryption</code> が必要です（コンソール版参照）。</p>
</blockquote>
<h3><span id="toc20">RDSへのMySQL接続テスト</span></h3>
<pre><code class="language-bash"># 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</code></pre>
<p>接続成功後、SQL操作を確認します。</p>
<pre><code class="language-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;</code></pre>
<p><!-- ![EC2からRDS MySQLへの接続成功画面](images/rds-mysql-connect.jpg) --></p>
<p>EC2からSSH接続を切断します。</p>
<pre><code class="language-bash">exit</code></pre>
<hr>
<h2><span id="toc21">⑥ スタックの削除</span></h2>
<p><strong>課金を止めるために、ハンズオン完了後は必ず削除します。</strong></p>
<pre><code class="language-cmd">aws cloudformation delete-stack ^
  --stack-name my-alb-stack ^
  --region ap-northeast-1</code></pre>
<blockquote>
<p><strong>注意</strong>: RDS削除には<strong>約10〜15分</strong>かかります。コマンド実行後もしばらく待つ必要があります。</p>
</blockquote>
<h3><span id="toc22">削除状況の確認</span></h3>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-alb-stack ^
  --query "Stacks[0].StackStatus" ^
  --output text</code></pre>
<table>
<thead>
<tr>
<th>ステータス</th>
<th>意味</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>DELETE_IN_PROGRESS</code></td>
<td>削除中（待つ）</td>
</tr>
<tr>
<td><code>DELETE_FAILED</code></td>
<td>削除失敗（トラブルシューティングを参照）</td>
</tr>
</tbody>
</table>
<p>削除完了の確認（以下のエラーが表示されれば削除完了）:</p>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-alb-stack ^
  --region ap-northeast-1</code></pre>
<pre><code class="language-plaintext">An error occurred (ValidationError) when calling the DescribeStacks operation:
Stack with id my-alb-stack does not exist</code></pre>
<h3><span id="toc23">キーペアの手動削除</span></h3>
<p>キーペアはCloudFormationで管理していないため手動で削除します。</p>
<p><strong>EC2 → キーペア → <code>my-alb-key</code> を選択 → 「アクション」→「削除」</strong></p>
<hr>
<h2><span id="toc24">コンソール版との比較</span></h2>
<table>
<thead>
<tr>
<th>作業</th>
<th>コンソール（手動）</th>
<th>CloudFormation</th>
</tr>
</thead>
<tbody>
<tr>
<td>VPC + サブネット4つ + IGW + ルートテーブル</td>
<td>約20〜30分</td>
<td>template.yaml に定義済み</td>
</tr>
<tr>
<td>SG 3つ（ALB/EC2/RDS）の作成</td>
<td>3画面で個別設定</td>
<td>template.yaml に定義済み</td>
</tr>
<tr>
<td>ALB + TG + Listener の作成</td>
<td>3つの画面で設定</td>
<td>template.yaml に定義済み</td>
</tr>
<tr>
<td>Tomcat UserDataの貼り付け</td>
<td>手動コピペ</td>
<td>template.yaml に組み込み済み</td>
</tr>
<tr>
<td>全体のデプロイ</td>
<td><strong>約40〜50分</strong></td>
<td>コマンド1本（RDS待ち15〜20分）</td>
</tr>
<tr>
<td>削除</td>
<td><strong>13ステップ手動</strong></td>
<td><code>delete-stack</code> 1本</td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc25">トラブルシューティング</span></h2>
<table>
<thead>
<tr>
<th>症状</th>
<th>原因</th>
<th>対処</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>CREATE_FAILED</code> → ROLLBACKになる</td>
<td>パラメータ不正・リソース上限など</td>
<td>コンソールのCloudFormationイベントタブでエラー詳細を確認</td>
</tr>
<tr>
<td>ALBに繋がらない</td>
<td>EC2のTomcatがまだ起動中</td>
<td>TGのヘルスチェックがHealthyになるまで待つ（5〜10分）</td>
</tr>
<tr>
<td>ALBに繋がるが502エラー</td>
<td>Tomcat起動失敗</td>
<td>SSH接続して <code>sudo cat /var/log/user-data.log</code> 確認</td>
</tr>
<tr>
<td><code>ERROR 2003</code>: MySQL接続できない</td>
<td>RDS SGのソース設定ミス</td>
<td>RDS SGのインバウンドルールを確認</td>
</tr>
<tr>
<td>Tomcatバージョンエラー</td>
<td>指定バージョンがアーカイブに存在しない</td>
<td><a href="https://archive.apache.org/dist/tomcat/tomcat-10/">https://archive.apache.org/dist/tomcat/tomcat-10/</a> で確認して <code>TomcatVersion</code> パラメータを更新</td>
</tr>
<tr>
<td><code>DELETE_FAILED</code></td>
<td>リソースの依存関係が残っている</td>
<td>コンソールからALBを手動削除してから <code>delete-stack</code> を再実行</td>
</tr>
<tr>
<td>スタック作成後しばらく <code>CREATE_IN_PROGRESS</code> が続く</td>
<td>RDSの作成に時間がかかっている</td>
<td>正常。約15〜20分待つ</td>
</tr>
</tbody>
</table>
<h3><span id="toc26">失敗時の詳細確認コマンド</span></h3>
<pre><code class="language-cmd">aws cloudformation describe-stack-events ^
  --stack-name my-alb-stack ^
  --query "StackEvents[?ResourceStatus=='CREATE_FAILED'].[ResourceType,ResourceStatusReason]" ^
  --output table</code></pre>
<hr>
<h2><span id="toc27">まとめ</span></h2>
<table>
<thead>
<tr>
<th>ステップ</th>
<th>内容</th>
</tr>
</thead>
<tbody>
<tr>
<td>①</td>
<td>キーペアをコンソールで作成</td>
</tr>
<tr>
<td>②③</td>
<td><code>alb-ec2-rds/</code> フォルダに移動し <code>create-stack</code> を実行</td>
</tr>
<tr>
<td>④</td>
<td><code>CREATE_COMPLETE</code> になったらOutputsでALBエンドポイント・EC2IP・RDSエンドポイントを確認</td>
</tr>
<tr>
<td>⑤</td>
<td>ヘルスチェックがHealthyになったらALB → EC2 → RDS MySQL への接続テスト</td>
</tr>
<tr>
<td>⑥</td>
<td><code>delete-stack</code> 1本で27リソースを一括削除</td>
</tr>
</tbody>
</table>
<p>CloudFormation版の最大のメリットは<strong>再現性</strong>と<strong>削除の簡単さ</strong>です。コンソールでは40〜50分・13ステップかかった作業が、コマンド1本（待ち時間込みで約20分）で完了します。</p>
<p>コンソール操作でALB・ターゲットグループ・ヘルスチェックを視覚的に確認したい場合は、<a href="https://caymezon.com/aws-handson-console-alb-ec2-rds/">AWSコンソール版ハンズオン</a>を参照してください。</p><p>The post <a href="https://caymezon.com/aws-handson-cloudformation-alb-ec2-rds/">CloudFormationでALB + EC2(Tomcat) + RDS 3層構成を自動構築する手順【27リソース一括デプロイ / コンソール版との比較付き】</a> first appeared on <a href="https://caymezon.com">CayTech Lab</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://caymezon.com/aws-handson-cloudformation-alb-ec2-rds/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>CloudFormationでRDS MySQL + EC2接続環境を自動構築する手順【22リソース一括デプロイ / コンソール版との比較付き】</title>
		<link>https://caymezon.com/aws-handson-cloudformation-rds-mysql-ec2/</link>
					<comments>https://caymezon.com/aws-handson-cloudformation-rds-mysql-ec2/#respond</comments>
		
		<dc:creator><![CDATA[caymezon]]></dc:creator>
		<pubDate>Sat, 11 Apr 2026 23:47:49 +0000</pubDate>
				<category><![CDATA[AWS Basic]]></category>
		<category><![CDATA[Cloud & Infra]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[AWSCLI]]></category>
		<category><![CDATA[CloudFormation]]></category>
		<category><![CDATA[DBサブネットグループ]]></category>
		<category><![CDATA[EC2]]></category>
		<category><![CDATA[IaC]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Parameter Store]]></category>
		<category><![CDATA[RDS]]></category>
		<category><![CDATA[SG参照]]></category>
		<category><![CDATA[SSM]]></category>
		<category><![CDATA[VPC]]></category>
		<category><![CDATA[セキュリティグループ]]></category>
		<category><![CDATA[ハンズオン]]></category>
		<category><![CDATA[マネージドサービス]]></category>
		<category><![CDATA[初心者]]></category>
		<guid isPermaLink="false">https://caymezon.com/?p=20336</guid>

					<description><![CDATA[<p>目次 はじめにCloudFormation vs コンソール：どれだけ違うかキーワード解説前提条件構築されるリソース（22個）template.yaml（完全版）作業順序【重要】⓪ 自分のIPアドレスを確認する① キーペ [&#8230;]</p>
<p>The post <a href="https://caymezon.com/aws-handson-cloudformation-rds-mysql-ec2/">CloudFormationでRDS MySQL + EC2接続環境を自動構築する手順【22リソース一括デプロイ / コンソール版との比較付き】</a> first appeared on <a href="https://caymezon.com">CayTech Lab</a>.</p>]]></description>
										<content:encoded><![CDATA[<div id="toc" class="toc tnt-number toc-center tnt-number border-element"><input type="checkbox" class="toc-checkbox" id="toc-checkbox-4" checked><label class="toc-title" for="toc-checkbox-4">目次</label>
    <div class="toc-content">
    <ol class="toc-list open"><li><a href="#toc1" tabindex="0">はじめに</a></li><li><a href="#toc2" tabindex="0">CloudFormation vs コンソール：どれだけ違うか</a></li><li><a href="#toc3" tabindex="0">キーワード解説</a></li><li><a href="#toc4" tabindex="0">前提条件</a></li><li><a href="#toc5" tabindex="0">構築されるリソース（22個）</a></li><li><a href="#toc6" tabindex="0">template.yaml（完全版）</a></li><li><a href="#toc7" tabindex="0">作業順序</a></li><li><a href="#toc8" tabindex="0">【重要】⓪ 自分のIPアドレスを確認する</a></li><li><a href="#toc9" tabindex="0">① キーペアの作成（コンソールで実施）</a></li><li><a href="#toc10" tabindex="0">② プロジェクトフォルダに移動する</a></li><li><a href="#toc11" tabindex="0">③ スタックの作成</a></li><li><a href="#toc12" tabindex="0">④ 作成完了・Outputsの確認</a><ol><li><a href="#toc13" tabindex="0">作成状況の確認</a></li><li><a href="#toc14" tabindex="0">Outputs（接続情報）の確認</a></li></ol></li><li><a href="#toc15" tabindex="0">⑤ 動作確認</a><ol><li><a href="#toc16" tabindex="0">Webブラウザで確認</a></li><li><a href="#toc17" tabindex="0">EC2にSSH接続する</a></li><li><a href="#toc18" tabindex="0">Parameter StoreからDBパスワードを取得する</a></li><li><a href="#toc19" tabindex="0">RDSへのMySQL接続テスト</a></li></ol></li><li><a href="#toc20" tabindex="0">⑥ スタックの削除</a><ol><li><a href="#toc21" tabindex="0">削除状況の確認</a></li><li><a href="#toc22" tabindex="0">キーペアの手動削除</a></li></ol></li><li><a href="#toc23" tabindex="0">コンソール版との比較</a></li><li><a href="#toc24" tabindex="0">トラブルシューティング</a><ol><li><a href="#toc25" tabindex="0">失敗時の詳細確認コマンド</a></li></ol></li><li><a href="#toc26" tabindex="0">まとめ</a></li></ol>
    </div>
  </div>

<h2><span id="toc1">はじめに</span></h2>
<p>「コンソールで40〜50分かかったRDS + EC2環境をコードで再現したい」——それを実現するのが <strong>AWS CloudFormation</strong> です。</p>
<p>この記事では、<code>template.yaml</code> 1ファイルにVPC・サブネット・セキュリティグループ・IAMロール・SSM Parameter Store・RDS MySQL・EC2（22リソース）を定義し、<strong>コマンド1本でRDS + EC2接続環境を一括構築するハンズオン</strong>を紹介します。コンソール版（<a href="https://caymezon.com/aws-handson-console-rds-mysql-ec2/">AWSコンソール版ハンズオン</a>）と全く同じ構成を、CloudFormationで自動化します。</p>
<pre><code class="language-plaintext">ローカル環境（VSCode）
  ├── template.yaml（22リソースを定義）
  └── AWS CLI コマンド
        ↓ スタック作成（コマンド1本）
AWS環境
  └── VPC（10.0.0.0/16）
        ├── パブリックサブネット（10.0.1.0/24 / AZ-a）
        │     └── EC2（Apache + mariadb client / パブリックIP あり）
        │           └── IAMロール → SSM Parameter Store
        │
        ├── DBサブネットグループ
        │     ├── プライベートサブネット1（10.0.2.0/24 / AZ-a）← RDS配置
        │     └── プライベートサブネット2（10.0.3.0/24 / AZ-c）← 2AZ要件
        │
        └── SSM Parameter Store: /my/rds/db-password</code></pre>
<p><strong>このハンズオンで体験できること：</strong></p>
<ul>
<li>VPC・サブネット3つ・IGW・ルートテーブル・SG・IAM・SSM・RDS・EC2を <code>template.yaml</code> 1ファイルで定義する方法</li>
<li><code>DBPassword</code> パラメータを <code>NoEcho: true</code> でマスクしてセキュアに渡す方法</li>
<li><code>DeletionPolicy: Delete</code> + <code>BackupRetentionPeriod: 0</code> でスタック削除時にRDSを確実に削除する設定</li>
<li><code>CloudFormation</code> が <code>SecureString</code> を作れない理由と代替手段</li>
</ul>
<p><strong>このハンズオンの特徴：</strong></p>
<ul>
<li>コンソール版では40〜50分かかった作業が、コマンド1本（RDS待ち15〜20分含む）で完了</li>
<li><code>delete-stack</code> 1本でRDS・VPC・IAMなど22リソースの依存関係を自動解決して削除</li>
</ul>
<hr>
<blockquote>
<p><strong>この記事は <a href="https://caymezon.com/aws-handson-console-rds-mysql-ec2/">AWSコンソール版ハンズオン</a> の比較記事です。</strong><br />コンソール版でRDS・DBサブネットグループ・Parameter Storeを視覚的に学んだ後に読むと理解が深まります。</p>
</blockquote>
<hr>
<p><!-- ![CloudFormationスタック作成完了・Outputs確認画面](images/cfn-create-complete.jpg) --></p>
<p><!-- <a rel="nofollow" href="//af.moshimo.com/af/c/click?a_id=1384942&p_id=170&pc_id=185&pl_id=4062&url=https%3A%2F%2Fwww.amazon.co.jp%2Fs%3Fk%3D%25E6%259C%25AC%2BAWS%2B%25E9%2596%258B%25E7%2599%25BA%26__mk_ja_JP%3D%25E3%2582%25AB%25E3%2582%25BF%25E3%2582%25AB%25E3%2583%258A%26crid%3D1DE63UBHFOR4K%26sprefix%3D%25E6%259C%25AC%2Baws%2B%25E9%2596%258B%25E7%2599%25BA%252Caps%252C167%26ref%3Dnb_sb_noss" referrerpolicy="no-referrer-when-downgrade" attributionsrc>Amazon検索[本 AWS 開発]</a><img decoding="async" src="//i.moshimo.com/af/i/impression?a_id=1384942&p_id=170&pc_id=185&pl_id=4062" width="1" height="1" style="border:none;" alt="" loading="lazy"> --></p>
<p><!-- <!-- START MoshimoAffiliateEasyLink --><script type="text/javascript">(function(b,c,f,g,a,d,e){b.MoshimoAffiliateObject=a;b[a]=b[a]||function(){arguments.currentScript=c.currentScript||c.scripts[c.scripts.length-2];(b[a].q=b[a].q||[]).push(arguments)};c.getElementById(a)||(d=c.createElement(f),d.src=g,d.id=a,e=c.getElementsByTagName("body")[0],e.appendChild(d))})(window,document,"script","//dn.msmstatic.com/site/cardlink/bundle.js?20220329","msmaflink");msmaflink({"n":"AWSの基本・仕組み・重要用語が全部わかる教科書 (見るだけ図解)","b":"SBクリエイティブ","t":"","d":"https:\/\/m.media-amazon.com","c_p":"\/images\/I","p":["\/51DEDQXj6oL._SL500_.jpg","\/41F589smNwL._SL500_.jpg","\/41R6f9yyCWL._SL500_.jpg","\/41HqWQ9BvmL._SL500_.jpg","\/41p8p0ZU79L._SL500_.jpg","\/41qLC-fndBL._SL500_.jpg","\/41fcLv9VT5L._SL500_.jpg","\/51lRvCsvHqL._SL500_.jpg"],"u":{"u":"https:\/\/www.amazon.co.jp\/dp\/4815607850","t":"amazon","r_v":""},"v":"2.1","b_l":[{"id":1,"u_tx":"Amazonで見る","u_bc":"#f79256","u_url":"https:\/\/www.amazon.co.jp\/dp\/4815607850","a_id":1384942,"p_id":170,"pl_id":27060,"pc_id":185,"s_n":"amazon","u_so":1},{"id":2,"u_tx":"楽天市場で見る","u_bc":"#f76956","u_url":"https:\/\/search.rakuten.co.jp\/search\/mall\/AWS%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%83%BB%E4%BB%95%E7%B5%84%E3%81%BF%E3%83%BB%E9%87%8D%E8%A6%81%E7%94%A8%E8%AA%9E%E3%81%8C%E5%85%A8%E9%83%A8%E3%82%8F%E3%81%8B%E3%82%8B%E6%95%99%E7%A7%91%E6%9B%B8%20(%E8%A6%8B%E3%82%8B%E3%81%A0%E3%81%91%E5%9B%B3%E8%A7%A3)\/","a_id":1384917,"p_id":54,"pl_id":27059,"pc_id":54,"s_n":"rakuten","u_so":2},{"id":3,"u_tx":"Yahoo!ショッピングで見る","u_bc":"#66a7ff","u_url":"https:\/\/shopping.yahoo.co.jp\/search?first=1\u0026p=AWS%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%83%BB%E4%BB%95%E7%B5%84%E3%81%BF%E3%83%BB%E9%87%8D%E8%A6%81%E7%94%A8%E8%AA%9E%E3%81%8C%E5%85%A8%E9%83%A8%E3%82%8F%E3%81%8B%E3%82%8B%E6%95%99%E7%A7%91%E6%9B%B8%20(%E8%A6%8B%E3%82%8B%E3%81%A0%E3%81%91%E5%9B%B3%E8%A7%A3)","a_id":1466950,"p_id":1225,"pl_id":27061,"pc_id":1925,"s_n":"yahoo","u_so":3}],"eid":"eaCUB","s":"s"});</script></p>
<div id="msmaflink-eaCUB">リンク</div>
<p><!-- MoshimoAffiliateEasyLink END --> --></p>
<h2><span id="toc2">CloudFormation vs コンソール：どれだけ違うか</span></h2>
<table>
<thead>
<tr>
<th>比較項目</th>
<th>CloudFormation</th>
<th>コンソール（手動）</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>VPC + サブネット3つ + IGW + ルートテーブル</strong></td>
<td>template.yaml に定義済み</td>
<td>約15〜20分・複数画面</td>
</tr>
<tr>
<td><strong>IAMロール作成</strong></td>
<td>template.yaml に定義済み</td>
<td>別画面で作成</td>
</tr>
<tr>
<td><strong>SSM Parameter Store作成</strong></td>
<td>template.yaml に定義済み</td>
<td>別画面で作成</td>
</tr>
<tr>
<td><strong>RDS DBサブネットグループ</strong></td>
<td>template.yaml に定義済み</td>
<td>別画面で作成</td>
</tr>
<tr>
<td><strong>RDS作成（設定項目20以上）</strong></td>
<td><code>--parameters DBPassword=...</code> を渡すだけ</td>
<td>画面でポチポチ</td>
</tr>
<tr>
<td><strong>全体のデプロイ</strong></td>
<td>コマンド1本（RDS待ち15〜20分）</td>
<td><strong>40〜50分</strong></td>
</tr>
<tr>
<td><strong>削除（依存関係あり）</strong></td>
<td><code>delete-stack</code> 1本（自動解決）</td>
<td>11ステップ・手動管理</td>
</tr>
<tr>
<td><strong>再現性</strong></td>
<td>高い（同じ構成を何度でも再現可能）</td>
<td>低い（手順漏れのリスク大）</td>
</tr>
<tr>
<td><strong>バージョン管理</strong></td>
<td>Gitで管理可能</td>
<td>不可</td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc3">キーワード解説</span></h2>
<table>
<thead>
<tr>
<th>用語</th>
<th>意味</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>CloudFormation</strong></td>
<td>AWSが提供するIaC（Infrastructure as Code）サービス。YAMLやJSONでリソースを定義し、一括作成・削除できる</td>
</tr>
<tr>
<td><strong>スタック</strong></td>
<td>CloudFormationが管理するリソースのまとまり。今回は22リソースを1スタックで管理</td>
</tr>
<tr>
<td><strong>DeletionPolicy</strong></td>
<td>スタック削除時のリソースの扱いを指定するプロパティ。<code>Delete</code>/<code>Retain</code>/<code>Snapshot</code>（RDSのみ）の3種類</td>
</tr>
<tr>
<td><strong>NoEcho</strong></td>
<td>CloudFormationパラメータの設定。<code>true</code> にするとコンソール上でパスワードが <code>****</code> にマスクされる</td>
</tr>
<tr>
<td><strong>SecureString</strong></td>
<td>Parameter StoreでKMS暗号化して保管するタイプ。<strong>CloudFormationでは作成不可</strong></td>
</tr>
<tr>
<td><strong>CAPABILITY_NAMED_IAM</strong></td>
<td>IAMリソースを含むスタックの作成・更新時に必要なフラグ</td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc4">前提条件</span></h2>
<table>
<thead>
<tr>
<th>ツール</th>
<th>確認コマンド</th>
</tr>
</thead>
<tbody>
<tr>
<td>AWS CLI v2</td>
<td><code>aws --version</code></td>
</tr>
</tbody>
</table>
<p>AWS認証確認:</p>
<pre><code class="language-cmd">aws sts get-caller-identity</code></pre>
<hr>
<h2><span id="toc5">構築されるリソース（22個）</span></h2>
<table>
<thead>
<tr>
<th>カテゴリ</th>
<th>リソース</th>
<th>論理ID</th>
</tr>
</thead>
<tbody>
<tr>
<td>ネットワーク</td>
<td>VPC</td>
<td><code>VPC</code></td>
</tr>
<tr>
<td>ネットワーク</td>
<td>インターネットゲートウェイ</td>
<td><code>InternetGateway</code> + <code>IGWAttachment</code></td>
</tr>
<tr>
<td>ネットワーク</td>
<td>パブリックサブネット</td>
<td><code>PublicSubnet</code></td>
</tr>
<tr>
<td>ネットワーク</td>
<td>プライベートサブネット1（RDS配置）</td>
<td><code>PrivateSubnet1</code></td>
</tr>
<tr>
<td>ネットワーク</td>
<td>プライベートサブネット2（2AZ要件）</td>
<td><code>PrivateSubnet2</code></td>
</tr>
<tr>
<td>ネットワーク</td>
<td>パブリックルートテーブル</td>
<td><code>PublicRouteTable</code> + <code>PublicRoute</code> + <code>PublicSubnetRTA</code></td>
</tr>
<tr>
<td>ネットワーク</td>
<td>プライベートルートテーブル</td>
<td><code>PrivateRouteTable</code> + <code>PrivateSubnet1RTA</code> + <code>PrivateSubnet2RTA</code></td>
</tr>
<tr>
<td>ネットワーク</td>
<td>S3 VPC Gateway Endpoint</td>
<td><code>S3VPCEndpoint</code></td>
</tr>
<tr>
<td>セキュリティ</td>
<td>EC2セキュリティグループ</td>
<td><code>EC2SecurityGroup</code></td>
</tr>
<tr>
<td>セキュリティ</td>
<td>RDSセキュリティグループ</td>
<td><code>RDSSecurityGroup</code></td>
</tr>
<tr>
<td>IAM</td>
<td>EC2ロール + インスタンスプロファイル</td>
<td><code>EC2Role</code> + <code>EC2InstanceProfile</code></td>
</tr>
<tr>
<td>パラメータ</td>
<td>SSM Parameter Store</td>
<td><code>DBPasswordParam</code></td>
</tr>
<tr>
<td>データベース</td>
<td>RDS DBサブネットグループ</td>
<td><code>DBSubnetGroup</code></td>
</tr>
<tr>
<td>データベース</td>
<td>RDS MySQLインスタンス</td>
<td><code>RDSInstance</code></td>
</tr>
<tr>
<td>コンピュート</td>
<td>EC2インスタンス</td>
<td><code>EC2Instance</code></td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc6">template.yaml（完全版）</span></h2>
<p>このファイルをそのまま使ってハンズオンを実施できます。<code>C:\my-aws\aws-learning-projects\rds-mysql-ec2\template.yaml</code> として保存してください。</p>
<pre><code class="language-yaml">AWSTemplateFormatVersion: '2010-09-09'
Description: 'RDS MySQL + EC2 connection hands-on with SSM Parameter Store password management'

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

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

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

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

Resources:

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

  # VPC: isolated network for this hands-on
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-rds-vpc'
        - Key: Cost
          Value: !Ref EmployeeId

  # Internet Gateway: connects public subnet to the internet
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-rds-igw'
        - Key: Cost
          Value: !Ref EmployeeId

  # Attach Internet Gateway to VPC
  IGWAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  # Public Subnet (AZ-a): EC2 AP server is placed here
  PublicSubnet:
    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}-public-subnet'
        - 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.2.0/24
      AvailabilityZone: !Select [0, !GetAZs '']
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-private-subnet-1'
        - Key: Cost
          Value: !Ref EmployeeId

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

  # Public Route Table: routes internet traffic via Internet Gateway
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-public-rt'
        - Key: Cost
          Value: !Ref EmployeeId

  # Default route for public subnet: 0.0.0.0/0 -&gt; Internet Gateway
  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: IGWAttachment
    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

  # Private Route Table: no internet route (intra-VPC traffic only)
  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-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 in public subnet 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
  # ============================================================

  # IAM Role: EC2 can use Session Manager and read SSM Parameter Store
  EC2Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub 'n${EmployeeId}-rds-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}-rds-ec2-role'
        - Key: Cost
          Value: !Ref EmployeeId

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

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

  # EC2 Security Group: SSH and HTTP access from MyIP
  EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: !Sub 'n${EmployeeId} EC2 AP server SG'
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref MyIP
          Description: SSH from my IP
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Ref MyIP
          Description: HTTP from my IP
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-rds-ec2-sg'
        - Key: Cost
          Value: !Ref EmployeeId

  # RDS Security Group: MySQL(3306) from EC2 SG only - not open to the internet
  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}-rds-sg'
        - Key: Cost
          Value: !Ref EmployeeId

  # ============================================================
  # SSM Parameter Store (password management)
  # NOTE: CloudFormation can only create String type, not SecureString.
  # In production, create a SecureString manually and reference it with
  # {{resolve:ssm-secure:/path/to/param}}
  # ============================================================
  DBPasswordParam:
    Type: AWS::SSM::Parameter
    Properties:
      Name: !Sub '/n${EmployeeId}/rds/db-password'
      Type: String
      Value: !Ref DBPassword
      Description: !Sub 'RDS MySQL master password for n${EmployeeId} hands-on'
      Tags:
        Cost: !Ref EmployeeId

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

  # DB Subnet Group: registers which subnets RDS can use
  # Requires subnets in at least 2 different Availability Zones
  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}-rds-subnet-group'
        - Key: Cost
          Value: !Ref EmployeeId

  # RDS MySQL Instance (single-AZ, db.t3.micro - free tier eligible)
  RDSInstance:
    Type: AWS::RDS::DBInstance
    DeletionPolicy: Delete
    Properties:
      DBInstanceIdentifier: !Sub 'n${EmployeeId}-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}-rds-mysql'
        - Key: Cost
          Value: !Ref EmployeeId

  # ============================================================
  # EC2 (AP Server)
  # ============================================================

  # AP Server: Apache + MySQL client in public subnet
  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 PublicSubnet
      SecurityGroupIds:
        - !GetAtt EC2SecurityGroup.GroupId
      IamInstanceProfile: !Ref EC2InstanceProfile
      UserData:
        Fn::Base64: |
          #!/bin/bash
          dnf update -y
          dnf install -y httpd mariadb105
          systemctl start httpd
          systemctl enable httpd
          echo "&lt;h1&gt;AP Server - RDS MySQL Hands-on&lt;/h1&gt;" &gt; /var/www/html/index.html
          echo "&lt;p&gt;This server connects to RDS MySQL in the private subnet.&lt;/p&gt;" &gt;&gt; /var/www/html/index.html
      Tags:
        - Key: Name
          Value: !Sub 'n${EmployeeId}-rds-ap-instance'
        - Key: Cost
          Value: !Ref EmployeeId

Outputs:
  EC2PublicIP:
    Description: EC2 AP server public IP
    Value: !GetAtt EC2Instance.PublicIp

  WebsiteURL:
    Description: EC2 AP server website URL
    Value: !Sub 'http://${EC2Instance.PublicIp}'

  RDSEndpoint:
    Description: RDS MySQL endpoint hostname (use this from EC2 to connect)
    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 this FROM the EC2 AP server)
    Value: !Sub 'mysql -h ${RDSInstance.Endpoint.Address} -u admin -pHandson1234! sampledb'</code></pre>
<p><strong>template.yaml の設計ポイント（RDS固有）:</strong></p>
<p><strong><code>DeletionPolicy</code> の3種類:</strong></p>
<table>
<thead>
<tr>
<th>設定</th>
<th>意味</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Delete</code>（今回）</td>
<td>スタック削除時にRDSも削除する</td>
</tr>
<tr>
<td><code>Retain</code></td>
<td>スタック削除時にRDSを残す</td>
</tr>
<tr>
<td><code>Snapshot</code>（RDSのみ）</td>
<td>スタック削除時にスナップショットを作成してから削除</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>なぜ <code>DeletionPolicy: Delete</code> を明示するか</strong>: RDSはデフォルトでスナップショット作成を要求される場合があります。明示的に <code>Delete</code> を指定することで確実にスキップできます。また <code>BackupRetentionPeriod: 0</code> で自動バックアップを無効にし、削除をよりスムーズにしています。</p>
</blockquote>
<p><strong>CloudFormationが <code>SecureString</code> を作れない理由:</strong></p>
<p><code>AWS::SSM::Parameter</code> は <code>String</code> と <code>StringList</code> のみサポートしています。<code>SecureString</code> は KMS の複雑な依存関係があるためサポート外です。本番環境では事前に SecureString を作成し <code>{{resolve:ssm-secure:/path}}</code> で参照する方法が推奨されます。</p>
<hr>
<h2><span id="toc7">作業順序</span></h2>
<pre><code class="language-plaintext">⓪ 自分のIPアドレスを確認する
      ↓
① キーペアを作成する（コンソールで実施）
      ↓
② プロジェクトフォルダに移動する
      ↓
③ スタックを作成する（aws cloudformation create-stack）
      ↓
④ 作成完了・Outputsを確認する（RDSは15〜20分かかる）
      ↓
⑤ 動作確認（Web・SSH・RDS接続）
      ↓
⑥ スタックを削除する（aws cloudformation delete-stack）</code></pre>
<hr>
<h2><span id="toc8">【重要】⓪ 自分のIPアドレスを確認する</span></h2>
<pre><code class="language-cmd">curl https://checkip.amazonaws.com</code></pre>
<p><strong>控えておく情報</strong>: 自分のIPアドレス（例: <code>203.0.113.1</code>）</p>
<hr>
<h2><span id="toc9">① キーペアの作成（コンソールで実施）</span></h2>
<p>CloudFormationではキーペアのダウンロードができないため、コンソールで先に作成します。</p>
<p><strong>AWSコンソール → EC2 → キーペア → 「キーペアを作成」</strong></p>
<table>
<thead>
<tr>
<th>設定項目</th>
<th>値</th>
</tr>
</thead>
<tbody>
<tr>
<td>名前</td>
<td><code>my-rds-mysql-key</code></td>
</tr>
<tr>
<td>キーペアのタイプ</td>
<td><strong>RSA</strong></td>
</tr>
<tr>
<td>プライベートキーファイル形式</td>
<td><strong>.pem</strong></td>
</tr>
</tbody>
</table>
<p>ダウンロードされた <code>my-rds-mysql-key.pem</code> を保存します。</p>
<pre><code class="language-plaintext">C:\Users\ユーザー名\.ssh\my-rds-mysql-key.pem</code></pre>
<hr>
<h2><span id="toc10">② プロジェクトフォルダに移動する</span></h2>
<p>VSCodeのターミナル（CMD）を開き、プロジェクトフォルダに移動します。</p>
<pre><code class="language-cmd">cd C:\my-aws\aws-learning-projects\rds-mysql-ec2</code></pre>
<p><code>template.yaml</code> があることを確認します。</p>
<pre><code class="language-cmd">dir template.yaml</code></pre>
<hr>
<h2><span id="toc11">③ スタックの作成</span></h2>
<p>以下のコマンドを実行します。<code>203.0.113.1</code> は⓪で確認した自分のIPアドレスに置き換えます。</p>
<pre><code class="language-cmd">aws cloudformation create-stack ^
  --stack-name my-rds-mysql-stack ^
  --template-body file://template.yaml ^
  --region ap-northeast-1 ^
  --capabilities CAPABILITY_NAMED_IAM ^
  --parameters ^
    ParameterKey=EmployeeId,ParameterValue=123456 ^
    ParameterKey=KeyName,ParameterValue=my-rds-mysql-key ^
    ParameterKey=MyIP,ParameterValue=203.0.113.1/32 ^
    ParameterKey=DBPassword,ParameterValue=Handson1234!</code></pre>
<blockquote>
<p><strong><code>DBPassword</code> パラメータについて</strong>: <code>NoEcho: true</code> が設定されているため、コンソール上では <code>****</code> とマスクされます。</p>
</blockquote>
<blockquote>
<p><strong><code>CAPABILITY_NAMED_IAM</code> について</strong>: <code>EC2Role</code> のように名前を指定したIAMリソースを作成する場合に必要なフラグです。</p>
</blockquote>
<p>成功すると以下のような StackId が表示されます。</p>
<pre><code class="language-json">{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:123456789012:stack/my-rds-mysql-stack/xxxxx"
}</code></pre>
<hr>
<h2><span id="toc12">④ 作成完了・Outputsの確認</span></h2>
<blockquote>
<p><strong>注意</strong>: RDSの作成には<strong>約15〜20分</strong>かかります。<code>CREATE_IN_PROGRESS</code> が続く間は正常です。</p>
</blockquote>
<h3><span id="toc13">作成状況の確認</span></h3>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-rds-mysql-stack ^
  --query "Stacks[0].StackStatus" ^
  --output text</code></pre>
<table>
<thead>
<tr>
<th>ステータス</th>
<th>意味</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>CREATE_IN_PROGRESS</code></td>
<td>作成中（待つ）</td>
</tr>
<tr>
<td><code>CREATE_COMPLETE</code></td>
<td>作成完了</td>
</tr>
<tr>
<td><code>CREATE_FAILED</code></td>
<td>作成失敗（後述のトラブルシューティングを参照）</td>
</tr>
<tr>
<td><code>ROLLBACK_COMPLETE</code></td>
<td>失敗してロールバック完了</td>
</tr>
</tbody>
</table>
<h3><span id="toc14">Outputs（接続情報）の確認</span></h3>
<p><code>CREATE_COMPLETE</code> になったら以下を実行します。</p>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-rds-mysql-stack ^
  --query "Stacks[0].Outputs" ^
  --output table</code></pre>
<pre><code class="language-plaintext">+---------------------+--------------------------------------------------------------------+
|  OutputKey          |  OutputValue                                                       |
+---------------------+--------------------------------------------------------------------+
|  EC2PublicIP        |  x.x.x.x                                                          |
|  WebsiteURL         |  http://x.x.x.x                                                   |
|  RDSEndpoint        |  my-rds-mysql.xxxxx.ap-northeast-1.rds.amazonaws.com         |
|  DBPasswordParamName|  /my/rds/db-password                                         |
|  SSHCommand         |  ssh -i C:\Users\...\.ssh\...pem ec2-user@x.x.x.x                 |
|  MySQLConnectCommand|  mysql -h my-rds-mysql.xxxxx...rds.amazonaws.com -u admin ... |
+---------------------+--------------------------------------------------------------------+</code></pre>
<p><strong>控えておく情報</strong>: <code>EC2PublicIP</code>、<code>RDSEndpoint</code></p>
<p><!-- ![CloudFormation Outputs確認コマンドの実行結果](images/cfn-outputs.jpg) --></p>
<hr>
<h2><span id="toc15">⑤ 動作確認</span></h2>
<h3><span id="toc16">Webブラウザで確認</span></h3>
<p>Outputsに表示された <code>WebsiteURL</code> をブラウザで開きます。</p>
<p>「AP Server - RDS MySQL Hands-on」と表示されればEC2は正常です。</p>
<h3><span id="toc17">EC2にSSH接続する</span></h3>
<p>Outputsの <code>SSHCommand</code> を実行します（パスは実際のパスに修正します）。</p>
<pre><code class="language-cmd">ssh -i C:\Users\ユーザー名\.ssh\my-rds-mysql-key.pem ec2-user@（EC2PublicIP）</code></pre>
<h3><span id="toc18">Parameter StoreからDBパスワードを取得する</span></h3>
<p>EC2に接続した状態で実行します。</p>
<pre><code class="language-bash">aws ssm get-parameter \
  --name "/my/rds/db-password" \
  --query "Parameter.Value" \
  --output text \
  --region ap-northeast-1</code></pre>
<p><code>Handson1234!</code> が表示されれば成功です。</p>
<blockquote>
<p><strong><code>--with-decryption</code> が不要な理由</strong>: CloudFormationで作成したパラメータは <code>Type: String</code>（平文）のため、フラグなしでそのまま値が返ります。コンソールで <strong>SecureString</strong> として作成した場合は <code>--with-decryption</code> が必要です（コンソール版参照）。</p>
</blockquote>
<h3><span id="toc19">RDSへのMySQL接続テスト</span></h3>
<pre><code class="language-bash"># RDSエンドポイントをOutputsの値に置き換える
RDS_ENDPOINT="my-rds-mysql.xxxxxxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com"

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

mysql -h $RDS_ENDPOINT -u admin -p"$DB_PASSWORD" sampledb</code></pre>
<p>または Outputsに表示された <code>MySQLConnectCommand</code> を直接実行してもかまいません。</p>
<p>接続成功後、SQL操作を確認します。</p>
<pre><code class="language-sql">-- テーブル作成
CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(50),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- データ挿入
INSERT INTO users (name) VALUES ('Alice'), ('Bob');

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

-- 後片付け
DROP TABLE users;

EXIT;</code></pre>
<p><!-- ![EC2からRDS MySQLへの接続成功画面](images/rds-mysql-connect.jpg) --></p>
<p>EC2からSSH接続を切断します。</p>
<pre><code class="language-bash">exit</code></pre>
<hr>
<h2><span id="toc20">⑥ スタックの削除</span></h2>
<p><strong>課金を止めるために、ハンズオン完了後は必ず削除します。</strong></p>
<pre><code class="language-cmd">aws cloudformation delete-stack ^
  --stack-name my-rds-mysql-stack ^
  --region ap-northeast-1</code></pre>
<blockquote>
<p><strong>注意</strong>: RDS削除には<strong>約10〜15分</strong>かかります。コマンド実行後もしばらく待つ必要があります。</p>
</blockquote>
<h3><span id="toc21">削除状況の確認</span></h3>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-rds-mysql-stack ^
  --query "Stacks[0].StackStatus" ^
  --output text</code></pre>
<table>
<thead>
<tr>
<th>ステータス</th>
<th>意味</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>DELETE_IN_PROGRESS</code></td>
<td>削除中（待つ）</td>
</tr>
<tr>
<td><code>DELETE_FAILED</code></td>
<td>削除失敗（トラブルシューティングを参照）</td>
</tr>
</tbody>
</table>
<p>削除完了の確認（以下のエラーが表示されれば削除完了）:</p>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-rds-mysql-stack ^
  --region ap-northeast-1</code></pre>
<pre><code class="language-plaintext">An error occurred (ValidationError) when calling the DescribeStacks operation:
Stack with id my-rds-mysql-stack does not exist</code></pre>
<p>このメッセージが表示されれば削除完了です。</p>
<h3><span id="toc22">キーペアの手動削除</span></h3>
<p>キーペアはCloudFormationで管理していないため手動で削除します。</p>
<p><strong>EC2 → キーペア → <code>my-rds-mysql-key</code> を選択 → 「アクション」→「削除」</strong></p>
<hr>
<h2><span id="toc23">コンソール版との比較</span></h2>
<table>
<thead>
<tr>
<th>作業</th>
<th>コンソール（手動）</th>
<th>CloudFormation</th>
</tr>
</thead>
<tbody>
<tr>
<td>VPC + サブネット3つ + IGW + ルートテーブル</td>
<td>約15〜20分</td>
<td>template.yaml に定義済み</td>
</tr>
<tr>
<td>IAMロール作成</td>
<td>別画面で作成</td>
<td>template.yaml に定義済み</td>
</tr>
<tr>
<td>RDS DBサブネットグループ作成</td>
<td>別画面で作成</td>
<td>template.yaml に定義済み</td>
</tr>
<tr>
<td>RDS作成（設定項目20以上）</td>
<td>画面でポチポチ</td>
<td><code>--parameters DBPassword=...</code> を渡すだけ</td>
</tr>
<tr>
<td>全体のデプロイ</td>
<td><strong>約40〜50分</strong></td>
<td>コマンド1本（RDS待ち15〜20分）</td>
</tr>
<tr>
<td>削除</td>
<td><strong>11ステップ（RDS削除待ちあり）</strong></td>
<td><code>delete-stack</code> 1本</td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc24">トラブルシューティング</span></h2>
<table>
<thead>
<tr>
<th>症状</th>
<th>原因</th>
<th>対処</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>CREATE_FAILED</code> → DBInstance失敗</td>
<td>指定AZに db.t3.micro が作成できない</td>
<td>コンソールで利用可能なAZを確認する</td>
</tr>
<tr>
<td><code>stackName failed to satisfy regular expression pattern</code></td>
<td>スタック名が数字始まり</td>
<td><code>my-rds-mysql-stack</code> のように英字で始める</td>
</tr>
<tr>
<td><code>Unable to load paramfile, text contents could not be decoded</code></td>
<td>template.yaml に日本語が含まれている</td>
<td>template.yaml のコメントは<strong>英語のみ</strong>で記述する（Windows環境の既知の問題）</td>
</tr>
<tr>
<td><code>ERROR 2003: Can&#39;t connect to MySQL server</code></td>
<td>RDSがまだ起動中またはSGの設定ミス</td>
<td>RDSのステータスが「利用可能」になっているか確認。RDS SGのソースがEC2 SGのIDになっているか確認</td>
</tr>
<tr>
<td><code>ERROR 1045: Access denied for user</code></td>
<td>パスワードが誤り</td>
<td><code>--parameters DBPassword=...</code> の値を確認</td>
</tr>
<tr>
<td><code>AccessDeniedException</code> on <code>ssm get-parameter</code></td>
<td>IAMロールに <code>AmazonSSMReadOnlyAccess</code> がない</td>
<td><code>EC2Role</code> に <code>AmazonSSMReadOnlyAccess</code> がアタッチされているか確認</td>
</tr>
<tr>
<td><code>DELETE_FAILED</code></td>
<td>スナップショット作成が失敗した</td>
<td>コンソールからRDSを手動削除（スナップショットなしで）してから <code>delete-stack</code> を再実行</td>
</tr>
<tr>
<td>スタック作成後しばらく <code>CREATE_IN_PROGRESS</code> が続く</td>
<td>RDSの作成に時間がかかっている</td>
<td>正常。約15〜20分待つ</td>
</tr>
</tbody>
</table>
<h3><span id="toc25">失敗時の詳細確認コマンド</span></h3>
<pre><code class="language-cmd">aws cloudformation describe-stack-events ^
  --stack-name my-rds-mysql-stack ^
  --query "StackEvents[?ResourceStatus=='CREATE_FAILED'].[ResourceType,ResourceStatusReason]" ^
  --output table</code></pre>
<hr>
<h2><span id="toc26">まとめ</span></h2>
<table>
<thead>
<tr>
<th>ステップ</th>
<th>内容</th>
</tr>
</thead>
<tbody>
<tr>
<td>①</td>
<td>キーペアをコンソールで作成</td>
</tr>
<tr>
<td>②③</td>
<td><code>rds-mysql-ec2/</code> フォルダに移動し <code>create-stack</code> を実行</td>
</tr>
<tr>
<td>④</td>
<td><code>CREATE_COMPLETE</code> になったらOutputsでEC2IP・RDSエンドポイントを確認</td>
</tr>
<tr>
<td>⑤</td>
<td>EC2 → Parameter Store → RDS MySQL への接続テスト</td>
</tr>
<tr>
<td>⑥</td>
<td><code>delete-stack</code> 1本で22リソースを一括削除</td>
</tr>
</tbody>
</table>
<p>CloudFormation版の最大のメリットは<strong>再現性</strong>と<strong>削除の簡単さ</strong>です。コンソールでは40〜50分・11ステップかかった作業が、コマンド1本（待ち時間込みで約20分）で完了します。</p>
<p>コンソール操作でRDS・DBサブネットグループ・Parameter Storeを視覚的に確認したい場合は、<a href="https://caymezon.com/aws-handson-console-rds-mysql-ec2/">AWSコンソール版ハンズオン</a>を参照してください。</p><p>The post <a href="https://caymezon.com/aws-handson-cloudformation-rds-mysql-ec2/">CloudFormationでRDS MySQL + EC2接続環境を自動構築する手順【22リソース一括デプロイ / コンソール版との比較付き】</a> first appeared on <a href="https://caymezon.com">CayTech Lab</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://caymezon.com/aws-handson-cloudformation-rds-mysql-ec2/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>CloudFormationでAP+DB 2層構成（VPC設計）を自動デプロイする手順【踏み台SSH / コンソール版との比較付き】</title>
		<link>https://caymezon.com/aws-handson-cloudformation-ec2-2tier/</link>
					<comments>https://caymezon.com/aws-handson-cloudformation-ec2-2tier/#respond</comments>
		
		<dc:creator><![CDATA[caymezon]]></dc:creator>
		<pubDate>Sat, 04 Apr 2026 07:33:58 +0000</pubDate>
				<category><![CDATA[AWS Basic]]></category>
		<category><![CDATA[Cloud & Infra]]></category>
		<category><![CDATA[2層構成]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[AWSCLI]]></category>
		<category><![CDATA[CloudFormation]]></category>
		<category><![CDATA[EC2]]></category>
		<category><![CDATA[IaC]]></category>
		<category><![CDATA[MariaDB]]></category>
		<category><![CDATA[SG参照]]></category>
		<category><![CDATA[VPC]]></category>
		<category><![CDATA[セキュリティグループ]]></category>
		<category><![CDATA[ハンズオン]]></category>
		<category><![CDATA[パブリックサブネット]]></category>
		<category><![CDATA[プライベートサブネット]]></category>
		<category><![CDATA[初心者]]></category>
		<category><![CDATA[踏み台]]></category>
		<guid isPermaLink="false">https://caymezon.com/?p=20323</guid>

					<description><![CDATA[<p>目次 はじめにCloudFormation vs コンソール：どれだけ違うかキーワード解説前提条件template.yaml の全文1. VpcId を指定した SecurityGroup2. S3 VPC Gatewa [&#8230;]</p>
<p>The post <a href="https://caymezon.com/aws-handson-cloudformation-ec2-2tier/">CloudFormationでAP+DB 2層構成（VPC設計）を自動デプロイする手順【踏み台SSH / コンソール版との比較付き】</a> first appeared on <a href="https://caymezon.com">CayTech Lab</a>.</p>]]></description>
										<content:encoded><![CDATA[<div id="toc" class="toc tnt-number toc-center tnt-number border-element"><input type="checkbox" class="toc-checkbox" id="toc-checkbox-6" checked><label class="toc-title" for="toc-checkbox-6">目次</label>
    <div class="toc-content">
    <ol class="toc-list open"><li><a href="#toc1" tabindex="0">はじめに</a></li><li><a href="#toc2" tabindex="0">CloudFormation vs コンソール：どれだけ違うか</a></li><li><a href="#toc3" tabindex="0">キーワード解説</a></li><li><a href="#toc4" tabindex="0">前提条件</a></li><li><a href="#toc5" tabindex="0">template.yaml の全文</a><ol><li><a href="#toc6" tabindex="0">1. VpcId を指定した SecurityGroup</a></li><li><a href="#toc7" tabindex="0">2. S3 VPC Gateway Endpoint（無料）</a></li><li><a href="#toc8" tabindex="0">3. DBInstance の DependsOn</a></li><li><a href="#toc9" tabindex="0">4. SubnetId でサブネットを指定</a></li></ol></li><li><a href="#toc10" tabindex="0">作業順序</a></li><li><a href="#toc11" tabindex="0">⓪ 自分のIPアドレスを確認する</a></li><li><a href="#toc12" tabindex="0">① キーペアの作成（コンソールで実施）</a></li><li><a href="#toc13" tabindex="0">② プロジェクトフォルダに移動する</a></li><li><a href="#toc14" tabindex="0">③ スタックの作成</a></li><li><a href="#toc15" tabindex="0">④ 作成完了・Outputsの確認</a><ol><li><a href="#toc16" tabindex="0">作成状況の確認</a></li><li><a href="#toc17" tabindex="0">Outputs の確認</a></li></ol></li><li><a href="#toc18" tabindex="0">⑤ 動作確認</a><ol><li><a href="#toc19" tabindex="0">5-1. APサーバのWeb確認</a></li><li><a href="#toc20" tabindex="0">5-2. APサーバへのSSH接続</a></li><li><a href="#toc21" tabindex="0">5-3. キーペアをAPサーバにコピーする（踏み台SSH準備）</a></li><li><a href="#toc22" tabindex="0">5-4. APサーバ経由でDBサーバにSSH接続（踏み台接続）</a></li><li><a href="#toc23" tabindex="0">5-5. APサーバからMySQLに接続（AP→DB通信確認）</a></li></ol></li><li><a href="#toc24" tabindex="0">⑥ スタックの削除</a></li><li><a href="#toc25" tabindex="0">コンソール版との比較</a></li><li><a href="#toc26" tabindex="0">トラブルシューティング</a></li><li><a href="#toc27" tabindex="0">まとめ</a><ol><li><a href="#toc28" tabindex="0">CloudFormationのメリットを実感できたポイント</a></li></ol></li><li><a href="#toc29" tabindex="0">コンソール版と比較してみる</a></li><li><a href="#toc30" tabindex="0">関連記事</a></li></ol>
    </div>
  </div>

<h2><span id="toc1">はじめに</span></h2>
<p>「コンソールで20ステップかかったVPC設計をコードで再現したい」——それを実現するのが <strong>AWS CloudFormation</strong> です。</p>
<p>この記事では、<code>template.yaml</code> 1ファイルにVPC・サブネット・ルートテーブル・セキュリティグループ・EC2（2台）を定義し、<strong>コマンド1本でAP+DB 2層構成を一括構築するハンズオン</strong>を紹介します。コンソール版（<a href="https://caymezon.com/aws-handson-console-ec2-2tier/">AWSコンソール版ハンズオン</a>）と全く同じ構成を、CloudFormationで自動化します。</p>
<pre><code class="language-plaintext">ローカル環境（VSCode）
  └── template.yaml + AWS CLI
        ↓ スタック作成（コマンド1本）
AWS環境
  └── VPC（10.0.0.0/16）
        ├── インターネットゲートウェイ
        │
        ├── [パブリックサブネット 10.0.1.0/24]
        │     ├── パブリックルートテーブル（0.0.0.0/0 → IGW）
        │     └── APサーバEC2（Apache / パブリックIP あり）
        │
        └── [プライベートサブネット 10.0.2.0/24]
              ├── プライベートルートテーブル（VPC内のみ）
              ├── S3 VPC Gateway Endpoint（dnf用・無料）
              └── DBサーバEC2（MariaDB / パブリックIP なし）</code></pre>
<p><strong>このハンズオンで体験できること：</strong></p>
<ul>
<li>VPC・サブネット・IGW・ルートテーブルを <code>template.yaml</code> 1ファイルで定義する方法</li>
<li>S3 VPC Gateway Endpoint の <code>DependsOn</code> による起動順序制御</li>
<li><code>SubnetId</code> でEC2の配置サブネットを明示する方法</li>
<li><code>aws cloudformation create-stack</code> コマンド1本での17リソース一括デプロイ</li>
<li><code>delete-stack</code> による複雑な依存関係を持つVPCリソースの一括削除</li>
</ul>
<p><strong>このハンズオンの特徴：</strong></p>
<ul>
<li>コンソール版では20ステップ以上・30〜40分かかった作業が、コマンド1本（8〜12分）で完了</li>
<li><code>delete-stack</code> 1本でVPC・サブネット・IGW・RTの依存関係を自動解決して削除</li>
</ul>
<hr>
<p><!-- ![CloudFormationスタック作成完了画面](images/cfn-create-complete.jpg) --></p>
<p><!-- <a rel="nofollow" href="//af.moshimo.com/af/c/click?a_id=1384942&p_id=170&pc_id=185&pl_id=4062&url=https%3A%2F%2Fwww.amazon.co.jp%2Fs%3Fk%3D%25E6%259C%25AC%2BAWS%2B%25E9%2596%258B%25E7%2599%25BA%26__mk_ja_JP%3D%25E3%2582%25AB%25E3%2582%25BF%25E3%2582%25AB%25E3%2583%258A%26crid%3D1DE63UBHFOR4K%26sprefix%3D%25E6%259C%25AC%2Baws%2B%25E9%2596%258B%25E7%2599%25BA%252Caps%252C167%26ref%3Dnb_sb_noss" referrerpolicy="no-referrer-when-downgrade" attributionsrc>Amazon検索[本 AWS 開発]</a><img decoding="async" src="//i.moshimo.com/af/i/impression?a_id=1384942&p_id=170&pc_id=185&pl_id=4062" width="1" height="1" style="border:none;" alt="" loading="lazy"> --></p>
<p><!-- <!-- START MoshimoAffiliateEasyLink --><script type="text/javascript">(function(b,c,f,g,a,d,e){b.MoshimoAffiliateObject=a;b[a]=b[a]||function(){arguments.currentScript=c.currentScript||c.scripts[c.scripts.length-2];(b[a].q=b[a].q||[]).push(arguments)};c.getElementById(a)||(d=c.createElement(f),d.src=g,d.id=a,e=c.getElementsByTagName("body")[0],e.appendChild(d))})(window,document,"script","//dn.msmstatic.com/site/cardlink/bundle.js?20220329","msmaflink");msmaflink({"n":"AWSの基本・仕組み・重要用語が全部わかる教科書 (見るだけ図解)","b":"SBクリエイティブ","t":"","d":"https:\/\/m.media-amazon.com","c_p":"\/images\/I","p":["\/51DEDQXj6oL._SL500_.jpg","\/41F589smNwL._SL500_.jpg","\/41R6f9yyCWL._SL500_.jpg","\/41HqWQ9BvmL._SL500_.jpg","\/41p8p0ZU79L._SL500_.jpg","\/41qLC-fndBL._SL500_.jpg","\/41fcLv9VT5L._SL500_.jpg","\/51lRvCsvHqL._SL500_.jpg"],"u":{"u":"https:\/\/www.amazon.co.jp\/dp\/4815607850","t":"amazon","r_v":""},"v":"2.1","b_l":[{"id":1,"u_tx":"Amazonで見る","u_bc":"#f79256","u_url":"https:\/\/www.amazon.co.jp\/dp\/4815607850","a_id":1384942,"p_id":170,"pl_id":27060,"pc_id":185,"s_n":"amazon","u_so":1},{"id":2,"u_tx":"楽天市場で見る","u_bc":"#f76956","u_url":"https:\/\/search.rakuten.co.jp\/search\/mall\/AWS%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%83%BB%E4%BB%95%E7%B5%84%E3%81%BF%E3%83%BB%E9%87%8D%E8%A6%81%E7%94%A8%E8%AA%9E%E3%81%8C%E5%85%A8%E9%83%A8%E3%82%8F%E3%81%8B%E3%82%8B%E6%95%99%E7%A7%91%E6%9B%B8%20(%E8%A6%8B%E3%82%8B%E3%81%A0%E3%81%91%E5%9B%B3%E8%A7%A3)\/","a_id":1384917,"p_id":54,"pl_id":27059,"pc_id":54,"s_n":"rakuten","u_so":2},{"id":3,"u_tx":"Yahoo!ショッピングで見る","u_bc":"#66a7ff","u_url":"https:\/\/shopping.yahoo.co.jp\/search?first=1\u0026p=AWS%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%83%BB%E4%BB%95%E7%B5%84%E3%81%BF%E3%83%BB%E9%87%8D%E8%A6%81%E7%94%A8%E8%AA%9E%E3%81%8C%E5%85%A8%E9%83%A8%E3%82%8F%E3%81%8B%E3%82%8B%E6%95%99%E7%A7%91%E6%9B%B8%20(%E8%A6%8B%E3%82%8B%E3%81%A0%E3%81%91%E5%9B%B3%E8%A7%A3)","a_id":1466950,"p_id":1225,"pl_id":27061,"pc_id":1925,"s_n":"yahoo","u_so":3}],"eid":"eaCUB","s":"s"});</script></p>
<div id="msmaflink-eaCUB">リンク</div>
<p><!-- MoshimoAffiliateEasyLink END --> --></p>
<h2><span id="toc2">CloudFormation vs コンソール：どれだけ違うか</span></h2>
<table>
<thead>
<tr>
<th>比較項目</th>
<th>CloudFormation</th>
<th>コンソール（手動）</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>VPC・IGW・サブネット・RTの設定</strong></td>
<td>template.yaml に定義済み</td>
<td>6画面・20ステップ以上</td>
</tr>
<tr>
<td><strong>S3 VPC Endpoint作成</strong></td>
<td>template.yaml に定義済み</td>
<td>画面でポチポチ</td>
</tr>
<tr>
<td><strong>EC2×2台の起動</strong></td>
<td>コマンド1本（並列作成）</td>
<td>2回インスタンス起動操作</td>
</tr>
<tr>
<td><strong>全体のデプロイ</strong></td>
<td>コマンド1本（8〜12分）</td>
<td>20ステップ以上（30〜40分）</td>
</tr>
<tr>
<td><strong>削除（依存関係あり）</strong></td>
<td><code>delete-stack</code> 1本（自動解決）</td>
<td>8ステップ・手動管理</td>
</tr>
<tr>
<td><strong>再現性</strong></td>
<td>高い（同じ構成を何度でも再現可能）</td>
<td>低い（手順漏れのリスク大）</td>
</tr>
<tr>
<td><strong>バージョン管理</strong></td>
<td>Gitで管理可能</td>
<td>不可</td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc3">キーワード解説</span></h2>
<table>
<thead>
<tr>
<th>用語</th>
<th>意味</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>CloudFormation</strong></td>
<td>AWSが提供するIaC（Infrastructure as Code）サービス</td>
</tr>
<tr>
<td><strong>スタック</strong></td>
<td>CloudFormationが管理するリソースのまとまり。今回は17リソースを1スタックで管理</td>
</tr>
<tr>
<td><strong><code>DependsOn</code></strong></td>
<td>CloudFormationでリソースの作成順序を明示するプロパティ</td>
</tr>
<tr>
<td><strong><code>SubnetId</code></strong></td>
<td>EC2インスタンスを特定のサブネットに配置するプロパティ</td>
</tr>
<tr>
<td><strong>S3 VPC Gateway Endpoint</strong></td>
<td>プライベートサブネットからS3にアクセスするための無料エンドポイント</td>
</tr>
<tr>
<td><strong><code>!Select [0, !GetAZs &#39;&#39;]</code></strong></td>
<td>リージョン内の最初のAZを自動取得するCloudFormation関数</td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc4">前提条件</span></h2>
<table>
<thead>
<tr>
<th>ツール</th>
<th>確認コマンド</th>
<th>最低バージョン目安</th>
</tr>
</thead>
<tbody>
<tr>
<td>AWS CLI v2</td>
<td><code>aws --version</code></td>
<td>2.x</td>
</tr>
<tr>
<td>Git</td>
<td><code>git --version</code></td>
<td>2.x</td>
</tr>
<tr>
<td>VSCode</td>
<td>-</td>
<td>-</td>
</tr>
</tbody>
</table>
<p>AWS認証確認:</p>
<pre><code class="language-cmd">aws sts get-caller-identity</code></pre>
<hr>
<h2><span id="toc5">template.yaml の全文</span></h2>
<p>以下が今回使用する <code>template.yaml</code> の全文です（17リソース）。プロジェクトフォルダ（<code>ec2-2tier-web/</code>）直下に配置します。</p>
<blockquote>
<p><strong>⚠️ コピー前に確認</strong>: <code>Default: &#39;123.456.78.901/32&#39;</code> の箇所は<strong>ダミーIPです</strong>。このまま使うとスタック作成は成功しますが、SSHもWebもアクセスできません。<code>--parameters</code> でIPを上書きするか（推奨）、Defaultを自分のIPに書き換えてから使ってください。</p>
</blockquote>
<pre><code class="language-yaml">AWSTemplateFormatVersion: '2010-09-09'
Description: 'EC2 2-tier Architecture (AP + DB) with VPC public/private subnet separation (Phase 1-4)'

Parameters:
  KeyName:
    Type: String
    Default: 'my-ec2-2tier-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 HTTP access (CIDR format e.g. 203.0.113.1/32)

Resources:

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

  # VPC: isolated network space for this hands-on
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: 'my-vpc'

  # Internet Gateway: connects the VPC to the internet
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: 'my-igw'

  # Attach Internet Gateway to VPC
  IGWAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  # Public Subnet: AP server goes here (internet accessible)
  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: !Select [0, !GetAZs '']
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: 'my-public-subnet'

  # Private Subnet: DB server goes here (no direct internet access)
  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.2.0/24
      AvailabilityZone: !Select [0, !GetAZs '']
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: 'my-private-subnet'

  # Public Route Table: routes internet traffic via Internet Gateway
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: 'my-public-rt'

  # Default route for public subnet: all traffic -&gt; Internet Gateway
  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: IGWAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  # Associate public subnet with public route table
  PublicSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref PublicRouteTable

  # Private Route Table: no internet route (local traffic only)
  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: 'my-private-rt'

  # Associate private subnet with private route table
  PrivateSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet
      RouteTableId: !Ref PrivateRouteTable

  # S3 VPC Gateway Endpoint: allows private subnet to reach S3 (for dnf package downloads)
  # Gateway endpoints are FREE - no hourly charge
  S3VPCEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref VPC
      ServiceName: !Sub 'com.amazonaws.${AWS::Region}.s3'
      VpcEndpointType: Gateway
      RouteTableIds:
        - !Ref PrivateRouteTable

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

  # IAM Role: allows EC2 to use Session Manager
  EC2Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: 'my-ec2-2tier-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-2tier-role'

  EC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: 'my-ec2-2tier-profile'
      Roles:
        - !Ref EC2Role

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

  # AP Server Security Group: SSH and HTTP from MyIP
  APSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: 'my AP server SG (public subnet)'
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref MyIP
          Description: SSH from my IP
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Ref MyIP
          Description: HTTP from my IP
      Tags:
        - Key: Name
          Value: 'my-ap-sg'

  # DB Server Security Group: MySQL from AP SG only, SSH from AP SG (via bastion)
  DBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: 'my DB server SG (private subnet)'
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          SourceSecurityGroupId: !GetAtt APSecurityGroup.GroupId
          Description: SSH from AP server only (bastion access)
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          SourceSecurityGroupId: !GetAtt APSecurityGroup.GroupId
          Description: MySQL from AP server only (SG-to-SG control)
      Tags:
        - Key: Name
          Value: 'my-db-sg'

  # ============================================================
  # EC2 Instances
  # ============================================================

  # AP Server: Apache in public subnet
  APInstance:
    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 PublicSubnet
      SecurityGroupIds:
        - !GetAtt APSecurityGroup.GroupId
      IamInstanceProfile: !Ref EC2InstanceProfile
      UserData:
        Fn::Base64: |
          #!/bin/bash
          dnf update -y
          dnf install -y httpd
          systemctl start httpd
          systemctl enable httpd
          cat &gt; /var/www/html/index.html &lt;&lt; 'HTMLEOF'
          &lt;!DOCTYPE html&gt;
          &lt;html&gt;
          &lt;head&gt;&lt;meta charset="UTF-8"&gt;&lt;title&gt;AP Server - Phase 1-4&lt;/title&gt;&lt;/head&gt;
          &lt;body&gt;
          &lt;h1&gt;AP Server (Public Subnet)&lt;/h1&gt;
          &lt;p&gt;This AP server is in the PUBLIC subnet and can reach the DB server in the PRIVATE subnet.&lt;/p&gt;
          &lt;/body&gt;
          &lt;/html&gt;
          HTMLEOF
      Tags:
        - Key: Name
          Value: 'my-ap-instance'

  # DB Server: MariaDB in private subnet (no public IP)
  DBInstance:
    Type: AWS::EC2::Instance
    DependsOn:
      - S3VPCEndpoint
      - PrivateSubnetRouteTableAssociation
    Properties:
      ImageId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64}}'
      InstanceType: t2.micro
      KeyName: !Ref KeyName
      SubnetId: !Ref PrivateSubnet
      SecurityGroupIds:
        - !GetAtt DBSecurityGroup.GroupId
      IamInstanceProfile: !Ref EC2InstanceProfile
      UserData:
        Fn::Base64: |
          #!/bin/bash
          dnf update -y
          dnf install -y mariadb105-server mariadb105
          systemctl start mariadb
          systemctl enable mariadb
          sudo mysql -u root &lt;&lt; '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-db-instance'

Outputs:
  VPCId:
    Description: VPC ID
    Value: !Ref VPC

  APPublicIP:
    Description: AP server public IP (open in browser or SSH)
    Value: !GetAtt APInstance.PublicIp

  DBPrivateIP:
    Description: DB server private IP (connect via AP server bastion)
    Value: !GetAtt DBInstance.PrivateIp

  WebsiteURL:
    Description: AP server website URL
    Value: !Sub 'http://${APInstance.PublicIp}'

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

  DBSSHCommand:
    Description: SSH command to DB server (run this FROM the AP server)
    Value: !Sub 'ssh -i ~/.ssh/${KeyName}.pem ec2-user@${DBInstance.PrivateIp}'

  MySQLConnectCommand:
    Description: MySQL connect command (run this FROM the AP server)
    Value: !Sub 'mysql -h ${DBInstance.PrivateIp} -u handson -pHandson1234! sampledb'</code></pre>
<p><strong>template.yaml の重要ポイント:</strong></p>
<h3><span id="toc6">1. VpcId を指定した SecurityGroup</span></h3>
<p>カスタムVPCを使う場合は <code>VpcId</code> を明示します。<code>VpcId</code> を指定することで <code>!GetAtt APSecurityGroup.GroupId</code> が確実にSG IDを返します。</p>
<pre><code class="language-yaml">APSecurityGroup:
  Type: AWS::EC2::SecurityGroup
  Properties:
    VpcId: !Ref VPC      # ← カスタムVPCを指定（必須）

DBSecurityGroup:
  SecurityGroupIngress:
    - SourceSecurityGroupId: !GetAtt APSecurityGroup.GroupId  # ← !Refではなく!GetAtt</code></pre>
<h3><span id="toc7">2. S3 VPC Gateway Endpoint（無料）</span></h3>
<p>プライベートサブネットのEC2は直接インターネットに出られませんが、Amazon Linux 2023のパッケージリポジトリはS3上にあります。S3 VPC Gateway EndpointをプライベートルートテーブルにアタッチするとS3経由で <code>dnf install</code> が動作します。</p>
<pre><code class="language-yaml">S3VPCEndpoint:
  Type: AWS::EC2::VPCEndpoint
  Properties:
    VpcEndpointType: Gateway     # Gatewayタイプは無料
    ServiceName: !Sub 'com.amazonaws.${AWS::Region}.s3'
    RouteTableIds:
      - !Ref PrivateRouteTable   # プライベートRTにS3ルートが追加される</code></pre>
<h3><span id="toc8">3. DBInstance の DependsOn</span></h3>
<p>DBサーバEC2のUserDataが <code>dnf install</code> を実行するとき、S3 VPC Endpointが有効になっている必要があります。</p>
<pre><code class="language-yaml">DBInstance:
  DependsOn:
    - S3VPCEndpoint
    - PrivateSubnetRouteTableAssociation</code></pre>
<h3><span id="toc9">4. SubnetId でサブネットを指定</span></h3>
<p>EC2インスタンスがどのサブネットに配置されるかを明示します。</p>
<pre><code class="language-yaml">APInstance:
  SubnetId: !Ref PublicSubnet   # パブリックサブネットに配置

DBInstance:
  SubnetId: !Ref PrivateSubnet  # プライベートサブネットに配置</code></pre>
<p><strong>構築されるリソース一覧（17個）:</strong></p>
<table>
<thead>
<tr>
<th>#</th>
<th>リソース</th>
<th>論理ID</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>VPC</td>
<td><code>VPC</code></td>
</tr>
<tr>
<td>2</td>
<td>インターネットゲートウェイ</td>
<td><code>InternetGateway</code></td>
</tr>
<tr>
<td>3</td>
<td>IGW-VPCアタッチ</td>
<td><code>IGWAttachment</code></td>
</tr>
<tr>
<td>4</td>
<td>パブリックサブネット</td>
<td><code>PublicSubnet</code></td>
</tr>
<tr>
<td>5</td>
<td>プライベートサブネット</td>
<td><code>PrivateSubnet</code></td>
</tr>
<tr>
<td>6</td>
<td>パブリックルートテーブル</td>
<td><code>PublicRouteTable</code></td>
</tr>
<tr>
<td>7</td>
<td>パブリックルート（→IGW）</td>
<td><code>PublicRoute</code></td>
</tr>
<tr>
<td>8</td>
<td>パブリックRT関連付け</td>
<td><code>PublicSubnetRouteTableAssociation</code></td>
</tr>
<tr>
<td>9</td>
<td>プライベートルートテーブル</td>
<td><code>PrivateRouteTable</code></td>
</tr>
<tr>
<td>10</td>
<td>プライベートRT関連付け</td>
<td><code>PrivateSubnetRouteTableAssociation</code></td>
</tr>
<tr>
<td>11</td>
<td>S3 VPC Gateway Endpoint</td>
<td><code>S3VPCEndpoint</code></td>
</tr>
<tr>
<td>12</td>
<td>IAMロール</td>
<td><code>EC2Role</code></td>
</tr>
<tr>
<td>13</td>
<td>インスタンスプロファイル</td>
<td><code>EC2InstanceProfile</code></td>
</tr>
<tr>
<td>14</td>
<td>APサーバSG</td>
<td><code>APSecurityGroup</code></td>
</tr>
<tr>
<td>15</td>
<td>DBサーバSG</td>
<td><code>DBSecurityGroup</code></td>
</tr>
<tr>
<td>16</td>
<td>APサーバEC2</td>
<td><code>APInstance</code></td>
</tr>
<tr>
<td>17</td>
<td>DBサーバEC2</td>
<td><code>DBInstance</code></td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc10">作業順序</span></h2>
<pre><code class="language-plaintext">⓪ 自分のIPアドレスを確認する
      ↓
① キーペアを作成する（コンソールで実施）
      ↓
② プロジェクトフォルダに移動する
      ↓
③ スタックを作成する（aws cloudformation create-stack）
      ↓
④ 作成完了・Outputsを確認する
      ↓
⑤ 動作確認（Web・踏み台SSH・MySQL接続）
      ↓
⑥ スタックを削除する（aws cloudformation delete-stack）</code></pre>
<hr>
<h2><span id="toc11">⓪ 自分のIPアドレスを確認する</span></h2>
<pre><code class="language-cmd">curl https://checkip.amazonaws.com</code></pre>
<p><strong>控えておく情報</strong>: 自分のIPアドレス（例: <code>203.0.113.1</code>）</p>
<hr>
<h2><span id="toc12">① キーペアの作成（コンソールで実施）</span></h2>
<p>キーペアのみコンソールで作成します。</p>
<p><strong>AWSコンソール → EC2 → キーペア → 「キーペアを作成」</strong></p>
<table>
<thead>
<tr>
<th>設定項目</th>
<th>値</th>
</tr>
</thead>
<tbody>
<tr>
<td>名前</td>
<td><code>my-ec2-2tier-key</code></td>
</tr>
<tr>
<td>キーペアのタイプ</td>
<td><strong>RSA</strong></td>
</tr>
<tr>
<td>プライベートキーファイル形式</td>
<td><strong>.pem</strong></td>
</tr>
</tbody>
</table>
<pre><code class="language-plaintext">C:\Users\ユーザー名\.ssh\my-ec2-2tier-key.pem</code></pre>
<hr>
<h2><span id="toc13">② プロジェクトフォルダに移動する</span></h2>
<pre><code class="language-cmd">cd C:\my-aws\aws-learning-projects\ec2-2tier-web</code></pre>
<pre><code class="language-cmd">dir template.yaml</code></pre>
<hr>
<h2><span id="toc14">③ スタックの作成</span></h2>
<p><code>203.0.113.1</code> は⓪で確認した自分のIPアドレスに置き換えてください。</p>
<pre><code class="language-cmd">aws cloudformation create-stack ^
  --stack-name my-ec2-2tier-stack ^
  --template-body file://template.yaml ^
  --region ap-northeast-1 ^
  --capabilities CAPABILITY_NAMED_IAM ^
  --parameters ^
    ParameterKey=KeyName,ParameterValue=my-ec2-2tier-key ^
    ParameterKey=MyIP,ParameterValue=203.0.113.1/32</code></pre>
<p><strong>各オプションの説明:</strong></p>
<table>
<thead>
<tr>
<th>オプション</th>
<th>意味</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>--stack-name my-ec2-2tier-stack</code></td>
<td>スタック名（英字始まり）</td>
</tr>
<tr>
<td><code>--template-body file://template.yaml</code></td>
<td>使用するテンプレートファイル</td>
</tr>
<tr>
<td><code>--region ap-northeast-1</code></td>
<td>デプロイ先リージョン（東京）</td>
</tr>
<tr>
<td><code>--capabilities CAPABILITY_NAMED_IAM</code></td>
<td>名前付きIAMロール作成の明示的な許可</td>
</tr>
<tr>
<td><code>--parameters ...</code></td>
<td>テンプレートのParametersに渡す値</td>
</tr>
</tbody>
</table>
<p>成功するとStackIdが表示されます。</p>
<pre><code class="language-json">{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:123456789012:stack/my-ec2-2tier-stack/xxxxx"
}</code></pre>
<p><!-- ![スタック作成コマンドの実行結果](images/cfn-create-stack-cmd.jpg) --></p>
<hr>
<h2><span id="toc15">④ 作成完了・Outputsの確認</span></h2>
<p>スタック作成完了まで<strong>約8〜12分</strong>かかります（VPCリソース×10個 + EC2×2台 + MariaDBインストールの時間）。</p>
<h3><span id="toc16">作成状況の確認</span></h3>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-ec2-2tier-stack ^
  --query "Stacks[0].StackStatus" ^
  --output text</code></pre>
<table>
<thead>
<tr>
<th>ステータス</th>
<th>意味</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>CREATE_IN_PROGRESS</code></td>
<td>作成中（しばらく待つ）</td>
</tr>
<tr>
<td><code>CREATE_COMPLETE</code></td>
<td>作成完了</td>
</tr>
<tr>
<td><code>CREATE_FAILED</code></td>
<td>作成失敗（トラブルシューティングを参照）</td>
</tr>
<tr>
<td><code>ROLLBACK_COMPLETE</code></td>
<td>失敗してロールバック完了</td>
</tr>
</tbody>
</table>
<h3><span id="toc17">Outputs の確認</span></h3>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-ec2-2tier-stack ^
  --query "Stacks[0].Outputs" ^
  --output table</code></pre>
<p>以下のような出力が表示されます。</p>
<pre><code class="language-plaintext">--------------------------------------------------------------------------------------
|                                  DescribeStacks                                    |
+--------------------+---------------------------------------------------------------+
|  OutputKey         |  OutputValue                                                  |
+--------------------+---------------------------------------------------------------+
|  VPCId             |  vpc-xxxxxxxxxx                                              |
|  APPublicIP        |  x.x.x.x                                                    |
|  DBPrivateIP       |  10.0.2.xxx   ← APサーバからの接続に使用                    |
|  WebsiteURL        |  http://x.x.x.x                                             |
|  APSSHCommand      |  ssh -i ...pem ec2-user@x.x.x.x                            |
|  DBSSHCommand      |  ssh -i ~/.ssh/...pem ec2-user@10.0.2.xxx                  |
|  MySQLConnectCommand| mysql -h 10.0.2.xxx -u handson -pHandson1234! sampledb     |
+--------------------+---------------------------------------------------------------+</code></pre>
<p><strong>控えておく情報:</strong></p>
<ul>
<li><code>APPublicIP</code>: APサーバのパブリックIP</li>
<li><code>DBPrivateIP</code>: DBサーバのプライベートIP（<code>10.0.2.xxx</code> 形式）</li>
<li><code>APSSHCommand</code>: APサーバへのSSHコマンド</li>
<li><code>DBSSHCommand</code>: APサーバからDBサーバへのSSHコマンド</li>
<li><code>MySQLConnectCommand</code>: APサーバから実行するMySQL接続コマンド</li>
</ul>
<p><!-- ![Outputs確認コマンドの実行結果](images/cfn-outputs-table.jpg) --></p>
<hr>
<h2><span id="toc18">⑤ 動作確認</span></h2>
<h3><span id="toc19">5-1. APサーバのWeb確認</span></h3>
<p><code>WebsiteURL</code> をブラウザで開きます。<code>AP Server (Public Subnet)</code> が表示されれば成功です。</p>
<h3><span id="toc20">5-2. APサーバへのSSH接続</span></h3>
<p><code>APSSHCommand</code> を実行します（パスを実際のパスに修正します）。</p>
<pre><code class="language-cmd">ssh -i C:\Users\ユーザー名\.ssh\my-ec2-2tier-key.pem ec2-user@（APPublicIP）</code></pre>
<h3><span id="toc21">5-3. キーペアをAPサーバにコピーする（踏み台SSH準備）</span></h3>
<p><strong>ローカルPCの別ターミナルで実行します:</strong></p>
<pre><code class="language-cmd">scp -i C:\Users\ユーザー名\.ssh\my-ec2-2tier-key.pem ^
  C:\Users\ユーザー名\.ssh\my-ec2-2tier-key.pem ^
  ec2-user@（APPublicIP）:~/.ssh/</code></pre>
<p>APサーバのSSHセッションでパーミッションを設定します。</p>
<pre><code class="language-bash">chmod 400 ~/.ssh/my-ec2-2tier-key.pem</code></pre>
<h3><span id="toc22">5-4. APサーバ経由でDBサーバにSSH接続（踏み台接続）</span></h3>
<p><strong>APサーバのSSHセッションから実行します:</strong></p>
<pre><code class="language-bash">ssh -i ~/.ssh/my-ec2-2tier-key.pem ec2-user@（DBPrivateIP）</code></pre>
<p><code>[ec2-user@ip-10-0-2-xxx ~]$</code> が表示されれば踏み台接続成功です。</p>
<pre><code class="language-bash"># DBサーバ上でMariaDBの状態を確認
sudo systemctl status mariadb

# MariaDBに接続
mysql -u root -pAdmin1234! -e "SHOW DATABASES;"

exit</code></pre>
<h3><span id="toc23">5-5. APサーバからMySQLに接続（AP→DB通信確認）</span></h3>
<p>APサーバのSSHセッションに戻り、<code>MySQLConnectCommand</code> をそのまま実行します。</p>
<pre><code class="language-bash"># mysqlクライアントをインストール
sudo dnf install -y mariadb105

# OutputsのMySQLConnectCommandをそのまま使う
mysql -h （DBPrivateIP） -u handson -pHandson1234! sampledb</code></pre>
<pre><code class="language-sql">CREATE TABLE IF NOT EXISTS messages (id INT AUTO_INCREMENT PRIMARY KEY, text VARCHAR(100));
INSERT INTO messages (text) VALUES ('Hello from AP server!');
SELECT * FROM messages;
EXIT;</code></pre>
<pre><code class="language-bash">exit</code></pre>
<p><!-- ![踏み台SSHとMySQL接続確認](images/bastion-mysql-connect.jpg) --></p>
<hr>
<h2><span id="toc24">⑥ スタックの削除</span></h2>
<p><strong>課金を止めるために、ハンズオン完了後は必ず削除してください。</strong></p>
<p>CloudFormationはVPC・サブネット・IGW・RT・SG・EC2の依存関係を自動解決して正しい順序で削除します。</p>
<pre><code class="language-cmd">aws cloudformation delete-stack ^
  --stack-name my-ec2-2tier-stack ^
  --region ap-northeast-1</code></pre>
<p>削除完了まで<strong>約8〜12分</strong>かかります。</p>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-ec2-2tier-stack ^
  --query "Stacks[0].StackStatus" ^
  --output text</code></pre>
<table>
<thead>
<tr>
<th>ステータス</th>
<th>意味</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>DELETE_IN_PROGRESS</code></td>
<td>削除中（しばらく待つ）</td>
</tr>
<tr>
<td><code>DELETE_FAILED</code></td>
<td>削除失敗（トラブルシューティングを参照）</td>
</tr>
</tbody>
</table>
<p>削除完了の確認（以下のエラーが表示されれば削除完了）。</p>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-ec2-2tier-stack ^
  --region ap-northeast-1</code></pre>
<pre><code class="language-plaintext">An error occurred (ValidationError) when calling the DescribeStacks operation:
Stack with id my-ec2-2tier-stack does not exist</code></pre>
<p>キーペアは手動で削除します。</p>
<p><strong>EC2 → キーペア → <code>my-ec2-2tier-key</code> → 「削除」</strong></p>
<hr>
<h2><span id="toc25">コンソール版との比較</span></h2>
<p>コンソール版で20ステップ以上かかった作業が、CloudFormationでは <code>template.yaml</code> に定義されています。</p>
<table>
<thead>
<tr>
<th>CFnの記述</th>
<th>コンソールでやること</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>VPC</code></td>
<td>VPC → VPCを作成</td>
</tr>
<tr>
<td><code>InternetGateway</code> + <code>IGWAttachment</code></td>
<td>IGWを作成 → VPCにアタッチ（2ステップ）</td>
</tr>
<tr>
<td><code>PublicSubnet</code> + <code>PrivateSubnet</code></td>
<td>サブネットを2つ作成</td>
</tr>
<tr>
<td><code>PublicRouteTable</code> + <code>PublicRoute</code> + <code>PublicSubnetRouteTableAssociation</code></td>
<td>パブリックRTを作成・ルート追加・サブネット関連付け（3ステップ）</td>
</tr>
<tr>
<td><code>PrivateRouteTable</code> + <code>PrivateSubnetRouteTableAssociation</code></td>
<td>プライベートRTを作成・サブネット関連付け（2ステップ）</td>
</tr>
<tr>
<td><code>S3VPCEndpoint</code></td>
<td>VPCエンドポイントを作成</td>
</tr>
<tr>
<td><code>APSecurityGroup</code> + <code>DBSecurityGroup</code></td>
<td>SGを2つ作成（APサーバSG → DBサーバSGの順）</td>
</tr>
<tr>
<td><code>APInstance</code> + <code>DBInstance</code></td>
<td>EC2を2回起動（各5〜10分）</td>
</tr>
<tr>
<td><code>delete-stack</code></td>
<td>8ステップの手動削除（依存関係を手動管理）</td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc26">トラブルシューティング</span></h2>
<table>
<thead>
<tr>
<th>症状</th>
<th>原因</th>
<th>対処</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>CREATE_FAILED</code> になる</td>
<td>キーペアが存在しない</td>
<td>コンソールでキーペアを確認・作成</td>
</tr>
<tr>
<td><code>CREATE_FAILED</code> になる</td>
<td>同名のIAMロールが既に存在する</td>
<td>IAMコンソールでロールを削除してから再実行</td>
</tr>
<tr>
<td><code>stackName failed to satisfy regular expression</code></td>
<td>スタック名が数字始まり</td>
<td>CloudFormationのスタック名は<strong>英字で始まる</strong>必要がある</td>
</tr>
<tr>
<td><code>Unable to load paramfile</code> エラー</td>
<td>template.yaml に日本語が含まれている</td>
<td>template.yaml のコメントは<strong>英語のみ</strong>で記述する</td>
</tr>
<tr>
<td>DBサーバのMariaDBが起動していない</td>
<td>S3 VPC EndpointのRouteTableへの反映が遅延</td>
<td>DBサーバに踏み台接続して <code>sudo dnf install -y mariadb105-server</code> を手動実行</td>
</tr>
<tr>
<td>APサーバからDBにSSH接続できない</td>
<td>DBサーバSGのSSHソースが正しくない</td>
<td>CloudFormationのイベントログで <code>CREATE_FAILED</code> を確認</td>
</tr>
<tr>
<td><code>DELETE_FAILED</code> になる</td>
<td>VPC内のリソースが残っている</td>
<td>コンソールで <code>my-vpc</code> 内の残存リソースを確認して手動削除</td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc27">まとめ</span></h2>
<p>今回のハンズオンで実現できたこと：</p>
<table>
<thead>
<tr>
<th>確認項目</th>
<th>内容</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>VPCリソースのIaC化</strong></td>
<td>VPC・IGW・サブネット・RT 10リソースを <code>template.yaml</code> 1ファイルで定義</td>
</tr>
<tr>
<td><strong>S3 VPC Endpoint</strong></td>
<td><code>DependsOn</code> でDBサーバEC2の起動前にEndpointが有効になることを保証</td>
</tr>
<tr>
<td><strong>一括デプロイ</strong></td>
<td><code>create-stack</code> 1本で17リソースを8〜12分で構築</td>
</tr>
<tr>
<td><strong>依存関係の自動解決</strong></td>
<td><code>delete-stack</code> 1本でVPC内のリソースを正しい順序で自動削除</td>
</tr>
<tr>
<td><strong>Outputs活用</strong></td>
<td><code>DBPrivateIP</code>・<code>MySQLConnectCommand</code> が自動生成されるため手動でコマンドを組み立てる必要なし</td>
</tr>
</tbody>
</table>
<h3><span id="toc28">CloudFormationのメリットを実感できたポイント</span></h3>
<ul>
<li>コンソール版では20ステップ以上・30〜40分かかった作業がコマンド1本（8〜12分）で完了</li>
<li>VPC・サブネット・RT・SGの複雑な削除順序をCloudFormationが自動管理</li>
<li><code>template.yaml</code> をGitで管理することで、ネットワーク設計の変更履歴が残せる</li>
</ul>
<hr>
<h2><span id="toc29">コンソール版と比較してみる</span></h2>
<p>CloudFormationが裏で何をやっているか、同じ構成をAWSコンソールのみで構築する手順をまとめました。VPCリソースの作成・削除順序や依存関係を視覚的に確認できます。</p>
<p><a href="https://caymezon.com/aws-handson-console-ec2-2tier/">AWSコンソールでVPC設計からAP+DB 2層構成を構築する手順</a></p>
<hr>
<h2><span id="toc30">関連記事</span></h2>
<p><!-- <a rel="nofollow" href="//af.moshimo.com/af/c/click?a_id=1384942&p_id=170&pc_id=185&pl_id=4062&url=https%3A%2F%2Fwww.amazon.co.jp%2Fs%3Fk%3D%25E6%259C%25AC%2BAWS%2B%25E9%2596%258B%25E7%2599%25BA%26__mk_ja_JP%3D%25E3%2582%25AB%25E3%2582%25BF%25E3%2582%25AB%25E3%2583%258A%26crid%3D1DE63UBHFOR4K%26sprefix%3D%25E6%259C%25AC%2Baws%2B%25E9%2596%258B%25E7%2599%25BA%252Caps%252C167%26ref%3Dnb_sb_noss" referrerpolicy="no-referrer-when-downgrade" attributionsrc>Amazon検索[本 AWS 開発]</a><img decoding="async" src="//i.moshimo.com/af/i/impression?a_id=1384942&p_id=170&pc_id=185&pl_id=4062" width="1" height="1" style="border:none;" alt="" loading="lazy"> --></p>
<p><!-- <!-- START MoshimoAffiliateEasyLink --><script type="text/javascript">(function(b,c,f,g,a,d,e){b.MoshimoAffiliateObject=a;b[a]=b[a]||function(){arguments.currentScript=c.currentScript||c.scripts[c.scripts.length-2];(b[a].q=b[a].q||[]).push(arguments)};c.getElementById(a)||(d=c.createElement(f),d.src=g,d.id=a,e=c.getElementsByTagName("body")[0],e.appendChild(d))})(window,document,"script","//dn.msmstatic.com/site/cardlink/bundle.js?20220329","msmaflink");msmaflink({"n":"AWS運用入門 改訂第2版 押さえておきたいAWSの基本と運用ノウハウ [AWS深掘りガイド]","b":"SBクリエイティブ","t":"","d":"https:\/\/m.media-amazon.com","c_p":"\/images\/I","p":["\/51AAOubymTL._SL500_.jpg","\/51VMG6YKHdL._SL500_.jpg","\/41EdPB8azAL._SL500_.jpg","\/41v2JFE-9jL._SL500_.jpg","\/41FEEqR-yDL._SL500_.jpg","\/41JfZAdnTPL._SL500_.jpg","\/41vGK0czQrL._SL500_.jpg","\/41-SnYtz2aL._SL500_.jpg","\/41sPrV5fi3L._SL500_.jpg","\/41p7JtvYJ1L._SL500_.jpg","\/4169GVNTs8L._SL500_.jpg","\/41BPI5HP3zL._SL500_.jpg","\/41QOyk60CYL._SL500_.jpg","\/41APjk6FphL._SL500_.jpg","\/41ezKUu7VRL._SL500_.jpg","\/41A1n3K+r5L._SL500_.jpg","\/41aY2T8lEOL._SL500_.jpg","\/419Ca1V6HZL._SL500_.jpg","\/41zQkYyLPzL._SL500_.jpg","\/41YpHcyxiTL._SL500_.jpg","\/41-tKN5mt6L._SL500_.jpg","\/419Mv6m55IL._SL500_.jpg"],"u":{"u":"https:\/\/www.amazon.co.jp\/dp\/4815631085","t":"amazon","r_v":""},"v":"2.1","b_l":[{"id":1,"u_tx":"Amazonで見る","u_bc":"#f79256","u_url":"https:\/\/www.amazon.co.jp\/dp\/4815631085","a_id":1384942,"p_id":170,"pl_id":27060,"pc_id":185,"s_n":"amazon","u_so":1},{"id":2,"u_tx":"楽天市場で見る","u_bc":"#f76956","u_url":"https:\/\/search.rakuten.co.jp\/search\/mall\/AWS%E9%81%8B%E7%94%A8%E5%85%A5%E9%96%80%20%E6%94%B9%E8%A8%82%E7%AC%AC2%E7%89%88%20%E6%8A%BC%E3%81%95%E3%81%88%E3%81%A6%E3%81%8A%E3%81%8D%E3%81%9F%E3%81%84AWS%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%81%A8%E9%81%8B%E7%94%A8%E3%83%8E%E3%82%A6%E3%83%8F%E3%82%A6%20%5BAWS%E6%B7%B1%E6%8E%98%E3%82%8A%E3%82%AC%E3%82%A4%E3%83%89%5D\/","a_id":1384917,"p_id":54,"pl_id":27059,"pc_id":54,"s_n":"rakuten","u_so":2},{"id":3,"u_tx":"Yahoo!ショッピングで見る","u_bc":"#66a7ff","u_url":"https:\/\/shopping.yahoo.co.jp\/search?first=1\u0026p=AWS%E9%81%8B%E7%94%A8%E5%85%A5%E9%96%80%20%E6%94%B9%E8%A8%82%E7%AC%AC2%E7%89%88%20%E6%8A%BC%E3%81%95%E3%81%88%E3%81%A6%E3%81%8A%E3%81%8D%E3%81%9F%E3%81%84AWS%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%81%A8%E9%81%8B%E7%94%A8%E3%83%8E%E3%82%A6%E3%83%8F%E3%82%A6%20%5BAWS%E6%B7%B1%E6%8E%98%E3%82%8A%E3%82%AC%E3%82%A4%E3%83%89%5D","a_id":1466950,"p_id":1225,"pl_id":27061,"pc_id":1925,"s_n":"yahoo","u_so":3}],"eid":"E8MM1","s":"s"});</script></p>
<div id="msmaflink-E8MM1">リンク</div>
<p><!-- MoshimoAffiliateEasyLink END --> --></p><p>The post <a href="https://caymezon.com/aws-handson-cloudformation-ec2-2tier/">CloudFormationでAP+DB 2層構成（VPC設計）を自動デプロイする手順【踏み台SSH / コンソール版との比較付き】</a> first appeared on <a href="https://caymezon.com">CayTech Lab</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://caymezon.com/aws-handson-cloudformation-ec2-2tier/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>EC2サーバ構築をIaC化：CloudFormationでEC2 MySQLのDBサーバを自動デプロイする手順【SG参照 / コンソール版との比較付き】</title>
		<link>https://caymezon.com/aws-handson-cloudformation-ec2-mysql/</link>
					<comments>https://caymezon.com/aws-handson-cloudformation-ec2-mysql/#respond</comments>
		
		<dc:creator><![CDATA[caymezon]]></dc:creator>
		<pubDate>Sun, 29 Mar 2026 07:32:34 +0000</pubDate>
				<category><![CDATA[AWS Basic]]></category>
		<category><![CDATA[Cloud & Infra]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[AWSCLI]]></category>
		<category><![CDATA[CloudFormation]]></category>
		<category><![CDATA[DBサーバー]]></category>
		<category><![CDATA[EC2]]></category>
		<category><![CDATA[ec2サーバ構築]]></category>
		<category><![CDATA[IaC]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[MariaDB]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[SG参照]]></category>
		<category><![CDATA[UserData]]></category>
		<category><![CDATA[セキュリティグループ]]></category>
		<category><![CDATA[ハンズオン]]></category>
		<category><![CDATA[初心者]]></category>
		<guid isPermaLink="false">https://caymezon.com/?p=20317</guid>

					<description><![CDATA[<p>目次 はじめにCloudFormation vs コンソール：どれだけ違うかキーワード解説前提条件template.yaml の全文作業順序⓪ 自分のIPアドレスを確認する① キーペアの作成（コンソールで実施）② プロジ [&#8230;]</p>
<p>The post <a href="https://caymezon.com/aws-handson-cloudformation-ec2-mysql/">EC2サーバ構築をIaC化：CloudFormationでEC2 MySQLのDBサーバを自動デプロイする手順【SG参照 / コンソール版との比較付き】</a> first appeared on <a href="https://caymezon.com">CayTech Lab</a>.</p>]]></description>
										<content:encoded><![CDATA[<div id="toc" class="toc tnt-number toc-center tnt-number border-element"><input type="checkbox" class="toc-checkbox" id="toc-checkbox-8" checked><label class="toc-title" for="toc-checkbox-8">目次</label>
    <div class="toc-content">
    <ol class="toc-list open"><li><a href="#toc1" tabindex="0">はじめに</a></li><li><a href="#toc2" tabindex="0">CloudFormation vs コンソール：どれだけ違うか</a></li><li><a href="#toc3" tabindex="0">キーワード解説</a></li><li><a href="#toc4" tabindex="0">前提条件</a></li><li><a href="#toc5" tabindex="0">template.yaml の全文</a></li><li><a href="#toc6" tabindex="0">作業順序</a></li><li><a href="#toc7" tabindex="0">⓪ 自分のIPアドレスを確認する</a></li><li><a href="#toc8" tabindex="0">① キーペアの作成（コンソールで実施）</a></li><li><a href="#toc9" tabindex="0">② プロジェクトフォルダに移動する</a></li><li><a href="#toc10" tabindex="0">③ スタックの作成</a></li><li><a href="#toc11" tabindex="0">④ 作成完了・Outputsの確認</a><ol><li><a href="#toc12" tabindex="0">作成状況の確認</a></li><li><a href="#toc13" tabindex="0">Outputs（接続情報）の確認</a></li></ol></li><li><a href="#toc14" tabindex="0">⑤ 動作確認</a><ol><li><a href="#toc15" tabindex="0">5-1. DBサーバの動作確認</a></li><li><a href="#toc16" tabindex="0">5-2. SG参照の確認：クライアントEC2からMySQL接続</a></li><li><a href="#toc17" tabindex="0">5-3. SG参照の効果を確認する（任意・スキップ可）</a></li></ol></li><li><a href="#toc18" tabindex="0">⑥ スタックの削除</a></li><li><a href="#toc19" tabindex="0">コンソール版との比較</a></li><li><a href="#toc20" tabindex="0">トラブルシューティング</a></li><li><a href="#toc21" tabindex="0">まとめ</a><ol><li><a href="#toc22" tabindex="0">CloudFormationのメリットを実感できたポイント</a></li></ol></li><li><a href="#toc23" tabindex="0">コンソール版と比較してみる</a></li><li><a href="#toc24" tabindex="0">関連記事</a></li></ol>
    </div>
  </div>

<h2><span id="toc1">はじめに</span></h2>
<p>「コンソールで手動構築したSG参照の構成を、コードで再現したい」——それを実現するのが <strong>AWS CloudFormation</strong> です。</p>
<p>この記事では、<code>template.yaml</code> 1ファイルにリソース構成を定義し、<strong>コマンド1本でEC2 MySQL（MariaDB）環境を一括構築するハンズオン</strong>を紹介します。コンソール版（<a href="https://caymezon.com/aws-handson-console-ec2-mysql/">AWSコンソール版ハンズオン</a>）と全く同じ構成を、CloudFormationで自動化します。</p>
<pre><code class="language-plaintext">ローカル環境（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コマンド）</code></pre>
<p><strong>このハンズオンで体験できること：</strong></p>
<ul>
<li>SG参照（SG-to-SG）を <code>SourceSecurityGroupId</code> でtemplate.yamlに記述する方法</li>
<li>2台のEC2・2つのSGを <code>template.yaml</code> 1ファイルで一括管理</li>
<li><code>aws cloudformation create-stack</code> コマンド1本での全リソース一括デプロイ</li>
<li><code>delete-stack</code> によるSGの依存関係を自動解決した一括削除</li>
</ul>
<p><strong>このハンズオンの特徴：</strong></p>
<ul>
<li>コンソール版では順番を意識しながら手動で作成・削除していたSGの依存関係を、CloudFormationが自動管理</li>
<li>2台分のEC2を並列で起動するため、コンソール版より短時間で構築完了</li>
</ul>
<hr>
<p><!-- ![CloudFormationスタック作成完了画面](images/cfn-create-complete.jpg) --></p>
<p><!-- <a rel="nofollow" href="//af.moshimo.com/af/c/click?a_id=1384942&p_id=170&pc_id=185&pl_id=4062&url=https%3A%2F%2Fwww.amazon.co.jp%2Fs%3Fk%3D%25E6%259C%25AC%2BAWS%2B%25E9%2596%258B%25E7%2599%25BA%26__mk_ja_JP%3D%25E3%2582%25AB%25E3%2582%25BF%25E3%2582%25AB%25E3%2583%258A%26crid%3D1DE63UBHFOR4K%26sprefix%3D%25E6%259C%25AC%2Baws%2B%25E9%2596%258B%25E7%2599%25BA%252Caps%252C167%26ref%3Dnb_sb_noss" referrerpolicy="no-referrer-when-downgrade" attributionsrc>Amazon検索[本 AWS 開発]</a><img decoding="async" src="//i.moshimo.com/af/i/impression?a_id=1384942&p_id=170&pc_id=185&pl_id=4062" width="1" height="1" style="border:none;" alt="" loading="lazy"> --></p>
<p><!-- <!-- START MoshimoAffiliateEasyLink --><script type="text/javascript">(function(b,c,f,g,a,d,e){b.MoshimoAffiliateObject=a;b[a]=b[a]||function(){arguments.currentScript=c.currentScript||c.scripts[c.scripts.length-2];(b[a].q=b[a].q||[]).push(arguments)};c.getElementById(a)||(d=c.createElement(f),d.src=g,d.id=a,e=c.getElementsByTagName("body")[0],e.appendChild(d))})(window,document,"script","//dn.msmstatic.com/site/cardlink/bundle.js?20220329","msmaflink");msmaflink({"n":"AWSの基本・仕組み・重要用語が全部わかる教科書 (見るだけ図解)","b":"SBクリエイティブ","t":"","d":"https:\/\/m.media-amazon.com","c_p":"\/images\/I","p":["\/51DEDQXj6oL._SL500_.jpg","\/41F589smNwL._SL500_.jpg","\/41R6f9yyCWL._SL500_.jpg","\/41HqWQ9BvmL._SL500_.jpg","\/41p8p0ZU79L._SL500_.jpg","\/41qLC-fndBL._SL500_.jpg","\/41fcLv9VT5L._SL500_.jpg","\/51lRvCsvHqL._SL500_.jpg"],"u":{"u":"https:\/\/www.amazon.co.jp\/dp\/4815607850","t":"amazon","r_v":""},"v":"2.1","b_l":[{"id":1,"u_tx":"Amazonで見る","u_bc":"#f79256","u_url":"https:\/\/www.amazon.co.jp\/dp\/4815607850","a_id":1384942,"p_id":170,"pl_id":27060,"pc_id":185,"s_n":"amazon","u_so":1},{"id":2,"u_tx":"楽天市場で見る","u_bc":"#f76956","u_url":"https:\/\/search.rakuten.co.jp\/search\/mall\/AWS%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%83%BB%E4%BB%95%E7%B5%84%E3%81%BF%E3%83%BB%E9%87%8D%E8%A6%81%E7%94%A8%E8%AA%9E%E3%81%8C%E5%85%A8%E9%83%A8%E3%82%8F%E3%81%8B%E3%82%8B%E6%95%99%E7%A7%91%E6%9B%B8%20(%E8%A6%8B%E3%82%8B%E3%81%A0%E3%81%91%E5%9B%B3%E8%A7%A3)\/","a_id":1384917,"p_id":54,"pl_id":27059,"pc_id":54,"s_n":"rakuten","u_so":2},{"id":3,"u_tx":"Yahoo!ショッピングで見る","u_bc":"#66a7ff","u_url":"https:\/\/shopping.yahoo.co.jp\/search?first=1\u0026p=AWS%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%83%BB%E4%BB%95%E7%B5%84%E3%81%BF%E3%83%BB%E9%87%8D%E8%A6%81%E7%94%A8%E8%AA%9E%E3%81%8C%E5%85%A8%E9%83%A8%E3%82%8F%E3%81%8B%E3%82%8B%E6%95%99%E7%A7%91%E6%9B%B8%20(%E8%A6%8B%E3%82%8B%E3%81%A0%E3%81%91%E5%9B%B3%E8%A7%A3)","a_id":1466950,"p_id":1225,"pl_id":27061,"pc_id":1925,"s_n":"yahoo","u_so":3}],"eid":"eaCUB","s":"s"});</script></p>
<div id="msmaflink-eaCUB">リンク</div>
<p><!-- MoshimoAffiliateEasyLink END --> --></p>
<h2><span id="toc2">CloudFormation vs コンソール：どれだけ違うか</span></h2>
<table>
<thead>
<tr>
<th>比較項目</th>
<th>CloudFormation</th>
<th>コンソール（手動）</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>SG×2台の作成</strong></td>
<td>template.yamlに定義（順序自動管理）</td>
<td>クライアントSG → DBサーバSGの順で手動作成</td>
</tr>
<tr>
<td><strong>EC2×2台の起動</strong></td>
<td>コマンド1本（並列作成）</td>
<td>2回インスタンス起動操作</td>
</tr>
<tr>
<td><strong>SG削除（依存関係あり）</strong></td>
<td><code>delete-stack</code> 1本（自動解決）</td>
<td>DBサーバSG → クライアントSGの順で手動管理</td>
</tr>
<tr>
<td><strong>全リソースのデプロイ</strong></td>
<td>コマンド1本（5〜8分）</td>
<td>複数画面を行き来（20〜30分）</td>
</tr>
<tr>
<td><strong>再現性</strong></td>
<td>高い（同じ構成を何度でも再現可能）</td>
<td>低い（手順ミスのリスク大）</td>
</tr>
<tr>
<td><strong>バージョン管理</strong></td>
<td>Gitで管理可能</td>
<td>不可</td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc3">キーワード解説</span></h2>
<table>
<thead>
<tr>
<th>用語</th>
<th>意味</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>CloudFormation</strong></td>
<td>AWSが提供するIaC（Infrastructure as Code）サービス。YAMLテンプレートでリソースをコード化する</td>
</tr>
<tr>
<td><strong>スタック</strong></td>
<td>CloudFormationが管理するリソースのまとまり。今回はEC2×2・SG×2・IAMロールを1スタックで管理</td>
</tr>
<tr>
<td><strong>SG参照（SG-to-SG）</strong></td>
<td>セキュリティグループのルールで、アクセス元として別のSGを指定する設定</td>
</tr>
<tr>
<td><strong><code>SourceSecurityGroupId</code></strong></td>
<td>CloudFormationでSG参照を設定するプロパティ。<code>!GetAtt ClientSecurityGroup.GroupId</code> でSGのIDを取得して指定する</td>
</tr>
<tr>
<td><strong><code>!GetAtt</code></strong></td>
<td>CloudFormation組み込み関数。リソースの属性値を取得する</td>
</tr>
<tr>
<td><strong>プライベートIP</strong></td>
<td>VPC内でのみ使用できるIPアドレス。EC2間の通信に使う</td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc4">前提条件</span></h2>
<table>
<thead>
<tr>
<th>ツール</th>
<th>確認コマンド</th>
<th>最低バージョン目安</th>
</tr>
</thead>
<tbody>
<tr>
<td>AWS CLI v2</td>
<td><code>aws --version</code></td>
<td>2.x</td>
</tr>
<tr>
<td>Git</td>
<td><code>git --version</code></td>
<td>2.x</td>
</tr>
<tr>
<td>VSCode</td>
<td>-</td>
<td>-</td>
</tr>
</tbody>
</table>
<p>AWS認証確認:</p>
<pre><code class="language-cmd">aws sts get-caller-identity</code></pre>
<p>アカウントIDが表示されれば認証設定済みです。</p>
<hr>
<h2><span id="toc5">template.yaml の全文</span></h2>
<p>以下が今回使用する <code>template.yaml</code> の全文です。プロジェクトフォルダ（<code>ec2-mysql-db/</code>）直下に配置します。</p>
<blockquote>
<p><strong>⚠️ コピー前に確認</strong>: <code>Default: &#39;123.456.78.901/32&#39;</code> の箇所は<strong>ダミーIPです</strong>。このまま使うとスタック作成は成功しますが、SSHもMySQLもアクセスできません。<code>--parameters</code> でIPを上書きするか（推奨）、Defaultを自分のIPに書き換えてから使ってください。</p>
</blockquote>
<pre><code class="language-yaml">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 &lt;&lt; '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'</code></pre>
<p><strong>SG参照の書き方：IPアドレス指定との違い:</strong></p>
<pre><code class="language-yaml">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)</code></pre>
<table>
<thead>
<tr>
<th>方法</th>
<th>指定するもの</th>
<th>特徴</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>CidrIp</code></td>
<td>IPアドレス（<code>/32</code> など）</td>
<td>IPが変わると設定変更が必要</td>
</tr>
<tr>
<td><code>SourceSecurityGroupId</code></td>
<td>セキュリティグループID</td>
<td>そのSGに属するEC2全体を対象にできる</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong><code>!Ref</code> vs <code>!GetAtt ... .GroupId</code> の違い</strong>: <code>VpcId</code> を明示していないSGに対して <code>!Ref</code> するとSGの<strong>名前</strong>が返るため <code>SourceSecurityGroupId</code>（IDを期待）でエラーになります。<code>!GetAtt ClientSecurityGroup.GroupId</code> を使うことで常にSGの<strong>ID</strong>（<code>sg-xxx</code>）を確実に取得できます。</p>
</blockquote>
<p><strong>構築されるリソースと論理ID:</strong></p>
<table>
<thead>
<tr>
<th>リソース</th>
<th>template.yaml上の論理ID</th>
</tr>
</thead>
<tbody>
<tr>
<td>IAMロール</td>
<td><code>EC2Role</code></td>
</tr>
<tr>
<td>インスタンスプロファイル</td>
<td><code>EC2InstanceProfile</code></td>
</tr>
<tr>
<td>クライアントSG</td>
<td><code>ClientSecurityGroup</code></td>
</tr>
<tr>
<td>DBサーバSG</td>
<td><code>DBSecurityGroup</code></td>
</tr>
<tr>
<td>DBサーバEC2</td>
<td><code>DBInstance</code></td>
</tr>
<tr>
<td>クライアントEC2</td>
<td><code>ClientInstance</code></td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc6">作業順序</span></h2>
<pre><code class="language-plaintext">⓪ 自分のIPアドレスを確認する
      ↓
① キーペアを作成する（コンソールで実施）
      ↓
② プロジェクトフォルダに移動する
      ↓
③ スタックを作成する（aws cloudformation create-stack）
      ↓
④ 作成完了・Outputsを確認する
      ↓
⑤ 動作確認（MariaDB・SG参照の確認）
      ↓
⑥ スタックを削除する（aws cloudformation delete-stack）</code></pre>
<hr>
<h2><span id="toc7">⓪ 自分のIPアドレスを確認する</span></h2>
<pre><code class="language-cmd">curl https://checkip.amazonaws.com</code></pre>
<p>またはルーター管理画面（<code>http://192.168.0.1</code> など）の「WAN IPアドレス」で確認します。</p>
<p><strong>控えておく情報</strong>: 自分のIPアドレス（例: <code>203.0.113.1</code>）</p>
<hr>
<h2><span id="toc8">① キーペアの作成（コンソールで実施）</span></h2>
<p>キーペアのみコンソールで作成します。CloudFormationではキーペアのダウンロードが行えないためです。</p>
<p><strong>AWSコンソール → EC2 → キーペア → 「キーペアを作成」</strong></p>
<table>
<thead>
<tr>
<th>設定項目</th>
<th>値</th>
</tr>
</thead>
<tbody>
<tr>
<td>名前</td>
<td><code>my-ec2-mysql-key</code></td>
</tr>
<tr>
<td>キーペアのタイプ</td>
<td><strong>RSA</strong></td>
</tr>
<tr>
<td>プライベートキーファイル形式</td>
<td><strong>.pem</strong></td>
</tr>
</tbody>
</table>
<p>ダウンロードされた <code>my-ec2-mysql-key.pem</code> を保存します。</p>
<pre><code class="language-plaintext">C:\Users\ユーザー名\.ssh\my-ec2-mysql-key.pem</code></pre>
<hr>
<h2><span id="toc9">② プロジェクトフォルダに移動する</span></h2>
<p>VSCodeのターミナル（CMD）を開き、プロジェクトフォルダに移動します。</p>
<pre><code class="language-cmd">cd C:\my-aws\aws-learning-projects\ec2-mysql-db</code></pre>
<p><code>template.yaml</code> があることを確認します。</p>
<pre><code class="language-cmd">dir template.yaml</code></pre>
<hr>
<h2><span id="toc10">③ スタックの作成</span></h2>
<p>以下のコマンドを実行します。<code>203.0.113.1</code> は⓪で確認した自分のIPアドレスに置き換えてください。</p>
<pre><code class="language-cmd">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</code></pre>
<p><strong>各オプションの説明:</strong></p>
<table>
<thead>
<tr>
<th>オプション</th>
<th>意味</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>--stack-name my-ec2-mysql-stack</code></td>
<td>スタック名（英字始まり）</td>
</tr>
<tr>
<td><code>--template-body file://template.yaml</code></td>
<td>使用するテンプレートファイル</td>
</tr>
<tr>
<td><code>--region ap-northeast-1</code></td>
<td>デプロイ先リージョン（東京）</td>
</tr>
<tr>
<td><code>--capabilities CAPABILITY_NAMED_IAM</code></td>
<td>名前付きIAMロール作成の明示的な許可</td>
</tr>
<tr>
<td><code>--parameters ...</code></td>
<td>テンプレートのParametersに渡す値</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong><code>^</code> について</strong>: Windowsのコマンドプロンプトで長いコマンドを複数行に分けるための改行エスケープ文字です。</p>
</blockquote>
<p>成功すると以下のようなStackIdが表示されます。</p>
<pre><code class="language-json">{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:123456789012:stack/my-ec2-mysql-stack/xxxxx"
}</code></pre>
<p><!-- ![スタック作成コマンドの実行結果](images/cfn-create-stack-cmd.jpg) --></p>
<hr>
<h2><span id="toc11">④ 作成完了・Outputsの確認</span></h2>
<p>スタック作成は完了まで<strong>約5〜8分</strong>かかります（EC2×2台の起動 + MariaDBインストールの時間）。</p>
<h3><span id="toc12">作成状況の確認</span></h3>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-ec2-mysql-stack ^
  --query "Stacks[0].StackStatus" ^
  --output text</code></pre>
<table>
<thead>
<tr>
<th>ステータス</th>
<th>意味</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>CREATE_IN_PROGRESS</code></td>
<td>作成中（しばらく待つ）</td>
</tr>
<tr>
<td><code>CREATE_COMPLETE</code></td>
<td>作成完了</td>
</tr>
<tr>
<td><code>CREATE_FAILED</code></td>
<td>作成失敗（トラブルシューティングを参照）</td>
</tr>
<tr>
<td><code>ROLLBACK_COMPLETE</code></td>
<td>失敗してロールバック完了</td>
</tr>
</tbody>
</table>
<h3><span id="toc13">Outputs（接続情報）の確認</span></h3>
<p><code>CREATE_COMPLETE</code> になったら以下を実行します。</p>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-ec2-mysql-stack ^
  --query "Stacks[0].Outputs" ^
  --output table</code></pre>
<p>以下のような出力が表示されます。</p>
<pre><code class="language-plaintext">---------------------------------------------------------------------------------------
|                                   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     |
+---------------------+---------------------------------------------------------------+</code></pre>
<p><strong>控えておく情報:</strong></p>
<ul>
<li><code>DBPrivateIP</code>: DBサーバのプライベートIP（クライアントEC2からの接続に使う）</li>
<li><code>DBSSHCommand</code>: DBサーバへのSSHコマンド</li>
<li><code>ClientSSHCommand</code>: クライアントEC2へのSSHコマンド</li>
<li><code>MySQLConnectCommand</code>: クライアントEC2から実行するMySQL接続コマンド</li>
</ul>
<p><!-- ![Outputs確認コマンドの実行結果](images/cfn-outputs-table.jpg) --></p>
<hr>
<h2><span id="toc14">⑤ 動作確認</span></h2>
<h3><span id="toc15">5-1. DBサーバの動作確認</span></h3>
<p>Outputsの <code>DBSSHCommand</code> を実行してDBサーバに接続します。</p>
<pre><code class="language-cmd">ssh -i C:\Users\ユーザー名\.ssh\my-ec2-mysql-key.pem ec2-user@（DBPublicIP）</code></pre>
<pre><code class="language-bash"># MariaDBのサービス状態を確認（active (running) と表示されれば正常）
sudo systemctl status mariadb

# rootでログイン
sudo mysql -u root</code></pre>
<p>MariaDBのプロンプト（<code>MariaDB [(none)]&gt;</code>）が表示されたら以下を実行します。</p>
<pre><code class="language-sql">-- ユーザー一覧（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;</code></pre>
<pre><code class="language-bash">exit</code></pre>
<h3><span id="toc16">5-2. SG参照の確認：クライアントEC2からMySQL接続</span></h3>
<p>Outputsの <code>ClientSSHCommand</code> を実行してクライアントEC2に接続します。</p>
<pre><code class="language-cmd">ssh -i C:\Users\ユーザー名\.ssh\my-ec2-mysql-key.pem ec2-user@（ClientPublicIP）</code></pre>
<p>Outputsに表示された <code>MySQLConnectCommand</code> をそのまま実行します。</p>
<pre><code class="language-bash">mysql -h （DBPrivateIP） -u handson -pHandson1234! sampledb</code></pre>
<p>接続成功すると <code>MariaDB [sampledb]&gt;</code> が表示されます。</p>
<pre><code class="language-sql">-- DBサーバで作成したデータが見えることを確認
SELECT * FROM users;

EXIT;</code></pre>
<pre><code class="language-bash">exit</code></pre>
<p><!-- ![クライアントEC2からのMySQL接続確認](images/client-mysql-connect.jpg) --></p>
<h3><span id="toc17">5-3. SG参照の効果を確認する（任意・スキップ可）</span></h3>
<blockquote>
<p><strong>前提</strong>: ローカルPCに <code>mysql</code> クライアントがインストールされている場合のみ実施します。5-2でクライアントEC2からの接続が成功していれば、このハンズオンの学習目的（SG参照によるEC2間通信制御）は達成済みです。</p>
</blockquote>
<p>ローカルPCに <code>mysql</code> コマンドがある場合、ローカルターミナルで以下を実行します。</p>
<pre><code class="language-cmd">mysql -h （DBPublicIP） -u handson -pHandson1234! sampledb</code></pre>
<blockquote>
<p><strong>本番環境でのベストプラクティス</strong>: DBサーバSGからMyIP指定のルールを削除し、SG参照のみにすることで、DBサーバをインターネットから完全に遮断できます。DBサーバにパブリックIPを付与しないことも有効です。</p>
</blockquote>
<hr>
<h2><span id="toc18">⑥ スタックの削除</span></h2>
<p><strong>課金を止めるために、ハンズオン完了後は必ず削除してください。</strong></p>
<p>CloudFormationはスタック削除で全リソースを一括削除できます。SGの依存関係（DBサーバSG → クライアントSGの削除順序）も自動的に解決されます。</p>
<pre><code class="language-cmd">aws cloudformation delete-stack ^
  --stack-name my-ec2-mysql-stack ^
  --region ap-northeast-1</code></pre>
<p>削除の進行状況を確認します（完了まで約5〜8分）。</p>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-ec2-mysql-stack ^
  --query "Stacks[0].StackStatus" ^
  --output text</code></pre>
<table>
<thead>
<tr>
<th>ステータス</th>
<th>意味</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>DELETE_IN_PROGRESS</code></td>
<td>削除中（しばらく待つ）</td>
</tr>
<tr>
<td><code>DELETE_FAILED</code></td>
<td>削除失敗（トラブルシューティングを参照）</td>
</tr>
</tbody>
</table>
<p>削除完了の確認（以下のエラーが表示されれば削除完了）。</p>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-ec2-mysql-stack ^
  --region ap-northeast-1</code></pre>
<pre><code class="language-plaintext">An error occurred (ValidationError) when calling the DescribeStacks operation:
Stack with id my-ec2-mysql-stack does not exist</code></pre>
<p>このメッセージが表示されれば削除完了です。キーペアはCloudFormationで管理していないため、手動で削除します。</p>
<p><strong>EC2 → キーペア → <code>my-ec2-mysql-key</code> を選択 → 「アクション」→「削除」</strong></p>
<p>ローカルの <code>.pem</code> ファイルも削除します。</p>
<hr>
<h2><span id="toc19">コンソール版との比較</span></h2>
<p>コンソール版でSGを手動で依存関係順に作成・削除していた手順が、CloudFormationではすべて自動管理されます。</p>
<table>
<thead>
<tr>
<th>CFnの記述</th>
<th>コンソールでやること</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ClientSecurityGroup</code>（クライアントSG定義）</td>
<td>EC2 → SGを作成（SSH:22ルール）</td>
</tr>
<tr>
<td><code>DBSecurityGroup</code>（DB SG定義・SG参照）</td>
<td>EC2 → SGを作成（SSH:22 + MySQL:3306のSG参照ルール）</td>
</tr>
<tr>
<td><code>DBInstance</code>（DBサーバEC2・UserData）</td>
<td>EC2 → インスタンス起動（UserData貼り付け・DB SGを選択）</td>
</tr>
<tr>
<td><code>ClientInstance</code>（クライアントEC2）</td>
<td>EC2 → インスタンス起動（クライアント SGを選択）</td>
</tr>
<tr>
<td><code>delete-stack</code></td>
<td>DBサーバSG → クライアントSG → IAMロール（順番管理が必要）</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>CloudFormationのSG削除順序の自動管理</strong>: コンソール版ではDBサーバSG → クライアントSGの順で手動削除が必要でしたが、CloudFormationはリソース間の依存関係を自動的に解決して正しい順序で削除します。</p>
</blockquote>
<hr>
<h2><span id="toc20">トラブルシューティング</span></h2>
<table>
<thead>
<tr>
<th>症状</th>
<th>原因</th>
<th>対処</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>CREATE_FAILED</code> になる</td>
<td>キーペアが存在しない</td>
<td>コンソールでキーペアを作成し、<code>KeyName</code> パラメータを確認</td>
</tr>
<tr>
<td><code>CREATE_FAILED</code> になる</td>
<td>同名のIAMロールが既に存在する</td>
<td>コンソールでロールを削除してから再実行</td>
</tr>
<tr>
<td><code>stackName failed to satisfy regular expression pattern</code></td>
<td>スタック名が数字始まり</td>
<td>CloudFormationのスタック名は<strong>英字で始まる</strong>必要がある</td>
</tr>
<tr>
<td><code>Unable to load paramfile, text contents could not be decoded</code></td>
<td>template.yaml に日本語が含まれている</td>
<td>template.yaml のコメントは<strong>英語のみ</strong>で記述する（Windows環境の既知の問題）</td>
</tr>
<tr>
<td>クライアントEC2からMySQLに接続できない</td>
<td>MariaDBのセットアップがまだ完了していない</td>
<td><code>CREATE_COMPLETE</code> 後2〜3分待ってから再試行。DBサーバで <code>sudo systemctl status mariadb</code> を確認</td>
</tr>
<tr>
<td><code>ERROR 2003: Can&#39;t connect to MySQL server</code></td>
<td>パブリックIPを指定している</td>
<td>DBサーバの<strong>プライベートIP</strong>（<code>DBPrivateIP</code>）を指定する</td>
</tr>
<tr>
<td><code>DELETE_FAILED</code> になる</td>
<td>手動でリソースを変更したためCFnが管理できない</td>
<td>コンソールで該当リソースを確認して手動削除後、<code>delete-stack</code> を再実行</td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc21">まとめ</span></h2>
<p>今回のハンズオンで実現できたこと：</p>
<table>
<thead>
<tr>
<th>確認項目</th>
<th>内容</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>SG参照のIaC化</strong></td>
<td><code>SourceSecurityGroupId: !GetAtt ClientSecurityGroup.GroupId</code> でSG参照をコードで表現</td>
</tr>
<tr>
<td><strong>複数リソースの一括管理</strong></td>
<td>EC2×2・SG×2・IAMロールを1ファイルで定義・一括デプロイ</td>
</tr>
<tr>
<td><strong>Outputs活用</strong></td>
<td>DBPrivateIP・MySQLConnectCommandを <code>describe-stacks</code> で自動取得</td>
</tr>
<tr>
<td><strong>依存関係の自動解決</strong></td>
<td>SGの削除順序をCloudFormationが自動管理（コンソール版では手動）</td>
</tr>
</tbody>
</table>
<h3><span id="toc22">CloudFormationのメリットを実感できたポイント</span></h3>
<ul>
<li>コンソール版では複数リソースの作成・削除に順番の意識が必要だったが、CloudFormationが自動管理</li>
<li><code>MySQLConnectCommand</code> がOutputsとして自動生成されるため、手動でコマンドを組み立てる必要がない</li>
<li><code>template.yaml</code> をGitで管理することで、SG参照を含む複雑な構成の変更履歴が残せる</li>
</ul>
<hr>
<h2><span id="toc23">コンソール版と比較してみる</span></h2>
<p>CloudFormationが裏で何をやっているか、同じ構成をAWSコンソールのみで構築する手順をまとめました。特にSGの依存関係管理とSG参照の設定手順を視覚的に確認できます。</p>
<p><a href="https://caymezon.com/aws-handson-console-ec2-mysql/">EC2サーバ構築ハンズオン：AWSコンソールでEC2 MySQLのDBサーバを構築する手順</a></p>
<hr>
<h2><span id="toc24">関連記事</span></h2>
<p><!-- <a rel="nofollow" href="//af.moshimo.com/af/c/click?a_id=1384942&p_id=170&pc_id=185&pl_id=4062&url=https%3A%2F%2Fwww.amazon.co.jp%2Fs%3Fk%3D%25E6%259C%25AC%2BAWS%2B%25E9%2596%258B%25E7%2599%25BA%26__mk_ja_JP%3D%25E3%2582%25AB%25E3%2582%25BF%25E3%2582%25AB%25E3%2583%258A%26crid%3D1DE63UBHFOR4K%26sprefix%3D%25E6%259C%25AC%2Baws%2B%25E9%2596%258B%25E7%2599%25BA%252Caps%252C167%26ref%3Dnb_sb_noss" referrerpolicy="no-referrer-when-downgrade" attributionsrc>Amazon検索[本 AWS 開発]</a><img decoding="async" src="//i.moshimo.com/af/i/impression?a_id=1384942&p_id=170&pc_id=185&pl_id=4062" width="1" height="1" style="border:none;" alt="" loading="lazy"> --></p>
<p><!-- <!-- START MoshimoAffiliateEasyLink --><script type="text/javascript">(function(b,c,f,g,a,d,e){b.MoshimoAffiliateObject=a;b[a]=b[a]||function(){arguments.currentScript=c.currentScript||c.scripts[c.scripts.length-2];(b[a].q=b[a].q||[]).push(arguments)};c.getElementById(a)||(d=c.createElement(f),d.src=g,d.id=a,e=c.getElementsByTagName("body")[0],e.appendChild(d))})(window,document,"script","//dn.msmstatic.com/site/cardlink/bundle.js?20220329","msmaflink");msmaflink({"n":"AWS運用入門 改訂第2版 押さえておきたいAWSの基本と運用ノウハウ [AWS深掘りガイド]","b":"SBクリエイティブ","t":"","d":"https:\/\/m.media-amazon.com","c_p":"\/images\/I","p":["\/51AAOubymTL._SL500_.jpg","\/51VMG6YKHdL._SL500_.jpg","\/41EdPB8azAL._SL500_.jpg","\/41v2JFE-9jL._SL500_.jpg","\/41FEEqR-yDL._SL500_.jpg","\/41JfZAdnTPL._SL500_.jpg","\/41vGK0czQrL._SL500_.jpg","\/41-SnYtz2aL._SL500_.jpg","\/41sPrV5fi3L._SL500_.jpg","\/41p7JtvYJ1L._SL500_.jpg","\/4169GVNTs8L._SL500_.jpg","\/41BPI5HP3zL._SL500_.jpg","\/41QOyk60CYL._SL500_.jpg","\/41APjk6FphL._SL500_.jpg","\/41ezKUu7VRL._SL500_.jpg","\/41A1n3K+r5L._SL500_.jpg","\/41aY2T8lEOL._SL500_.jpg","\/419Ca1V6HZL._SL500_.jpg","\/41zQkYyLPzL._SL500_.jpg","\/41YpHcyxiTL._SL500_.jpg","\/41-tKN5mt6L._SL500_.jpg","\/419Mv6m55IL._SL500_.jpg"],"u":{"u":"https:\/\/www.amazon.co.jp\/dp\/4815631085","t":"amazon","r_v":""},"v":"2.1","b_l":[{"id":1,"u_tx":"Amazonで見る","u_bc":"#f79256","u_url":"https:\/\/www.amazon.co.jp\/dp\/4815631085","a_id":1384942,"p_id":170,"pl_id":27060,"pc_id":185,"s_n":"amazon","u_so":1},{"id":2,"u_tx":"楽天市場で見る","u_bc":"#f76956","u_url":"https:\/\/search.rakuten.co.jp\/search\/mall\/AWS%E9%81%8B%E7%94%A8%E5%85%A5%E9%96%80%20%E6%94%B9%E8%A8%82%E7%AC%AC2%E7%89%88%20%E6%8A%BC%E3%81%95%E3%81%88%E3%81%A6%E3%81%8A%E3%81%8D%E3%81%9F%E3%81%84AWS%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%81%A8%E9%81%8B%E7%94%A8%E3%83%8E%E3%82%A6%E3%83%8F%E3%82%A6%20%5BAWS%E6%B7%B1%E6%8E%98%E3%82%8A%E3%82%AC%E3%82%A4%E3%83%89%5D\/","a_id":1384917,"p_id":54,"pl_id":27059,"pc_id":54,"s_n":"rakuten","u_so":2},{"id":3,"u_tx":"Yahoo!ショッピングで見る","u_bc":"#66a7ff","u_url":"https:\/\/shopping.yahoo.co.jp\/search?first=1\u0026p=AWS%E9%81%8B%E7%94%A8%E5%85%A5%E9%96%80%20%E6%94%B9%E8%A8%82%E7%AC%AC2%E7%89%88%20%E6%8A%BC%E3%81%95%E3%81%88%E3%81%A6%E3%81%8A%E3%81%8D%E3%81%9F%E3%81%84AWS%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%81%A8%E9%81%8B%E7%94%A8%E3%83%8E%E3%82%A6%E3%83%8F%E3%82%A6%20%5BAWS%E6%B7%B1%E6%8E%98%E3%82%8A%E3%82%AC%E3%82%A4%E3%83%89%5D","a_id":1466950,"p_id":1225,"pl_id":27061,"pc_id":1925,"s_n":"yahoo","u_so":3}],"eid":"E8MM1","s":"s"});</script></p>
<div id="msmaflink-E8MM1">リンク</div>
<p><!-- MoshimoAffiliateEasyLink END --> --></p><p>The post <a href="https://caymezon.com/aws-handson-cloudformation-ec2-mysql/">EC2サーバ構築をIaC化：CloudFormationでEC2 MySQLのDBサーバを自動デプロイする手順【SG参照 / コンソール版との比較付き】</a> first appeared on <a href="https://caymezon.com">CayTech Lab</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://caymezon.com/aws-handson-cloudformation-ec2-mysql/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>EC2サーバ構築をIaC化：CloudFormationでEC2 Tomcatを自動デプロイする手順【コンソール版との比較付き】</title>
		<link>https://caymezon.com/aws-handson-cloudformation-ec2-tomcat/</link>
					<comments>https://caymezon.com/aws-handson-cloudformation-ec2-tomcat/#respond</comments>
		
		<dc:creator><![CDATA[caymezon]]></dc:creator>
		<pubDate>Sun, 29 Mar 2026 04:44:49 +0000</pubDate>
				<category><![CDATA[AWS Basic]]></category>
		<category><![CDATA[Cloud & Infra]]></category>
		<category><![CDATA[APサーバー]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[AWSCLI]]></category>
		<category><![CDATA[CloudFormation]]></category>
		<category><![CDATA[EC2]]></category>
		<category><![CDATA[ec2サーバ構築]]></category>
		<category><![CDATA[IaC]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[Tomcat]]></category>
		<category><![CDATA[UserData]]></category>
		<category><![CDATA[セキュリティグループ]]></category>
		<category><![CDATA[ハンズオン]]></category>
		<category><![CDATA[初心者]]></category>
		<guid isPermaLink="false">https://caymezon.com/?p=20311</guid>

					<description><![CDATA[<p>目次 はじめにCloudFormation vs コンソール：どれだけ違うかキーワード解説前提条件template.yaml の全文作業順序【重要】⓪ 自分のIPアドレスを確認する方法A: ルーター管理画面で確認（推奨） [&#8230;]</p>
<p>The post <a href="https://caymezon.com/aws-handson-cloudformation-ec2-tomcat/">EC2サーバ構築をIaC化：CloudFormationでEC2 Tomcatを自動デプロイする手順【コンソール版との比較付き】</a> first appeared on <a href="https://caymezon.com">CayTech Lab</a>.</p>]]></description>
										<content:encoded><![CDATA[<div id="toc" class="toc tnt-number toc-center tnt-number border-element"><input type="checkbox" class="toc-checkbox" id="toc-checkbox-10" checked><label class="toc-title" for="toc-checkbox-10">目次</label>
    <div class="toc-content">
    <ol class="toc-list open"><li><a href="#toc1" tabindex="0">はじめに</a></li><li><a href="#toc2" tabindex="0">CloudFormation vs コンソール：どれだけ違うか</a></li><li><a href="#toc3" tabindex="0">キーワード解説</a></li><li><a href="#toc4" tabindex="0">前提条件</a></li><li><a href="#toc5" tabindex="0">template.yaml の全文</a></li><li><a href="#toc6" tabindex="0">作業順序</a></li><li><a href="#toc7" tabindex="0">【重要】⓪ 自分のIPアドレスを確認する</a><ol><li><a href="#toc8" tabindex="0">方法A: ルーター管理画面で確認（推奨）</a></li><li><a href="#toc9" tabindex="0">方法B: コマンドで確認</a></li></ol></li><li><a href="#toc10" tabindex="0">① キーペアの作成（コンソールで実施）</a></li><li><a href="#toc11" tabindex="0">② プロジェクトフォルダに移動する</a></li><li><a href="#toc12" tabindex="0">③ スタックの作成</a></li><li><a href="#toc13" tabindex="0">④ 作成完了・Outputsの確認</a><ol><li><a href="#toc14" tabindex="0">作成状況の確認</a></li><li><a href="#toc15" tabindex="0">Outputs（接続情報）の確認</a></li></ol></li><li><a href="#toc16" tabindex="0">⑤ 動作確認</a><ol><li><a href="#toc17" tabindex="0">ブラウザで確認</a></li><li><a href="#toc18" tabindex="0">SSHで接続して確認</a></li><li><a href="#toc19" tabindex="0">【接続できない場合】真のIPアドレスを確認する</a></li></ol></li><li><a href="#toc20" tabindex="0">⑥ スタックの削除</a></li><li><a href="#toc21" tabindex="0">コンソール版との比較</a></li><li><a href="#toc22" tabindex="0">トラブルシューティング</a></li><li><a href="#toc23" tabindex="0">まとめ</a><ol><li><a href="#toc24" tabindex="0">CloudFormationのメリットを実感できたポイント</a></li></ol></li><li><a href="#toc25" tabindex="0">コンソール版と比較してみる</a></li><li><a href="#toc26" tabindex="0">関連記事</a></li></ol>
    </div>
  </div>

<h2><span id="toc1">はじめに</span></h2>
<p>「コンソールで手動構築できた構成を、コードで再現したい」——それを実現するのが <strong>AWS CloudFormation</strong> です。</p>
<p>この記事では、<code>template.yaml</code> 1ファイルにリソース構成を定義し、<strong>コマンド1本でEC2 Tomcat環境を一括構築するハンズオン</strong>を紹介します。コンソール版（<a href="https://caymezon.com/aws-handson-console-ec2-tomcat/">AWSコンソール版ハンズオン</a>）と全く同じ構成を、CloudFormationで自動化します。</p>
<pre><code class="language-plaintext">ローカル環境（VSCode）
  ├── template.yaml（CloudFormationテンプレート）
  └── AWS CLI コマンド
        ↓ スタック作成（コマンド1本）
AWS環境
  ├── IAMロール（SSM接続用）                       ← template.yamlで定義
  ├── セキュリティグループ（SSH:22・Tomcat:8080）    ← template.yamlで定義
  └── EC2インスタンス（t2.micro / AL2023 + Java17 + Tomcat10）← template.yamlで定義</code></pre>
<p><strong>このハンズオンで体験できること：</strong></p>
<ul>
<li><code>template.yaml</code> へのリソース定義（IAMロール・セキュリティグループ・EC2インスタンス）</li>
<li><code>!Ref</code> / <code>!Sub</code> / <code>!GetAtt</code> を使ったリソース間参照</li>
<li><code>aws cloudformation create-stack</code> コマンド1本での全リソース一括デプロイ</li>
<li><code>aws cloudformation delete-stack</code> コマンド1本での全リソース一括削除</li>
</ul>
<p><strong>このハンズオンの特徴：</strong></p>
<ul>
<li>コンソール版では20〜30分かかった手動作業が、コマンド1本（約5〜10分）で完了する</li>
<li><code>delete-stack</code> 1本で依存関係を考慮した順番で全リソースを自動削除できる</li>
</ul>
<hr>
<p><!-- ![CloudFormationスタック作成完了画面](images/cfn-create-complete.jpg) --></p>
<p><!-- <a rel="nofollow" href="//af.moshimo.com/af/c/click?a_id=1384942&p_id=170&pc_id=185&pl_id=4062&url=https%3A%2F%2Fwww.amazon.co.jp%2Fs%3Fk%3D%25E6%259C%25AC%2BAWS%2B%25E9%2596%258B%25E7%2599%25BA%26__mk_ja_JP%3D%25E3%2582%25AB%25E3%2582%25BF%25E3%2582%25AB%25E3%2583%258A%26crid%3D1DE63UBHFOR4K%26sprefix%3D%25E6%259C%25AC%2Baws%2B%25E9%2596%258B%25E7%2599%25BA%252Caps%252C167%26ref%3Dnb_sb_noss" referrerpolicy="no-referrer-when-downgrade" attributionsrc>Amazon検索[本 AWS 開発]</a><img decoding="async" src="//i.moshimo.com/af/i/impression?a_id=1384942&p_id=170&pc_id=185&pl_id=4062" width="1" height="1" style="border:none;" alt="" loading="lazy"> --></p>
<p><!-- <!-- START MoshimoAffiliateEasyLink --><script type="text/javascript">(function(b,c,f,g,a,d,e){b.MoshimoAffiliateObject=a;b[a]=b[a]||function(){arguments.currentScript=c.currentScript||c.scripts[c.scripts.length-2];(b[a].q=b[a].q||[]).push(arguments)};c.getElementById(a)||(d=c.createElement(f),d.src=g,d.id=a,e=c.getElementsByTagName("body")[0],e.appendChild(d))})(window,document,"script","//dn.msmstatic.com/site/cardlink/bundle.js?20220329","msmaflink");msmaflink({"n":"AWSの基本・仕組み・重要用語が全部わかる教科書 (見るだけ図解)","b":"SBクリエイティブ","t":"","d":"https:\/\/m.media-amazon.com","c_p":"\/images\/I","p":["\/51DEDQXj6oL._SL500_.jpg","\/41F589smNwL._SL500_.jpg","\/41R6f9yyCWL._SL500_.jpg","\/41HqWQ9BvmL._SL500_.jpg","\/41p8p0ZU79L._SL500_.jpg","\/41qLC-fndBL._SL500_.jpg","\/41fcLv9VT5L._SL500_.jpg","\/51lRvCsvHqL._SL500_.jpg"],"u":{"u":"https:\/\/www.amazon.co.jp\/dp\/4815607850","t":"amazon","r_v":""},"v":"2.1","b_l":[{"id":1,"u_tx":"Amazonで見る","u_bc":"#f79256","u_url":"https:\/\/www.amazon.co.jp\/dp\/4815607850","a_id":1384942,"p_id":170,"pl_id":27060,"pc_id":185,"s_n":"amazon","u_so":1},{"id":2,"u_tx":"楽天市場で見る","u_bc":"#f76956","u_url":"https:\/\/search.rakuten.co.jp\/search\/mall\/AWS%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%83%BB%E4%BB%95%E7%B5%84%E3%81%BF%E3%83%BB%E9%87%8D%E8%A6%81%E7%94%A8%E8%AA%9E%E3%81%8C%E5%85%A8%E9%83%A8%E3%82%8F%E3%81%8B%E3%82%8B%E6%95%99%E7%A7%91%E6%9B%B8%20(%E8%A6%8B%E3%82%8B%E3%81%A0%E3%81%91%E5%9B%B3%E8%A7%A3)\/","a_id":1384917,"p_id":54,"pl_id":27059,"pc_id":54,"s_n":"rakuten","u_so":2},{"id":3,"u_tx":"Yahoo!ショッピングで見る","u_bc":"#66a7ff","u_url":"https:\/\/shopping.yahoo.co.jp\/search?first=1\u0026p=AWS%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%83%BB%E4%BB%95%E7%B5%84%E3%81%BF%E3%83%BB%E9%87%8D%E8%A6%81%E7%94%A8%E8%AA%9E%E3%81%8C%E5%85%A8%E9%83%A8%E3%82%8F%E3%81%8B%E3%82%8B%E6%95%99%E7%A7%91%E6%9B%B8%20(%E8%A6%8B%E3%82%8B%E3%81%A0%E3%81%91%E5%9B%B3%E8%A7%A3)","a_id":1466950,"p_id":1225,"pl_id":27061,"pc_id":1925,"s_n":"yahoo","u_so":3}],"eid":"eaCUB","s":"s"});</script></p>
<div id="msmaflink-eaCUB">リンク</div>
<p><!-- MoshimoAffiliateEasyLink END --> --></p>
<h2><span id="toc2">CloudFormation vs コンソール：どれだけ違うか</span></h2>
<table>
<thead>
<tr>
<th>比較項目</th>
<th>CloudFormation</th>
<th>コンソール（手動）</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>IAMロール作成</strong></td>
<td><code>--parameters</code> で値を渡すだけ</td>
<td>画面でポチポチ（5クリック以上）</td>
</tr>
<tr>
<td><strong>セキュリティグループ作成</strong></td>
<td>同上</td>
<td>画面でポチポチ</td>
</tr>
<tr>
<td><strong>EC2起動（UserData含む）</strong></td>
<td>同上</td>
<td>画面でポチポチ</td>
</tr>
<tr>
<td><strong>全リソースのデプロイ</strong></td>
<td>コマンド1本（5〜10分）</td>
<td>複数画面を行き来（20〜30分）</td>
</tr>
<tr>
<td><strong>リソース削除</strong></td>
<td><code>delete-stack</code> 1本</td>
<td>依存関係順に個別削除（4ステップ）</td>
</tr>
<tr>
<td><strong>再現性</strong></td>
<td>高い（同じ構成を何度でも再現可能）</td>
<td>低い（手動ミスのリスクがある）</td>
</tr>
<tr>
<td><strong>バージョン管理</strong></td>
<td>Gitで管理可能</td>
<td>不可（過去の設定を記録できない）</td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc3">キーワード解説</span></h2>
<table>
<thead>
<tr>
<th>用語</th>
<th>意味</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>CloudFormation</strong></td>
<td>AWSが提供するIaC（Infrastructure as Code）サービス。YAMLテンプレートでリソースをコード化する</td>
</tr>
<tr>
<td><strong>スタック</strong></td>
<td>CloudFormationが管理するリソースのまとまり。作成・削除・更新をまとめて操作できる</td>
</tr>
<tr>
<td><strong><code>!Ref</code></strong></td>
<td>CloudFormation組み込み関数。パラメータの値やリソースのIDを参照する</td>
</tr>
<tr>
<td><strong><code>!Sub</code></strong></td>
<td>CloudFormation組み込み関数。文字列内の <code>${変数}</code> を展開する</td>
</tr>
<tr>
<td><strong><code>!GetAtt</code></strong></td>
<td>CloudFormation組み込み関数。リソースの属性値（ARNなど）を取得する</td>
</tr>
<tr>
<td><strong>CAPABILITY_NAMED_IAM</strong></td>
<td>名前付きIAMリソースを作成する場合に必要な明示的な許可フラグ</td>
</tr>
<tr>
<td><strong>UserData</strong></td>
<td>EC2の初回起動時のみ自動実行されるシェルスクリプト</td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc4">前提条件</span></h2>
<table>
<thead>
<tr>
<th>ツール</th>
<th>確認コマンド</th>
<th>最低バージョン目安</th>
</tr>
</thead>
<tbody>
<tr>
<td>AWS CLI v2</td>
<td><code>aws --version</code></td>
<td>2.x</td>
</tr>
<tr>
<td>Git</td>
<td><code>git --version</code></td>
<td>2.x</td>
</tr>
<tr>
<td>VSCode</td>
<td>-</td>
<td>-</td>
</tr>
</tbody>
</table>
<p>AWS認証確認:</p>
<pre><code class="language-cmd">aws sts get-caller-identity</code></pre>
<p>アカウントIDが表示されれば認証設定済みです。</p>
<hr>
<h2><span id="toc5">template.yaml の全文</span></h2>
<p>以下が今回使用する <code>template.yaml</code> の全文です。プロジェクトフォルダ（<code>ec2-tomcat-web/</code>）直下に配置します。</p>
<blockquote>
<p><strong>⚠️ コピー前に確認</strong>: <code>Default: &#39;123.456.78.901/32&#39;</code> の箇所は<strong>ダミーIPです</strong>。このまま使うとスタック作成は成功しますが、SSHもブラウザもアクセスできません。<code>--parameters</code> でIPを上書きするか（推奨）、Defaultを自分のIPに書き換えてから使ってください。</p>
</blockquote>
<pre><code class="language-yaml">AWSTemplateFormatVersion: '2010-09-09'
Description: 'EC2 + Tomcat Application Server Hands-on (Phase 1-2)'

Parameters:
  KeyName:
    Type: String
    Default: 'my-ec2-tomcat-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 Tomcat access (CIDR format e.g. 203.0.113.1/32)

Resources:
  EC2Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: 'my-ec2-tomcat-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-tomcat-role'

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

  # Security Group: allow SSH(22) and Tomcat(8080) from MyIP only
  EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: 'my EC2 Tomcat hands-on SG'
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref MyIP
          Description: SSH access from my IP
        - IpProtocol: tcp
          FromPort: 8080
          ToPort: 8080
          CidrIp: !Ref MyIP
          Description: Tomcat HTTP access from my IP
      Tags:
        - Key: Name
          Value: 'my-ec2-tomcat-sg'

  # EC2 Instance: Amazon Linux 2023 + Java 17 + Tomcat 10 (installed via UserData)
  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
      SecurityGroupIds:
        - !Ref EC2SecurityGroup
      IamInstanceProfile: !Ref EC2InstanceProfile
      # UserData: shell script executed automatically on first boot
      UserData:
        Fn::Base64: |
          #!/bin/bash
          dnf update -y
          dnf install -y java-17-amazon-corretto

          useradd -m -d /opt/tomcat -U -s /bin/false tomcat

          TOMCAT_VERSION="10.1.25"
          cd /tmp
          wget -q "https://archive.apache.org/dist/tomcat/tomcat-10/v${TOMCAT_VERSION}/bin/apache-tomcat-${TOMCAT_VERSION}.tar.gz" \
            -O apache-tomcat.tar.gz
          tar xzf apache-tomcat.tar.gz
          mv "apache-tomcat-${TOMCAT_VERSION}" /opt/tomcat/latest
          chown -R tomcat:tomcat /opt/tomcat
          chmod -R u+x /opt/tomcat/latest/bin

          cat &gt; /etc/systemd/system/tomcat.service &lt;&lt; 'SVCEOF'
          [Unit]
          Description=Apache Tomcat Web Application Container
          After=network.target

          [Service]
          Type=forking
          User=tomcat
          Group=tomcat
          Environment="JAVA_HOME=/usr/lib/jvm/java-17-amazon-corretto"
          Environment="CATALINA_HOME=/opt/tomcat/latest"
          Environment="CATALINA_BASE=/opt/tomcat/latest"
          Environment="CATALINA_PID=/opt/tomcat/latest/temp/tomcat.pid"
          Environment="JAVA_OPTS=-Xms256m -Xmx512m"
          ExecStart=/opt/tomcat/latest/bin/startup.sh
          ExecStop=/opt/tomcat/latest/bin/shutdown.sh
          Restart=on-failure

          [Install]
          WantedBy=multi-user.target
          SVCEOF

          systemctl daemon-reload
          systemctl enable tomcat
          systemctl start tomcat

          cat &gt; /opt/tomcat/latest/webapps/ROOT/index.jsp &lt;&lt; 'JSPEOF'
          &lt;%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%&gt;
          &lt;!DOCTYPE html&gt;
          &lt;html&gt;
          &lt;head&gt;&lt;meta charset="UTF-8"&gt;&lt;title&gt;Tomcat on EC2&lt;/title&gt;&lt;/head&gt;
          &lt;body&gt;
          &lt;h1&gt;Hello from Tomcat on Amazon Linux 2023!&lt;/h1&gt;
          &lt;p&gt;Java Version: &lt;%= System.getProperty("java.version") %&gt;&lt;/p&gt;
          &lt;p&gt;Tomcat Version: &lt;%= application.getServerInfo() %&gt;&lt;/p&gt;
          &lt;/body&gt;
          &lt;/html&gt;
          JSPEOF
          chown tomcat:tomcat /opt/tomcat/latest/webapps/ROOT/index.jsp
      Tags:
        - Key: Name
          Value: 'my-ec2-tomcat-instance'

Outputs:
  InstanceId:
    Description: EC2 Instance ID
    Value: !Ref EC2Instance

  PublicIP:
    Description: Public IP Address
    Value: !GetAtt EC2Instance.PublicIp

  TomcatURL:
    Description: Tomcat URL (open in browser)
    Value: !Sub 'http://${EC2Instance.PublicIp}:8080'

  SSHCommand:
    Description: SSH command (update the .pem path as needed)
    Value: !Sub 'ssh -i C:\Users\username\.ssh\${KeyName}.pem ec2-user@${EC2Instance.PublicIp}'</code></pre>
<p><strong><code>!Ref</code> と <code>!Sub</code> の使い分け:</strong></p>
<table>
<thead>
<tr>
<th>関数</th>
<th>意味</th>
<th>例</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>!Ref パラメータ名</code></td>
<td>パラメータの値を参照する</td>
<td><code>!Ref MyIP</code> → <code>203.0.113.1/32</code></td>
</tr>
<tr>
<td><code>!Ref リソースID</code></td>
<td>リソースのIDを参照する</td>
<td><code>!Ref EC2SecurityGroup</code> → <code>sg-xxxxx</code></td>
</tr>
<tr>
<td><code>!Sub &#39;文字列&#39;</code></td>
<td>文字列内の <code>${変数}</code> を展開する</td>
<td><code>!Sub &#39;http://${EC2Instance.PublicIp}:8080&#39;</code> → <code>http://x.x.x.x:8080</code></td>
</tr>
<tr>
<td><code>!GetAtt リソース.属性</code></td>
<td>リソースの属性値を取得する</td>
<td><code>!GetAtt EC2Instance.PublicIp</code> → IPアドレス</td>
</tr>
</tbody>
</table>
<p><strong>Apache版との主な違い（template.yaml）:</strong></p>
<table>
<thead>
<tr>
<th>項目</th>
<th>Apache版</th>
<th>Tomcat版（今回）</th>
</tr>
</thead>
<tbody>
<tr>
<td>セキュリティグループ</td>
<td>SSH(22) + HTTP(80)</td>
<td>SSH(22) + TCP(8080)</td>
</tr>
<tr>
<td>UserData</td>
<td>httpd のインストール</td>
<td>Java17 + Tomcat10 のインストール</td>
</tr>
<tr>
<td>Outputs</td>
<td>WebsiteURL（port 80）</td>
<td>TomcatURL（port 8080）</td>
</tr>
</tbody>
</table>
<p><strong>構築されるリソースと論理ID:</strong></p>
<table>
<thead>
<tr>
<th>リソース</th>
<th>template.yaml上の論理ID</th>
</tr>
</thead>
<tbody>
<tr>
<td>IAMロール</td>
<td><code>EC2Role</code></td>
</tr>
<tr>
<td>インスタンスプロファイル</td>
<td><code>EC2InstanceProfile</code></td>
</tr>
<tr>
<td>セキュリティグループ</td>
<td><code>EC2SecurityGroup</code></td>
</tr>
<tr>
<td>EC2インスタンス</td>
<td><code>EC2Instance</code></td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc6">作業順序</span></h2>
<pre><code class="language-plaintext">⓪ 自分のIPアドレスを確認する（重要）
      ↓
① キーペアを作成する（コンソールで実施）
      ↓
② プロジェクトフォルダに移動する
      ↓
③ スタックを作成する（aws cloudformation create-stack）
      ↓
④ 作成状況・Outputsを確認する
      ↓
⑤ 動作確認（ブラウザ・SSH）
      ↓
⑥ スタックを削除する（aws cloudformation delete-stack）</code></pre>
<hr>
<h2><span id="toc7">【重要】⓪ 自分のIPアドレスを確認する</span></h2>
<p>セキュリティグループで自分のIPのみを許可するために、<strong>正確なIPアドレス</strong>を事前に確認します。</p>
<blockquote>
<p><strong>注意</strong>: <code>curl https://checkip.amazonaws.com</code> で表示されるIPと、EC2への実際の接続元IPが<strong>一致しない場合</strong>があります（ISPやプロキシの経路の違いによる）。</p>
</blockquote>
<h3><span id="toc8">方法A: ルーター管理画面で確認（推奨）</span></h3>
<ol>
<li>ブラウザで以下のURLにアクセスします
<pre><code class="language-plaintext">http://192.168.0.1  または  http://192.168.1.1</code></pre>
</li>
<li>「ネットワークマップ」または「インターネット」項目を開く</li>
<li>**「インターネットIPアドレス」または「WAN IPアドレス」**の値を控えます</li>
</ol>
<h3><span id="toc9">方法B: コマンドで確認</span></h3>
<pre><code class="language-cmd">curl https://checkip.amazonaws.com</code></pre>
<p><strong>控えておく情報</strong>: 自分のIPアドレス（例: <code>203.0.113.1</code>）</p>
<hr>
<h2><span id="toc10">① キーペアの作成（コンソールで実施）</span></h2>
<p>キーペアのみコンソールで作成します。CloudFormationではキーペアのダウンロードが行えないためです。</p>
<p><strong>AWSコンソール → EC2 → キーペア → 「キーペアを作成」</strong></p>
<table>
<thead>
<tr>
<th>設定項目</th>
<th>値</th>
</tr>
</thead>
<tbody>
<tr>
<td>名前</td>
<td><code>my-ec2-tomcat-key</code></td>
</tr>
<tr>
<td>キーペアのタイプ</td>
<td><strong>RSA</strong></td>
</tr>
<tr>
<td>プライベートキーファイル形式</td>
<td><strong>.pem</strong></td>
</tr>
</tbody>
</table>
<p>ダウンロードされた <code>my-ec2-tomcat-key.pem</code> を保存します。</p>
<pre><code class="language-plaintext">C:\Users\ユーザー名\.ssh\my-ec2-tomcat-key.pem</code></pre>
<blockquote>
<p><strong>前回のキーペアの流用について</strong>: Apacheハンズオンで作成した <code>my-ec2-apache-key</code> を流用する場合、この手順はスキップし、後続コマンドの <code>KeyName</code> の値を <code>my-ec2-apache-key</code> に変更します。</p>
</blockquote>
<hr>
<h2><span id="toc11">② プロジェクトフォルダに移動する</span></h2>
<p>VSCodeのターミナル（CMD）を開き、プロジェクトフォルダに移動します。</p>
<pre><code class="language-cmd">cd C:\my-aws\aws-learning-projects\ec2-tomcat-web</code></pre>
<p><code>template.yaml</code> があることを確認します。</p>
<pre><code class="language-cmd">dir template.yaml</code></pre>
<hr>
<h2><span id="toc12">③ スタックの作成</span></h2>
<p>以下のコマンドを実行します。<code>203.0.113.1</code> は⓪で確認した自分のIPアドレスに置き換えてください。</p>
<pre><code class="language-cmd">aws cloudformation create-stack ^
  --stack-name my-ec2-tomcat-stack ^
  --template-body file://template.yaml ^
  --region ap-northeast-1 ^
  --capabilities CAPABILITY_NAMED_IAM ^
  --parameters ^
    ParameterKey=KeyName,ParameterValue=my-ec2-tomcat-key ^
    ParameterKey=MyIP,ParameterValue=203.0.113.1/32</code></pre>
<p><strong>各オプションの説明:</strong></p>
<table>
<thead>
<tr>
<th>オプション</th>
<th>意味</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>--stack-name my-ec2-tomcat-stack</code></td>
<td>スタック名（英字始まり）</td>
</tr>
<tr>
<td><code>--template-body file://template.yaml</code></td>
<td>使用するテンプレートファイル</td>
</tr>
<tr>
<td><code>--region ap-northeast-1</code></td>
<td>デプロイ先リージョン（東京）</td>
</tr>
<tr>
<td><code>--capabilities CAPABILITY_NAMED_IAM</code></td>
<td>名前付きIAMロール作成の明示的な許可</td>
</tr>
<tr>
<td><code>--parameters ...</code></td>
<td>テンプレートのParametersに渡す値</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong><code>^</code> について</strong>: Windowsのコマンドプロンプトで長いコマンドを複数行に分けるための改行エスケープ文字です。1行で書いても動作します。</p>
</blockquote>
<blockquote>
<p><strong>template.yamlのコメントは英語のみ</strong>: 日本語コメントが含まれているとWindows上のAWS CLIがエンコードエラーを起こすため、コメントはすべて英語で記述しています。</p>
</blockquote>
<p>成功すると以下のようなStackIdが表示されます。</p>
<pre><code class="language-json">{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:123456789012:stack/my-ec2-tomcat-stack/xxxxx"
}</code></pre>
<p><!-- ![スタック作成コマンドの実行結果](images/cfn-create-stack-cmd.jpg) --></p>
<hr>
<h2><span id="toc13">④ 作成完了・Outputsの確認</span></h2>
<p>スタック作成は完了まで<strong>約3〜5分</strong>かかります。ただし、Tomcatのインストール（UserData）はさらに<strong>5〜10分</strong>かかるため、スタック完了後もすぐにブラウザでアクセスできないことがあります。</p>
<h3><span id="toc14">作成状況の確認</span></h3>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-ec2-tomcat-stack ^
  --query "Stacks[0].StackStatus" ^
  --output text</code></pre>
<table>
<thead>
<tr>
<th>ステータス</th>
<th>意味</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>CREATE_IN_PROGRESS</code></td>
<td>作成中（しばらく待つ）</td>
</tr>
<tr>
<td><code>CREATE_COMPLETE</code></td>
<td>作成完了</td>
</tr>
<tr>
<td><code>CREATE_FAILED</code></td>
<td>作成失敗（トラブルシューティングを参照）</td>
</tr>
<tr>
<td><code>ROLLBACK_COMPLETE</code></td>
<td>失敗してロールバック完了（リソースは作成されていない）</td>
</tr>
</tbody>
</table>
<h3><span id="toc15">Outputs（接続情報）の確認</span></h3>
<p><code>CREATE_COMPLETE</code> になったら以下を実行します。</p>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-ec2-tomcat-stack ^
  --query "Stacks[0].Outputs" ^
  --output table</code></pre>
<p>以下のような出力が表示されます。</p>
<pre><code class="language-plaintext">------------------------------------------------------------------------------------------
|                                    DescribeStacks                                      |
+----------------+-----------------------------------------------------------------------+
|  OutputKey     |  OutputValue                                                          |
+----------------+-----------------------------------------------------------------------+
|  InstanceId    |  i-0123456789abcdef0                                                 |
|  PublicIP      |  x.x.x.x                                                             |
|  TomcatURL     |  http://x.x.x.x:8080                                                 |
|  SSHCommand    |  ssh -i C:\Users\...\.ssh\my-ec2-tomcat-key.pem ec2-user@x.x.x.x     |
+----------------+-----------------------------------------------------------------------+</code></pre>
<p><strong>控えておく情報</strong>: <code>PublicIP</code> と <code>TomcatURL</code></p>
<p><!-- ![Outputs確認コマンドの実行結果](images/cfn-outputs-table.jpg) --></p>
<hr>
<h2><span id="toc16">⑤ 動作確認</span></h2>
<h3><span id="toc17">ブラウザで確認</span></h3>
<p>Outputsに表示された <code>TomcatURL</code>（<code>http://x.x.x.x:8080</code>）をブラウザで開きます。</p>
<pre><code class="language-plaintext">Hello from Tomcat on Amazon Linux 2023!
Java Version: 17.x.x
Tomcat Version: Apache Tomcat/10.1.25</code></pre>
<p>が表示されれば成功です。</p>
<blockquote>
<p><strong>表示されない場合</strong>: UserDataのインストール完了まで5〜10分かかります。スタックが <code>CREATE_COMPLETE</code> になった後も少し待ってからアクセスしてください。</p>
</blockquote>
<p><!-- ![ブラウザでのTomcat動作確認](images/tomcat-web-browser.jpg) --></p>
<h3><span id="toc18">SSHで接続して確認</span></h3>
<p>Outputsに表示された <code>SSHCommand</code> を実行します（パスは実際のパスに修正します）。</p>
<pre><code class="language-cmd">ssh -i C:\Users\ユーザー名\.ssh\my-ec2-tomcat-key.pem ec2-user@（PublicIP）</code></pre>
<p>初回接続時の確認には <code>yes</code> を入力します。</p>
<pre><code class="language-bash"># Javaのバージョン確認
java -version

# Tomcatサービスの状態確認（active (running) と表示されれば正常）
sudo systemctl status tomcat

# TomcatのログでERRORがないか確認
sudo tail -50 /opt/tomcat/latest/logs/catalina.out

# UserDataの実行ログ確認（インストール状況を詳細に確認できる）
sudo cat /var/log/cloud-init-output.log

# 8080番ポートのListen確認
sudo ss -tlnp | grep :8080

exit</code></pre>
<h3><span id="toc19">【接続できない場合】真のIPアドレスを確認する</span></h3>
<p><code>curl</code> で確認したIPを設定したが接続できない場合、実際の接続元IPが異なる可能性があります。</p>
<p><strong>セキュリティグループIDを取得:</strong></p>
<pre><code class="language-cmd">aws cloudformation describe-stack-resources ^
  --stack-name my-ec2-tomcat-stack ^
  --query "StackResources[?ResourceType=='AWS::EC2::SecurityGroup'].PhysicalResourceId" ^
  --output text</code></pre>
<p><strong>SSH全開放（一時的・テスト用）:</strong></p>
<pre><code class="language-cmd">aws ec2 authorize-security-group-ingress ^
  --group-id sg-XXXXXXXXX ^
  --protocol tcp ^
  --port 22 ^
  --cidr 0.0.0.0/0 ^
  --region ap-northeast-1</code></pre>
<p>SSH接続して真のIPを確認します。</p>
<pre><code class="language-bash">echo $SSH_CLIENT
# 出力例: 203.0.113.1 17508 22
# 先頭の値が真の接続元IP
exit</code></pre>
<p>確認後、全開放を削除して真のIPで制限し直します。</p>
<pre><code class="language-cmd">rem 全開放を削除
aws ec2 revoke-security-group-ingress ^
  --group-id sg-XXXXXXXXX ^
  --protocol tcp ^
  --port 22 ^
  --cidr 0.0.0.0/0 ^
  --region ap-northeast-1

rem 真のIPで再制限
aws ec2 authorize-security-group-ingress ^
  --group-id sg-XXXXXXXXX ^
  --protocol tcp ^
  --port 22 ^
  --cidr 真のIP/32 ^
  --region ap-northeast-1</code></pre>
<hr>
<h2><span id="toc20">⑥ スタックの削除</span></h2>
<p><strong>課金を止めるために、ハンズオン完了後は必ず削除してください。</strong></p>
<p>CloudFormationはスタック削除で全リソースを一括削除できます。手動でリソースを1つ1つ削除する必要はありません。</p>
<pre><code class="language-cmd">aws cloudformation delete-stack ^
  --stack-name my-ec2-tomcat-stack ^
  --region ap-northeast-1</code></pre>
<p>削除の進行状況を確認します（完了まで約3〜5分）。</p>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-ec2-tomcat-stack ^
  --query "Stacks[0].StackStatus" ^
  --output text</code></pre>
<table>
<thead>
<tr>
<th>ステータス</th>
<th>意味</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>DELETE_IN_PROGRESS</code></td>
<td>削除中（しばらく待つ）</td>
</tr>
<tr>
<td><code>DELETE_FAILED</code></td>
<td>削除失敗（トラブルシューティングを参照）</td>
</tr>
</tbody>
</table>
<p>削除完了の確認（以下のエラーが表示されれば削除完了）。</p>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-ec2-tomcat-stack ^
  --region ap-northeast-1</code></pre>
<pre><code class="language-plaintext">An error occurred (ValidationError) when calling the DescribeStacks operation:
Stack with id my-ec2-tomcat-stack does not exist</code></pre>
<p>このメッセージが表示されれば削除完了です。キーペアはCloudFormationで管理していないため、手動で削除します。</p>
<p><strong>EC2 → キーペア → <code>my-ec2-tomcat-key</code> を選択 → 「アクション」→「削除」</strong></p>
<p>ローカルの <code>.pem</code> ファイルも削除します。</p>
<hr>
<h2><span id="toc21">コンソール版との比較</span></h2>
<p>コンソール版でIAMロール・セキュリティグループ・EC2を手動作成した手順が、CloudFormationではすべて <code>template.yaml</code> に定義されています。</p>
<table>
<thead>
<tr>
<th>CFnの記述</th>
<th>コンソールでやること</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>EC2Role</code>（IAMロール定義）</td>
<td>IAM → ロールを作成（AmazonSSMManagedInstanceCore付与）</td>
</tr>
<tr>
<td><code>EC2SecurityGroup</code>（SGルール定義）</td>
<td>EC2 → セキュリティグループを作成（SSH/TCP8080各ルール追加）</td>
</tr>
<tr>
<td><code>EC2Instance</code>（UserData含む）</td>
<td>EC2 → インスタンスを起動（UserData貼り付け・キーペア選択）</td>
</tr>
<tr>
<td><code>--parameters MyIP=...</code></td>
<td>セキュリティグループのインバウンドルールに自分のIPを入力</td>
</tr>
<tr>
<td><code>delete-stack</code></td>
<td>インスタンス終了 → SG削除 → IAMロール削除（順番が重要）</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>コンソール版で詰まりやすいポイント:</strong><br />UserDataのTomcatインストールが5〜10分かかるため、コンソール版では「インスタンスが動いているのにアクセスできない」状態が続きます。<br />CloudFormationでも待ち時間は同じですが、デプロイ自体はコマンド1本で完了します。</p>
</blockquote>
<hr>
<h2><span id="toc22">トラブルシューティング</span></h2>
<table>
<thead>
<tr>
<th>症状</th>
<th>原因</th>
<th>対処</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>CREATE_FAILED</code> になる</td>
<td>キーペアが存在しない</td>
<td>コンソールでキーペアを作成し、<code>--parameters KeyName=...</code> の値を確認</td>
</tr>
<tr>
<td><code>CREATE_FAILED</code> になる</td>
<td><code>CAPABILITY_NAMED_IAM</code> が不足</td>
<td>コマンドに <code>--capabilities CAPABILITY_NAMED_IAM</code> が含まれているか確認</td>
</tr>
<tr>
<td><code>stackName failed to satisfy regular expression pattern</code></td>
<td>スタック名が数字始まり</td>
<td>CloudFormationのスタック名は<strong>英字で始まる</strong>必要がある</td>
</tr>
<tr>
<td><code>Unable to load paramfile, text contents could not be decoded</code></td>
<td>template.yaml に日本語が含まれている</td>
<td>template.yaml のコメントを<strong>すべて英語</strong>で記述する（Windows環境の既知の問題）</td>
</tr>
<tr>
<td>ブラウザで8080に接続できない</td>
<td>UserDataのインストールが未完了</td>
<td>スタック完了後5〜10分待つ。SSHで <code>sudo cat /var/log/cloud-init-output.log</code> を確認</td>
</tr>
<tr>
<td>Tomcatは起動しているがJSPが動かない</td>
<td>index.jspがTomcat起動後に配置されなかった</td>
<td><code>sudo systemctl restart tomcat</code> でTomcatを再起動する</td>
</tr>
<tr>
<td>SSH接続タイムアウト</td>
<td>セキュリティグループのIP設定誤り</td>
<td>SGのインバウンドルールでSSH(22)のIPを確認</td>
</tr>
<tr>
<td><code>DELETE_FAILED</code> になる</td>
<td>手動でリソースを変更したためCFnが管理できない</td>
<td>コンソールで該当リソースを確認して手動削除後、<code>delete-stack</code> を再実行</td>
</tr>
<tr>
<td><code>ValidationError: Template format error</code></td>
<td>template.yamlのインデントが崩れている</td>
<td>YAMLはインデントが厳格。スペース2つ単位でインデントを確認する</td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc23">まとめ</span></h2>
<p>今回のハンズオンで実現できたこと：</p>
<table>
<thead>
<tr>
<th>確認項目</th>
<th>内容</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>IaCの体験</strong></td>
<td>template.yaml 1ファイルにIAMロール・SG・EC2をコードで定義</td>
</tr>
<tr>
<td><strong>一括デプロイ</strong></td>
<td><code>create-stack</code> コマンド1本でコンソール版と同じ構成を5〜10分で構築</td>
</tr>
<tr>
<td><strong>Outputs活用</strong></td>
<td>デプロイ後にIPアドレス・TomcatURL・SSHコマンドを <code>describe-stacks</code> で自動取得</td>
</tr>
<tr>
<td><strong>一括削除</strong></td>
<td><code>delete-stack</code> 1本でリソースの依存関係を自動解決して全削除</td>
</tr>
</tbody>
</table>
<h3><span id="toc24">CloudFormationのメリットを実感できたポイント</span></h3>
<ul>
<li>コンソール版では20〜30分かかった作業がコマンド1本（5〜10分）で完了した</li>
<li><code>delete-stack</code> で依存関係を気にせず全リソースを一括削除できた</li>
<li><code>template.yaml</code> をGitで管理することで、環境構成の変更履歴が残せる</li>
</ul>
<hr>
<h2><span id="toc25">コンソール版と比較してみる</span></h2>
<p>CloudFormationが裏で何をやっているか、同じ構成をAWSコンソールのみで構築する手順をまとめました。コンソール版ではキーペア・IAMロール・セキュリティグループ・EC2を依存関係順に手動で作成する必要があり、各リソースの役割と繋がりが視覚的に理解できます。</p>
<p><a href="https://caymezon.com/aws-handson-console-ec2-tomcat/">EC2サーバ構築ハンズオン：AWSコンソールでTomcatを構築する手順</a></p>
<hr>
<h2><span id="toc26">関連記事</span></h2>
<p><!-- <a rel="nofollow" href="//af.moshimo.com/af/c/click?a_id=1384942&p_id=170&pc_id=185&pl_id=4062&url=https%3A%2F%2Fwww.amazon.co.jp%2Fs%3Fk%3D%25E6%259C%25AC%2BAWS%2B%25E9%2596%258B%25E7%2599%25BA%26__mk_ja_JP%3D%25E3%2582%25AB%25E3%2582%25BF%25E3%2582%25AB%25E3%2583%258A%26crid%3D1DE63UBHFOR4K%26sprefix%3D%25E6%259C%25AC%2Baws%2B%25E9%2596%258B%25E7%2599%25BA%252Caps%252C167%26ref%3Dnb_sb_noss" referrerpolicy="no-referrer-when-downgrade" attributionsrc>Amazon検索[本 AWS 開発]</a><img decoding="async" src="//i.moshimo.com/af/i/impression?a_id=1384942&p_id=170&pc_id=185&pl_id=4062" width="1" height="1" style="border:none;" alt="" loading="lazy"> --></p>
<p><!-- <!-- START MoshimoAffiliateEasyLink --><script type="text/javascript">(function(b,c,f,g,a,d,e){b.MoshimoAffiliateObject=a;b[a]=b[a]||function(){arguments.currentScript=c.currentScript||c.scripts[c.scripts.length-2];(b[a].q=b[a].q||[]).push(arguments)};c.getElementById(a)||(d=c.createElement(f),d.src=g,d.id=a,e=c.getElementsByTagName("body")[0],e.appendChild(d))})(window,document,"script","//dn.msmstatic.com/site/cardlink/bundle.js?20220329","msmaflink");msmaflink({"n":"AWS運用入門 改訂第2版 押さえておきたいAWSの基本と運用ノウハウ [AWS深掘りガイド]","b":"SBクリエイティブ","t":"","d":"https:\/\/m.media-amazon.com","c_p":"\/images\/I","p":["\/51AAOubymTL._SL500_.jpg","\/51VMG6YKHdL._SL500_.jpg","\/41EdPB8azAL._SL500_.jpg","\/41v2JFE-9jL._SL500_.jpg","\/41FEEqR-yDL._SL500_.jpg","\/41JfZAdnTPL._SL500_.jpg","\/41vGK0czQrL._SL500_.jpg","\/41-SnYtz2aL._SL500_.jpg","\/41sPrV5fi3L._SL500_.jpg","\/41p7JtvYJ1L._SL500_.jpg","\/4169GVNTs8L._SL500_.jpg","\/41BPI5HP3zL._SL500_.jpg","\/41QOyk60CYL._SL500_.jpg","\/41APjk6FphL._SL500_.jpg","\/41ezKUu7VRL._SL500_.jpg","\/41A1n3K+r5L._SL500_.jpg","\/41aY2T8lEOL._SL500_.jpg","\/419Ca1V6HZL._SL500_.jpg","\/41zQkYyLPzL._SL500_.jpg","\/41YpHcyxiTL._SL500_.jpg","\/41-tKN5mt6L._SL500_.jpg","\/419Mv6m55IL._SL500_.jpg"],"u":{"u":"https:\/\/www.amazon.co.jp\/dp\/4815631085","t":"amazon","r_v":""},"v":"2.1","b_l":[{"id":1,"u_tx":"Amazonで見る","u_bc":"#f79256","u_url":"https:\/\/www.amazon.co.jp\/dp\/4815631085","a_id":1384942,"p_id":170,"pl_id":27060,"pc_id":185,"s_n":"amazon","u_so":1},{"id":2,"u_tx":"楽天市場で見る","u_bc":"#f76956","u_url":"https:\/\/search.rakuten.co.jp\/search\/mall\/AWS%E9%81%8B%E7%94%A8%E5%85%A5%E9%96%80%20%E6%94%B9%E8%A8%82%E7%AC%AC2%E7%89%88%20%E6%8A%BC%E3%81%95%E3%81%88%E3%81%A6%E3%81%8A%E3%81%8D%E3%81%9F%E3%81%84AWS%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%81%A8%E9%81%8B%E7%94%A8%E3%83%8E%E3%82%A6%E3%83%8F%E3%82%A6%20%5BAWS%E6%B7%B1%E6%8E%98%E3%82%8A%E3%82%AC%E3%82%A4%E3%83%89%5D\/","a_id":1384917,"p_id":54,"pl_id":27059,"pc_id":54,"s_n":"rakuten","u_so":2},{"id":3,"u_tx":"Yahoo!ショッピングで見る","u_bc":"#66a7ff","u_url":"https:\/\/shopping.yahoo.co.jp\/search?first=1\u0026p=AWS%E9%81%8B%E7%94%A8%E5%85%A5%E9%96%80%20%E6%94%B9%E8%A8%82%E7%AC%AC2%E7%89%88%20%E6%8A%BC%E3%81%95%E3%81%88%E3%81%A6%E3%81%8A%E3%81%8D%E3%81%9F%E3%81%84AWS%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%81%A8%E9%81%8B%E7%94%A8%E3%83%8E%E3%82%A6%E3%83%8F%E3%82%A6%20%5BAWS%E6%B7%B1%E6%8E%98%E3%82%8A%E3%82%AC%E3%82%A4%E3%83%89%5D","a_id":1466950,"p_id":1225,"pl_id":27061,"pc_id":1925,"s_n":"yahoo","u_so":3}],"eid":"E8MM1","s":"s"});</script></p>
<div id="msmaflink-E8MM1">リンク</div>
<p><!-- MoshimoAffiliateEasyLink END --> --></p><p>The post <a href="https://caymezon.com/aws-handson-cloudformation-ec2-tomcat/">EC2サーバ構築をIaC化：CloudFormationでEC2 Tomcatを自動デプロイする手順【コンソール版との比較付き】</a> first appeared on <a href="https://caymezon.com">CayTech Lab</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://caymezon.com/aws-handson-cloudformation-ec2-tomcat/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>AWS CloudFormationでEC2 + ApacheウェブサーバーをIaC化しよう【EC2ハンズオン / コンソール版との比較付き】</title>
		<link>https://caymezon.com/aws-handson-cloudformation-ec2-apache/</link>
					<comments>https://caymezon.com/aws-handson-cloudformation-ec2-apache/#respond</comments>
		
		<dc:creator><![CDATA[caymezon]]></dc:creator>
		<pubDate>Mon, 23 Mar 2026 08:38:50 +0000</pubDate>
				<category><![CDATA[AWS Basic]]></category>
		<category><![CDATA[Cloud & Infra]]></category>
		<category><![CDATA[Apache]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[AWSCLI]]></category>
		<category><![CDATA[CloudFormation]]></category>
		<category><![CDATA[EC2]]></category>
		<category><![CDATA[IaC]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[UserData]]></category>
		<category><![CDATA[Webサーバー]]></category>
		<category><![CDATA[セキュリティグループ]]></category>
		<category><![CDATA[ハンズオン]]></category>
		<category><![CDATA[初心者]]></category>
		<guid isPermaLink="false">https://caymezon.com/?p=20287</guid>

					<description><![CDATA[<p>目次 はじめにCloudFormation vs コンソール：どれだけ違うかキーワード解説前提条件template.yaml の全文作業順序【重要】⓪ 自分のIPアドレスを確認する方法A: ルーター管理画面で確認（推奨） [&#8230;]</p>
<p>The post <a href="https://caymezon.com/aws-handson-cloudformation-ec2-apache/">AWS CloudFormationでEC2 + ApacheウェブサーバーをIaC化しよう【EC2ハンズオン / コンソール版との比較付き】</a> first appeared on <a href="https://caymezon.com">CayTech Lab</a>.</p>]]></description>
										<content:encoded><![CDATA[<div id="toc" class="toc tnt-number toc-center tnt-number border-element"><input type="checkbox" class="toc-checkbox" id="toc-checkbox-12" checked><label class="toc-title" for="toc-checkbox-12">目次</label>
    <div class="toc-content">
    <ol class="toc-list open"><li><a href="#toc1" tabindex="0">はじめに</a></li><li><a href="#toc2" tabindex="0">CloudFormation vs コンソール：どれだけ違うか</a></li><li><a href="#toc3" tabindex="0">キーワード解説</a></li><li><a href="#toc4" tabindex="0">前提条件</a></li><li><a href="#toc5" tabindex="0">template.yaml の全文</a></li><li><a href="#toc6" tabindex="0">作業順序</a></li><li><a href="#toc7" tabindex="0">【重要】⓪ 自分のIPアドレスを確認する</a><ol><li><a href="#toc8" tabindex="0">方法A: ルーター管理画面で確認（推奨）</a></li><li><a href="#toc9" tabindex="0">方法B: コマンドで確認</a></li></ol></li><li><a href="#toc10" tabindex="0">① キーペアの作成（コンソールで実施）</a></li><li><a href="#toc11" tabindex="0">② プロジェクトフォルダに移動する</a></li><li><a href="#toc12" tabindex="0">③ スタックの作成</a></li><li><a href="#toc13" tabindex="0">④ 作成完了・Outputsの確認</a><ol><li><a href="#toc14" tabindex="0">作成状況の確認</a></li><li><a href="#toc15" tabindex="0">Outputs（接続情報）の確認</a></li></ol></li><li><a href="#toc16" tabindex="0">⑤ 動作確認</a><ol><li><a href="#toc17" tabindex="0">Webブラウザで確認</a></li><li><a href="#toc18" tabindex="0">SSHで接続して確認</a></li><li><a href="#toc19" tabindex="0">【接続できない場合】真のIPアドレスを確認する</a></li></ol></li><li><a href="#toc20" tabindex="0">⑥ スタックの削除</a></li><li><a href="#toc21" tabindex="0">コンソール版との比較</a></li><li><a href="#toc22" tabindex="0">トラブルシューティング</a></li><li><a href="#toc23" tabindex="0">まとめ</a><ol><li><a href="#toc24" tabindex="0">CloudFormationのメリットを実感できたポイント</a></li></ol></li><li><a href="#toc25" tabindex="0">コンソール版と比較してみる</a></li><li><a href="#toc26" tabindex="0">関連記事</a></li></ol>
    </div>
  </div>

<h2><span id="toc1">はじめに</span></h2>
<p>「コンソールで手動構築できた構成を、コードで再現したい」——それを実現するのが <strong>AWS CloudFormation</strong> です。</p>
<p>この記事では、<code>template.yaml</code> 1ファイルにリソース構成を定義し、<strong>コマンド1本でEC2 + Apache環境を一括構築するハンズオン</strong>を紹介します。コンソール版（<a href="#">AWSコンソール版ハンズオン</a>）と全く同じ構成を、CloudFormationで自動化します。</p>
<pre><code class="language-plaintext">ローカル環境（VSCode）
  ├── template.yaml（CloudFormationテンプレート）
  └── AWS CLI コマンド
        ↓ スタック作成（コマンド1本）
AWS環境
  ├── IAMロール（SSM接続用）         ← template.yamlで定義
  ├── セキュリティグループ（SSH22・HTTP80）← template.yamlで定義
  └── EC2インスタンス（t2.micro / Amazon Linux 2023 + Apache）← template.yamlで定義</code></pre>
<p><strong>このハンズオンで体験できること：</strong></p>
<ul>
<li><code>template.yaml</code> へのリソース定義（IAMロール・セキュリティグループ・EC2インスタンス）</li>
<li><code>!Ref</code> / <code>!Sub</code> / <code>!GetAtt</code> を使ったリソース間参照</li>
<li><code>aws cloudformation create-stack</code> コマンド1本での全リソース一括デプロイ</li>
<li><code>aws cloudformation delete-stack</code> コマンド1本での全リソース一括削除</li>
</ul>
<p><strong>このハンズオンの特徴：</strong></p>
<ul>
<li>コンソール版では15〜20分かかった手動作業が、コマンド1本（約5分）で完了する</li>
<li><code>delete-stack</code> 1本で依存関係を考慮した順番で全リソースを自動削除できる</li>
</ul>
<hr>
<p><!-- ![CloudFormationスタック作成完了画面](images/cfn-create-complete.jpg) --></p>
<p><!-- <a rel="nofollow" href="//af.moshimo.com/af/c/click?a_id=1384942&p_id=170&pc_id=185&pl_id=4062&url=https%3A%2F%2Fwww.amazon.co.jp%2Fs%3Fk%3D%25E6%259C%25AC%2BAWS%2B%25E9%2596%258B%25E7%2599%25BA%26__mk_ja_JP%3D%25E3%2582%25AB%25E3%2582%25BF%25E3%2582%25AB%25E3%2583%258A%26crid%3D1DE63UBHFOR4K%26sprefix%3D%25E6%259C%25AC%2Baws%2B%25E9%2596%258B%25E7%2599%25BA%252Caps%252C167%26ref%3Dnb_sb_noss" referrerpolicy="no-referrer-when-downgrade" attributionsrc>Amazon検索[本 AWS 開発]</a><img decoding="async" src="//i.moshimo.com/af/i/impression?a_id=1384942&p_id=170&pc_id=185&pl_id=4062" width="1" height="1" style="border:none;" alt="" loading="lazy"> --></p>
<p><!-- <!-- START MoshimoAffiliateEasyLink --><script type="text/javascript">(function(b,c,f,g,a,d,e){b.MoshimoAffiliateObject=a;b[a]=b[a]||function(){arguments.currentScript=c.currentScript||c.scripts[c.scripts.length-2];(b[a].q=b[a].q||[]).push(arguments)};c.getElementById(a)||(d=c.createElement(f),d.src=g,d.id=a,e=c.getElementsByTagName("body")[0],e.appendChild(d))})(window,document,"script","//dn.msmstatic.com/site/cardlink/bundle.js?20220329","msmaflink");msmaflink({"n":"AWSの基本・仕組み・重要用語が全部わかる教科書 (見るだけ図解)","b":"SBクリエイティブ","t":"","d":"https:\/\/m.media-amazon.com","c_p":"\/images\/I","p":["\/51DEDQXj6oL._SL500_.jpg","\/41F589smNwL._SL500_.jpg","\/41R6f9yyCWL._SL500_.jpg","\/41HqWQ9BvmL._SL500_.jpg","\/41p8p0ZU79L._SL500_.jpg","\/41qLC-fndBL._SL500_.jpg","\/41fcLv9VT5L._SL500_.jpg","\/51lRvCsvHqL._SL500_.jpg"],"u":{"u":"https:\/\/www.amazon.co.jp\/dp\/4815607850","t":"amazon","r_v":""},"v":"2.1","b_l":[{"id":1,"u_tx":"Amazonで見る","u_bc":"#f79256","u_url":"https:\/\/www.amazon.co.jp\/dp\/4815607850","a_id":1384942,"p_id":170,"pl_id":27060,"pc_id":185,"s_n":"amazon","u_so":1},{"id":2,"u_tx":"楽天市場で見る","u_bc":"#f76956","u_url":"https:\/\/search.rakuten.co.jp\/search\/mall\/AWS%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%83%BB%E4%BB%95%E7%B5%84%E3%81%BF%E3%83%BB%E9%87%8D%E8%A6%81%E7%94%A8%E8%AA%9E%E3%81%8C%E5%85%A8%E9%83%A8%E3%82%8F%E3%81%8B%E3%82%8B%E6%95%99%E7%A7%91%E6%9B%B8%20(%E8%A6%8B%E3%82%8B%E3%81%A0%E3%81%91%E5%9B%B3%E8%A7%A3)\/","a_id":1384917,"p_id":54,"pl_id":27059,"pc_id":54,"s_n":"rakuten","u_so":2},{"id":3,"u_tx":"Yahoo!ショッピングで見る","u_bc":"#66a7ff","u_url":"https:\/\/shopping.yahoo.co.jp\/search?first=1\u0026p=AWS%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%83%BB%E4%BB%95%E7%B5%84%E3%81%BF%E3%83%BB%E9%87%8D%E8%A6%81%E7%94%A8%E8%AA%9E%E3%81%8C%E5%85%A8%E9%83%A8%E3%82%8F%E3%81%8B%E3%82%8B%E6%95%99%E7%A7%91%E6%9B%B8%20(%E8%A6%8B%E3%82%8B%E3%81%A0%E3%81%91%E5%9B%B3%E8%A7%A3)","a_id":1466950,"p_id":1225,"pl_id":27061,"pc_id":1925,"s_n":"yahoo","u_so":3}],"eid":"eaCUB","s":"s"});</script></p>
<div id="msmaflink-eaCUB">リンク</div>
<p><!-- MoshimoAffiliateEasyLink END --> --></p>
<h2><span id="toc2">CloudFormation vs コンソール：どれだけ違うか</span></h2>
<table>
<thead>
<tr>
<th>比較項目</th>
<th>CloudFormation</th>
<th>コンソール（手動）</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>IAMロール作成</strong></td>
<td><code>--parameters</code> で値を渡すだけ</td>
<td>画面でポチポチ（5クリック以上）</td>
</tr>
<tr>
<td><strong>セキュリティグループ作成</strong></td>
<td>同上</td>
<td>画面でポチポチ</td>
</tr>
<tr>
<td><strong>EC2起動（UserData含む）</strong></td>
<td>同上</td>
<td>画面でポチポチ</td>
</tr>
<tr>
<td><strong>全リソースのデプロイ</strong></td>
<td>コマンド1本（3〜5分）</td>
<td>複数画面を行き来（15〜20分）</td>
</tr>
<tr>
<td><strong>リソース削除</strong></td>
<td><code>delete-stack</code> 1本</td>
<td>依存関係順に個別削除（4ステップ）</td>
</tr>
<tr>
<td><strong>再現性</strong></td>
<td>高い（同じ構成を何度でも再現可能）</td>
<td>低い（手動ミスのリスクがある）</td>
</tr>
<tr>
<td><strong>バージョン管理</strong></td>
<td>Gitで管理可能</td>
<td>不可（過去の設定を記録できない）</td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc3">キーワード解説</span></h2>
<table>
<thead>
<tr>
<th>用語</th>
<th>意味</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>CloudFormation</strong></td>
<td>AWSが提供するIaC（Infrastructure as Code）サービス。YAMLテンプレートでリソースをコード化する</td>
</tr>
<tr>
<td><strong>スタック</strong></td>
<td>CloudFormationが管理するリソースのまとまり。作成・削除・更新をまとめて操作できる</td>
</tr>
<tr>
<td><strong><code>!Ref</code></strong></td>
<td>CloudFormation組み込み関数。パラメータの値やリソースのIDを参照する</td>
</tr>
<tr>
<td><strong><code>!Sub</code></strong></td>
<td>CloudFormation組み込み関数。文字列内の <code>${変数}</code> を展開する</td>
</tr>
<tr>
<td><strong><code>!GetAtt</code></strong></td>
<td>CloudFormation組み込み関数。リソースの属性値（ARNなど）を取得する</td>
</tr>
<tr>
<td><strong>CAPABILITY_NAMED_IAM</strong></td>
<td>名前付きIAMリソースを作成する場合に必要な明示的な許可フラグ</td>
</tr>
<tr>
<td><strong>UserData</strong></td>
<td>EC2の初回起動時のみ自動実行されるシェルスクリプト</td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc4">前提条件</span></h2>
<table>
<thead>
<tr>
<th>ツール</th>
<th>確認コマンド</th>
<th>最低バージョン目安</th>
</tr>
</thead>
<tbody>
<tr>
<td>AWS CLI v2</td>
<td><code>aws --version</code></td>
<td>2.x</td>
</tr>
<tr>
<td>Git</td>
<td><code>git --version</code></td>
<td>2.x</td>
</tr>
<tr>
<td>VSCode</td>
<td>-</td>
<td>-</td>
</tr>
</tbody>
</table>
<p>AWS認証確認:</p>
<pre><code class="language-cmd">aws sts get-caller-identity</code></pre>
<p>アカウントIDが表示されれば認証設定済みです。</p>
<hr>
<h2><span id="toc5">template.yaml の全文</span></h2>
<p>以下が今回使用する <code>template.yaml</code> の全文です。プロジェクトフォルダ（<code>ec2-apache-web/</code>）直下に配置します。</p>
<blockquote>
<p><strong>⚠️ コピー前に確認</strong>: <code>Default: &#39;123.456.78.901/32&#39;</code> の箇所は<strong>ダミーIPです</strong>。このまま使うとスタック作成は成功しますが、SSHもWebもアクセスできません。<code>--parameters</code> でIPを上書きするか（推奨）、Defaultを自分のIPに書き換えてから使ってください。</p>
</blockquote>
<pre><code class="language-yaml">AWSTemplateFormatVersion: '2010-09-09'
Description: 'EC2 + Apache Web Server Hands-on (Phase 1-1)'

Parameters:
  KeyName:
    Type: String
    Default: 'my-ec2-apache-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 HTTP access (CIDR format e.g. 203.0.113.1/32)

Resources:
  EC2Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: 'my-ec2-apache-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-apache-role'

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

  # Security Group: allow SSH(22) and HTTP(80) from MyIP only
  EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: 'my EC2 Apache hands-on SG'
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref MyIP
          Description: SSH access from my IP
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Ref MyIP
          Description: HTTP access from my IP
      Tags:
        - Key: Name
          Value: 'my-ec2-apache-sg'

  # EC2 Instance: Amazon Linux 2023 + Apache (installed via UserData)
  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      # Automatically fetches the latest Amazon Linux 2023 AMI ID from SSM Parameter Store
      ImageId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64}}'
      InstanceType: t2.micro
      KeyName: !Ref KeyName
      SecurityGroupIds:
        - !Ref EC2SecurityGroup
      IamInstanceProfile: !Ref EC2InstanceProfile
      # UserData: shell script executed automatically on first boot
      UserData:
        Fn::Base64: |
          #!/bin/bash
          yum update -y
          yum install -y httpd
          systemctl start httpd
          systemctl enable httpd
          echo "&lt;h1&gt;Hello from Amazon Linux 2023!&lt;/h1&gt;" &gt; /var/www/html/index.html
          echo "&lt;p&gt;Instance ID: $(ec2-metadata --instance-id | cut -d ' ' -f 2)&lt;/p&gt;" &gt;&gt; /var/www/html/index.html
          echo "&lt;p&gt;Availability Zone: $(ec2-metadata --availability-zone | cut -d ' ' -f 2)&lt;/p&gt;" &gt;&gt; /var/www/html/index.html
      Tags:
        - Key: Name
          Value: 'my-ec2-apache-instance'

Outputs:
  InstanceId:
    Description: EC2 Instance ID
    Value: !Ref EC2Instance

  PublicIP:
    Description: Public IP Address
    Value: !GetAtt EC2Instance.PublicIp

  WebsiteURL:
    Description: Website URL (open in browser)
    Value: !Sub 'http://${EC2Instance.PublicIp}'

  SSHCommand:
    Description: SSH command (update the .pem path as needed)
    Value: !Sub 'ssh -i C:\Users\username\.ssh\${KeyName}.pem ec2-user@${EC2Instance.PublicIp}'</code></pre>
<p><strong><code>!Ref</code> と <code>!Sub</code> の使い分け:</strong></p>
<table>
<thead>
<tr>
<th>関数</th>
<th>意味</th>
<th>例</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>!Ref パラメータ名</code></td>
<td>パラメータの値を参照する</td>
<td><code>!Ref MyIP</code> → <code>203.0.113.1/32</code></td>
</tr>
<tr>
<td><code>!Ref リソースID</code></td>
<td>リソースのIDを参照する</td>
<td><code>!Ref EC2SecurityGroup</code> → <code>sg-xxxxx</code></td>
</tr>
<tr>
<td><code>!Sub &#39;文字列&#39;</code></td>
<td>文字列内の <code>${変数}</code> を展開する</td>
<td><code>!Sub &#39;http://${EC2Instance.PublicIp}&#39;</code> → <code>http://x.x.x.x</code></td>
</tr>
<tr>
<td><code>!GetAtt リソース.属性</code></td>
<td>リソースの属性値を取得する</td>
<td><code>!GetAtt EC2Instance.PublicIp</code> → IPアドレス</td>
</tr>
</tbody>
</table>
<p><strong>構築されるリソースと論理ID:</strong></p>
<table>
<thead>
<tr>
<th>リソース</th>
<th>template.yaml上の論理ID</th>
</tr>
</thead>
<tbody>
<tr>
<td>IAMロール</td>
<td><code>EC2Role</code></td>
</tr>
<tr>
<td>インスタンスプロファイル</td>
<td><code>EC2InstanceProfile</code></td>
</tr>
<tr>
<td>セキュリティグループ</td>
<td><code>EC2SecurityGroup</code></td>
</tr>
<tr>
<td>EC2インスタンス</td>
<td><code>EC2Instance</code></td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc6">作業順序</span></h2>
<pre><code class="language-plaintext">⓪ 自分のIPアドレスを確認する（重要）
      ↓
① キーペアを作成する（コンソールで実施）
      ↓
② プロジェクトフォルダに移動する
      ↓
③ スタックを作成する（aws cloudformation create-stack）
      ↓
④ 作成状況・Outputsを確認する
      ↓
⑤ 動作確認（Web・SSH）
      ↓
⑥ スタックを削除する（aws cloudformation delete-stack）</code></pre>
<hr>
<h2><span id="toc7">【重要】⓪ 自分のIPアドレスを確認する</span></h2>
<p>セキュリティグループで自分のIPのみを許可するために、<strong>正確なIPアドレス</strong>を事前に確認します。</p>
<blockquote>
<p><strong>注意</strong>: <code>curl https://checkip.amazonaws.com</code> で表示されるIPと、EC2への実際の接続元IPが<strong>一致しない場合</strong>があります（ISPやプロキシの経路の違いによる）。</p>
</blockquote>
<h3><span id="toc8">方法A: ルーター管理画面で確認（推奨）</span></h3>
<ol>
<li>ブラウザで以下のURLにアクセスします
<pre><code class="language-plaintext">http://192.168.0.1  または  http://192.168.1.1</code></pre>
</li>
<li>「ネットワークマップ」または「インターネット」項目を開く</li>
<li>**「インターネットIPアドレス」または「WAN IPアドレス」**の値を控えます</li>
</ol>
<h3><span id="toc9">方法B: コマンドで確認</span></h3>
<pre><code class="language-cmd">curl https://checkip.amazonaws.com</code></pre>
<p><strong>控えておく情報</strong>: 自分のIPアドレス（例: <code>203.0.113.1</code>）</p>
<hr>
<h2><span id="toc10">① キーペアの作成（コンソールで実施）</span></h2>
<p>キーペアのみコンソールで作成します。CloudFormationではキーペアのダウンロードが行えないためです。</p>
<p><strong>AWSコンソール → EC2 → キーペア → 「キーペアを作成」</strong></p>
<table>
<thead>
<tr>
<th>設定項目</th>
<th>値</th>
</tr>
</thead>
<tbody>
<tr>
<td>名前</td>
<td><code>my-ec2-apache-key</code></td>
</tr>
<tr>
<td>キーペアのタイプ</td>
<td><strong>RSA</strong></td>
</tr>
<tr>
<td>プライベートキーファイル形式</td>
<td><strong>.pem</strong></td>
</tr>
</tbody>
</table>
<p>ダウンロードされた <code>my-ec2-apache-key.pem</code> を保存します。</p>
<pre><code class="language-plaintext">C:\Users\ユーザー名\.ssh\my-ec2-apache-key.pem</code></pre>
<hr>
<h2><span id="toc11">② プロジェクトフォルダに移動する</span></h2>
<p>VSCodeのターミナル（CMD）を開き、プロジェクトフォルダに移動します。</p>
<pre><code class="language-cmd">cd C:\my-aws\aws-learning-projects\ec2-apache-web</code></pre>
<p><code>template.yaml</code> があることを確認します。</p>
<pre><code class="language-cmd">dir template.yaml</code></pre>
<hr>
<h2><span id="toc12">③ スタックの作成</span></h2>
<p>以下のコマンドを実行します。<code>203.0.113.1</code> は⓪で確認した自分のIPアドレスに置き換えてください。</p>
<pre><code class="language-cmd">aws cloudformation create-stack ^
  --stack-name my-ec2-apache-stack ^
  --template-body file://template.yaml ^
  --region ap-northeast-1 ^
  --capabilities CAPABILITY_NAMED_IAM ^
  --parameters ^
    ParameterKey=KeyName,ParameterValue=my-ec2-apache-key ^
    ParameterKey=MyIP,ParameterValue=203.0.113.1/32</code></pre>
<p><strong>各オプションの説明:</strong></p>
<table>
<thead>
<tr>
<th>オプション</th>
<th>意味</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>--stack-name my-ec2-apache-stack</code></td>
<td>スタック名（任意の名前）</td>
</tr>
<tr>
<td><code>--template-body file://template.yaml</code></td>
<td>使用するテンプレートファイル</td>
</tr>
<tr>
<td><code>--region ap-northeast-1</code></td>
<td>デプロイ先リージョン（東京）</td>
</tr>
<tr>
<td><code>--capabilities CAPABILITY_NAMED_IAM</code></td>
<td>名前付きIAMロールを作成するための明示的な許可</td>
</tr>
<tr>
<td><code>--parameters ...</code></td>
<td>テンプレートのParametersに渡す値</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong><code>^</code> について</strong>: Windowsのコマンドプロンプトで長いコマンドを複数行に分けるための改行エスケープ文字です。1行で書いても動作します。</p>
</blockquote>
<p>成功すると以下のようなStackIdが表示されます。</p>
<pre><code class="language-json">{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:123456789012:stack/my-ec2-apache-stack/xxxxx"
}</code></pre>
<p><!-- ![スタック作成コマンドの実行結果](images/cfn-create-stack-cmd.jpg) --></p>
<hr>
<h2><span id="toc13">④ 作成完了・Outputsの確認</span></h2>
<p>スタック作成は完了まで<strong>約3〜5分</strong>かかります。</p>
<h3><span id="toc14">作成状況の確認</span></h3>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-ec2-apache-stack ^
  --query "Stacks[0].StackStatus" ^
  --output text</code></pre>
<table>
<thead>
<tr>
<th>ステータス</th>
<th>意味</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>CREATE_IN_PROGRESS</code></td>
<td>作成中（しばらく待つ）</td>
</tr>
<tr>
<td><code>CREATE_COMPLETE</code></td>
<td>作成完了</td>
</tr>
<tr>
<td><code>CREATE_FAILED</code></td>
<td>作成失敗（トラブルシューティングを参照）</td>
</tr>
<tr>
<td><code>ROLLBACK_COMPLETE</code></td>
<td>失敗してロールバック完了（リソースは作成されていない）</td>
</tr>
</tbody>
</table>
<h3><span id="toc15">Outputs（接続情報）の確認</span></h3>
<p><code>CREATE_COMPLETE</code> になったら以下を実行します。</p>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-ec2-apache-stack ^
  --query "Stacks[0].Outputs" ^
  --output table</code></pre>
<p>以下のような出力が表示されます。</p>
<pre><code class="language-plaintext">------------------------------------------------------------------------------------
|                              DescribeStacks                                      |
+----------------+-----------------------------------------------------------------+
|  OutputKey     |  OutputValue                                                    |
+----------------+-----------------------------------------------------------------+
|  InstanceId    |  i-0123456789abcdef0                                           |
|  PublicIP      |  x.x.x.x                                                       |
|  WebsiteURL    |  http://x.x.x.x                                                |
|  SSHCommand    |  ssh -i C:\Users\...\.ssh\my-ec2-apache-key.pem ec2-user@x.x.x.x |
+----------------+-----------------------------------------------------------------+</code></pre>
<p><strong>控えておく情報</strong>: <code>PublicIP</code> と <code>WebsiteURL</code></p>
<p><!-- ![Outputs確認コマンドの実行結果](images/cfn-outputs-table.jpg) --></p>
<hr>
<h2><span id="toc16">⑤ 動作確認</span></h2>
<h3><span id="toc17">Webブラウザで確認</span></h3>
<p>Outputsに表示された <code>WebsiteURL</code>（<code>http://x.x.x.x</code>）をブラウザで開きます。</p>
<pre><code class="language-plaintext">Hello from Amazon Linux 2023!
Instance ID: i-0123456789abcdef0
Availability Zone: ap-northeast-1a</code></pre>
<p>が表示されれば成功です。</p>
<blockquote>
<p><strong>表示されない場合</strong>: UserDataのApacheインストールが完了するまで2〜3分かかります。少し待ってからリロードしてください。</p>
</blockquote>
<p><!-- ![ブラウザでのWebページ確認](images/apache-web-browser.jpg) --></p>
<h3><span id="toc18">SSHで接続して確認</span></h3>
<p>Outputsに表示された <code>SSHCommand</code> のコマンドを実行します（パスは実際のパスに修正します）。</p>
<pre><code class="language-cmd">ssh -i C:\Users\ユーザー名\.ssh\my-ec2-apache-key.pem ec2-user@（PublicIP）</code></pre>
<p>初回接続時の確認には <code>yes</code> を入力します。</p>
<pre><code class="language-bash"># Apacheの動作状態を確認（active (running) と表示されれば正常）
sudo systemctl status httpd

# ApacheのHTMLファイルを確認
cat /var/www/html/index.html

# EC2から切断
exit</code></pre>
<h3><span id="toc19">【接続できない場合】真のIPアドレスを確認する</span></h3>
<p><code>curl</code> で確認したIPを設定したが接続できない場合、実際の接続元IPが異なる可能性があります。</p>
<p><strong>セキュリティグループIDを取得:</strong></p>
<pre><code class="language-cmd">aws cloudformation describe-stack-resources ^
  --stack-name my-ec2-apache-stack ^
  --query "StackResources[?ResourceType=='AWS::EC2::SecurityGroup'].PhysicalResourceId" ^
  --output text</code></pre>
<p><strong>SSH全開放（一時的・テスト用）:</strong></p>
<pre><code class="language-cmd">aws ec2 authorize-security-group-ingress ^
  --group-id sg-XXXXXXXXX ^
  --protocol tcp ^
  --port 22 ^
  --cidr 0.0.0.0/0 ^
  --region ap-northeast-1</code></pre>
<p>SSH接続して真のIPを確認します。</p>
<pre><code class="language-bash">echo $SSH_CLIENT
# 出力例: 203.0.113.1 17508 22
# 先頭の値が真の接続元IP
exit</code></pre>
<p>確認後、全開放を削除して真のIPで制限し直します。</p>
<pre><code class="language-cmd">rem 全開放を削除
aws ec2 revoke-security-group-ingress ^
  --group-id sg-XXXXXXXXX ^
  --protocol tcp ^
  --port 22 ^
  --cidr 0.0.0.0/0 ^
  --region ap-northeast-1

rem 真のIPで再制限
aws ec2 authorize-security-group-ingress ^
  --group-id sg-XXXXXXXXX ^
  --protocol tcp ^
  --port 22 ^
  --cidr 真のIP/32 ^
  --region ap-northeast-1</code></pre>
<hr>
<h2><span id="toc20">⑥ スタックの削除</span></h2>
<p><strong>課金を止めるために、ハンズオン完了後は必ず削除してください。</strong></p>
<p>CloudFormationはスタック削除で全リソースを一括削除できます。手動でリソースを1つ1つ削除する必要はありません。</p>
<pre><code class="language-cmd">aws cloudformation delete-stack ^
  --stack-name my-ec2-apache-stack ^
  --region ap-northeast-1</code></pre>
<p>削除の進行状況を確認します（完了まで約3〜5分）。</p>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-ec2-apache-stack ^
  --query "Stacks[0].StackStatus" ^
  --output text</code></pre>
<table>
<thead>
<tr>
<th>ステータス</th>
<th>意味</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>DELETE_IN_PROGRESS</code></td>
<td>削除中（しばらく待つ）</td>
</tr>
<tr>
<td><code>DELETE_FAILED</code></td>
<td>削除失敗（トラブルシューティングを参照）</td>
</tr>
</tbody>
</table>
<p>削除完了の確認（以下のエラーが表示されれば削除完了）。</p>
<pre><code class="language-cmd">aws cloudformation describe-stacks ^
  --stack-name my-ec2-apache-stack ^
  --region ap-northeast-1</code></pre>
<pre><code class="language-plaintext">An error occurred (ValidationError) when calling the DescribeStacks operation:
Stack with id my-ec2-apache-stack does not exist</code></pre>
<p>このメッセージが表示されれば削除完了です。キーペアはCloudFormationで管理していないため、手動で削除します。</p>
<p><strong>EC2 → キーペア → <code>my-ec2-apache-key</code> を選択 → 「アクション」→「削除」</strong></p>
<p>ローカルの <code>.pem</code> ファイルも削除します。</p>
<hr>
<h2><span id="toc21">コンソール版との比較</span></h2>
<p>コンソール版でIAMロール・セキュリティグループ・EC2を手動作成した手順が、CloudFormationではすべて <code>template.yaml</code> に定義されています。</p>
<table>
<thead>
<tr>
<th>SAM/CFnの記述</th>
<th>コンソールでやること</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>EC2Role</code>（IAMロール定義）</td>
<td>IAM → ロールを作成（AmazonSSMManagedInstanceCore付与）</td>
</tr>
<tr>
<td><code>EC2SecurityGroup</code>（SGルール定義）</td>
<td>EC2 → セキュリティグループを作成（SSH/HTTP各ルール追加）</td>
</tr>
<tr>
<td><code>EC2Instance</code>（UserData含む）</td>
<td>EC2 → インスタンスを起動（UserData貼り付け・キーペア選択）</td>
</tr>
<tr>
<td><code>--parameters MyIP=...</code></td>
<td>セキュリティグループのインバウンドルールに自分のIPを入力</td>
</tr>
<tr>
<td><code>delete-stack</code></td>
<td>インスタンス終了 → SG削除 → IAMロール削除（順番が重要）</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>コンソール版で詰まりやすいポイント:</strong><br />セキュリティグループ削除時に「使用中」エラーが出るのは、EC2インスタンスが「終了済み」になっていないためです。<br />CloudFormationなら削除順序を自動で解決してくれるので、このエラーは発生しません。</p>
</blockquote>
<hr>
<h2><span id="toc22">トラブルシューティング</span></h2>
<table>
<thead>
<tr>
<th>症状</th>
<th>原因</th>
<th>対処</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>CREATE_FAILED</code> になる</td>
<td>キーペアが存在しない</td>
<td>コンソールでキーペアを作成し、<code>--parameters KeyName=...</code> の値を確認</td>
</tr>
<tr>
<td><code>CREATE_FAILED</code> になる</td>
<td><code>CAPABILITY_NAMED_IAM</code> が不足</td>
<td>コマンドに <code>--capabilities CAPABILITY_NAMED_IAM</code> が含まれているか確認</td>
</tr>
<tr>
<td><code>stackName failed to satisfy regular expression pattern</code></td>
<td>スタック名が数字始まり</td>
<td>CloudFormationのスタック名は<strong>英字で始まる</strong>必要がある</td>
</tr>
<tr>
<td><code>Unable to load paramfile, text contents could not be decoded</code></td>
<td>template.yaml に日本語が含まれている</td>
<td>template.yaml のコメント・説明文を<strong>すべて英語</strong>で記述する（Windows環境の既知の問題）</td>
</tr>
<tr>
<td>SSH接続タイムアウト</td>
<td>セキュリティグループのIP設定誤り</td>
<td>「真のIPアドレスを確認する」手順を実行</td>
</tr>
<tr>
<td>Webページが表示されない（起動直後）</td>
<td>ApacheのUserDataが未完了</td>
<td>2〜3分待ってからリロード</td>
</tr>
<tr>
<td><code>DELETE_FAILED</code> になる</td>
<td>手動でリソースを変更したためCFnが管理できない</td>
<td>コンソールで該当リソースを確認して手動削除後、<code>delete-stack</code> を再実行</td>
</tr>
<tr>
<td><code>ValidationError: Template format error</code></td>
<td>template.yamlのインデントが崩れている</td>
<td>YAMLはインデントが厳格。スペース2つ単位でインデントを確認する</td>
</tr>
</tbody>
</table>
<hr>
<h2><span id="toc23">まとめ</span></h2>
<p>今回のハンズオンで実現できたこと：</p>
<table>
<thead>
<tr>
<th>確認項目</th>
<th>内容</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>IaCの体験</strong></td>
<td>template.yaml 1ファイルにIAMロール・SG・EC2をコードで定義</td>
</tr>
<tr>
<td><strong>一括デプロイ</strong></td>
<td><code>create-stack</code> コマンド1本でコンソール版と同じ構成を5分で構築</td>
</tr>
<tr>
<td><strong>Outputs活用</strong></td>
<td>デプロイ後にIPアドレス・SSHコマンドを <code>describe-stacks</code> で自動取得</td>
</tr>
<tr>
<td><strong>一括削除</strong></td>
<td><code>delete-stack</code> 1本でリソースの依存関係を自動解決して全削除</td>
</tr>
</tbody>
</table>
<h3><span id="toc24">CloudFormationのメリットを実感できたポイント</span></h3>
<ul>
<li>コンソール版では15〜20分かかった作業がコマンド1本（5分）で完了した</li>
<li><code>delete-stack</code> で依存関係を気にせず全リソースを一括削除できた</li>
<li><code>template.yaml</code> をGitで管理することで、環境構成の変更履歴が残せる</li>
</ul>
<hr>
<h2><span id="toc25">コンソール版と比較してみる</span></h2>
<p>CloudFormationが裏で何をやっているか、同じ構成をAWSコンソールのみで構築する手順をまとめました。コンソール版ではキーペア・IAMロール・セキュリティグループ・EC2を依存関係順に手動で作成する必要があり、各リソースの役割と繋がりが視覚的に理解できます。</p>
<p><!-- TODO: コンソール版記事へのリンクを追加 --></p>
<hr>
<h2><span id="toc26">関連記事</span></h2>
<p><!-- <a rel="nofollow" href="//af.moshimo.com/af/c/click?a_id=1384942&p_id=170&pc_id=185&pl_id=4062&url=https%3A%2F%2Fwww.amazon.co.jp%2Fs%3Fk%3D%25E6%259C%25AC%2BAWS%2B%25E9%2596%258B%25E7%2599%25BA%26__mk_ja_JP%3D%25E3%2582%25AB%25E3%2582%25BF%25E3%2582%25AB%25E3%2583%258A%26crid%3D1DE63UBHFOR4K%26sprefix%3D%25E6%259C%25AC%2Baws%2B%25E9%2596%258B%25E7%2599%25BA%252Caps%252C167%26ref%3Dnb_sb_noss" referrerpolicy="no-referrer-when-downgrade" attributionsrc>Amazon検索[本 AWS 開発]</a><img decoding="async" src="//i.moshimo.com/af/i/impression?a_id=1384942&p_id=170&pc_id=185&pl_id=4062" width="1" height="1" style="border:none;" alt="" loading="lazy"> --></p>
<p><!-- <!-- START MoshimoAffiliateEasyLink --><script type="text/javascript">(function(b,c,f,g,a,d,e){b.MoshimoAffiliateObject=a;b[a]=b[a]||function(){arguments.currentScript=c.currentScript||c.scripts[c.scripts.length-2];(b[a].q=b[a].q||[]).push(arguments)};c.getElementById(a)||(d=c.createElement(f),d.src=g,d.id=a,e=c.getElementsByTagName("body")[0],e.appendChild(d))})(window,document,"script","//dn.msmstatic.com/site/cardlink/bundle.js?20220329","msmaflink");msmaflink({"n":"AWS運用入門 改訂第2版 押さえておきたいAWSの基本と運用ノウハウ [AWS深掘りガイド]","b":"SBクリエイティブ","t":"","d":"https:\/\/m.media-amazon.com","c_p":"\/images\/I","p":["\/51AAOubymTL._SL500_.jpg","\/51VMG6YKHdL._SL500_.jpg","\/41EdPB8azAL._SL500_.jpg","\/41v2JFE-9jL._SL500_.jpg","\/41FEEqR-yDL._SL500_.jpg","\/41JfZAdnTPL._SL500_.jpg","\/41vGK0czQrL._SL500_.jpg","\/41-SnYtz2aL._SL500_.jpg","\/41sPrV5fi3L._SL500_.jpg","\/41p7JtvYJ1L._SL500_.jpg","\/4169GVNTs8L._SL500_.jpg","\/41BPI5HP3zL._SL500_.jpg","\/41QOyk60CYL._SL500_.jpg","\/41APjk6FphL._SL500_.jpg","\/41ezKUu7VRL._SL500_.jpg","\/41A1n3K+r5L._SL500_.jpg","\/41aY2T8lEOL._SL500_.jpg","\/419Ca1V6HZL._SL500_.jpg","\/41zQkYyLPzL._SL500_.jpg","\/41YpHcyxiTL._SL500_.jpg","\/41-tKN5mt6L._SL500_.jpg","\/419Mv6m55IL._SL500_.jpg"],"u":{"u":"https:\/\/www.amazon.co.jp\/dp\/4815631085","t":"amazon","r_v":""},"v":"2.1","b_l":[{"id":1,"u_tx":"Amazonで見る","u_bc":"#f79256","u_url":"https:\/\/www.amazon.co.jp\/dp\/4815631085","a_id":1384942,"p_id":170,"pl_id":27060,"pc_id":185,"s_n":"amazon","u_so":1},{"id":2,"u_tx":"楽天市場で見る","u_bc":"#f76956","u_url":"https:\/\/search.rakuten.co.jp\/search\/mall\/AWS%E9%81%8B%E7%94%A8%E5%85%A5%E9%96%80%20%E6%94%B9%E8%A8%82%E7%AC%AC2%E7%89%88%20%E6%8A%BC%E3%81%95%E3%81%88%E3%81%A6%E3%81%8A%E3%81%8D%E3%81%9F%E3%81%84AWS%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%81%A8%E9%81%8B%E7%94%A8%E3%83%8E%E3%82%A6%E3%83%8F%E3%82%A6%20%5BAWS%E6%B7%B1%E6%8E%98%E3%82%8A%E3%82%AC%E3%82%A4%E3%83%89%5D\/","a_id":1384917,"p_id":54,"pl_id":27059,"pc_id":54,"s_n":"rakuten","u_so":2},{"id":3,"u_tx":"Yahoo!ショッピングで見る","u_bc":"#66a7ff","u_url":"https:\/\/shopping.yahoo.co.jp\/search?first=1\u0026p=AWS%E9%81%8B%E7%94%A8%E5%85%A5%E9%96%80%20%E6%94%B9%E8%A8%82%E7%AC%AC2%E7%89%88%20%E6%8A%BC%E3%81%95%E3%81%88%E3%81%A6%E3%81%8A%E3%81%8D%E3%81%9F%E3%81%84AWS%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%81%A8%E9%81%8B%E7%94%A8%E3%83%8E%E3%82%A6%E3%83%8F%E3%82%A6%20%5BAWS%E6%B7%B1%E6%8E%98%E3%82%8A%E3%82%AC%E3%82%A4%E3%83%89%5D","a_id":1466950,"p_id":1225,"pl_id":27061,"pc_id":1925,"s_n":"yahoo","u_so":3}],"eid":"E8MM1","s":"s"});</script></p>
<div id="msmaflink-E8MM1">リンク</div>
<p><!-- MoshimoAffiliateEasyLink END --> --></p><p>The post <a href="https://caymezon.com/aws-handson-cloudformation-ec2-apache/">AWS CloudFormationでEC2 + ApacheウェブサーバーをIaC化しよう【EC2ハンズオン / コンソール版との比較付き】</a> first appeared on <a href="https://caymezon.com">CayTech Lab</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://caymezon.com/aws-handson-cloudformation-ec2-apache/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
