AWS CloudWatch Synthetics 合成監控

AWS CloudWatch Synthetics Synthetic Monitoring

AWS CloudWatch Synthetics 是一項強大的合成監控服務,能夠讓您主動監控應用程式的端點和 API,在使用者發現問題之前就偵測並解決潛在的效能或可用性問題。本文將深入介紹 Synthetics 的核心概念、Canary 腳本類型、實作範例以及最佳實務。

Synthetics 服務概述

什麼是合成監控

合成監控(Synthetic Monitoring)是一種主動式監控方法,透過模擬使用者行為來測試應用程式的可用性和效能。與傳統的被動式監控不同,合成監控能夠在真實使用者遇到問題之前就發現異常。

CloudWatch Synthetics 核心功能

CloudWatch Synthetics 提供以下核心功能:

  • Canary 腳本:可配置的腳本,按照排程執行以監控端點和 API
  • 視覺回歸測試:自動擷取螢幕截圖並進行基線比對
  • HAR 檔案生成:記錄詳細的網路請求和回應資訊
  • CloudWatch 整合:自動將指標發布到 CloudWatch Metrics
  • 告警整合:與 CloudWatch Alarms 和 SNS 無縫整合

架構概覽

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
┌─────────────────────────────────────────────────────────────┐
│                    CloudWatch Synthetics                     │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐  │
│  │   Canary 1   │    │   Canary 2   │    │   Canary N   │  │
│  │  (Web App)   │    │    (API)     │    │   (Custom)   │  │
│  └──────┬───────┘    └──────┬───────┘    └──────┬───────┘  │
│         │                   │                   │           │
│         └───────────────────┼───────────────────┘           │
│                             │                               │
│                             ▼                               │
│  ┌──────────────────────────────────────────────────────┐  │
│  │                   Lambda Runtime                      │  │
│  │  (Puppeteer / Selenium for Node.js / Python)         │  │
│  └──────────────────────────────────────────────────────┘  │
│                             │                               │
└─────────────────────────────┼───────────────────────────────┘
         ┌────────────────────┴────────────────────┐
         │                                         │
         ▼                                         ▼
┌─────────────────┐                     ┌─────────────────────┐
│   S3 Bucket     │                     │  CloudWatch Metrics │
│ (Screenshots,   │                     │  CloudWatch Logs    │
│  HAR files)     │                     │  CloudWatch Alarms  │
└─────────────────┘                     └─────────────────────┘

Canary 腳本類型

CloudWatch Synthetics 提供多種預建的 Canary 藍圖(Blueprints),適用於不同的監控場景。

心跳監控 (Heartbeat Monitoring)

最基本的監控類型,用於檢查 URL 是否可達:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// syn-nodejs-puppeteer-heartbeat
const synthetics = require('Synthetics');
const log = require('SyntheticsLogger');

const pageLoadBlueprint = async function () {
    const URL = "https://example.com";

    let page = await synthetics.getPage();

    const response = await page.goto(URL, {
        waitUntil: 'domcontentloaded',
        timeout: 30000
    });

    if (response.status() !== 200) {
        throw new Error(`Failed to load ${URL}`);
    }

    await synthetics.takeScreenshot('loaded', 'result');
};

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

API Canary

專門用於測試 REST API 端點:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// syn-nodejs-puppeteer-api
const synthetics = require('Synthetics');
const log = require('SyntheticsLogger');
const https = require('https');

const apiCanaryBlueprint = async function () {
    const hostname = 'api.example.com';
    const path = '/v1/health';

    const options = {
        hostname: hostname,
        path: path,
        method: 'GET',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer YOUR_TOKEN'
        }
    };

    return new Promise((resolve, reject) => {
        const req = https.request(options, (res) => {
            let data = '';

            res.on('data', (chunk) => {
                data += chunk;
            });

            res.on('end', () => {
                log.info(`Status: ${res.statusCode}`);
                log.info(`Response: ${data}`);

                if (res.statusCode === 200) {
                    resolve('API check passed');
                } else {
                    reject(new Error(`API check failed with status ${res.statusCode}`));
                }
            });
        });

        req.on('error', (error) => {
            reject(error);
        });

        req.end();
    });
};

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

自動檢查頁面上的所有連結是否有效:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// syn-nodejs-puppeteer-broken-link-checker
const synthetics = require('Synthetics');
const log = require('SyntheticsLogger');

const brokenLinkCheckerBlueprint = async function () {
    const URL = "https://example.com";

    let page = await synthetics.getPage();
    await page.goto(URL, { waitUntil: 'networkidle0' });

    // 取得所有連結
    const links = await page.$$eval('a', (anchors) => {
        return anchors.map(anchor => anchor.href).filter(href => href);
    });

    log.info(`Found ${links.length} links`);

    const brokenLinks = [];

    for (const link of links) {
        try {
            const response = await page.goto(link, {
                waitUntil: 'domcontentloaded',
                timeout: 10000
            });

            if (response.status() >= 400) {
                brokenLinks.push({ url: link, status: response.status() });
            }
        } catch (error) {
            brokenLinks.push({ url: link, error: error.message });
        }
    }

    if (brokenLinks.length > 0) {
        log.error(`Found ${brokenLinks.length} broken links`);
        throw new Error(`Broken links detected: ${JSON.stringify(brokenLinks)}`);
    }

    return 'All links are valid';
};

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

