ハンズオンの目的

このハンズオンは、佐賀の皆さんにAWSの楽しさを知ってもらうための特別編です! 「アプリのログを収集し、可視化・通知する」という Observability (O11y / 可観測性) の基本を学びます。 また、意図的に脆弱性を含んだパッケージを導入し、セキュリティツール(Snyk)を使って検知・修復する DevSecOps の基本的な流れを体験します。

このハンズオンの「Saga」は、マイクロサービスの難しい用語(Sagaパターン)ではなく、**私たちの「佐賀」**を指しています!佐賀のエンジニアや学習者の皆さんが、クラウドの運用とセキュリティの一歩を踏み出すためのガイドです。

システムの内部状態を「ログ」「メトリクス」「トレース」で把握できるようにする考え方です。お医者さんの診察に例えると、**「熱がある(異常)」と気づくのが「監視」なら、「なぜ熱が出たのか(原因)」を調べるのが「O11y」**です。複雑なシステムで「どこが悪いか」を素早く突き止めるための地図を作る作業だと考えてください。

「Development(開発)」「Security(セキュリティ)」「Operations(運用)」を組み合わせた言葉です。従来はアプリを作った後にセキュリティチェックを行っていましたが、DevSecOps では開発の早い段階からセキュリティを組み込むことで、弱点を早期に発見・修正します。

ソフトウェアに存在するセキュリティ上の欠陥や弱点のことです。お店の鍵が壊れていたり、壁に穴が開いていたりするイメージです。放置するとデータの漏洩などの被害につながるため、定期的な点検が必要です。

アーキテクチャと使用技術

「Software as a Service」の略で、自分でサーバーを用意してインストールすることなく、ブラウザ経由で利用できるサービスのことです。Mackerel や Snyk などは、クラウド上で提供されているサービスをそのまま利用します。

2つの環境の使い分けに注意!

本ハンズオンでは、2つの異なる Linux 環境を使い分けます。

  1. 開発環境 (Kiro-IDE / Codespaces / ローカルPC): あなたの「作業用 PC」です。ここで設計図(CloudFormation)を書いたり、セキュリティスキャンを実行したりします。
  2. ToDo アプリの EC2: CloudFormation で作成される「サーバー」環境です。アプリが実際に動き、ログが保存される場所です。

Kiro-IDE Remoteなどのツールを利用・連携するために、「AWS Builder ID」が必要となります。 AWS Builder ID は AWS や Amazon.co.jp アカウントとは異なる、個人のための無料アカウントです(クレジットカードの登録なども不要です)。

  1. AWS Builder ID の作成画面 にアクセスします。
  2. 個人のメールアドレスを入力し [次へ] をクリックします。
  3. 表示される「名前」を入力して [次へ] をクリックします。
  4. 入力したメールアドレス宛に届いた認証コードを入力して [認証] します。
  5. パスワードの条件(8〜64文字・大文字と小文字・数値・英数字以外の文字)を満たすパスワードを設定し、[AWS Builder IDを作成] をクリックします。

Kiro-IDE 以外(オプション2〜4)の環境を利用する場合は、AWS リソースを操作するためのアクセスキー(認証情報)が必要です。以下の手順で IAM ユーザーのアクセスキーを発行し、設定してください。

IAM ユーザーの永続的なアクセスキーを発行する本手順は、あくまでも今回のハンズオンを簡単に進めるための限定的な手段です。実際の開発現場や本番運用においては、セキュリティの観点から永続的なアクセスキーの利用は推奨されていません。現在は aws login(AWS IAM Identity Center との統合)や aws sso login などを利用し、一時的な(有効期限のある)認証トークンを取得して利用することが前提・ベストプラクティスとされています。

アクセスキーの発行手順

  1. AWS マネジメントコンソールにログインし、IAM (Identity and Access Management) サービスを開きます。
  2. 左側のメニューから「ユーザー」を選択し、本ハンズオン用のユーザー(例: AdministratorAccess 権限を持つユーザー)をクリックします。
  3. セキュリティ認証情報」タブを選択します。
  4. アクセスキーを作成」ボタンをクリックします。
  5. ユースケースとして「コマンドラインインターフェイス (CLI)」を選択し、確認のチェックを入れて「次へ」をクリックします。
  6. (任意) 説明タグを入力し、「アクセスキーを作成」をクリックします。
  7. 発行された「アクセスキー」と「シークレットアクセスキー」をメモするか、CSVファイルをダウンロードします(※この画面を閉じるとシークレットアクセスキーは二度と確認できません)。

アクセスキーの設定 (aws configure)

ご自身の開発環境のターミナルで以下のコマンドを実行し、先ほど発行したキーを設定します。

aws configure

対話形式で以下のように入力してください。

設定後、以下のコマンドでエラーが出ず、ご自身のアカウント情報が表示されれば設定は完了です。

aws sts get-caller-identity

本ハンズオンでは、以下の4つの方法からお好きな開発環境を選択して進めることができます。

  1. Kiro-IDE Remote を使う場合 (推奨: AWS上に環境を自動構築)
  2. GitHub Codespaces を使う場合 (ブラウザ上で完結)
  3. ローカルの VSCode を使う場合 (自身の端末)
  4. 自身の端末を利用する場合 (ローカル環境)

オプション1: Kiro-IDE Remote を使う場合

AWS 上に簡単に Web 開発環境を構築できる Kiro-IDE Remote を使用します。ローカル PC に環境を構築することなく、また IAM の設定なども自動で行われるため、最も推奨される手順です。

