Terraform で CloudWatch Synthetics の外形監視を構築し挙動を確認してみた

Terraform で CloudWatch Synthetics の外形監視を構築し挙動を確認してみた

CloudWatch Syntheticsを利用してCloudfrontの外形監視を行おうとしたブログです。
2026.07.31

クラウド事業本部、あきやまです。

Webサービスを運用していると、お客様が見るページがなどが問題なく表示されているか気になることありませんか?私はあります。直近、業務AWSが提供するCloudWatch Syntheticsを利用する場面がありましたので、Terraform で実装後、どのように外形監視ができるか簡易的ですが流れを記載します。

今回の要件

  • 監視対象: EC2 上に配置した静的 HTML サイト(CloudFront 経由で配信)
  • 求める監視レベル: URL が開けるか + HTTP ステータス 200 が返るか、のシンプルな疎通確認のみ
  • 監視間隔: 1分間に1回(今回は検証のため1分間で監視していますが、料金もかかるのでご注意ください)
  • 失敗時のアラーム通知: 今回は対象外(まずはコンソールでの稼働確認まで)

やってみた

Step 1: 監視対象環境のアーキテクチャ図(EC2 + CloudFront)

スクリーンショット 2026-07-31 23.35.43

Step 2: CloudWatch Synthetics Canary の構築

要件が「URL疎通 + ステータス200確認」のみだったため、複数エンドポイントを扱う API Canary ではなく、Heartbeat Monitor 相当の構成を採用しました。

synthetics.tf
resource "aws_s3_bucket" "canary_artifacts" {
  bucket        = "${var.project_name}-artifacts-${data.aws_caller_identity.current.account_id}"
  force_destroy = true

  tags = {
    Name = "${var.project_name}-canary-artifacts"
  }
}

resource "aws_iam_role" "canary" {
  name = "${var.project_name}-canary-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect    = "Allow"
        Principal = { Service = "lambda.amazonaws.com" }
        Action    = "sts:AssumeRole"
      }
    ]
  })
}

resource "aws_iam_role_policy" "canary" {
  name = "${var.project_name}-canary-policy"
  role = aws_iam_role.canary.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = ["s3:PutObject", "s3:GetObject"]
        Resource = ["${aws_s3_bucket.canary_artifacts.arn}/*"]
      },
      {
        Effect   = "Allow"
        Action   = ["s3:GetBucketLocation"]
        Resource = [aws_s3_bucket.canary_artifacts.arn]
      },
      {
        Effect   = "Allow"
        Action   = ["s3:ListAllMyBuckets"]
        Resource = ["*"]
      },
      {
        Effect   = "Allow"
        Action   = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]
        Resource = ["arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/lambda/cwsyn-*"]
      },
      {
        Effect   = "Allow"
        Action   = ["xray:PutTraceSegments"]
        Resource = ["*"]
      },
      {
        Effect   = "Allow"
        Action   = ["cloudwatch:PutMetricData"]
        Resource = ["*"]
        Condition = {
          StringEquals = {
            "cloudwatch:namespace" = "CloudWatchSynthetics"
          }
        }
      }
    ]
  })
}

resource "local_file" "canary_script" {
  content = templatefile("${path.module}/templates/canary.js.tpl", {
    target_url = "https://${aws_cloudfront_distribution.web.domain_name}"
  })
  filename = "${path.module}/build/nodejs/node_modules/canary.js"
}

data "archive_file" "canary" {
  type        = "zip"
  source_dir  = "${path.module}/build"
  output_path = "${path.module}/build/canary.zip"

  depends_on = [local_file.canary_script]
}

resource "aws_synthetics_canary" "web" {
  name                 = "invest-heartbeat"
  artifact_s3_location = "s3://${aws_s3_bucket.canary_artifacts.bucket}/canary/"
  execution_role_arn   = aws_iam_role.canary.arn
  handler              = "canary.handler"
  zip_file             = data.archive_file.canary.output_path
  runtime_version      = "syn-nodejs-puppeteer-10.0"
  start_canary         = true

  success_retention_period = 2
  failure_retention_period = 14

  schedule {
    expression = var.canary_schedule_expression
  }

  depends_on = [aws_iam_role_policy.canary]
}

実装するサンプルコード例

canary.js
const synthetics = require('Synthetics');
const log = require('SyntheticsLogger');

const heartbeatCheck = async function () {
    const url = "https://xxxxxxx.cloudfront.net";
    let page = await synthetics.getPage();
    const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });

    if (!response) {
        throw new Error(`No response returned for url: ${url}`);
    }

    const statusCode = response.status();
    if (statusCode < 200 || statusCode > 299) {
        throw new Error(`Failed to load url: ${url} . Status code: ${statusCode}`);
    }

    log.info(`Loaded ${url} with status ${statusCode}`);
    await synthetics.takeScreenshot('loaded', 'loaded');
};

exports.handler = async () => {
    return await heartbeatCheck();
};

ポイント:

  • 実行間隔は rate(1 minute) を指定(あくまで検証環境のため、短い間隔で測定しています。料金もかかりますので実行感覚にはご注意ください)
  • 監視対象が CloudFront のパブリック HTTPS エンドポイントのため、Canary 側は VPC 設定(ENI)を追加せず、通常のインターネット経由実行のままで到達可能でした
  • 失敗時のアラーム通知(SNS 等)は今回スコープ外としたため未構築

Step 3: 動作確認

確認したところ1分間隔で正しく撮れていること確認しました。

スクリーンショット 2026-07-31 23.43.19

上記で問題なく動くこと確認した後、Cloudfrontを無効化してどのように変化するか確認しました。
無効化もしっかりメトリクスに反映されていますね。
スクリーンショット 2026-07-31 23.47.46

今回は簡易的ですが、他にもより複雑な外形監視を実装することが可能です。外形監視を検討されている方は一度ご検討いただくのもいいかもしれません。現場からは以上となります。

参考

この記事をシェアする

関連記事