AWS CloudFormationでEC2 + ApacheウェブサーバーをIaC化しよう【EC2ハンズオン / コンソール版との比較付き】

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

はじめに

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

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

ローカル環境(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で定義

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

  • template.yaml へのリソース定義(IAMロール・セキュリティグループ・EC2インスタンス)
  • !Ref / !Sub / !GetAtt を使ったリソース間参照
  • aws cloudformation create-stack コマンド1本での全リソース一括デプロイ
  • aws cloudformation delete-stack コマンド1本での全リソース一括削除

このハンズオンの特徴:

  • コンソール版では15〜20分かかった手動作業が、コマンド1本(約5分)で完了する
  • delete-stack 1本で依存関係を考慮した順番で全リソースを自動削除できる

-->

スポンサーリンク

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

比較項目CloudFormationコンソール(手動)
IAMロール作成--parameters で値を渡すだけ画面でポチポチ(5クリック以上)
セキュリティグループ作成同上画面でポチポチ
EC2起動(UserData含む)同上画面でポチポチ
全リソースのデプロイコマンド1本(3〜5分)複数画面を行き来(15〜20分)
リソース削除delete-stack 1本依存関係順に個別削除(4ステップ)
再現性高い(同じ構成を何度でも再現可能)低い(手動ミスのリスクがある)
バージョン管理Gitで管理可能不可(過去の設定を記録できない)

スポンサーリンク

キーワード解説

用語意味
CloudFormationAWSが提供するIaC(Infrastructure as Code)サービス。YAMLテンプレートでリソースをコード化する
スタックCloudFormationが管理するリソースのまとまり。作成・削除・更新をまとめて操作できる
!RefCloudFormation組み込み関数。パラメータの値やリソースのIDを参照する
!SubCloudFormation組み込み関数。文字列内の ${変数} を展開する
!GetAttCloudFormation組み込み関数。リソースの属性値(ARNなど)を取得する
CAPABILITY_NAMED_IAM名前付きIAMリソースを作成する場合に必要な明示的な許可フラグ
UserDataEC2の初回起動時のみ自動実行されるシェルスクリプト

前提条件

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

AWS認証確認:

aws sts get-caller-identity

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


template.yaml の全文

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

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

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 "<h1>Hello from Amazon Linux 2023!</h1>" > /var/www/html/index.html
          echo "<p>Instance ID: $(ec2-metadata --instance-id | cut -d ' ' -f 2)</p>" >> /var/www/html/index.html
          echo "<p>Availability Zone: $(ec2-metadata --availability-zone | cut -d ' ' -f 2)</p>" >> /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}'

!Ref!Sub の使い分け:

関数意味
!Ref パラメータ名パラメータの値を参照する!Ref MyIP203.0.113.1/32
!Ref リソースIDリソースのIDを参照する!Ref EC2SecurityGroupsg-xxxxx
!Sub '文字列'文字列内の ${変数} を展開する!Sub 'http://${EC2Instance.PublicIp}'http://x.x.x.x
!GetAtt リソース.属性リソースの属性値を取得する!GetAtt EC2Instance.PublicIp → IPアドレス

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

リソースtemplate.yaml上の論理ID
IAMロールEC2Role
インスタンスプロファイルEC2InstanceProfile
セキュリティグループEC2SecurityGroup
EC2インスタンスEC2Instance

作業順序

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

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

セキュリティグループで自分のIPのみを許可するために、正確なIPアドレスを事前に確認します。

注意: curl https://checkip.amazonaws.com で表示されるIPと、EC2への実際の接続元IPが一致しない場合があります(ISPやプロキシの経路の違いによる)。

方法A: ルーター管理画面で確認(推奨)

  1. ブラウザで以下のURLにアクセスします
    http://192.168.0.1  または  http://192.168.1.1
  2. 「ネットワークマップ」または「インターネット」項目を開く
  3. **「インターネットIPアドレス」または「WAN IPアドレス」**の値を控えます

方法B: コマンドで確認

curl https://checkip.amazonaws.com

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


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

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

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

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

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

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

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

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

cd C:\my-aws\aws-learning-projects\ec2-apache-web

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

dir template.yaml

③ スタックの作成

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

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

各オプションの説明:

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

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

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

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


④ 作成完了・Outputsの確認

スタック作成は完了まで約3〜5分かかります。

作成状況の確認

aws cloudformation describe-stacks ^
  --stack-name my-ec2-apache-stack ^
  --query "Stacks[0].StackStatus" ^
  --output text
ステータス意味
CREATE_IN_PROGRESS作成中(しばらく待つ)
CREATE_COMPLETE作成完了
CREATE_FAILED作成失敗(トラブルシューティングを参照)
ROLLBACK_COMPLETE失敗してロールバック完了(リソースは作成されていない)

Outputs(接続情報)の確認

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

aws cloudformation describe-stacks ^
  --stack-name my-ec2-apache-stack ^
  --query "Stacks[0].Outputs" ^
  --output table

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