Kiro-IDE の起動

以下のリンク先に用意されている「Deploy」ボタンから、ワンクリックでデプロイが可能です。

Kiro IDE Remote のマニュアルページ

  1. サインイン済みの AWS アカウントがある状態で、マニュアルページ内の [Deploy] ボタンをクリックします。
  2. AWS CloudFormation の「スタックのクイック作成」画面が開きます。
  3. デプロイに必要な以下のパラメータを確認・入力します:
    • UserEmail: 構築完了メールや通知を受け取るご自身のメールアドレスを入力します。
    • Language: OS の言語設定です。JP(日本語)を選択しておくことを推奨します。
    • EnableAdministratorAccess: 重要。本ハンズオンでは CloudFormation などの AWS リソースを作成・操作するため、必ず true に設定してください。これにより、Kiro-IDE のターミナルから AWS コマンドを実行するための権限(IAM ロール)が自動的に付与されます。
  4. 画面最下部の「AWS CloudFormation が IAM リソースを作成する場合があることを承認します。」にチェックを入れます。
  5. [スタックの作成] をクリックし、作成プロセスが完了(ステータスが CREATE_COMPLETE になる)するまで約5〜10分程度待ちます。
    • ※デプロイが開始されると、入力したメールアドレス宛に通知のサブスクリプション確認メールが届きます。「Confirm subscription」をクリックして承認を行ってください。
  6. デプロイが完了すると、「[One Click Gen AI Solutions] Kiro IDE - Deployment completed」というメールが届きます。本文(または CloudFormation の「出力(Outputs)」タブ)から以下の情報を確認します:
    • KiroIDEURL: アクセス用URL
    • Username: ログインユーザー名
    • Password: 初期パスワード
  7. 指定された URL にアクセスし、ユーザー名とパスワードでログインしてください。
  8. ログイン後、ブラウザ上で VS Code ライクなエディタとターミナルが使用できることを確認してください。
    • エディタ: 左側のファイルツリーからファイルを管理し、右側の画面で編集します。
    • ターミナル: 画面下部(またはメニューの Terminal > New Terminal)に表示される Linux シェルです。以降のコマンド操作はここで行います。
    • ※デスクトップ上の Kiro アイコンは最初無効化されています。右クリックで起動を許可(Allow Launching)してから実行してください。
  9. Session Manager プラグインのインストール: Kiro 上の AI チャット機能(Vibe)を開き、「Session Manager プラグインをインストールする」と入力して送信します。AI が提示した手順やコマンドに従って、Session Manager プラグインをインストールしてください。

オプション2: GitHub Codespaces を使う場合

ブラウザ上で完結する開発環境である GitHub Codespaces を利用することもできます。

Codespace の起動とツールのインストール

GitHub Codespaces にアクセスし、任意のテンプレート(Blank template など)から新しい Codespace を作成して起動します。 Codespace が起動したら、ターミナルで以下のコマンドを順番に実行し、必要なツールをインストール・更新します。

  1. AWS CLI (v2) のインストール
    curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
    unzip awscliv2.zip
    sudo ./aws/install
    
  2. AWS SSM Plugin のインストール
    curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" -o "session-manager-plugin.deb"
    sudo dpkg -i session-manager-plugin.deb
    
  3. Python を最新にする
    sudo apt-get update
    sudo apt-get install -y python3 python3-pip
    
  4. Node (npm) を最新にする Codespaces には nvm (Node Version Manager) がプリインストールされているため、それを利用して最新の Node.js をインストールします。
    nvm install node
    nvm use node
    npm install -g npm@latest
    

GitHub Codespaces を使用する場合は、別途 IAM ユーザー(AdministratorAccess 等の権限を持つもの)を作成し、ターミナルで aws configure コマンドを使用してアクセスキーとシークレットアクセスキーを設定する必要があります。

オプション3: ローカルの VSCode を使う場合

ご自身の PC のローカル環境で VSCode を使って進めることも可能です。必要な開発環境が自動で揃うテンプレートリポジトリを活用します。

テンプレートからリポジトリを作成

  1. GitHub にログインします
  2. テンプレートリポジトリ midnight480/aws-handson-template を開きます
  3. Use this templateCreate a new repository をクリックします
  4. リポジトリ名に任意の名前を入力し、Create repository をクリックします

リポジトリのクローンと VSCode での起動

作成したリポジトリをご自身の PC にクローン(ダウンロード)し、VSCode で開きます。

AWS CLI の動作確認

VSCode のターミナルを開き、AWS CLI が利用可能になっていることを確認します。

aws --version

aws-cli/2.x.x のようにバージョンが表示されればOKです。

オプション4: 自身の端末を利用する場合

ご自身の PC (Windows, Mac, Linux など) のローカル環境で進めることも可能です。その場合は、以下のツールがあらかじめインストールされている必要があります。

  1. AWS CLI (v2)
  2. Session Manager プラグイン (SSM Plugin)
  3. Node.js (npm)
    • 後半の Snyk CLI インストールや動作のために必要になります。
    • Node.js 公式サイト などからインストールしてください。
  4. AWS 認証情報の設定
    • ターミナルで aws configure を実行し、AWS へのアクセス権限を持つ IAM ユーザーのアクセスキーを設定してください。
    $ aws configure
    
    Tip: You can deliver temporary credentials to the AWS CLI using your AWS Console session by running the command 'aws login'.
    
    AWS Access Key ID [None]: AK***
    AWS Secret Access Key [None]: ***
    Default region name [None]: ap-northeast-1
    Default output format [None]: json
    
    $ aws sts get-caller-identity
    {
        "UserId": "***",
        "Account": "***",
        "Arn": "arn:aws:iam::***:user/handson-admin"
    }
    

