การเขียน Lambda ใน Custom widget บน CloudWatch dashboard

ผมได้สร้าง Custom widget บน CloudWatch dashboard และจะมาแนะนำวิธีการทำในบทความนี้ครับ

บทความนี้แปลมาจากบทความที่เป็นภาษาญี่ปุ่นที่ชื่อว่า CloudWatchのカスタムウィジェットのLambda書いてみた โดยเจ้าของบทความนี้คือ คุณ eetann (えーたんさん) เป็นคนญี่ปุ่นครับ

เมื่อแปลจากภาษาญี่ปุ่นมาเป็นภาษาไทยแล้วผมได้เรียบเรียงเนื้อหาใหม่เพื่อให้เข้าใจง่ายขึ้น เนื่องจากบางครั้งอาจมีการอัปเดตข้อมูลใหม่ จึงมีความจำเป็นต้องอัปเดตให้เป็นข้อมูลปัจจุบัน


วันก่อนผมได้รู้ว่าสามารถแสดงผลลัพธ์การดำเนินการของ Lambda เป็น Custom widget ใน CloudWatch dashboard ได้ ผมจึงได้ศึกษาและทดลองการใช้งานมาแล้ว ครั้งนี้จึงอยากจะมาสร้าง widget ที่เปลี่ยนเนื้อหาการแสดงตามปุ่มที่คลิก

หน้าจอ Confirmation จะปรากฏขึ้นมาเมื่อทำการคลิกปุ่ม

สิ่งที่ต้องเตรียม

※EC2 Instance Amazon Linux 2023 ที่ติดตั้ง CDK TypeScript project (custom-widget-lambda-sample) (หรือหากมีสภาพแวดล้มอื่นก็สามารถใช้ได้เช่นกัน)

ดูตัวอย่างได้ที่ลิงก์ด้านล่างนี้โดยอ่านหมายเหตุก่อนเริ่มดำเนินการ

หมายเหตุ: สำหรับขั้นตอนของหัวข้อด้านล่างนี้ยังไม่ต้องทำ เนื่องจากต้องแก้ไขไฟล์บางส่วนให้เป็นภาษาไทยก่อน แล้วจึงดำเนินการ Deploy โปรเจกต์ตามขั้นตอนด้านล่างนี้ และเพื่อให้ง่ายต่อการดำเนินการ ผมจะเขียนลิงก์ไว้ให้ในหัวข้อ Deploy CDK ไปยัง Lambda อีกทีครับ

เมื่อเตรียมสภาพแวดล้อมเสร็จแล้วให้เริ่มทำขั้นตอนถัดไปได้เลยครับ

แก้ไข Code ในไฟล์

รายละเอียดเกี่ยวกับไฟล์มีดังนี้

  • ไฟล์ lib/custom-widget-lambda-sample-stack.ts (ไม่ต้องทำการแก้ไข)
  • ไฟล์ lambda/custom-widget-function.ts (ให้แก้ไขโดยคลิกขยายเพื่อดู Codeและคัดลอกโค้ดนี้ไปวางแทนที่โค้ดเก่าทั้งหมด เนื่องจากไฟล์เดิมมีเนื้อหาบางส่วนเป็นภาษาญี่ปุ่น)

ครั้งนี้เป็นการสร้าง Lambda โดยใช้ NodejsFunction บน CDK

lib/custom-widget-lambda-sample-stack.ts

import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";

export class CustomWidgetLambdaSampleStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    new NodejsFunction(this, "CustomWidgetLambda", {
      entry: "lambda/custom-widget-function.ts",
    });
  }
}


ต่อไปนี้เป็นการเขียนโดยละเว้นบางส่วนเพื่อให้เข้าใจกระบวนการทั้งหมดที่เขียนใน Lambda ครับ

lambda/custom-widget-function.ts

const DOCS = `
สามารถเขียน Document ใน Markdown ได้
`;

export const handler = async (event: Event, context: Context) => {
  if (event.describe) {
    return DOCS;
  }

  // รับค่าที่ส่งมา
  const person = event.person || "";
  const go = event.go || false;

  return `
    <div>
      ส่งเมื่อคลิกโดยใช้แท็ก a และ cwdb-action
      แสดงข้อความที่สร้างจากค่าที่ส่งมา
    </div>
  `;
};

เนื่องจาก Code มีความยาว จึงเขียนฉบับเต็มซ่อนไว้ที่ด้านล่างนี้

คลิกขยายเพื่อดู Code
type Event = {
  describe: boolean;
  widgetContext: any;
  person: string;
  go: boolean;
};

