MENU

問い合わせ


    【Laravel+Vue.jsで作る管理画面をECSで動かす 第12回】運用編:ログ・監視・スケーリング・切り戻し

    連載の最終回です。ここまでで、管理画面を ECS で動かし、GitHub Actions から自動でデプロイできるようになりました。最後に、動かし続けるための仕組みとして、アラーム(監視)、オートスケーリング(台数の自動調整)、ECS Exec(稼働中のコンテナでの調査)、失敗したデプロイの自動の切り戻しを加えます。あわせて、12回の内容と、本番運用の前に残っている作業、AWS の費用の構造をまとめます。

    「システムは完成したけれど、公開した後に何かあったとき、誰がどうやって気づいて、どう戻すのかが決まっていない…」

    結論から言うと、運用の仕組みは「利用者より先に気づくアラーム」「負荷に合わせて台数を変えるオートスケーリング」「壊れたデプロイを自動で戻すサーキットブレーカー」「調べ方と戻し方を書いた手順書」の4点セットで考えます。コードで作れるのは前の3つで、4つ目の手順書と「誰がアラームを受けるか」は、発注者と開発会社で決める運用のルールです。

    前回(第11回:GitHub Actions で CI/CD)では、OIDC を使ったテストとデプロイの自動化を作りました。

    これまでと同じく、執筆環境には AWS アカウントがないため terraform apply は行っておらず、アラームの通知や ECS Exec、切り戻しを AWS 上で試してはいません。terraform validate と AWS に接続しない plan、ローカルの Docker でのアプリ側の動作確認までを行っています。

    目次

    運用のために追加するもの

    ファイル内容
    infra/terraform/monitoring.tfSNS(メール通知)、アラーム10個、ログからのメトリクス、よく使うログ検索の保存
    infra/terraform/autoscaling.tfweb サービスのオートスケーリング(CPU の目標追跡)
    infra/terraform/ecs_{web,worker,scheduler}.tfデプロイサーキットブレーカー、ECS Exec の有効化
    infra/terraform/iam.tfECS Exec のためのタスクロールの権限
    routes/console.phpスケジューラの heartbeat(5分ごとのログ)
    app/Console/Commands/CreateUser.php最初の管理者ユーザーを作るコマンド
    .github/workflows/deploy.yml切り戻しが起きたらワークフローを失敗にする確認ステップ
    docs/runbook.mdアラームを受けたときの確認・調査・切り戻しの手順書

    アラーム:利用者より先に気づく

    何を見張るか

    アラームは、SNS(=AWS の通知サービス)のトピックに送り、メールで受け取ります。見張る対象は、画面・裏の処理・アプリ・DB の4つに分けました。

    対象アラーム気づけること
    画面正常な web タスクが 0 台画面が完全に止まった
    画面ALB の 5xx(ALB 自身・アプリ)エラー画面が出ている
    画面web の CPU が高い状態が続く台数の上限に達して遅くなっている
    裏の処理いちばん古いジョブが10分以上処理されないworker が止まっている
    裏の処理DLQ にメッセージがある処理できなかったジョブがある(第10回)
    裏の処理スケジューラの heartbeat が15分出ない締め処理などの定期実行が止まっている
    アプリLaravel の ERROR 以上のログが増えた例外や外部サービスの失敗
    DBRDS の CPU、空きストレージ重いクエリ、容量不足

    infra/terraform/monitoring.tf(worker の停止を検知するアラームの抜粋)

    # いちばん古いジョブが 10 分以上処理されていない(worker が止まっている・追いついていない)
    resource "aws_cloudwatch_metric_alarm" "jobs_backlog" {
      alarm_name          = "${local.name_prefix}-jobs-backlog"
      alarm_description   = "ジョブのキューに 10 分以上処理されていないメッセージがある。worker サービスの状態とログ(/ecs/${local.name_prefix}/worker)を確認する"
      namespace           = "AWS/SQS"
      metric_name         = "ApproximateAgeOfOldestMessage"
      dimensions          = { QueueName = aws_sqs_queue.jobs.name }
      statistic           = "Maximum"
      period              = 300
      evaluation_periods  = 2
      threshold           = 600
      comparison_operator = "GreaterThanOrEqualToThreshold"
      treat_missing_data  = "notBreaching"
      alarm_actions       = local.alarm_actions
      ok_actions          = local.alarm_actions
    }

    キューのメッセージの「件数」ではなく「いちばん古いメッセージの経過時間」を見ているのがポイントです。件数は月初の締め処理で一時的に増えるのが普通ですが、10分以上手つかずのジョブがあるなら、worker が止まっているか、明らかに追いついていません。alarm_description には「最初に見るところ」を書いておき、通知を受けた人がすぐ動けるようにしました。

    treat_missing_data(データが無いときの扱い)も重要です。5xx の件数はアクセスが無ければデータ自体が無いので「正常」、正常なタスクの数はデータが来ないこと自体が異常なので「異常」として扱います。

    📰 出典:Amazon CloudWatch でのアラームの使用(Amazon CloudWatch ユーザーガイド)

    スケジューラの停止は heartbeat で気づく

    第10回で「裏で動く処理は止まっても誰も気づかない」という課題を挙げました。worker はキューの滞留で気づけますが、スケジューラは止まっても何も溜まりません。そこで、5分ごとにログを1行出す heartbeat(=生存確認の合図)を加え、それが途絶えたらアラームにしました。

    routes/console.php(追加部分)

    // 5分ごと:スケジューラが動いていることを示すログ(第12回)
    // CloudWatch Logs のメトリクスフィルターで数え、一定時間出なければアラームを出す(infra/terraform/monitoring.tf)
    Schedule::call(function (): void {
        Log::info('scheduler heartbeat');
    })->everyFiveMinutes()->name('scheduler-heartbeat');

    infra/terraform/monitoring.tf(heartbeat を数えるメトリクスフィルターとアラームの抜粋)

    resource "aws_cloudwatch_log_metric_filter" "scheduler_heartbeat" {
      name           = "${local.name_prefix}-scheduler-heartbeat"
      log_group_name = aws_cloudwatch_log_group.ecs["scheduler"].name
      pattern        = "\"scheduler heartbeat\""
    
      metric_transformation {
        name      = "SchedulerHeartbeat"
        namespace = "${var.project}/${var.environment}"
        value     = "1"
      }
    }
    
    # 15 分間 heartbeat が無い(スケジューラが止まっている)
    resource "aws_cloudwatch_metric_alarm" "scheduler_stopped" {
      alarm_name          = "${local.name_prefix}-scheduler-stopped"
      namespace           = "${var.project}/${var.environment}"
      metric_name         = aws_cloudwatch_log_metric_filter.scheduler_heartbeat.metric_transformation[0].name
      statistic           = "Sum"
      period              = 900
      evaluation_periods  = 1
      threshold           = 1
      comparison_operator = "LessThanThreshold"
      treat_missing_data  = "breaching" # ログが 1 件も無い=止まっている
      # (alarm_description と通知先は省略)
    }

    Laravel の ERROR 以上のログも、同じメトリクスフィルターの仕組みで数えています(LOG_CHANNEL=stderr のログは production.ERROR: のような形で出るため、その文字列で数えます)。

    ログは Logs Insights で探す

    ログは第9回から CloudWatch Logs に集めています(保持期間は既定30日)。障害時に毎回検索の書き方を考えなくて済むよう、よく使う検索を Logs Insights(=ログを SQL に似た書き方で検索・集計する機能)の保存済みクエリとして Terraform で登録しました。

    infra/terraform/monitoring.tf(保存済みクエリの抜粋)

    resource "aws_cloudwatch_query_definition" "errors" {
      name            = "${local.name_prefix}/errors"
      log_group_names = [for k in ["web", "worker", "scheduler"] : aws_cloudwatch_log_group.ecs[k].name]
      query_string    = <<-EOT
        fields @timestamp, @logStream, @message
        | filter @message like /production\.(ERROR|CRITICAL|ALERT|EMERGENCY)/
        | sort @timestamp desc
        | limit 100
      EOT
    }

    このほか、nginx のアクセスログから 5xx と1秒以上かかったリクエストを探すクエリも登録しています。

    📰 出典:CloudWatch Logs Insights を使用したログデータの分析(Amazon CloudWatch Logs ユーザーガイド)

    オートスケーリング:CPU に合わせて web の台数を変える

    infra/terraform/autoscaling.tf(抜粋)

    resource "aws_appautoscaling_target" "web" {
      service_namespace  = "ecs"
      resource_id        = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.web.name}"
      scalable_dimension = "ecs:service:DesiredCount"
      min_capacity       = var.web_min_count
      max_capacity       = var.web_max_count
    }
    
    resource "aws_appautoscaling_policy" "web_cpu" {
      name               = "${local.name_prefix}-web-cpu"
      policy_type        = "TargetTrackingScaling"
      service_namespace  = aws_appautoscaling_target.web.service_namespace
      resource_id        = aws_appautoscaling_target.web.resource_id
      scalable_dimension = aws_appautoscaling_target.web.scalable_dimension
    
      target_tracking_scaling_policy_configuration {
        predefined_metric_specification {
          predefined_metric_type = "ECSServiceAverageCPUUtilization"
        }
        target_value       = var.web_cpu_target
        scale_out_cooldown = 60
        scale_in_cooldown  = 300
      }
    }

    目標追跡(=指標が目標値になるように台数を自動で増減する方式)で、web の平均 CPU が 60% 前後になるように、2〜4台の間で台数を変えます。増やすときは早く、減らすときはゆっくりにして、増減を繰り返さないようにしています。台数はオートスケーリングが変えるので、web サービスの lifecycle の ignore_changes に desired_count を追加しました。

    web_max_count は「費用の上限」でもあります。上限に達しても CPU が高いままなら、前述の CPU のアラームで気づけます。worker も、キューの滞留に合わせて台数を変える設定にできますが、この規模では1台で足りるため、連載では台数を固定にしています。

    📰 出典:Amazon ECS サービスを自動的にスケールする(Amazon ECS デベロッパーガイド)

    失敗したデプロイを自動で戻す(サーキットブレーカー)

    infra/terraform/ecs_web.tf(サービスに追加。worker・scheduler も同じ)

      # 第12回:新しいタスクが起動しない・ヘルスチェックに通らないデプロイを自動で止め、直前の正常なリビジョンに戻す
      deployment_circuit_breaker {
        enable   = true
        rollback = true
      }
    
      # 第12回:ECS Exec(稼働中のコンテナで php artisan などを実行する)を使えるようにする
      enable_execute_command = var.enable_execute_command

    デプロイサーキットブレーカーは、新しいタスクが起動に失敗し続けたり、ALB のヘルスチェックに通らなかったりしたときに、デプロイを失敗として止め、直前の正常なリビジョンに戻す機能です。壊れたイメージを出してしまっても、画面が止まり続けることを防げます。

    📰 出典:Amazon ECS デプロイサーキットブレーカーが障害を検出する方法(Amazon ECS デベロッパーガイド)

    注意したいのは、自動で戻ったあとのサービスは「古いリビジョンで安定している」ことです。デプロイのワークフローから見ると安定を待つステップが成功してしまう可能性があるため、最後に「実際に動いているイメージが今回のタグか」を確かめるステップを加えました。

    .github/workflows/deploy.yml(追加したステップ)

          # 第12回:サーキットブレーカーが前のリビジョンに戻した場合も「サービスは安定」に見えるため、
          # 各サービスで実際に動いているイメージが今回のタグかを確認し、違えば失敗にする
          - name: Verify deployed images
            env:
              CLUSTER: ${{ vars.ECS_CLUSTER }}
            run: |
              for service in web worker scheduler; do
                td=$(aws ecs describe-services --cluster "$CLUSTER" --services "$service" \
                  --query "services[0].deployments[?status=='PRIMARY'].taskDefinition | [0]" --output text)
                images=$(aws ecs describe-task-definition --task-definition "$td" \
                  --query 'taskDefinition.containerDefinitions[].image' --output text)
                echo "$service: $td $images"
                for image in $images; do
                  if [ "${image##*:}" != "$IMAGE_TAG" ]; then
                    echo "::error::$service is not running $IMAGE_TAG (rolled back?)"
                    exit 1
                  fi
                done
              done

    サーキットブレーカーが戻せるのは「起動しない」「ヘルスチェックに通らない」壊れ方だけです。画面は表示されるが計算が間違っている、といった不具合は、人が判断して前のリビジョンに戻します。その手順(aws ecs update-service で前のリビジョンを指定する、または revert してデプロイし直す)は docs/runbook.md に書きました。マイグレーションは自動では戻らないため、第11回で書いたとおり「古いコードでも動く」形でマイグレーションを書くことが、切り戻しやすさにつながります。

    ECS Exec:稼働中のコンテナで調べる・最初の管理者を作る

    ECS Exec は、稼働中のコンテナで php artisan などのコマンドを実行できる機能です。サーバーに SSH で入る代わりの手段で、タスクロールに SSM のチャネルの権限(ssmmessages:*)を与え、サービスで有効にします。使う人には別途 ecs:ExecuteCommand の IAM 権限が必要です。

    第10回で「最初の管理者ユーザーは ECS Exec などで作る」と書きました。本番イメージにはシーダー用のパッケージが入っていないため、ユーザーを作るコマンドを用意しました。

    app/Console/Commands/CreateUser.php(抜粋)

    protected $signature = 'users:create
        {email : メールアドレス(ログイン ID)}
        {name : 表示名}
        {--role=viewer : admin / staff / viewer}';
    
    public function handle(): int
    {
        $password = (string) $this->secret('パスワード(8文字以上)');
        $confirmation = (string) $this->secret('パスワード(確認)');
    
        // (Validator でメールアドレスの重複・ロール・パスワードの長さと一致を確認。失敗したらエラーを表示して終了)
    
        $user = new User([
            'name' => $this->argument('name'),
            'email' => $this->argument('email'),
            'password' => $password,
        ]);
        // role は $fillable に入れていない(第3回)ので、個別に代入する
        $user->role = Role::from((string) $this->option('role'));
        // 管理者が作るユーザーなので、メールアドレスの確認は済みとして扱う
        $user->email_verified_at = now();
        $user->save();
    
        $this->info("ユーザーを作成しました:{$user->email}({$user->role->label()})");
    
        return self::SUCCESS;
    }

    パスワードはコマンドの引数にせず、対話で入力します。引数にすると、端末の履歴や ECS Exec のセッションの記録に残るおそれがあるためです。

    aws ecs execute-command --cluster admin-stg --task <タスク ID> --container php --interactive \
      --command "php artisan users:create admin@example.com 管理者 --role=admin"

    📰 出典:ECS Exec を使用して Amazon ECS コンテナをモニタリングする(Amazon ECS デベロッパーガイド)

    ECS Exec は本番のデータを直接変更できる強い手段です。使える人を IAM で限定し、誰がいつ何を実行したかを記録する運用にします。

    動作確認の方法

    筆者の環境で確認したことは次のとおりです。

    • php artisan test は 71件すべて成功(ユーザー作成コマンドの正常系・異常系、heartbeat のスケジュールの3件を追加)
    • 本番用イメージを compose.prod.yaml で起動し、php artisan users:create で管理者を作成 → そのユーザーでログインして請求の一覧が表示される。php artisan about で本番設定(Debug Mode OFF など)を確認
    • scheduler のコンテナのログに、5分ごとに production.INFO: scheduler heartbeat が出る。Log::error() のログが production.ERROR: の形で出る(メトリクスフィルターの条件と一致)
    • nginx のアクセスログの実際の行で、保存済みクエリの解析用の正規表現が各項目を取り出せることを確認
    • Terraform は fmt -check・validate が成功。AWS に接続しない plan で Plan: 91 to add(第11回の70件に、SNS とメール購読・アラーム10個・メトリクスフィルター4つ・保存済みクエリ2つ・オートスケーリング2つ・ECS Exec の権限の21件)。サービスにサーキットブレーカー(enable = true・rollback = true)と enable_execute_command = true が入る
    • デプロイのワークフローは actionlint でエラーなし。確認ステップの JMESPath(AWS CLI の --query の書き方)をサンプルのデータで確認

    AWS 上での確認(5xx を発生させてアラームのメールが届くこと、aws ecs execute-command で php artisan about が実行できること、壊れたイメージのデプロイがサーキットブレーカーで前のリビジョンに戻ること、オートスケーリングで台数が変わること、Logs Insights のクエリの結果)はできていません。導入時は、stg 環境で一つずつ試してから本番に広げてください。

    連載のまとめ:12回で作ったもの

    回内容
    第1回全体像と開発環境:Laravel 12+Vue 3 スターターキットを Docker で動かす
    第2回顧客マスタの一覧と登録:FormRequest でバリデーションする
    第3回ロールと Policy:誰が何をできるかをコードで決める
    第4回案件管理と一覧画面の作り込み:検索・並べ替え・ページング
    第5回請求と非同期処理:キューとスケジューラをローカルで動かす
    第6回本番用コンテナイメージを作る:マルチステージ Dockerfile
    第7回Terraform の土台とネットワーク:state 管理と VPC
    第8回RDS for MySQL と秘密情報:パスワードをコードに書かない
    第9回ECR・ECS on Fargate・ALB:管理画面をインターネットに公開する
    第10回キューワーカーとスケジューラを ECS で動かす
    第11回GitHub Actions で CI/CD:OIDC でキーを持たずにデプロイ
    第12回運用編:ログ・監視・スケーリング・切り戻し(この記事)

    前半(第1〜5回)でアプリを、後半(第6〜12回)でそれを本番で動かす仕組みを作りました。どの回も「動くコード」と「確認の方法」をセットにしてきましたが、AWS 上での動作は一度も確認していない点は、最後にあらためてお伝えしておきます。

    本番運用の前に残っている作業

    連載では説明を絞るために省いたものがあります。本番で使う前に、少なくとも次の点を検討してください。

    残作業内容関係する回
    会員登録を閉じるスターターキットの会員登録画面が残ったまま。ルートを外し、ログイン画面のリンクも消す。ユーザーは今回の users:create か、管理者用のユーザー管理画面で作る第3回・第9回
    メール送信本番の MAIL_MAILER が未設定(ログに出るだけ)なので、パスワード再設定のメールが届かない。Amazon SES などの送信の設定と、送信元ドメインの認証が必要第1回
    バックアップの復元訓練RDS の自動バックアップはあるが、復元を試していない。復元の手順・所要時間を記録し、必要なら別リージョンへのコピーも検討第8回
    DB のユーザーとパスワードの変更アプリがマスターユーザーで接続している。アプリ用のユーザーを作り、自動ローテーション後のタスクの入れ替えも運用に組み込む第8回・第10回
    信頼するプロキシの範囲の見直しALB の前に CloudFront などを置く場合は、TRUSTED_PROXIES と ALB のセキュリティグループを合わせて見直す第9回
    公開範囲と WAFIP 制限を外して公開する場合は、AWS WAF などでログイン画面への総当たりを防ぐ。二要素認証の検討も第9回
    依存関係とベースイメージの更新サンプルは2025年5月時点の版に固定している。composer audit などで確認し、定期的に更新する第1回・第6回
    本番環境の分離stg と prod を別の AWS アカウントに分け、prod のデプロイに承認者を設定する第7回・第11回
    費用の確認毎月、タグ(Project / Environment)ごとに費用を確認し、台数・ログの保持期間・NAT Gateway の構成を見直す第7回・第12回

    処理の速さが課題になった場合は、Laravel Octane(アプリを常駐させて起動の手間を省く仕組み)への移行が次の一手になります。ただし、リクエストをまたいで状態が残ることへの注意が増えるため、連載では nginx と php-fpm の構成にとどめました。

    発注者向けメモ:AWS の月額は「常に動く台数」「ログ」「通信」で決まる

    連載の構成で費用がかかる部品を、性質で分けると次のようになります(具体的な金額は、リージョン・時期・使い方で変わるため、AWS の料金ページと見積もりツールで確認してください)。

    性質部品費用を左右するもの
    動いている時間だけかかる(常時)Fargate のタスク(web・worker・scheduler)台数 × CPU・メモリ × 時間。web の最小台数とオートスケーリングの上限
    動いている時間だけかかる(常時)RDSインスタンスのサイズ、Multi-AZ の有無、ストレージ、バックアップの保持日数
    動いている時間だけかかる(常時)ALB、NAT Gateway、インターフェイス型 VPC エンドポイント(使う場合)台数(NAT を AZ ごとに置くか)と、使った分の処理量
    使った分だけかかるCloudWatch Logsログの量と保持期間。アクセスログやデバッグログを出しすぎない
    使った分だけかかるデータ転送、SQS、ECR の保管、Secrets Manager、アラーム多くはこの規模では小さいが、ゼロではない

    費用を下げるときの考え方は、「夜間や検証環境は台数を減らす」「ログの保持期間を業務で必要な日数にする」「検証環境は NAT Gateway 1台・Multi-AZ なし」のように、止まってよい時間とのトレードオフで決めることです。

    運用のルールとして、次の点を開発会社と決めておくと安心です。

    • 「アラームは誰が受け取り、夜間・休日は何分以内に対応しますか?その体制は保守契約に含まれていますか?」
    • 「障害時に前の版に戻す手順はありますか?最後に試したのはいつですか?」
    • 「本番のコンテナでコマンドを実行できる人は誰ですか?実行の記録は残りますか?」
    • 「毎月の AWS の費用は誰が確認し、増えたときに何を見直しますか?」
    • 「依存ライブラリやベースイメージの更新は、どのくらいの頻度で、誰の費用で行いますか?」

    まとめ

    第12回では、動かし続けるための仕組みを加えて、連載を締めくくりました。

    • アラームは画面・裏の処理・アプリ・DB の4つを見張り、説明文に「最初に見るところ」を書く
    • スケジューラの停止は heartbeat のログが途絶えたことで気づく
    • web は CPU の目標追跡で台数を自動で変え、上限は費用の上限として決める
    • サーキットブレーカーで壊れたデプロイを自動で戻し、戻ったことをワークフローでも検知する
    • ECS Exec で調査と最初の管理者の作成を行い、手順と権限を手順書で管理する

    12回にわたってお読みいただき、ありがとうございました。管理画面を「作る」ことと「運用し続ける」ことは、どちらも発注の段階で決めておくことがたくさんあります。この連載のコードと発注者向けメモが、開発会社との打ち合わせのたたき台になれば幸いです。

    この連載の記事一覧

    この記事は連載「Laravel+Vue.jsで作る管理画面をECSで動かす」の1回です。連載のほかの回は次のとおりです(連載の一覧ページ)。

    システム制作・運用・保守のお問い合わせはこちら


      よかったらシェアしてね!
      • URLをコピーしました!
      • URLをコピーしました!

      この記事を書いた人

      株式会社THIRD HERO代表取締役 朝野貴朗
      Webシステム開発を中心に、toC向けサービスサイトの運営、ツール開発などを行ってまいりました。

      目次