CloudFormationテンプレートの作成と実行

ToDoアプリを動かすためのEC2インスタンスを、CloudFormationを利用して構築します。

AWS上のインフラ環境(サーバーやネットワークなど)を、設計図(テンプレート)としてコード化して自動構築するサービスです。「スタック」とは、その設計図をもとに作られたAWSリソースのグループのことです。手動で一つずつ設定するよりも、早く・正確にインフラを用意できます。

  1. 開発環境のエディタを開き、新規ファイル todo-app-template.yaml を作成します。
    • 作成方法: 左側のファイルエクスプローラーで右クリックし「New File」を選択するか、上部メニューの File > New Text File から作成し、名前を付けて保存します。

もしかしたら、デフォルトで英語になっているかもしれません。Windowsの場合は Ctrl + Space、Macの場合は Control + SpaceCmd + Space などで直接/日本語入力を切り替えられる場合があります。半角・全角キーでの切り替えを行いたい場合一度設定を再起動してください。

  1. 以下の内容をコピーして、作成したファイルに貼り付けて保存します。 (※このテンプレートは EC2 の構築、Python・Flask のセットアップ、および脆弱性のあるパッケージの導入を自動で行います)

YAML はインデント(行頭の空白の数)で設定の階層構造を表現します。コピー&ペーストする際に空白がズレてしまうとエラーになるため、そのままの形で貼り付いているか必ず確認してください。

AWSTemplateFormatVersion: '2010-09-09'
Description: 'ToDo App EC2 Instance for Observability Hands-on'
Resources:
  # --- ① IAMロール: EC2がAWSサービス(SSM, CloudWatch)と連携するための「許可証」---
  InstanceRole:
    Type: 'AWS::IAM::Role'
    Properties:
      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/CloudWatchAgentServerPolicy'

  # --- ② インスタンスプロファイル: IAMロールをEC2に紐づけるための「入れ物」---
  InstanceProfile:
    Type: 'AWS::IAM::InstanceProfile'
    Properties:
      Roles:
        - !Ref InstanceRole

  # --- ③ セキュリティグループ: EC2への通信を制御する「ファイアウォール」---
  InstanceSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupDescription: 'Allow HTTP traffic for ToDo App'
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 8080
          ToPort: 8080
          CidrIp: 0.0.0.0/0

  # --- ④ EC2インスタンス本体: アプリが動くサーバー ---
  ToDoAppInstance:
    Type: 'AWS::EC2::Instance'
    Properties:
      InstanceType: t4g.small
      ImageId: 'resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-arm64'
      IamInstanceProfile: !Ref InstanceProfile
      SecurityGroupIds:
        - !Ref InstanceSecurityGroup
      # --- ⑤ UserData: EC2起動時に自動実行されるセットアップスクリプト ---
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          dnf update -y
          dnf install -y python3-pip
          mkdir -p /opt/todo-app /var/log/todo-app
          cd /opt/todo-app
          # 意図的に古い脆弱性のあるパッケージを導入(ハンズオン用)
          pip3 install Flask==2.0.1 Jinja2==3.0.1 Werkzeug==2.0.1
          cat << 'PYEOF' > app.py
          import logging, time, random
          from flask import Flask, request, render_template_string, redirect, jsonify

          app = Flask(__name__)
          logging.basicConfig(filename='/var/log/todo-app/app.log', level=logging.INFO,
                              format='%(asctime)s %(levelname)s: %(message)s')

          todos = [
              {"task": "AWS CloudWatchを学ぶ", "done": False},
              {"task": "X-Rayのセットアップ", "done": False},
              {"task": "ToDoアプリをデプロイする", "done": True},
          ]

          HTML = """
          <!DOCTYPE html>
          <html lang="ja">
          <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>ToDo App - O11y Hands-on</title>
            <style>
              * { box-sizing: border-box; margin: 0; padding: 0; }
              body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
                     background: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%);
                     color: #f8fafc; min-height: 100vh; padding: 2rem; }
              .container { max-width: 640px; margin: 0 auto; }
              h1 { font-size: 1.8rem; margin-bottom: 0.3rem;
                   background: linear-gradient(to right, #a78bfa, #38bdf8);
                   -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
              .subtitle { color: #94a3b8; margin-bottom: 1.5rem; font-size: 0.9rem; }
              .card { background: rgba(30,41,59,0.7); backdrop-filter: blur(12px);
                      border: 1px solid rgba(255,255,255,0.1); border-radius: 16px;
                      padding: 1.5rem; margin-bottom: 1.5rem; }
              .stats { display: flex; gap: 1rem; margin-bottom: 1rem; }
              .stat { flex: 1; text-align: center; padding: 0.8rem; border-radius: 10px;
                      background: rgba(255,255,255,0.05); }
              .stat-num { font-size: 1.5rem; font-weight: 700; }
              .stat-label { font-size: 0.75rem; color: #94a3b8; }
              .stat-num.total { color: #38bdf8; }
              .stat-num.done { color: #4ade80; }
              .stat-num.pending { color: #fbbf24; }
              form { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
              input[type="text"] { flex: 1; padding: 0.7rem 1rem; border-radius: 8px;
                     border: 1px solid rgba(255,255,255,0.15); background: rgba(15,23,42,0.6);
                     color: white; font-size: 1rem; outline: none; }
              input:focus { border-color: #8b5cf6; box-shadow: 0 0 0 2px rgba(139,92,246,0.3); }
              button, .btn { padding: 0.7rem 1.2rem; border-radius: 8px; border: none;
                     font-size: 0.9rem; font-weight: 600; cursor: pointer; color: white;
                     text-decoration: none; display: inline-block; transition: 0.2s; }
              .btn-add { background: #8b5cf6; }
              .btn-add:hover { background: #7c3aed; }
              .btn-sm { padding: 0.3rem 0.6rem; font-size: 0.75rem; border-radius: 6px; }
              .btn-done { background: #4ade80; color: #0f172a; }
              .btn-undo { background: #fbbf24; color: #0f172a; }
              .btn-del { background: #ef4444; }
              ul { list-style: none; }
              li { display: flex; align-items: center; justify-content: space-between;
                   padding: 0.7rem 0; border-bottom: 1px solid rgba(255,255,255,0.06); }
              li:last-child { border-bottom: none; }
              .task-text { flex: 1; margin: 0 0.8rem; }
              .task-done { text-decoration: line-through; color: #64748b; }
              .actions { display: flex; gap: 0.3rem; }
              .test-section { margin-top: 1rem; padding-top: 1rem;
                              border-top: 1px solid rgba(255,255,255,0.1); }
              .test-section h3 { font-size: 0.85rem; color: #94a3b8; margin-bottom: 0.5rem; }
              .test-btns { display: flex; gap: 0.5rem; flex-wrap: wrap; }
              .btn-warn { background: #f97316; }
              .btn-err { background: #ef4444; }
              .btn-slow { background: #6366f1; }
              .badge { display: inline-block; width: 8px; height: 8px; border-radius: 50%;
                       margin-right: 0.3rem; }
              .badge-done { background: #4ade80; }
              .badge-pending { background: #fbbf24; }
              .flash { background: rgba(139,92,246,0.15); border: 1px solid #8b5cf6;
                       border-radius: 8px; padding: 0.6rem 1rem; margin-bottom: 1rem;
                       font-size: 0.85rem; }
            </style>
          </head>
          <body>
            <div class="container">
              <h1>📝 ToDo App</h1>
              <p class="subtitle">AWS Observability ハンズオン用アプリケーション</p>
              {% if message %}<div class="flash">{{ message }}</div>{% endif %}
              <div class="card">
                <div class="stats">
                  <div class="stat"><div class="stat-num total">{{ total }}</div><div class="stat-label">Total</div></div>
                  <div class="stat"><div class="stat-num done">{{ done_count }}</div><div class="stat-label">Done</div></div>
                  <div class="stat"><div class="stat-num pending">{{ pending }}</div><div class="stat-label">Pending</div></div>
                </div>
                <form action="/" method="POST">
                  <input type="text" name="task" placeholder="新しいタスクを入力..." required>
                  <button type="submit" class="btn btn-add">追加</button>
                </form>
                <ul>
                {% for i, todo in todos %}
                  <li>
                    <span class="badge {{ 'badge-done' if todo.done else 'badge-pending' }}"></span>
                    <span class="task-text {{ 'task-done' if todo.done }}">{{ todo.task }}</span>
                    <div class="actions">
                      {% if todo.done %}
                        <a href="/undo/{{ i }}" class="btn btn-sm btn-undo">↩</a>
                      {% else %}
                        <a href="/done/{{ i }}" class="btn btn-sm btn-done">✓</a>
                      {% endif %}
                      <a href="/delete/{{ i }}" class="btn btn-sm btn-del">✕</a>
                    </div>
                  </li>
                {% endfor %}
                </ul>
              </div>
              <div class="card">
                <div class="test-section">
                  <h3>🔧 Observability テスト(ハンズオン用)</h3>
                  <div class="test-btns">
                    <a href="/error" class="btn btn-sm btn-err">500 Error</a>
                    <a href="/warn" class="btn btn-sm btn-warn">Warning Log</a>
                    <a href="/slow" class="btn btn-sm btn-slow">Slow Response</a>
                    <a href="/health" class="btn btn-sm btn-done" style="color:white">Health Check</a>
                  </div>
                </div>
              </div>
            </div>
          </body>
          </html>
          """

          def render(message=None):
              done_count = sum(1 for t in todos if t["done"])
              return render_template_string(HTML, todos=list(enumerate(todos)),
                  total=len(todos), done_count=done_count, pending=len(todos)-done_count,
                  message=message)

          @app.route('/', methods=['GET'])
          def get_todos():
              app.logger.info("GET / - Fetched ToDos")
              return render()

          @app.route('/', methods=['POST'])
          def add_todo():
              task = request.form.get('task', '').strip()
              if not task:
                  app.logger.error("POST / - Empty task submitted")
                  return render("タスクを入力してください"), 400
              todos.append({"task": task, "done": False})
              app.logger.info(f"POST / - Added ToDo: {task}")
              return redirect('/')

          @app.route('/done/<int:idx>')
          def mark_done(idx):
              if 0 <= idx < len(todos):
                  todos[idx]["done"] = True
                  app.logger.info(f"GET /done/{idx} - Marked done: {todos[idx]['task']}")
              return redirect('/')

          @app.route('/undo/<int:idx>')
          def mark_undo(idx):
              if 0 <= idx < len(todos):
                  todos[idx]["done"] = False
                  app.logger.info(f"GET /undo/{idx} - Marked undone: {todos[idx]['task']}")
              return redirect('/')

          @app.route('/delete/<int:idx>')
          def delete_todo(idx):
              if 0 <= idx < len(todos):
                  removed = todos.pop(idx)
                  app.logger.info(f"GET /delete/{idx} - Deleted: {removed['task']}")
              return redirect('/')

          @app.route('/error')
          def trigger_error():
              app.logger.error("GET /error - Intentional 500 error triggered!")
              return render("⚠️ 500 Internal Server Error をログに記録しました"), 500

          @app.route('/warn')
          def trigger_warn():
              app.logger.warning("GET /warn - Intentional warning triggered!")
              return render("⚠️ Warning をログに記録しました")

          @app.route('/slow')
          def trigger_slow():
              delay = random.uniform(2.0, 5.0)
              app.logger.warning(f"GET /slow - Slow response: {delay:.1f}s")
              time.sleep(delay)
              return render(f"🐢 {delay:.1f}秒の遅延レスポンスを返しました")

          @app.route('/health')
          def health():
              app.logger.info("GET /health - Health check OK")
              return jsonify({"status": "healthy", "todos": len(todos)}), 200

          if __name__ == '__main__':
              app.run(host='0.0.0.0', port=8080)
          PYEOF
          nohup python3 app.py > /dev/null 2>&1 &