GUI 工作流程 (GUI Workflow)

模擬複雜的使用者互動流程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// syn-nodejs-puppeteer-gui-workflow
const synthetics = require('Synthetics');
const log = require('SyntheticsLogger');

const guiWorkflowBlueprint = async function () {
    let page = await synthetics.getPage();

    // 步驟 1: 載入首頁
    await synthetics.executeStep('loadHomepage', async function () {
        await page.goto('https://example.com', { waitUntil: 'networkidle0' });
        await synthetics.takeScreenshot('homepage', 'loaded');
    });

    // 步驟 2: 點擊登入按鈕
    await synthetics.executeStep('clickLogin', async function () {
        await page.click('#login-button');
        await page.waitForNavigation({ waitUntil: 'networkidle0' });
        await synthetics.takeScreenshot('login-page', 'loaded');
    });

    // 步驟 3: 輸入憑證
    await synthetics.executeStep('enterCredentials', async function () {
        await page.type('#username', 'test-user');
        await page.type('#password', 'test-password');
        await synthetics.takeScreenshot('credentials-entered', 'result');
    });

    // 步驟 4: 提交表單
    await synthetics.executeStep('submitForm', async function () {
        await page.click('#submit-button');
        await page.waitForNavigation({ waitUntil: 'networkidle0' });

        // 驗證登入成功
        const welcomeText = await page.$eval('.welcome-message', el => el.textContent);
        if (!welcomeText.includes('Welcome')) {
            throw new Error('Login failed');
        }

        await synthetics.takeScreenshot('logged-in', 'result');
    });
};

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

建立與設定 Canary

使用 AWS CLI 建立 Canary

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# 建立 Canary 執行角色
aws iam create-role \
    --role-name CloudWatchSyntheticsRole \
    --assume-role-policy-document '{
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Principal": {
                    "Service": "lambda.amazonaws.com"
                },
                "Action": "sts:AssumeRole"
            }
        ]
    }'

# 附加必要政策
aws iam attach-role-policy \
    --role-name CloudWatchSyntheticsRole \
    --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

# 建立自訂政策
aws iam put-role-policy \
    --role-name CloudWatchSyntheticsRole \
    --policy-name SyntheticsPolicy \
    --policy-document '{
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "s3:PutObject",
                    "s3:GetObject"
                ],
                "Resource": "arn:aws:s3:::my-synthetics-bucket/*"
            },
            {
                "Effect": "Allow",
                "Action": [
                    "s3:GetBucketLocation"
                ],
                "Resource": "arn:aws:s3:::my-synthetics-bucket"
            },
            {
                "Effect": "Allow",
                "Action": [
                    "cloudwatch:PutMetricData"
                ],
                "Resource": "*",
                "Condition": {
                    "StringEquals": {
                        "cloudwatch:namespace": "CloudWatchSynthetics"
                    }
                }
            },
            {
                "Effect": "Allow",
                "Action": [
                    "logs:CreateLogStream",
                    "logs:PutLogEvents",
                    "logs:CreateLogGroup"
                ],
                "Resource": "arn:aws:logs:*:*:log-group:/aws/lambda/cwsyn-*"
            }
        ]
    }'

使用 AWS CLI 建立 Canary

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 建立 S3 儲存桶存放結果
aws s3 mb s3://my-synthetics-bucket-$(aws sts get-caller-identity --query Account --output text)