type Context = {
  invokedFunctionArn: string;
};

const DOCS = `
## custom-widget-function
สามารถเขียน Document ใน Markdown ได้ เช่น **ตัวหนา**, *ตัวเอียง*, ~~ขีดทับ~~, \`Inline code\` เป็นต้น

* รายการ
* รายการ
  * รายการย่อย
* รายการ


1. รายการลำดับหมายเลข
2. รายการลำดับหมายเลข
  1. รายการลำดับหมายเลขย่อย
3. รายการลำดับหมายเลข


[ลิงก์](https://dev.classmethod.jp/author/tinnakorn-maneewong/)

![Image](https://raw.githubusercontent.com/eetann/choomame/main/public/icons/icon-128x128.png)

---

\`\`\` python
print("piyopiyo")
\`\`\`

### Table ก็สามารถเขียนได้
name | ok
---|---
Kerry | true
Johnny | false
Goro | false

| name | ok |
|---|---|
| Kerry | true |
| Johnny | false |
| Goro | false |

`;

export const handler = async (event: Event, context: Context) => {
  if (event.describe) {
    return DOCS;
  }

  const timestamp = new Date();
  const person = event.person || "";
  const go = event.go || false;

  let response = "";

  const people = [
    { name: "Kerry", ok: true, cls: "btn kerry" },
    { name: "Johnny", ok: false, cls: "btn btn-primary" },
    { name: "Goro", ok: false, cls: "btn" },
  ];

  const rows = people
    .map(({ name, ok, cls }) => {
      return `
      <tr>
        <td>${name}</td>
        <td>
          <a class="${cls}">เลือก</a>
          <cwdb-action
            action="call"
            endpoint="${context.invokedFunctionArn}" 
            confirmation="คุณจะเลือก ${name} จริงไหม?">
            { "person": "${name}", "go": ${ok} }
          </cwdb-action>
        </td>
        </td>
      </tr>
    `;
      // });
    })
    .join("");

  if (person) {
    if (go) {
      response = `${person} "ถ้าถึงแล้วจะบอก"`;
    } else {
      response = `${person} "ถ้าว่างก็จะไป"`;
    }
  }

  const _event = event;
  _event.widgetContext.accountId = 123456789012;

  return `
    <div>
      <table>
        ${rows}
      </table>
      <p>${timestamp}</p>
      <p>${response}</p>
      <pre>${JSON.stringify(_event, null, 2)}</pre>
    </div>
    <style>
    .kerry {
      color: white !important;
      background-color: black !important;
    }
    </style>
  `;
};

Deploy CDK ไปยัง Lambda

ก่อนทำการ Deploy CDK ไปยัง Lambda ให้ตรวจสอบไฟล์ lambda/custom-widget-function.ts อีกครั้งเพื่อให้มั่นใจว่าเราแก้ไขไฟล์ดังกล่าวไปแล้ว

เมื่อมั่นใจว่าแก้ไขไฟล์ lambda/custom-widget-function.ts เสร็จแล้ว ให้ทำการ Deploy โปรเจกต์ตามลิงก์ด้านล่างนี้ดังที่กล่าวไว้ในหมายเหตุตอนแรก

เมื่อ Deploy โปรเจกต์เสร็จแล้ว Lambda จะถูกสร้างขึ้นแบบนี้ครับ

เตรียม Dashboard และสร้าง Custom widget ใน CloudWatch

เตรียม Dashboard ใน CloudWatch

ก่อนอื่นเราต้องมี Dashboard ใน CloudWatch ก่อนครับ
ครั้งนี้ผมจะใช้ Dashboard ชื่อว่า custom-widget-lambda-samble

ไปที่ช่องค้นหา พิมพ์ชื่อบริการ CloudWatch แล้วเลือก CloudWatch

คลิก Dashboards จากเมนูด้านซ้าย

คลิก Create dashboard ปุ่มไหนก็ได้

ป้อนชื่อลงใน "Dashboard name" ตามต้องการ เช่น custom-widget-lambda-samble แล้วคลิก Create dashboard

สร้าง Custom widget

เมื่อสร้างเสร็จแล้ว โดยปกติจะแสดงเป็นหน้าจอเริ่มต้นแบบนี้ (หากไม่แสดงหน้าจอแบบนี้ให้คลิก + ด้านขวาบน)
แล้วเลือก Custom widget แล้วคลิก Next

คลิก Next

แล้วทำการตั้งค่าดังนี้
Lambda function: CustomWidgetLambdaSampleS-CustomWidgetLambdaxxxxxx-xxxxxxxxxxxx (เลือกชื่อโปรเจกต์ที่ Deploy ในตอนแรก)
Documentation: Get documentation (เมื่อคลิกแล้ว Document จะแสดงตามภาพ)
Preview: Preview widget (เมื่อคลิกแล้ว Preview จะแสดงตามภาพ)
Widget title: นี่คือหัวข้อ (ป้อนชื่อหัวข้อตามต้องการ)

เกี่ยวกับ Code

ผมจะแบ่งอธิบาย Code แยกทีละส่วนครับ

ก่อนอื่น ในส่วนของ if แรกที่ใช้ event.describe คือการแสดง Document

if (event.describe) {
  return DOCS;
}

Document สามารถเขียนด้วย Markdown และแสดงเมื่อเพิ่มหรือแก้ไข Custom widget ได้

เมื่อได้ลองเขียน Document ดูจริงๆ นอกจากการตกแต่งข้อความและรายการต่างๆ แล้ว ยังสามารถแสดงภาพทั่วไปได้
ถ้าทำการตั้งค่าและตรวจสอบเสร็จแล้ว ให้คลิก Add widget ได้เลย

ต่อไป เป็นการแทนที่ person และ go เป็นตัวแปรจาก event argument แรกของ Lambda handler
และ event.person และ event.go จะใส่ค่าที่ส่งไปโดยใช้แท็ก cwdb-action ของ HTML

const person = event.person || "";
const go = event.go || false;


แท็ก cwdb-action เป็นตัวกำหนดการกระทำเมื่อเลือกตัวเลือก (Kerry, Johnny, Goro)
ในตัวอย่างครั้งนี้ จะเกิด Action ก็ต่อเมื่อทำการคลิกปุ่มตัวเลือกของแท็ก a "การเรียก Lambda (=Lambda เดียวกัน) ที่แสดงใน Custom widget" ซึ่งหน้าที่จริงๆ คือการส่งค่าที่เขียนในแท็ก cwdb-action

<a class="${cls}">เลือก</a>
<cwdb-action
  action="call"
  endpoint="${context.invokedFunctionArn}" 
  confirmation="คุณจะเลือก ${name} จริงไหม?">
  { "person": "${name}", "go": ${ok} }
</cwdb-action>

จึงทำให้หน้าจอ Confirmation ปรากฏขึ้นมาโดยใช้ attribute confirmation

ผมได้กำหนดให้แสดงเป็นปุ่มใน class ของแท็ก a

นอกจาก style btn, btn-primary ที่มีให้ใน Custom widget แล้ว เรายังสามารถตั้งค่า style ในแบบของเราได้ด้วยแท็ก style

ผมได้ลองแสดงเนื้อหาของ event argument แรกของ Lambda handler โดยใช้แท็ก pre จึงได้รู้ว่ามี arguments ที่ 2 และ 3 ใน JSON.stringify()

<pre>${JSON.stringify(_event, null, 2)}</pre>

ด้านล่างนี้คือเนื้อหาที่แสดง นอกจาก person และ go ที่ส่งด้วยแท็ก cwdb-action แล้ว ยังมีหลายค่าที่ใส่เข้าไปใน widgetContext key และดูเหมือนว่าขนาดของ widget เป็นขนาดที่นิยมใช้กันอีกด้วย

{
  "person": "Kerry",
  "go": true,
  "widgetContext": {
    "dashboardName": "custom-widget-lambda-sample",
    "widgetId": "widget-1",
    "domain": "https://ap-northeast-1.console.aws.amazon.com",
    "accountId": 123456789012,
    "locale": "ja",
    "timezone": {
      "label": "Local",
      "offsetISO": "+09:00",
      "offsetInMinutes": -540
    },
    "period": 300,
    "isAutoPeriod": true,
    "timeRange": {
      "mode": "relative",
      "start": 1668725221893,
      "end": 1668736021893,
      "relativeStart": 10800004
    },
    "theme": "light",
    "linkCharts": true,
    "title": "นี่คือหัวข้อ",
    "params": null,
    "forms": {
      "all": {}
    },
    "width": 1363,
    "height": 764
  }
}

จัดการกับ Comma ปริศนาที่แสดงขึ้นมา

ผมได้สร้าง character string ที่มีแท็กต่างๆ เช่น tr จากรายการตามด้านล่างนี้เพื่อแสดง Table