# --- ⑥ Outputs: スタック作成後に表示される情報 ---
Outputs:
  AppURL:
    Value: !Sub 'http://${ToDoAppInstance.PublicDnsName}:8080'
    Description: ToDo App Access URL
  InstanceId:
    Value: !Ref ToDoAppInstance
    Description: EC2 Instance ID

上記のテンプレートは、以下の6つのパーツで構成されています。

  1. InstanceRole (IAMロール): EC2 が SSM(リモート接続)や CloudWatch(ログ送信)を使うための「許可証」です。人間でいう「社員証」のようなもので、これがないと EC2 は他の AWS サービスと連携できません。
  2. InstanceProfile: IAM ロールを EC2 に渡すための「入れ物」です。EC2 にロールを直接付けることはできないため、この仲介役が必要です。
  3. InstanceSecurityGroup: EC2 への通信を制御する「ファイアウォール(防火壁)」です。ここでは TCP ポート 8080 番への通信を全 IP アドレス(0.0.0.0/0)から許可しています。
  4. ToDoAppInstance: 実際の EC2 サーバー本体です。t4g.small は Graviton プロセッサを搭載したコストパフォーマンスに優れた小さなサーバーサイズです。ImageId は OS のテンプレート(Amazon Linux 2023)を指定しています。
  5. UserData: EC2 が初めて起動した時に自動実行されるスクリプトです。Python や Flask のインストール、アプリのコード配置、アプリの起動までを自動で行います。
  6. Outputs: スタック作成完了後に表示される情報です。アプリの URL やインスタンス ID を確認できます。
  1. 開発環境のターミナルで以下のコマンドを実行し、ファイルからCloudFormationスタックを作成します。
aws cloudformation create-stack \
  --stack-name saga-o11y-handson \
  --template-body file://todo-app-template.yaml \
  --capabilities CAPABILITY_IAM
  1. スタックの構築完了と出力結果を確認します。AWS マネジメントコンソールの CloudFormation 画面から saga-o11y-handson のステータス(CREATE_COMPLETE)と「出力 (Outputs)」タブを確認することもできますが、ターミナルから以下のコマンドで確認することも可能です。進行状況を待機するコマンド(構築が完了するまで待機します。数分後にプロンプトが戻れば完了です):
    aws cloudformation wait stack-create-complete --stack-name saga-o11y-handson
    
    出力結果を確認するコマンド(完了後に実行します):
    aws cloudformation describe-stacks \
      --stack-name saga-o11y-handson \
      --query 'Stacks[0].Outputs'
    
    • 出力結果に表示された AppURL の値(http://ec2-...)をコピーしてブラウザで開き、ToDo アプリの画面が表示されるか確認してください。
  2. 表示された ToDo アプリの画面にある入力フォームから、新しいタスク(例: CloudWatchのログを確認する)を入力し、「追加」ボタンを押して登録できるかテストしてみましょう。
  3. この ToDo アプリは背景でログファイル(例: /var/log/todo-app/app.log)にアクセスログやエラーを出力していることを確認しておきましょう。

まずはAWSのネイティブサービスである CloudWatch を利用してアプリのログを収集・可視化します。

CloudWatch Agent のインストールと設定

EC2 インスタンスにログイン(※マネジメントコンソールから「SSMセッションマネージャー」を使用すると、ブラウザだけで安全・簡単にLinuxサーバへ接続できます)し、CloudWatch Agentをインストールします。

SSH キーの設定なしに EC2 へ安全に接続できる便利な機能です。以下のいずれかの方法で接続してください。方法A: 開発環境のターミナルから接続する (推奨)Kiro-IDE や Codespaces などのターミナルから、以下のコマンドを実行します。aws ssm start-session --target <インスタンスID><インスタンスID>i-0123456789abcdef0 のような文字列です。事前の CloudFormation 出力結果で表示された InstanceId の値に置き換えて実行してください。方法B: AWS マネジメントコンソール(ブラウザ)から接続する

  1. コンソール上部の検索バーで「EC2」と入力して EC2 ダッシュボードを開きます。
  2. 左メニューの「インスタンス」をクリックし、作成されたインスタンスのチェックボックスを選択して、画面上部の「接続」ボタンをクリックします。
  3. 「セッションマネージャー」タブを選択し、「接続」をクリックします。

※ 接続できない場合は、EC2 インスタンスが「実行中 (running)」であること、および CloudFormation テンプレートで IAM ロールが正しく設定されていることを確認してください(方法Aの場合は Session Manager プラグインがインストール済みであることも確認してください)。

接続直後は sh-5.2$ のようなプロンプト(入力待ち表示)になっています。bash と入力して Enter を押すと、より使いやすいシェルに切り替わります。また、sudo su - と入力すると管理者(root)ユーザーに切り替わり、sudo を毎回付ける必要がなくなります。

以下のコマンドの先頭にある sudo は、「管理者(スーパーユーザー)権限で実行する」という意味です。ソフトウェアのインストールなどシステム全体に関わる操作を行う際に必要になります。また、yumdnf は、ソフトウェアをダウンロード・インストールするためのパッケージ管理ツールです。

sudo yum install amazon-cloudwatch-agent -y

ログを転送するための設定ファイル(config.json)を作成し、起動します。

  1. まず、開発環境のエディタconfig.json という名前の新規ファイルを作成し、以下の JSON 内容を貼り付けて保存します。
  2. 保存した内容をコピーします。
  3. EC2 インスタンスのターミナル(セッションマネージャー)で sudo nano config.json を実行します。
  4. コピーした内容をターミナルに貼り付け(Windows: Ctrl + Shift + V / Mac: Cmd + V)て保存します。

これが、GUI を活用しつつサーバー上のファイルを書き換える最も確実な方法です!

EC2 のターミナルで以下の設定を作成します。

{
  "logs": {
    "logs_collected": {
      "files": {
        "collect_list": [
          {
            "file_path": "/var/log/todo-app/app.log",
            "log_group_name": "todo-app-logs",
            "log_stream_name": "{instance_id}"
          }
        ]
      }
    }
  }
}

エディタを使わずに、以下のワンライナーコマンドをターミナルにコピー&ペーストして実行するだけで、一発でファイルを作成することもできます。

echo '{"logs": {"logs_collected": {"files": {"collect_list": [{"file_path": "/var/log/todo-app/app.log", "log_group_name": "todo-app-logs", "log_stream_name": "{instance_id}"}]}}}}' > config.json
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -s -c file:config.json

このコマンドは CloudWatch Agent を設定ファイルを読み込んで起動するものです。

通知用 SNS トピックの作成とメール購読 (CLI)

まずは、アラーム発生時にメールを通知するための SNS トピックを作成します。開発環境のターミナルから以下のコマンドを入力します。 (最後に設定する自分のメールアドレス your_email@example.com は、受信可能なものに変更して実行してください)

AWS が提供する通知サービスです。「トピック」という通知チャンネルを作り、そこにメールアドレスなどの「購読者(サブスクライバー)」を登録しておくと、トピックにメッセージが送られた時に自動で通知が届きます。LINEグループのようなイメージです。

# SNS トピックの作成
TOPIC_ARN=$(aws sns create-topic --name todo-app-alerts --query 'TopicArn' --output text)

# メールアドレスの登録(★各自のメールアドレスに変更)
aws sns subscribe \
  --topic-arn $TOPIC_ARN \
  --protocol email \
  --notification-endpoint "your_email@example.com"

「Amazon Resource Name」の略で、AWS 上のあらゆるリソースを一意に識別するための住所のようなものです。arn:aws:sns:ap-northeast-1:123456789012:todo-app-alerts のような形式をしています。

上記のコマンドでは、SNS トピック作成時に出力される識別子 (ARN) を TOPIC_ARN という「変数」に一時保存し、直後のメール登録コマンドで $TOPIC_ARN として再利用しています。また、行末の \ (バックスラッシュまたは円記号) は、「次の行も同じコマンドとして続く」ことを意味しています。長いコマンドを見やすく改行するための工夫です。

※ コマンド実行後、入力したメールアドレス宛に「AWS Notification - Subscription Confirmation」というメールが届きます。本文中の Confirm subscription を必ずクリックして承認してください。

メトリクスフィルターとアラームの作成 (CLI)

アプリのログログループ(todo-app-logs)の中から、ERROR という文字列を含んだ行をカウントし、1分間に1回でも発生したら先ほどの SNS(メール)経由で通知する設定を行います。

この2つを組み合わせることで、「ログに ERROR が出たらメールで知らせる」という自動通知が実現できます。

# ロググループが存在することを保証するために作成(既に存在する場合は作成済みのエラーが出ますが無視して構いません)
aws logs create-log-group --log-group-name todo-app-logs || true

# メトリクスフィルター(ERROR の検知)を作成
aws logs put-metric-filter \
  --log-group-name "todo-app-logs" \
  --filter-name "ErrorFilter" \
  --filter-pattern "ERROR" \
  --metric-transformations \
      metricName=ErrorCount,metricNamespace=ToDoAppMetrics,metricValue=1,defaultValue=0

# アラーム(1分間に1回以上 ERROR が出たら通知)の作成
aws cloudwatch put-metric-alarm \
  --alarm-name "ToDoAppErrorAlarm" \
  --metric-name "ErrorCount" \
  --namespace "ToDoAppMetrics" \
  --statistic Sum \
  --period 60 \
  --evaluation-periods 1 \
  --threshold 1 \
  --comparison-operator GreaterThanOrEqualToThreshold \
  --alarm-actions $TOPIC_ARN \
  --treat-missing-data notBreaching

マネジメントコンソールでの確認とエラー発生テスト

CLI で一気に立ち上げた設定を、実際に画面から確認&テストしてみましょう。

  1. AWSマネジメントコンソール にログインし、CloudWatch の画面を開きます。
  2. 左メニューの ログ > ロググループ から todo-app-logs を開くと、EC2 が送信したアプリのログ(ログストリーム)を見ることができます。
  3. 左メニューの アラーム > すべてのアラーム を開くと、「ToDoAppErrorAlarm」が作成されていることを確認できます(最初は「データ不足」や「OK」になっています)。
  4. 開発環境のブラウザまたは別タブから、ToDo アプリの エラー発生用 URL にアクセスし、意図的にエラーログを書き込みます。
    • アクセス先例: http://:8080/error
  5. 1〜2分後、CloudWatchアラームの画面をリロードし、状態が「アラーム (In alarm)」という赤い状態に変わったことを確認してください。
  6. 同時に、ご自身のメールアドレス宛に CloudWatch からの警告通知メールが届いていれば、一連のログの検知&通知フローは成功です!

さらに、外部の SaaS 型監視ツール(Mackerel など)を利用してみましょう。 ※本シナリオでは Mackerel の例を記載します。

Mackerel エージェントの導入

  1. Mackerel にサインアップし、オーガニゼーションを作成します。
  2. 「新規ホストの登録」画面から、インストールコマンド(APIキーが含まれています)をコピーします。
  3. EC2 インスタンスのターミナル(セッションマネージャー) にて、コピーしたコマンドを実行します。
# Mackerelのインストールと起動(ダミー例。実際のコマンドを使用)
curl -fsSL https://mackerel.io/file/script/setup-all-yum-v2.sh | MACKEREL_APIKEY='YOUR_API_KEY' sh

アプリケーション監視の設定(ログ監視)

ダッシュボードへのリソース表示とあわせて、アプリケションログの監視(チェック監視)を追加してみましょう。

  1. Mackerel のダッシュボードに EC2 の CPU やメモリのリソースが表示されていることを確認します。
  2. ログ監視を行うために、Mackerel公式のチェックプラグイン集をインストールします。EC2 のターミナルで以下のコマンドを実行します。
sudo dnf install -y mackerel-check-plugins
  1. Mackerel エージェントの設定ファイルを開きます。
sudo nano /etc/mackerel-agent/mackerel-agent.conf

Linuxのテキストエディタといえば vivim が定番ですが、今回使用している環境(Ubuntuベースのブラウザターミナルなど)では、キーボードの特殊な操作がうまく受け付けられないケースがあるため、本ハンズオンではより確実に操作できる nano を採用しています。

nano は Linux で使えるシンプルなテキストエディタです。画面下部にショートカットキーの一覧が表示されます(^ は Windows/Mac ともに Ctrl キーを意味します。Mac の Cmd ではない点に注意してください)。

  1. 先ほどのアプリのログ (/var/log/todo-app/app.log) から、特定のエラー(ERROR)や警告(WARNING)の文字列を検知してアラートを発火させるため、ファイルの末尾に以下の設定を追記し、保存して閉じます。(※ nano エディタの場合は Ctrl+O -> Enter で保存し、Ctrl+X で終了します。Macでも Cmd ではなく Ctrl キーを使用します)
[plugin.checks.todo-app-log]
command = ["check-log", "--file", "/var/log/todo-app/app.log", "--pattern", "ERROR|WARNING"]

エディタの操作に自信がない、またはキーボードがうまく反応しない場合は、以下の echo を使ったワンライナーコマンド(1行のコマンド)を貼り付けて実行するだけで、ファイルの末尾に設定を追記できます。

echo -e '\n[plugin.checks.todo-app-log]\ncommand = ["check-log", "--file", "/var/log/todo-app/app.log", "--pattern", "ERROR|WARNING"]' | sudo tee -a /etc/mackerel-agent/mackerel-agent.conf
  1. 設定を反映させるために Mackerel エージェントを再起動します。
sudo systemctl restart mackerel-agent

systemctl は Linux のサービス(バックグラウンドで動くプログラム)を管理するコマンドです。

設定ファイルを変更した後は restart で再起動しないと変更が反映されません。

  1. アプリの エラー発生用 URLhttp://:8080/error)へ何度かアクセスし、意図的にエラーログを発生させます。
  2. Mackerel のダッシュボードの左メニュー「Alerts (アラート)」から、指定したログ監視アラート(todo-app-log)が CRITICAL として検知・発報していることを確認します。

開発と運用(DevSecOps)において、使用しているライブラリ(パッケージ)に脆弱性がないかチェックすることは重要です。 ここではあえて脆弱な古いパッケージを導入し、セキュリティツールで検知・修復する流れを体験します。

このセクションでは、学習のために意図的に古い脆弱なバージョンをインストールします。実際の開発現場では、常に最新の安定版を使用し、古いバージョンをそのまま使い続けることは絶対に避けてください!

脆弱なパッケージの導入

開発環境 または EC2 のターミナル上で、作業用のソースコードディレクトリ(フォルダ)に移動し、わざと古いパッケージを指定してインストールします。

Linux 環境で特定のフォルダ(ディレクトリ)に移動する場合は、cd (change directory) コマンドを使用します。今回の EC2 の例では、アプリの配置場所である /opt/todo-app に移動する必要があります(例: cd /opt/todo-app を実行)。

# Pythonの例
# あえて脆弱性がある古いバージョン(2.19.1)を指定します
python3 -m pip install requests==2.19.1
python3 -m pip freeze > requirements.txt

セキュリティスキャンの実行(例: Snyk)

「Snyk(スニーク)」は、開発中にセキュリティ上の問題を自動で見つけてくれる便利なツールです。佐賀の現場でも、こういったツールを導入して安全な開発を行うことが推奨されます。

  1. Snyk のアカウントを作成し、CLI メニューの手順に従って開発環境に Snyk CLI をインストールします。
  2. 認証を行います。

「Node Package Manager」の略で、JavaScript / TypeScript のライブラリ(パッケージ)を管理するツールです。Python における pip と同じ役割です。sudo npm install -g snyk-g は「グローバル(システム全体)にインストールする」という意味で、システム共通のディレクトリにインストールされるため sudo による管理者権限が必要になります。

Snyk CLI を動かすには Node.js が必要です。Kiro-IDE や Codespaces 環境には通常プリインストールされていますが、node --version を実行してバージョンが表示されない場合は、以下のコマンド等で Node.js をインストールしてください。

# Amazon Linux 2023 の場合
sudo dnf install -y nodejs
sudo npm install -g snyk

クラウド開発環境や EC2 上で単に snyk auth を実行すると、ブラウザからのコールバック通信(localhost へのリダイレクト)が失敗して認証が完了しない場合があります。そのため、以下の手順で手動でトークンを取得して認証を行ってください。

  1. ブラウザで Snyk にログイン します。
  2. 左下の自分のアカウントアイコンをクリックし、「Account settings」を開きます。
  3. General」メニューの中央付近にある「Auth Token」セクションで、Click to show を押して表示された長いトークン文字列をコピーします。
  4. ターミナルに戻り、コピーしたトークンを使って以下のコマンドを実行します。 :

snyk auth <コピーしたトークン文字列>

Your account has been authenticated. Snyk is now ready to be used. と表示されれば認証成功です。

ローカルの環境であれば、単に snyk auth と実行するだけでブラウザが開き、自動的に認証を完了させることができます。

  1. プロジェクトディレクトリでスキャンを実行します。
# Amazon Linux 環境などでは python ではなく python3 コマンドを使うため、--command オプションで指定します
snyk test --command=python3

snyk test は、カレントディレクトリ(現在いるフォルダ)にある requirements.txtpackage.json などの依存関係ファイルを読み取り、使用しているライブラリに既知の脆弱性がないかをチェックします。結果には脆弱性の深刻度(Low / Medium / High / Critical)、影響を受けるパッケージ名、推奨される修正バージョンなどが表示されます。

  1. 古い requests パッケージに「High」や「Critical」な脆弱性が複数見つかることを確認してください。

修復 (Remediation) 作業

出力されたアドバイスに従い、安全なバージョンにアップデートします。

# 提案された安全なバージョンや最新バージョンへアップデート
python3 -m pip install requests --upgrade
python3 -m pip freeze > requirements.txt

再度 snyk test を実行し、脆弱性が「0」になったことを確認します。

ハンズオンで使用した AWS リソースや、外部サービスのデータは、課金や不要なアラートの原因になるため忘れずに削除しましょう。

CloudFormationスタックの削除

開発環境 または AWS マネジメントコンソールから、最初に作成した CloudFormation スタックを削除します。

aws cloudformation delete-stack --stack-name saga-o11y-handson

delete-stack は、指定したスタック名に紐づくすべての AWS リソース(EC2、セキュリティグループ、IAM ロールなど)をまとめて削除します。手動で一つずつ消す必要がないのが CloudFormation の大きなメリットです。

delete-stack コマンドは即座に完了しますが、実際のリソース削除には数分かかります。以下のいずれかの方法で削除完了を確認してください。

aws cloudformation describe-stacks --stack-name saga-o11y-handson

ステータスが DELETE_FAILED になった場合は、CloudFormation の「イベント」タブでエラー原因を確認してください。手動で追加したリソース(例: セキュリティグループのルールなど)が原因で削除できないことがあります。その場合は、該当リソースを手動で削除してから再度 delete-stack を実行してください。

手動で作成したリソースの削除

CloudFormation で管理されていない、CLI で手動作成したリソースも忘れずに削除しましょう。

# SNS トピックの削除(TOPIC_ARN 変数が残っている場合)
aws sns delete-topic --topic-arn $TOPIC_ARN

# CloudWatch アラームの削除
aws cloudwatch delete-alarms --alarm-names "ToDoAppErrorAlarm"

# メトリクスフィルターの削除
aws logs delete-metric-filter --log-group-name "todo-app-logs" --filter-name "ErrorFilter"

# ロググループの削除
aws logs delete-log-group --log-group-name "todo-app-logs"

ターミナルを閉じると変数は消えてしまいます。その場合は以下のコマンドで ARN を確認できます。

aws sns list-topics

環境のお片付け

ハンズオン用に構築した EC2 インスタンス等の CloudFormation スタックを削除します。ターミナルで以下のコマンドを実行してください。

aws cloudformation delete-stack --stack-name saga-o11y-handson

# 削除が完了するまで待機する場合(数分かかります)
aws cloudformation wait stack-delete-complete --stack-name saga-o11y-handson

外部サービスの後処理

以上で本ハンズオンは終了です。お疲れ様でした!