------------------------------------------------------------------------------------
|                              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 |
+----------------+-----------------------------------------------------------------+

控えておく情報: PublicIPWebsiteURL


⑤ 動作確認

Webブラウザで確認

Outputsに表示された WebsiteURLhttp://x.x.x.x)をブラウザで開きます。

Hello from Amazon Linux 2023!
Instance ID: i-0123456789abcdef0
Availability Zone: ap-northeast-1a

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

表示されない場合: UserDataのApacheインストールが完了するまで2〜3分かかります。少し待ってからリロードしてください。

SSHで接続して確認

Outputsに表示された SSHCommand のコマンドを実行します(パスは実際のパスに修正します)。

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

初回接続時の確認には yes を入力します。

# Apacheの動作状態を確認(active (running) と表示されれば正常)
sudo systemctl status httpd

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

# EC2から切断
exit

【接続できない場合】真のIPアドレスを確認する

curl で確認したIPを設定したが接続できない場合、実際の接続元IPが異なる可能性があります。

セキュリティグループIDを取得:

aws cloudformation describe-stack-resources ^
  --stack-name my-ec2-apache-stack ^
  --query "StackResources[?ResourceType=='AWS::EC2::SecurityGroup'].PhysicalResourceId" ^
  --output text

SSH全開放(一時的・テスト用):

aws ec2 authorize-security-group-ingress ^
  --group-id sg-XXXXXXXXX ^
  --protocol tcp ^
  --port 22 ^
  --cidr 0.0.0.0/0 ^
  --region ap-northeast-1

SSH接続して真のIPを確認します。

echo $SSH_CLIENT
# 出力例: 203.0.113.1 17508 22
# 先頭の値が真の接続元IP
exit

確認後、全開放を削除して真のIPで制限し直します。

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

⑥ スタックの削除

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

CloudFormationはスタック削除で全リソースを一括削除できます。手動でリソースを1つ1つ削除する必要はありません。

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

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

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

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

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

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

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

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


コンソール版との比較

コンソール版でIAMロール・セキュリティグループ・EC2を手動作成した手順が、CloudFormationではすべて template.yaml に定義されています。

SAM/CFnの記述コンソールでやること
EC2Role(IAMロール定義)IAM → ロールを作成(AmazonSSMManagedInstanceCore付与)
EC2SecurityGroup(SGルール定義)EC2 → セキュリティグループを作成(SSH/HTTP各ルール追加)
EC2Instance(UserData含む)EC2 → インスタンスを起動(UserData貼り付け・キーペア選択)
--parameters MyIP=...セキュリティグループのインバウンドルールに自分のIPを入力
delete-stackインスタンス終了 → SG削除 → IAMロール削除(順番が重要)

コンソール版で詰まりやすいポイント:
セキュリティグループ削除時に「使用中」エラーが出るのは、EC2インスタンスが「終了済み」になっていないためです。
CloudFormationなら削除順序を自動で解決してくれるので、このエラーは発生しません。


トラブルシューティング

症状原因対処
CREATE_FAILED になるキーペアが存在しないコンソールでキーペアを作成し、--parameters KeyName=... の値を確認
CREATE_FAILED になるCAPABILITY_NAMED_IAM が不足コマンドに --capabilities CAPABILITY_NAMED_IAM が含まれているか確認
stackName failed to satisfy regular expression patternスタック名が数字始まりCloudFormationのスタック名は英字で始まる必要がある
Unable to load paramfile, text contents could not be decodedtemplate.yaml に日本語が含まれているtemplate.yaml のコメント・説明文をすべて英語で記述する(Windows環境の既知の問題)
SSH接続タイムアウトセキュリティグループのIP設定誤り「真のIPアドレスを確認する」手順を実行
Webページが表示されない(起動直後)ApacheのUserDataが未完了2〜3分待ってからリロード
DELETE_FAILED になる手動でリソースを変更したためCFnが管理できないコンソールで該当リソースを確認して手動削除後、delete-stack を再実行
ValidationError: Template format errortemplate.yamlのインデントが崩れているYAMLはインデントが厳格。スペース2つ単位でインデントを確認する

まとめ

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

確認項目内容
IaCの体験template.yaml 1ファイルにIAMロール・SG・EC2をコードで定義
一括デプロイcreate-stack コマンド1本でコンソール版と同じ構成を5分で構築
Outputs活用デプロイ後にIPアドレス・SSHコマンドを describe-stacks で自動取得
一括削除delete-stack 1本でリソースの依存関係を自動解決して全削除

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

  • コンソール版では15〜20分かかった作業がコマンド1本(5分)で完了した
  • delete-stack で依存関係を気にせず全リソースを一括削除できた
  • template.yaml をGitで管理することで、環境構成の変更履歴が残せる

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

CloudFormationが裏で何をやっているか、同じ構成をAWSコンソールのみで構築する手順をまとめました。コンソール版ではキーペア・IAMロール・セキュリティグループ・EC2を依存関係順に手動で作成する必要があり、各リソースの役割と繋がりが視覚的に理解できます。


関連記事

-->

コメント