Data leaks เป็นปัญหาใหญ่บนอินเทอร์เน็ต ตาม Statista ในปี 2022 มีผู้ได้รับผลกระทบจาก data leak มากกว่า 400 ล้านคน ไ никтоไม่ต้องการข้อมูลของตนรั่วไหล ทำให้การสร้างแอปพลิเคชันที่ปลอดภัยและเคารพความเป็นส่วนตัวสำคัญมากในอุตสาหกรรมหลายแห่ง
หนึ่งในวิธีต่อสู้กับ leaks คือกรองข้อมูลที่มีปัญหาก่อนส่งมอบไปยังบริการอื่นหรือผู้ใช้ วิธีนี้ต้องการ filters ที่ทันสมัย เพื่อให้ข้อมูลไม่หลุดพ้น และ infrastructure low latency เพื่อให้ผลกระทบต่อประสิทธิภาพน้อยที่สุด
Upstash for Redis® และ Vercel Edge Functions รวมกันเป็นทีมที่แข็งแกร่งแก้ปัญหานี้ได้พร้อมตอบสนองทั้งสองข้อความ ทั้งสอง low-latency serverless solutions นี้สามารถ deploy ใกล้ผู้ใช้ได้ ด้วยคุณสมบัติ cron ใหม่ของ Vercel เราสามารถรับรองได้ว่า filter data จะได้รับการอัปเดตเป็นประจำ
เพื่อสาธิตว่าตัวกรองแบบนี้ทำงานอย่างไร เราจะสร้าง frontend และ backend ที่ใช้ serverless edge technology นี้กรอง profanities (คำหยาบ/คำไม่เหมาะสม)
ฟีเจอร์
แอปจะใช้ฟีเจอร์ cron ของ Vercel อัปเดต Upstash for Redis database ด้วยคำปัจจุบันจาก remote API
เราจะมี 3 วิธีในการรับข้อมูล:
- ส่งเว็บไซต์พร้อมข้อความที่กรองแล้ว
- ส่ง JSON พร้อมข้อความที่กรองแล้วจาก data store
- รับข้อความและส่ง JSON พร้อมข้อความที่กรองแล้ว
เทคโนโลยี
เราจะสร้างแอปด้วย Next.js และ deploy บน Vercel เพื่อให้ได้ seamless serverless development experience เมื่อใช้ edge functions
ใช้ Upstash for Redis® เป็น data storage เพราะ low latency และใช้งานง่าย
ทั้งสองบริการมี free tiers และ on-demand pricing
ข้อกำหนดเบื้องต้น
ต้องมีบัญชีสำหรับบริการ:
- GitHub เพื่ออัปโหลดโค้ดให้ Vercel ดาวน์โหลดและ deploy
- Vercel สำหรับ host แอปพลิเคชัน พร้อม home page และ edge functions
- Upstash สำหรับเก็บรายการคำที่ต้องการกรอง
การนำไปใช้
เริ่มต้นด้วยการสร้าง GitHub repository ใหม่ และตรวจสอบ ให้เช็ก "Add a README file" เพื่อไม่ให้ว่างเปล่า เนื่องจาก repository ไม่ว่าง GitHub จึงอนุญาตให้เริ่ม Codespace ซึ่งมาพร้อม Node.js และ Git-to-GitHub connection ที่ตั้งค่าไว้แล้ว
Figure 1: Start Codespace
ก่อนอื่น สร้าง Next.js project ใหม่และติดตั้ง Upstash Redis client:
$ npx create-next-app@latest --typescript
$ npm i @upstash/redis
Implementing the Refresh Function
ฟีเจอร์แรกคือฟังก์ชัน refresh รายการคำหยาบ สร้างไฟล์ใหม่ที่ pages/api/refresh-list.ts เนื้อหาดังนี้:
File pages/api/refresh-list.ts:
import { Redis } from "@upstash/redis";
export const config = {
runtime: "edge"
};
const redisClient = new Redis({
url: process.env.UPSTASH_REDIS_URL,
token: process.env.UPSTASH_REDIS_TOKEN,
});
export default async function handler() {
const wordResponse = await fetch(
"https://raw.githubusercontent.com/kay-is/List-of-Dirty-Naughty-Obscene-and-Otherwise-Bad-Words/master/en",
);
const words = await wordResponse.text();
const redisCommands = redisClient.pipeline();
words
.trim()
.split("\n")
.forEach((word) => redisCommands.sadd("words", word));
await redisCommands.exec();
}
เริ่มต้นด้วยการ config function ให้รันบน edge แม้จะไม่จำเป็นสำหรับฟังก์ชันที่รันใน background แต่ Vercel Edge Functions ขับเคลื่อนด้วย Cloudflare Workers ทำให้ใช้ fetch method ได้ ซึ่ง Node.js รองรับไม่ได้แบบ native
Handler จะโหลด text file ที่มีคำหยาบเข้า Upstash Redis ฟีเจอร์ pipeline รับประกันว่าส่งคำสั่ง Redis ทั้งหมดใน request เดียว
เราใช้ set เก็บคำ เพื่อไม่ให้ซ้ำ และอนุญาตให้โหลดคำทั้งหมดเป็น array of strings ด้วยคำสั่งเดียวภายหลัง
หากต้องการกรองข้อมูลส่วนตัว สามารถใช้ account database ดึง emails, phone numbers, names, addresses ของผู้ใช้เป็นฐานสำหรับ filter
เพื่อบอก Vercel ให้ function นี้เป็น cron function ต้องสร้างไฟล์ vercel.json ใน project root เนื้อหาดังนี้:
File vercel.json:
{
"crons": [
{
"path": "/api/refresh-list",
"schedule": "0 10 * * *"
}
]
}
Config นี้จะทำให้ Vercel รัน refresh-list function ทุกวันเวลา 10:00 UTC
Free Vercel accounts รองรับ auto execution วันละ 1 ครั้ง สำหรับตัวอย่างพอ แต่ถ้าข้อมูลเปลี่ยนบ่อยกว่านี้ ควรเพิ่มอัตราการอัปเดต
Implementing the Filter Utility
ฟีเจอร์ต่อไปคือ utility function ที่จะ mask คำใน text เมื่อตรงกับคำใน database สร้างไฟล์ใหม่ที่ utils/word-filter.ts ใส่โค้ด:
File utils/word-filter.ts:
import { Redis } from "@upstash/redis";
const redisClient = new Redis({
url: process.env.UPSTASH_REDIS_URL,
token: process.env.UPSTASH_REDIS_TOKEN,
});
export async function filter(text: string) {
const filteredWords = await redisClient.smembers("words");
let maskedText = text;
for (let word of filteredWords)
maskedText = maskedText.replaceAll(new RegExp(word, "gi"), "[REDACTED]");
return maskedText;
}
ฟังก์ชันนี้ใช้ Upstash Redis client เหมือนกัน แต่ครั้งนี้โหลดข้อมูลที่บันทึกไว้ก่อนหน้า
เนื่องจากได้ array of strings สามารถ loop ผ่านและเรียก replacement function ที่แทนที่คำหยาบทุกคำใน text ด้วย "[REDACTED]"
ฟังก์ชันนี้ไม่สนใจประเภทของคำที่กรอง ในที่นี้คำคือ "not safe for work" แต่กระบวนการ filter ขึ้นอยู่กับข้อมูลที่เก็บไว้ก่อนหน้าเท่านั้น
Implementing the Home Page
เพื่อดู filter ทำงาน แทนที่เนื้อหา pages/index.ts ด้วย:
File pages/index.ts:
import Head from "next/head";
import { filter } from "@/utils/word-filter";
export const config = {
runtime: "experimental-edge"
};
interface HomeProps {
maskedText: string;
}
export default function Home(props: HomeProps) {
return (
<>
<Head>
<title>Text with Filtered Words</title>
</Head>
<div>
<h1>Text with Filtered Words</h1>
<p>{props.maskedText}</p>
</div>
</>
);
}
export async function getServerSideProps(): Promise<{ props: HomeProps }> {
const maskedText = await filter(
"He slipped and fell on his butt. Well, that wasn't very sexy."
);
return { props: { maskedText } };
}
Config รับรองว่าทุกอย่างรันบน edge รวมถึง server-side rendering ฟีเจอร์นี้ของ Vercel ยังเป็น experimental
ส่วนน่าสนใจคือฟังก์ชัน getServerSideProps ใช้ filter function จากก่อนหน้ากับ static text เรียกบน server เท่านั้น ดังนั้น unfiltered data ไม่มีโอกาสไปถึง client
ในแอปพลิเคชันจริง ข้อความนี้อาจมาจาก database ที่มี personal data ต้อง clean ก่อนแสดง
Implementing the First API Route
API route แรกทำงานเหมือน home page ส่ง JSON ไม่ใช่ HTML สร้างไฟล์ที่ pages/api/filtered-data.ts โค้ดนี้:
File pages/api/filtered-data.ts:
import { filter } from "@/utils/word-filter";
export const config = {
runtime: "edge"
};
export default async function handler() {
const maskedText = await filter(
"He slipped and fell on his butt. Well, that wasn't very sexy.",
);
return new Response(JSON.stringify({ text: maskedText }), {
status: 200,
headers: { "content-type": "application/json" },
});
}
อีกครั้ง runtime คือ edge เหมือนกับ getServerSideProps function ใช้ static text
Implementing the Second API Route
Route นี้รับ text ผ่าน request และส่ง filtered version กลับ สร้างไฟล์ใหม่ที่ pages/api/filter.ts ใส่โค้ด:
File pages/api/filter.ts:
import type { NextApiRequest } from "next";
import { filter } from "@/utils/word-filter";
export const config = {
runtime: "edge"
};
export default async function handler(request: NextApiRequest) {
const { text } = await new Response(request.body).json();
const maskedText = await filter(text);
return new Response(JSON.stringify({ text: maskedText }), {
status: 200,
headers: { "content-type": "application/json" },
});
}
ครั้งนี้ต้อง parse body เพื่อได้ text ที่จะกรอง ใน Vercel Edge Functions body เป็น ReadableStream ถ้าแปลงเป็น Response จะใช้ native JSON parser ดึงข้อมูลได้
หลังได้ข้อมูลจาก request ทุกอย่างทำงานเหมือนเดิม
Push Code Changes
ตอนนี้ implement เสร็จแล้ว ต้อง push โค้ดไป GitHub repository:
$ git add -A
$ git commit -m "Init"
$ git push
หลังจากนี้ โค้ดพร้อมออนไลน์สำหรับ Vercel ดาวน์โหลดและ deploy
Deployment
ต้องสร้าง Upstash Redis database เพื่อรับ credentials สำหรับ environment variables และ Vercel project
สร้าง Redis Database
สร้าง Redis database ใหม่ใน Upstash Console คลิกปุ่ม "Create database" Figure 2 แสดงการตั้งค่า สำหรับตัวอย่าง regional database พอ แต่ถ้ามีผู้ใช้กระจายทั่วโลกและต้องการ latency ต่ำ สามารถเลือก global type ได้
Figure 2: Create a new database
หลังสร้างเสร็จ หา URL และ token สำหรับ environment variables ภายใต้หมวด REST API ประมาณ Figure 3
Figure 3: Database credentials
สร้าง Vercel Project
เปิด Vercel Dashboard ใน browser คลิก "Create a New Project" ตรงกลาง หลังเชื่อม Vercel กับ GitHub เลือก repository ที่จะ import
เก็บ default configuration เพิ่ม environment variables ด้วย Upstash Redis credentials จากข้างบน Figure 4 แสดง UI การสร้างของ Vercel
Figure 4: Vercel project creation
ชื่อ environment variables คือ UPSTASH_REDIS_URL และ UPSTASH_REDIS_TOKEN ใช้ค่าจากขั้นตอนก่อนหน้าสร้าง
หลังคลิก "Deploy" Vercel จะดาวน์โหลดและ deploy โค้ดจาก GitHub repository
ทดสอบแอป
หลัง deploy แอปยังแสดงคำที่ไม่กรอง เพราะ cron job ยังไม่รัน แต่เรารันครั้งแรก manual ได้ คลิก "Continue to Dashboard" เลือกแท็บ "Cron Jobs"
จะเห็นฟังก์ชัน /api/refresh-list พร้อมปุ่ม "Run" คลิกได้
เมื่อฟังก์ชันเสร็จ ไปที่แท็บ "Project" คลิก URL ใดๆ ภายใต้ "Domain" จะเปิดเว็บไซต์พร้อมข้อความที่กรองแล้ว ควรหน้าตาแบบ Figure 5
Figure 5: Filtered website
เพิ่ม /api/filtered-data ต่อท้าย URL จะเห็นว่าทำงานสำหรับ API responses ด้วย ควรหน้าตาแบบนี้:
{
"text": "He slipped and fell on his [REDACTED]. Well, that wasn't very [REDACTED]y."
}
สุดท้าย ส่ง request ผ่าน cURL ไป /api/filter endpoint ได้ custom text ที่กรองแล้ว แทนที่ <PROJECT> ด้วย Vercel project ของคุณ
$ curl -X POST https://<PROJECT>.vercel.app/api/filter \
-H "Content-Type: application/json" \
-d '{"text":"He fell on his butt."}'
Response:
{
"text": "He fell on his [REDACTED]."
}
What's Next
หลัง tutorial นี้ คุณอาจสงสัยว่าจะเกิดอะไรขึ้นถ้าข้อมูลอัปเดตแต่ cron job ยังไม่อัปเดต database ดีมากที่สังเกตได้!
Cron job trigger refresh function แต่ยังเป็น regular API function สามารถเรียกได้อย่างอื่น
กรณี real data filter อาจต้องการ trigger function ตอบสนองต่อการเปลี่ยนแปลงข้อมูล แต่รายละเอียดการนำไปใช้ขึ้นอยู่กับ data storage ที่ใช้เป็นฐานสำหรับ filters จงจำไว้ตอน build!
Additional Resources
โปรเจกต์เต็มดูได้ที่ GitHub