const rows = people
  .map(({ name, ok, cls }) => {
    return `
    <tr>
      <td>${name}</td>
      <td>
        <a class="${cls}">เลือก</a>
        <cwdb-action
          action="call"
          endpoint="${context.invokedFunctionArn}" 
          confirmation="คุณจะเลือก ${name} จริงไหม?">
          { "person": "${name}", "go": ${ok} }
        </cwdb-action>
      </td>
      </td>
    </tr>
  `;
    // });
  })
  .join("");

หากลืมเชื่อมต่อด้วย .join("") ที่อยู่บรรทัดสุดท้ายนี้ ก็จะมี Comma แสดงที่หน้า Table ดังนั้นโปรดอย่าลืมเชื่อมต่อไว้ครับ

เวลาการอัปเดต Custom widget

การอัปเดต Custom widget ไม่ใช่เพียงแค่ตอนที่โหลดหน้าเว็บหรือกดปุ่มรีเฟรชเพื่ออัปเดตด้วยตนเองเท่านั้น แต่เราสามารถควบคุมได้ว่าจะอัปเดตตามเวลาที่เกิดการกระทำจาก 3 ตัวเลือกของ Update on(Refresh, Resize, Time Range) หรือไม่ และการควบคุมเวลาของการอัปเดตสามารตั้งค่าในขณะที่สร้างหรือแก้ไข Custom widget ได้

เมื่อกดปุ่มรีเฟรชอัตโนมัติบน Dashboard

Custom widget สามารถควบคุมได้ว่าจะอัปเดตเมื่อกดปุ่มรีเฟรชอัตโนมัติบน Dashboard หรือไม่

เมื่อปรับขนาด Widget

Custom widget สามารถควบคุมได้ว่าจะอัปเดตเมื่อปรับขนาดของ widget หรือไม่

ดูเหมือนว่าขนาดของ widget จะรวมอยู่ใน width และ height ของ widgetContext ของ event argument แรกของ Lambda handler

เมื่อเปลี่ยนช่วงเวลา Dashboard

Custom widget สามารถควบคุมได้ว่าจะอัปเดตเมื่อเปลี่ยนช่วงเวลา Dashboard หรือไม่

ดูเหมือนว่าช่วงเวลาของ dashboard จะรวมอยู่ใน widgetContext.timeRange ของ event argument แรกของ Lambda handler

อย่าลืม Save Dashboard

เมื่อสร้าง Custom widget เสร็จแล้ว ให้บันทึกโดยกดปุ่ม Save ที่แสดงอยู่ด้านขวาบนของ dashboard และหากทำการแก้ไขก็ต้องกดปุ่ม Save ทุกครั้ง แม้แต่ทางผู้เขียนเองก็ลืมค่อนข้างบ่อย ดังนั้นอย่าลืมที่จะ Save นะครับ

นอกจากนี้หากไม่ต้องการกดปุ่ม Save ด้วยตัวเอง สามารถเปิดใช้งานเป็น Autosave: On ได้ครับ

เมื่อลองแก้ไขบางอย่างแล้วจะแสดง ✅ Saved แบบนี้

แต่ข้อเสียคือเมื่อมีการรีโหลดหน้าจอ จะทำให้สถานะกลับมาเป็น Autosave: Off ดังนั้นให้ระวังในส่วนนี้ด้วยครับ

การลบ AWS Resource

การลบ CloudWatch Dashboard

Service name Function name
CloudWatch Dashboard

เข้าไปที่ Service "CloudWatch > Dashboards" ค้นหาและเลือกอันที่ต้องการลบ คลิก Delete และยืนยันการลบตามคำแนะนำ

การลบ AWS Resource ที่ใช้สร้างและ Deploy CDK ไปยัง Lambda

สำหรับวิธีการลบ AWS Resource ที่ใช้สร้างและ Deploy CDK ไปยัง Lambda ตามที่แสดงในตารางทั้งหมด ดูที่ลิงก์ด้านล่างนี้

Service name Function name
CloudFormation Stacks
Lambda Functions
EC2 Instances
IAM Roles

ดูวิธีการลบที่หัวข้อนี้: การลบ AWS Resource

ดูโค้ดตัวอย่างได้ที่ลิงก์ GitHub ด้านล่างนี้ (ในโปรเจกต์มีข้อความบางส่วนเป็นภาษาญี่ปุ่น แนะนำให้แก้ไขเป็นภาษาไทยก่อน Deploy)

Link อ้างอิง

บทความต้นฉบับ

แปลโดย: POP จากบริษัท Classmethod (Thailand) ครับ !