# 壓縮 Canary 腳本
zip canary-script.zip nodejs/node_modules/* canary.js

# 上傳腳本到 S3
aws s3 cp canary-script.zip s3://my-synthetics-bucket/scripts/

# 建立 Canary
aws synthetics create-canary \
    --name my-website-canary \
    --artifact-s3-location "s3://my-synthetics-bucket/artifacts/" \
    --execution-role-arn arn:aws:iam::123456789012:role/CloudWatchSyntheticsRole \
    --schedule "Expression=rate(5 minutes)" \
    --code "Handler=canary.handler,S3Bucket=my-synthetics-bucket,S3Key=scripts/canary-script.zip" \
    --runtime-version syn-nodejs-puppeteer-6.2 \
    --run-config "TimeoutInSeconds=60,MemoryInMB=1024,ActiveTracing=true"

# 啟動 Canary
aws synthetics start-canary --name my-website-canary

使用 Terraform 建立 Canary

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# providers.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

# variables.tf
variable "canary_name" {
  description = "Name of the Canary"
  type        = string
  default     = "website-monitor"
}

variable "target_url" {
  description = "URL to monitor"
  type        = string
  default     = "https://example.com"
}

variable "schedule_expression" {
  description = "Schedule expression for Canary runs"
  type        = string
  default     = "rate(5 minutes)"
}

# main.tf
resource "aws_s3_bucket" "synthetics_artifacts" {
  bucket = "synthetics-artifacts-${data.aws_caller_identity.current.account_id}"
}

resource "aws_s3_bucket_lifecycle_configuration" "artifacts_lifecycle" {
  bucket = aws_s3_bucket.synthetics_artifacts.id

  rule {
    id     = "cleanup-old-artifacts"
    status = "Enabled"

    expiration {
      days = 30
    }
  }
}

data "aws_caller_identity" "current" {}

resource "aws_iam_role" "synthetics_role" {
  name = "${var.canary_name}-synthetics-role"

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

resource "aws_iam_role_policy" "synthetics_policy" {
  name = "${var.canary_name}-synthetics-policy"
  role = aws_iam_role.synthetics_role.id

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

resource "aws_synthetics_canary" "main" {
  name                 = var.canary_name
  artifact_s3_location = "s3://${aws_s3_bucket.synthetics_artifacts.id}/"
  execution_role_arn   = aws_iam_role.synthetics_role.arn
  handler              = "pageLoadBlueprint.handler"
  zip_file             = data.archive_file.canary_script.output_path
  runtime_version      = "syn-nodejs-puppeteer-6.2"
  start_canary         = true

  schedule {
    expression = var.schedule_expression
  }

  run_config {
    timeout_in_seconds = 60
    memory_in_mb       = 1024
    active_tracing     = true
  }

  success_retention_period = 31
  failure_retention_period = 31

  tags = {
    Environment = "production"
    Purpose     = "website-monitoring"
  }
}

data "archive_file" "canary_script" {
  type        = "zip"
  output_path = "${path.module}/canary_script.zip"

  source {
    content = templatefile("${path.module}/templates/canary.js.tpl", {
      target_url = var.target_url
    })
    filename = "nodejs/node_modules/pageLoadBlueprint.js"
  }
}

# outputs.tf
output "canary_arn" {
  description = "ARN of the Canary"
  value       = aws_synthetics_canary.main.arn
}

output "canary_status" {
  description = "Status of the Canary"
  value       = aws_synthetics_canary.main.status
}

Canary 腳本模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// templates/canary.js.tpl
const synthetics = require('Synthetics');
const log = require('SyntheticsLogger');

const pageLoadBlueprint = async function () {
    const URL = "${target_url}";

    let page = await synthetics.getPage();

    // 設定 viewport
    await page.setViewport({
        width: 1920,
        height: 1080
    });

    // 載入頁面
    const response = await page.goto(URL, {
        waitUntil: ['load', 'networkidle0'],
        timeout: 30000
    });

    // 檢查回應狀態
    if (response.status() !== 200) {
        throw new Error(`Failed to load page: ${response.status()}`);
    }

    // 擷取螢幕截圖
    await synthetics.takeScreenshot('loaded', 'result');

    // 記錄效能指標
    const performanceMetrics = await page.metrics();
    log.info(`Performance Metrics: ${JSON.stringify(performanceMetrics)}`);

    // 取得載入時間
    const timing = JSON.parse(
        await page.evaluate(() => JSON.stringify(window.performance.timing))
    );

    const loadTime = timing.loadEventEnd - timing.navigationStart;
    log.info(`Page Load Time: ${loadTime}ms`);

    return 'Page loaded successfully';
};

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

網頁監控範例

完整的網頁監控 Canary

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
const synthetics = require('Synthetics');
const log = require('SyntheticsLogger');
const syntheticsConfiguration = require('SyntheticsConfiguration');

const webMonitorCanary = async function () {
    // 設定 Synthetics 配置
    syntheticsConfiguration.setConfig({
        screenshotOnStepStart: true,
        screenshotOnStepSuccess: true,
        screenshotOnStepFailure: true,
        includeRequestHeaders: true,
        includeResponseHeaders: true,
        restrictedHeaders: ['Authorization', 'Cookie'],
        includeRequestBody: true,
        includeResponseBody: true
    });

    let page = await synthetics.getPage();

    // 設定請求攔截
    await page.setRequestInterception(true);
    page.on('request', request => {
        // 可以在這裡修改請求或記錄
        log.info(`Request: ${request.method()} ${request.url()}`);
        request.continue();
    });

    page.on('response', response => {
        log.info(`Response: ${response.status()} ${response.url()}`);
    });

    // 步驟 1: 載入首頁
    await synthetics.executeStep('HomePage', async function () {
        await page.goto('https://example.com', {
            waitUntil: 'networkidle0',
            timeout: 30000
        });

        // 驗證頁面標題
        const title = await page.title();
        log.info(`Page title: ${title}`);

        if (!title.includes('Example')) {
            throw new Error(`Unexpected page title: ${title}`);
        }
    });

    // 步驟 2: 檢查關鍵元素
    await synthetics.executeStep('CheckElements', async function () {
        // 等待導航欄載入
        await page.waitForSelector('nav', { timeout: 10000 });

        // 檢查 logo
        const logo = await page.$('img.logo');
        if (!logo) {
            throw new Error('Logo not found');
        }

        // 檢查主要內容區塊
        const mainContent = await page.$('main');
        if (!mainContent) {
            throw new Error('Main content not found');
        }

        // 檢查頁尾
        const footer = await page.$('footer');
        if (!footer) {
            throw new Error('Footer not found');
        }
    });

    // 步驟 3: 測試搜尋功能
    await synthetics.executeStep('TestSearch', async function () {
        // 找到搜尋框
        const searchInput = await page.$('input[type="search"]');
        if (searchInput) {
            await searchInput.type('test query');
            await page.keyboard.press('Enter');
            await page.waitForNavigation({ waitUntil: 'networkidle0' });

            // 驗證搜尋結果
            const results = await page.$$('.search-result');
            log.info(`Found ${results.length} search results`);
        }
    });

    // 步驟 4: 效能分析
    await synthetics.executeStep('PerformanceAnalysis', async function () {
        const performanceMetrics = await page.evaluate(() => {
            const timing = window.performance.timing;
            const navigation = performance.getEntriesByType('navigation')[0];

            return {
                dns: timing.domainLookupEnd - timing.domainLookupStart,
                tcp: timing.connectEnd - timing.connectStart,
                ttfb: timing.responseStart - timing.requestStart,
                download: timing.responseEnd - timing.responseStart,
                domReady: timing.domContentLoadedEventEnd - timing.navigationStart,
                load: timing.loadEventEnd - timing.navigationStart,
                transferSize: navigation ? navigation.transferSize : 0
            };
        });

        log.info(`Performance Metrics: ${JSON.stringify(performanceMetrics, null, 2)}`);

        // 設定效能閾值
        if (performanceMetrics.load > 5000) {
            log.warn('Page load time exceeded 5 seconds');
        }

        if (performanceMetrics.ttfb > 1000) {
            log.warn('TTFB exceeded 1 second');
        }
    });

    // 步驟 5: 檢查 JavaScript 錯誤
    await synthetics.executeStep('CheckErrors', async function () {
        const errors = [];

        page.on('pageerror', error => {
            errors.push(error.message);
        });

        page.on('console', msg => {
            if (msg.type() === 'error') {
                errors.push(msg.text());
            }
        });

        // 等待一段時間收集錯誤
        await page.waitForTimeout(2000);

        if (errors.length > 0) {
            log.warn(`JavaScript Errors: ${JSON.stringify(errors)}`);
        }
    });
};

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

多頁面監控

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const synthetics = require('Synthetics');
const log = require('SyntheticsLogger');

const multiPageCanary = async function () {
    const pages = [
        { name: 'Home', url: 'https://example.com/' },
        { name: 'About', url: 'https://example.com/about' },
        { name: 'Products', url: 'https://example.com/products' },
        { name: 'Contact', url: 'https://example.com/contact' }
    ];

    let page = await synthetics.getPage();
    const results = [];

    for (const pageInfo of pages) {
        await synthetics.executeStep(pageInfo.name, async function () {
            const startTime = Date.now();

            const response = await page.goto(pageInfo.url, {
                waitUntil: 'networkidle0',
                timeout: 30000
            });

            const loadTime = Date.now() - startTime;

            results.push({
                page: pageInfo.name,
                url: pageInfo.url,
                status: response.status(),
                loadTime: loadTime
            });

            log.info(`${pageInfo.name}: Status ${response.status()}, Load time ${loadTime}ms`);

            if (response.status() !== 200) {
                throw new Error(`${pageInfo.name} returned status ${response.status()}`);
            }

            await synthetics.takeScreenshot(pageInfo.name, 'result');
        });
    }

    log.info(`All pages checked: ${JSON.stringify(results)}`);
    return results;
};

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

API 監控範例

REST API 監控

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
const synthetics = require('Synthetics');
const log = require('SyntheticsLogger');
const https = require('https');

const apiMonitorCanary = async function () {
    const apiEndpoints = [
        {
            name: 'HealthCheck',
            method: 'GET',
            path: '/api/v1/health',
            expectedStatus: 200
        },
        {
            name: 'GetUsers',
            method: 'GET',
            path: '/api/v1/users',
            expectedStatus: 200,
            headers: {
                'Authorization': 'Bearer ${process.env.API_TOKEN}'
            }
        },
        {
            name: 'CreateUser',
            method: 'POST',
            path: '/api/v1/users',
            body: JSON.stringify({
                name: 'Test User',
                email: 'test@example.com'
            }),
            expectedStatus: 201,
            headers: {
                'Authorization': 'Bearer ${process.env.API_TOKEN}',
                'Content-Type': 'application/json'
            }
        }
    ];

    const hostname = 'api.example.com';

    for (const endpoint of apiEndpoints) {
        await synthetics.executeStep(endpoint.name, async function () {
            const result = await makeRequest(hostname, endpoint);
            log.info(`${endpoint.name}: ${JSON.stringify(result)}`);

            if (result.status !== endpoint.expectedStatus) {
                throw new Error(
                    `${endpoint.name} returned ${result.status}, expected ${endpoint.expectedStatus}`
                );
            }
        });
    }
};

function makeRequest(hostname, endpoint) {
    return new Promise((resolve, reject) => {
        const options = {
            hostname: hostname,
            port: 443,
            path: endpoint.path,
            method: endpoint.method,
            headers: endpoint.headers || {}
        };

        const startTime = Date.now();

        const req = https.request(options, (res) => {
            let data = '';

            res.on('data', (chunk) => {
                data += chunk;
            });

            res.on('end', () => {
                const responseTime = Date.now() - startTime;

                resolve({
                    status: res.statusCode,
                    headers: res.headers,
                    body: data,
                    responseTime: responseTime
                });
            });
        });

        req.on('error', (error) => {
            reject(error);
        });

        if (endpoint.body) {
            req.write(endpoint.body);
        }

        req.end();
    });
}

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

GraphQL API 監控

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
const synthetics = require('Synthetics');
const log = require('SyntheticsLogger');
const https = require('https');

const graphqlCanary = async function () {
    const graphqlEndpoint = 'api.example.com';
    const graphqlPath = '/graphql';

    const queries = [
        {
            name: 'GetProducts',
            query: `
                query GetProducts {
                    products(first: 10) {
                        edges {
                            node {
                                id
                                name
                                price
                            }
                        }
                    }
                }
            `,
            validateResponse: (data) => {
                return data.data && data.data.products && data.data.products.edges;
            }
        },
        {
            name: 'GetUser',
            query: `
                query GetUser($id: ID!) {
                    user(id: $id) {
                        id
                        name
                        email
                    }
                }
            `,
            variables: { id: 'user-123' },
            validateResponse: (data) => {
                return data.data && data.data.user;
            }
        }
    ];

    for (const queryDef of queries) {
        await synthetics.executeStep(queryDef.name, async function () {
            const body = JSON.stringify({
                query: queryDef.query,
                variables: queryDef.variables || {}
            });

            const result = await executeGraphQL(graphqlEndpoint, graphqlPath, body);

            log.info(`${queryDef.name} response time: ${result.responseTime}ms`);

            if (result.status !== 200) {
                throw new Error(`GraphQL request failed with status ${result.status}`);
            }

            const responseData = JSON.parse(result.body);

            if (responseData.errors) {
                throw new Error(`GraphQL errors: ${JSON.stringify(responseData.errors)}`);
            }

            if (!queryDef.validateResponse(responseData)) {
                throw new Error('Response validation failed');
            }
        });
    }
};

function executeGraphQL(hostname, path, body) {
    return new Promise((resolve, reject) => {
        const options = {
            hostname: hostname,
            port: 443,
            path: path,
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Content-Length': Buffer.byteLength(body)
            }
        };

        const startTime = Date.now();

        const req = https.request(options, (res) => {
            let data = '';

            res.on('data', (chunk) => {
                data += chunk;
            });

            res.on('end', () => {
                resolve({
                    status: res.statusCode,
                    body: data,
                    responseTime: Date.now() - startTime
                });
            });
        });

        req.on('error', reject);
        req.write(body);
        req.end();
    });
}

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

視覺回歸測試

基線比對 Canary

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
const synthetics = require('Synthetics');
const log = require('SyntheticsLogger');
const AWS = require('aws-sdk');
const crypto = require('crypto');

const visualRegressionCanary = async function () {
    const s3 = new AWS.S3();
    const BUCKET_NAME = process.env.BASELINE_BUCKET;
    const BASELINE_PREFIX = 'baselines/';
    const THRESHOLD = 0.05; // 5% 差異閾值

    let page = await synthetics.getPage();

    await page.setViewport({
        width: 1920,
        height: 1080
    });

    // 載入頁面
    await synthetics.executeStep('LoadPage', async function () {
        await page.goto('https://example.com', {
            waitUntil: 'networkidle0'
        });
    });

    // 擷取並比對螢幕截圖
    await synthetics.executeStep('VisualComparison', async function () {
        // 擷取當前螢幕截圖
        const screenshot = await page.screenshot({
            fullPage: true,
            type: 'png'
        });

        const screenshotHash = crypto
            .createHash('md5')
            .update(screenshot)
            .digest('hex');

        // 嘗試取得基線
        try {
            const baselineData = await s3.getObject({
                Bucket: BUCKET_NAME,
                Key: `${BASELINE_PREFIX}homepage.png`
            }).promise();

            const baselineHash = crypto
                .createHash('md5')
                .update(baselineData.Body)
                .digest('hex');

            if (screenshotHash !== baselineHash) {
                // 進行詳細比對
                const difference = await compareImages(baselineData.Body, screenshot);

                if (difference > THRESHOLD) {
                    // 儲存差異截圖
                    await synthetics.takeScreenshot('visual-difference', 'error');
                    throw new Error(`Visual difference detected: ${(difference * 100).toFixed(2)}%`);
                }

                log.info(`Visual difference within threshold: ${(difference * 100).toFixed(2)}%`);
            } else {
                log.info('No visual changes detected');
            }
        } catch (error) {
            if (error.code === 'NoSuchKey') {
                // 沒有基線,建立新的
                log.info('No baseline found, creating new baseline');
                await s3.putObject({
                    Bucket: BUCKET_NAME,
                    Key: `${BASELINE_PREFIX}homepage.png`,
                    Body: screenshot,
                    ContentType: 'image/png'
                }).promise();
            } else {
                throw error;
            }
        }
    });
};

async function compareImages(baseline, current) {
    // 簡化的圖片比對邏輯
    // 在實際應用中,您可能會使用 Pixelmatch 或類似的庫
    const baselineBuffer = Buffer.isBuffer(baseline) ? baseline : Buffer.from(baseline);
    const currentBuffer = Buffer.isBuffer(current) ? current : Buffer.from(current);

    let differences = 0;
    const minLength = Math.min(baselineBuffer.length, currentBuffer.length);

    for (let i = 0; i < minLength; i++) {
        if (baselineBuffer[i] !== currentBuffer[i]) {
            differences++;
        }
    }

    return differences / minLength;
}

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

多視窗大小測試

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const synthetics = require('Synthetics');
const log = require('SyntheticsLogger');

const responsiveTestCanary = async function () {
    const viewports = [
        { name: 'Desktop', width: 1920, height: 1080 },
        { name: 'Laptop', width: 1366, height: 768 },
        { name: 'Tablet', width: 768, height: 1024 },
        { name: 'Mobile', width: 375, height: 812 }
    ];

    let page = await synthetics.getPage();

    for (const viewport of viewports) {
        await synthetics.executeStep(`Test-${viewport.name}`, async function () {
            await page.setViewport({
                width: viewport.width,
                height: viewport.height,
                isMobile: viewport.width < 768
            });

            await page.goto('https://example.com', {
                waitUntil: 'networkidle0'
            });

            // 驗證響應式設計元素
            if (viewport.width < 768) {
                // 行動版應該顯示漢堡選單
                const mobileMenu = await page.$('.mobile-menu-button');
                if (!mobileMenu) {
                    throw new Error('Mobile menu not found on mobile viewport');
                }
            } else {
                // 桌面版應該顯示完整導航
                const desktopNav = await page.$('.desktop-navigation');
                if (!desktopNav) {
                    throw new Error('Desktop navigation not found');
                }
            }

            await synthetics.takeScreenshot(`${viewport.name}-view`, 'result');
        });
    }
};

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

告警與通知整合

CloudWatch Alarms 設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 建立 Canary 失敗告警
aws cloudwatch put-metric-alarm \
    --alarm-name "Canary-Failure-Alarm" \
    --alarm-description "Alert when canary fails" \
    --namespace CloudWatchSynthetics \
    --metric-name Failed \
    --dimensions Name=CanaryName,Value=my-website-canary \
    --statistic Sum \
    --period 300 \
    --threshold 1 \
    --comparison-operator GreaterThanOrEqualToThreshold \
    --evaluation-periods 1 \
    --treat-missing-data notBreaching \
    --alarm-actions arn:aws:sns:ap-northeast-1:123456789012:alerts-topic

# 建立 Canary 持續時間告警
aws cloudwatch put-metric-alarm \
    --alarm-name "Canary-Duration-Alarm" \
    --alarm-description "Alert when canary takes too long" \
    --namespace CloudWatchSynthetics \
    --metric-name Duration \
    --dimensions Name=CanaryName,Value=my-website-canary \
    --statistic Average \
    --period 300 \
    --threshold 30000 \
    --comparison-operator GreaterThanThreshold \
    --evaluation-periods 2 \
    --treat-missing-data notBreaching \
    --alarm-actions arn:aws:sns:ap-northeast-1:123456789012:alerts-topic

# 建立成功率告警
aws cloudwatch put-metric-alarm \
    --alarm-name "Canary-SuccessRate-Alarm" \
    --alarm-description "Alert when success rate drops" \
    --namespace CloudWatchSynthetics \
    --metric-name SuccessPercent \
    --dimensions Name=CanaryName,Value=my-website-canary \
    --statistic Average \
    --period 900 \
    --threshold 95 \
    --comparison-operator LessThanThreshold \
    --evaluation-periods 2 \
    --treat-missing-data notBreaching \
    --alarm-actions arn:aws:sns:ap-northeast-1:123456789012:alerts-topic

Terraform 告警設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# CloudWatch Alarms for Synthetics
resource "aws_cloudwatch_metric_alarm" "canary_failure" {
  alarm_name          = "${var.canary_name}-failure"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = 1
  metric_name         = "Failed"
  namespace           = "CloudWatchSynthetics"
  period              = 300
  statistic           = "Sum"
  threshold           = 1
  alarm_description   = "Canary ${var.canary_name} has failed"
  treat_missing_data  = "notBreaching"

  dimensions = {
    CanaryName = aws_synthetics_canary.main.name
  }

  alarm_actions = [aws_sns_topic.alerts.arn]
  ok_actions    = [aws_sns_topic.alerts.arn]
}

resource "aws_cloudwatch_metric_alarm" "canary_duration" {
  alarm_name          = "${var.canary_name}-duration"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 2
  metric_name         = "Duration"
  namespace           = "CloudWatchSynthetics"
  period              = 300
  statistic           = "Average"
  threshold           = 30000
  alarm_description   = "Canary ${var.canary_name} is taking too long"
  treat_missing_data  = "notBreaching"

  dimensions = {
    CanaryName = aws_synthetics_canary.main.name
  }

  alarm_actions = [aws_sns_topic.alerts.arn]
}

resource "aws_sns_topic" "alerts" {
  name = "${var.canary_name}-alerts"
}

resource "aws_sns_topic_subscription" "email" {
  topic_arn = aws_sns_topic.alerts.arn
  protocol  = "email"
  endpoint  = var.alert_email
}

# Lambda 告警處理函數
resource "aws_lambda_function" "alert_handler" {
  filename         = "alert_handler.zip"
  function_name    = "${var.canary_name}-alert-handler"
  role             = aws_iam_role.alert_handler_role.arn
  handler          = "index.handler"
  runtime          = "nodejs18.x"

  environment {
    variables = {
      SLACK_WEBHOOK_URL = var.slack_webhook_url
    }
  }
}

resource "aws_sns_topic_subscription" "lambda" {
  topic_arn = aws_sns_topic.alerts.arn
  protocol  = "lambda"
  endpoint  = aws_lambda_function.alert_handler.arn
}

resource "aws_lambda_permission" "sns" {
  statement_id  = "AllowSNSInvoke"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.alert_handler.function_name
  principal     = "sns.amazonaws.com"
  source_arn    = aws_sns_topic.alerts.arn
}

Slack 通知 Lambda

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// alert_handler/index.js
const https = require('https');

exports.handler = async (event) => {
    const snsMessage = JSON.parse(event.Records[0].Sns.Message);

    const slackMessage = {
        blocks: [
            {
                type: 'header',
                text: {
                    type: 'plain_text',
                    text: snsMessage.AlarmName.includes('ALARM') ?
                        '🚨 Synthetics Canary Alert' :
                        '✅ Synthetics Canary Recovered'
                }
            },
            {
                type: 'section',
                fields: [
                    {
                        type: 'mrkdwn',
                        text: `*Alarm Name:*\n${snsMessage.AlarmName}`
                    },
                    {
                        type: 'mrkdwn',
                        text: `*State:*\n${snsMessage.NewStateValue}`
                    },
                    {
                        type: 'mrkdwn',
                        text: `*Reason:*\n${snsMessage.NewStateReason}`
                    },
                    {
                        type: 'mrkdwn',
                        text: `*Time:*\n${snsMessage.StateChangeTime}`
                    }
                ]
            },
            {
                type: 'actions',
                elements: [
                    {
                        type: 'button',
                        text: {
                            type: 'plain_text',
                            text: 'View in CloudWatch'
                        },
                        url: `https://console.aws.amazon.com/cloudwatch/home?region=${process.env.AWS_REGION}#alarmsV2:alarm/${snsMessage.AlarmName}`
                    }
                ]
            }
        ]
    };

    await sendToSlack(slackMessage);

    return { statusCode: 200 };
};

function sendToSlack(message) {
    return new Promise((resolve, reject) => {
        const webhookUrl = new URL(process.env.SLACK_WEBHOOK_URL);
        const body = JSON.stringify(message);

        const options = {
            hostname: webhookUrl.hostname,
            path: webhookUrl.pathname,
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Content-Length': Buffer.byteLength(body)
            }
        };

        const req = https.request(options, (res) => {
            if (res.statusCode === 200) {
                resolve();
            } else {
                reject(new Error(`Slack returned status ${res.statusCode}`));
            }
        });

        req.on('error', reject);
        req.write(body);
        req.end();
    });
}

成本優化與最佳實務

成本優化策略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# 成本優化的 Canary 設定
resource "aws_synthetics_canary" "cost_optimized" {
  name                 = "cost-optimized-canary"
  artifact_s3_location = "s3://${aws_s3_bucket.synthetics_artifacts.id}/"
  execution_role_arn   = aws_iam_role.synthetics_role.arn
  handler              = "pageLoadBlueprint.handler"
  zip_file             = data.archive_file.canary_script.output_path
  runtime_version      = "syn-nodejs-puppeteer-6.2"
  start_canary         = true

  schedule {
    # 非尖峰時段降低頻率
    expression = "rate(15 minutes)"
  }

  run_config {
    # 最佳化記憶體使用
    timeout_in_seconds = 60
    memory_in_mb       = 960  # 最低建議值
    active_tracing     = false  # 除非需要 X-Ray,否則關閉
  }

  # 縮短結果保留期限
  success_retention_period = 7   # 成功結果保留 7 天
  failure_retention_period = 31  # 失敗結果保留 31 天

  # 使用 VPC 時的成本考量
  # vpc_config {
  #   subnet_ids         = var.private_subnet_ids
  #   security_group_ids = [aws_security_group.canary.id]
  # }
}

# S3 生命週期政策以控制儲存成本
resource "aws_s3_bucket_lifecycle_configuration" "artifacts_cost_control" {
  bucket = aws_s3_bucket.synthetics_artifacts.id

  rule {
    id     = "delete-old-artifacts"
    status = "Enabled"

    filter {
      prefix = ""
    }

    expiration {
      days = 14
    }
  }

  rule {
    id     = "transition-to-ia"
    status = "Enabled"

    filter {
      prefix = ""
    }

    transition {
      days          = 7
      storage_class = "STANDARD_IA"
    }
  }
}

Canary 最佳實務

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
const synthetics = require('Synthetics');
const log = require('SyntheticsLogger');
const syntheticsConfiguration = require('SyntheticsConfiguration');

const bestPracticesCanary = async function () {
    // 最佳實務 1: 適當的設定配置
    syntheticsConfiguration.setConfig({
        // 只在步驟失敗時擷取截圖以節省儲存空間
        screenshotOnStepStart: false,
        screenshotOnStepSuccess: false,
        screenshotOnStepFailure: true,

        // 限制請求/回應記錄大小
        includeRequestHeaders: true,
        includeResponseHeaders: true,
        includeRequestBody: false,
        includeResponseBody: false,

        // 隱藏敏感標頭
        restrictedHeaders: [
            'Authorization',
            'Cookie',
            'X-API-Key',
            'X-Auth-Token'
        ]
    });

    let page = await synthetics.getPage();

    // 最佳實務 2: 設定合理的超時
    page.setDefaultNavigationTimeout(30000);
    page.setDefaultTimeout(10000);

    // 最佳實務 3: 錯誤處理
    try {
        await synthetics.executeStep('MainCheck', async function () {
            const response = await page.goto('https://example.com', {
                waitUntil: 'domcontentloaded', // 比 networkidle0 更快
                timeout: 30000
            });

            // 驗證回應
            if (!response.ok()) {
                throw new Error(`HTTP ${response.status()}: ${response.statusText()}`);
            }

            // 最佳實務 4: 等待特定元素而非固定時間
            await page.waitForSelector('.main-content', { timeout: 10000 });
        });
    } catch (error) {
        // 最佳實務 5: 結構化的錯誤記錄
        log.error(JSON.stringify({
            error: error.message,
            stack: error.stack,
            timestamp: new Date().toISOString(),
            url: 'https://example.com'
        }));

        // 擷取失敗截圖
        await synthetics.takeScreenshot('failure', 'error');

        throw error;
    }

    // 最佳實務 6: 清理資源
    await page.close();
};

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

監控儀表板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# 建立 CloudWatch 儀表板
aws cloudwatch put-dashboard \
    --dashboard-name "Synthetics-Monitoring" \
    --dashboard-body '{
        "widgets": [
            {
                "type": "metric",
                "x": 0,
                "y": 0,
                "width": 12,
                "height": 6,
                "properties": {
                    "metrics": [
                        ["CloudWatchSynthetics", "SuccessPercent", "CanaryName", "my-website-canary"],
                        [".", "Failed", ".", "."]
                    ],
                    "title": "Canary Success Rate",
                    "period": 300,
                    "stat": "Average",
                    "region": "ap-northeast-1"
                }
            },
            {
                "type": "metric",
                "x": 12,
                "y": 0,
                "width": 12,
                "height": 6,
                "properties": {
                    "metrics": [
                        ["CloudWatchSynthetics", "Duration", "CanaryName", "my-website-canary"]
                    ],
                    "title": "Canary Duration",
                    "period": 300,
                    "stat": "Average",
                    "region": "ap-northeast-1"
                }
            },
            {
                "type": "metric",
                "x": 0,
                "y": 6,
                "width": 24,
                "height": 6,
                "properties": {
                    "metrics": [
                        ["CloudWatchSynthetics", "2xx", "CanaryName", "my-website-canary"],
                        [".", "4xx", ".", "."],
                        [".", "5xx", ".", "."]
                    ],
                    "title": "HTTP Status Codes",
                    "period": 300,
                    "stat": "Sum",
                    "region": "ap-northeast-1"
                }
            }
        ]
    }'

安全性最佳實務

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 使用 Secrets Manager 管理敏感資訊
resource "aws_secretsmanager_secret" "canary_credentials" {
  name = "synthetics/canary-credentials"
}

resource "aws_secretsmanager_secret_version" "canary_credentials" {
  secret_id = aws_secretsmanager_secret.canary_credentials.id
  secret_string = jsonencode({
    username = var.test_username
    password = var.test_password
    api_key  = var.api_key
  })
}

# 更新 Canary IAM 角色
resource "aws_iam_role_policy" "secrets_access" {
  name = "${var.canary_name}-secrets-policy"
  role = aws_iam_role.synthetics_role.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "secretsmanager:GetSecretValue"
        ]
        Resource = aws_secretsmanager_secret.canary_credentials.arn
      }
    ]
  })
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 在 Canary 中使用 Secrets Manager
const AWS = require('aws-sdk');
const synthetics = require('Synthetics');

const secretsManager = new AWS.SecretsManager();

async function getCredentials() {
    const secret = await secretsManager.getSecretValue({
        SecretId: 'synthetics/canary-credentials'
    }).promise();

    return JSON.parse(secret.SecretString);
}

const secureCanary = async function () {
    const credentials = await getCredentials();

    let page = await synthetics.getPage();

    await synthetics.executeStep('SecureLogin', async function () {
        await page.goto('https://example.com/login', {
            waitUntil: 'networkidle0'
        });

        await page.type('#username', credentials.username);
        await page.type('#password', credentials.password);
        await page.click('#login-button');

        await page.waitForNavigation();
    });
};

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

總結

AWS CloudWatch Synthetics 是一個功能強大的合成監控服務,能夠幫助您:

  1. 主動監控:在使用者發現問題之前偵測異常
  2. 自動化測試:定期執行網頁和 API 測試
  3. 視覺回歸:追蹤 UI 變化並偵測非預期的修改
  4. 整合告警:與 CloudWatch Alarms 和 SNS 無縫整合

透過本文介紹的最佳實務和範例程式碼,您可以有效地實施合成監控策略,確保應用程式的高可用性和良好的使用者體驗。

延伸閱讀

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy