<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Serverless on</title><link>https://blog.hungran.cloud/tags/serverless/</link><description>Recent content in Serverless on</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Wed, 14 Oct 2026 14:00:00 +0700</lastBuildDate><atom:link href="https://blog.hungran.cloud/tags/serverless/index.xml" rel="self" type="application/rss+xml"/><item><title>Chuyển blog từ Cloudflare Pages sang CloudFront + Lambda Web Functions</title><link>https://blog.hungran.cloud/posts/aws/cloudflare-pages-to-lambda-web-functions/</link><pubDate>Wed, 14 Oct 2026 14:00:00 +0700</pubDate><guid>https://blog.hungran.cloud/posts/aws/cloudflare-pages-to-lambda-web-functions/</guid><description>Ngày 12/10/2026 AWS ra mắt Lambda Web Functions — một kiểu Lambda mới mà code của mình chỉ việc listen trên một port như một HTTP server bình thường, không cần adapter, không cần handler event/context gì cả. Đọc announcement xong thì ngứa tay 😆, và nạn nhân đầu tiên là chính cái blog này.
Bài này ghi lại quá trình chuyển blog.hungran.cloud từ Cloudflare Pages sang CloudFront + Lambda Web Functions: kiến trúc, code, cách test local và mấy cái hố đã lỡ bước vào.</description><content>&lt;p>Ngày 12/10/2026 AWS ra mắt &lt;strong>Lambda Web Functions&lt;/strong> — một kiểu Lambda mới mà code của mình chỉ việc &lt;em>listen trên một port&lt;/em> như một HTTP server bình thường, không cần adapter, không cần handler &lt;code>event&lt;/code>/&lt;code>context&lt;/code> gì cả. Đọc announcement xong thì ngứa tay 😆, và nạn nhân đầu tiên là chính cái blog này.&lt;/p>
&lt;p>Bài này ghi lại quá trình chuyển &lt;code>blog.hungran.cloud&lt;/code> từ &lt;strong>Cloudflare Pages&lt;/strong> sang &lt;strong>CloudFront + Lambda Web Functions&lt;/strong>: kiến trúc, code, cách test local và mấy cái hố đã lỡ bước vào.&lt;/p>
&lt;h2 id="-lambda-web-functions-là-gì">🤔 Lambda Web Functions là gì?&lt;/h2>
&lt;p>Tóm tắt nhanh từ docs:&lt;/p>
&lt;ul>
&lt;li>Code là một &lt;strong>HTTP server Node.js&lt;/strong> bình thường (Express, Hono, Fastify, Next.js&amp;hellip; hay &lt;code>node:http&lt;/code> thuần đều được). Lambda báo cho mình host:port qua biến môi trường &lt;code>AWS_LAMBDA_HTTP_ENDPOINT&lt;/code>.&lt;/li>
&lt;li>Một execution environment phục vụ &lt;strong>nhiều request đồng thời&lt;/strong>, giống web server thật → ít cold start hơn hẳn so với mô hình 1 request / 1 environment.&lt;/li>
&lt;li>Mỗi endpoint có sẵn &lt;strong>domain HTTPS&lt;/strong> dạng &lt;code>xxxx.lambda-web.&amp;lt;region&amp;gt;.on.aws&lt;/code>, hỗ trợ &lt;strong>streaming&lt;/strong> response.&lt;/li>
&lt;li>Tính tiền theo request, &lt;strong>CPU thực sự dùng&lt;/strong>, memory trong suốt vòng đời environment và data transfer — thời gian ngồi chờ I/O gần như không tốn CPU.&lt;/li>
&lt;li>Có &lt;strong>revision&lt;/strong> (snapshot bất biến của code + config) và &lt;strong>endpoint&lt;/strong> route traffic tới 1–2 revision → canary / blue-green có sẵn (đúng bài &lt;a href="https://blog.hungran.cloud/posts/zero-downtime-deployment/">Zero-Downtime Deployment&lt;/a> hôm trước 😎).&lt;/li>
&lt;/ul>
&lt;p>Một vài giới hạn cần biết lúc mới ra mắt:&lt;/p>
&lt;ul>
&lt;li>Hiện chỉ có runtime &lt;strong>&lt;code>nodejs24.x&lt;/code>&lt;/strong>, chạy trên &lt;strong>Arm64&lt;/strong>.&lt;/li>
&lt;li>Region: N. Virginia, Ohio, Oregon, &lt;strong>Tokyo&lt;/strong>, Ireland → gần Việt Nam nhất là &lt;code>ap-northeast-1&lt;/code>.&lt;/li>
&lt;li>Endpoint &lt;strong>không gắn custom domain trực tiếp&lt;/strong> được — muốn dùng domain riêng thì phải đặt &lt;strong>CloudFront&lt;/strong> phía trước.&lt;/li>
&lt;li>Server phải bắt đầu listen trong &lt;strong>10 giây&lt;/strong> kể từ lúc khởi động.&lt;/li>
&lt;/ul>
&lt;h2 id="-kiến-trúc-trước-và-sau">🏗️ Kiến trúc: trước và sau&lt;/h2>
&lt;p>&lt;strong>Trước:&lt;/strong>&lt;/p>
&lt;pre>&lt;code>git push ─▶ Cloudflare Pages (build Hugo) ─▶ Cloudflare CDN ─▶ blog.hungran.cloud
&lt;/code>&lt;/pre>&lt;p>&lt;strong>Sau:&lt;/strong>&lt;/p>
&lt;pre>&lt;code>git push ─▶ GitHub Actions (Hugo build → zip → aws lambda-web deploy)
│
browser ─▶ Cloudflare DNS (grey cloud) ─▶ CloudFront (ACM cert, cache)
│ cache miss
▼
Lambda Web Function endpoint (Tokyo, nodejs24.x)
└─ node index.js ── serve ./public
&lt;/code>&lt;/pre>&lt;p>Hugo vẫn là static site generator, không đổi gì ở &lt;code>content/&lt;/code> hay theme. Thứ thay đổi là &lt;strong>ai serve&lt;/strong> cái thư mục &lt;code>public/&lt;/code>.&lt;/p>
&lt;h2 id="-server-120-dòng-không-dependency">🧑‍💻 Server: ~120 dòng, không dependency&lt;/h2>
&lt;p>Vì blog là static site nên không cần Express. Một file &lt;code>index.js&lt;/code> dùng &lt;code>node:http&lt;/code> thuần là đủ. Phần quan trọng nhất chỉ có vài dòng:&lt;/p>
&lt;div class="highlight">&lt;pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-js" data-lang="js">&lt;span style="color:#66d9ef">const&lt;/span> [&lt;span style="color:#a6e22e">HOST&lt;/span>, &lt;span style="color:#a6e22e">PORT&lt;/span>] &lt;span style="color:#f92672">=&lt;/span> (&lt;span style="color:#a6e22e">process&lt;/span>.&lt;span style="color:#a6e22e">env&lt;/span>.&lt;span style="color:#a6e22e">AWS_LAMBDA_HTTP_ENDPOINT&lt;/span> &lt;span style="color:#f92672">||&lt;/span> &lt;span style="color:#e6db74">&amp;#39;localhost:3000&amp;#39;&lt;/span>).&lt;span style="color:#a6e22e">split&lt;/span>(&lt;span style="color:#e6db74">&amp;#39;:&amp;#39;&lt;/span>);
&lt;span style="color:#a6e22e">server&lt;/span>.&lt;span style="color:#a6e22e">listen&lt;/span>(Number(&lt;span style="color:#a6e22e">PORT&lt;/span>), &lt;span style="color:#a6e22e">HOST&lt;/span>, () =&amp;gt; &lt;span style="color:#a6e22e">console&lt;/span>.&lt;span style="color:#a6e22e">log&lt;/span>(&lt;span style="color:#e6db74">`serving on &lt;/span>&lt;span style="color:#e6db74">${&lt;/span>&lt;span style="color:#a6e22e">HOST&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">:&lt;/span>&lt;span style="color:#e6db74">${&lt;/span>&lt;span style="color:#a6e22e">PORT&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">`&lt;/span>));
&lt;span style="color:#a6e22e">process&lt;/span>.&lt;span style="color:#a6e22e">on&lt;/span>(&lt;span style="color:#e6db74">&amp;#39;SIGTERM&amp;#39;&lt;/span>, () =&amp;gt; &lt;span style="color:#a6e22e">server&lt;/span>.&lt;span style="color:#a6e22e">close&lt;/span>(() =&amp;gt; &lt;span style="color:#a6e22e">process&lt;/span>.&lt;span style="color:#a6e22e">exit&lt;/span>(&lt;span style="color:#ae81ff">0&lt;/span>)));
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Phần còn lại là những thứ mà Cloudflare Pages trước đây làm hộ mình, giờ phải tự làm:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Pretty URL kiểu Hugo&lt;/strong>: &lt;code>/about&lt;/code> → 301 sang &lt;code>/about/&lt;/code> → trả &lt;code>about/index.html&lt;/code>&lt;/li>
&lt;li>&lt;strong>Custom 404&lt;/strong> dùng luôn &lt;code>404.html&lt;/code> của theme&lt;/li>
&lt;li>&lt;strong>ETag / 304&lt;/strong> để browser và CloudFront revalidate rẻ&lt;/li>
&lt;li>&lt;strong>Brotli / gzip&lt;/strong> cho text (HTML, CSS, XML, SVG)&lt;/li>
&lt;li>&lt;strong>Cache-Control&lt;/strong> theo loại file: HTML &lt;code>s-maxage=300&lt;/code>, asset &lt;code>max-age=86400, s-maxage=604800&lt;/code>&lt;/li>
&lt;li>&lt;strong>Chặn path traversal&lt;/strong> (&lt;code>/../../etc/passwd&lt;/code> → 404)&lt;/li>
&lt;li>&lt;strong>Redirect URL viết hoa&lt;/strong> (menu cũ có &lt;code>/About&lt;/code>, Hugo lại xuất ra &lt;code>/about/&lt;/code>)&lt;/li>
&lt;li>&lt;code>/healthz&lt;/code> cho smoke test&lt;/li>
&lt;/ul>
&lt;h2 id="-static-content-nằm-ở-đâu">📦 Static content nằm ở đâu?&lt;/h2>
&lt;p>Không có S3 bucket riêng cho website. Toàn bộ &lt;code>public/&lt;/code> được &lt;strong>đóng gói chung với &lt;code>index.js&lt;/code>&lt;/strong> thành một file zip:&lt;/p>
&lt;ol>
&lt;li>&lt;code>hugo --minify&lt;/code> → &lt;code>public/&lt;/code>&lt;/li>
&lt;li>Copy &lt;code>index.js&lt;/code> + &lt;code>package.json&lt;/code> + &lt;code>public/&lt;/code> vào một thư mục build → zip&lt;/li>
&lt;li>&lt;code>aws lambda-web deploy&lt;/code> upload zip lên S3 bucket (có versioning) mà CLI tự tạo&lt;/li>
&lt;li>Lambda tạo &lt;strong>revision&lt;/strong> từ zip đó; mỗi environment có một bản copy trên disk local và serve trực tiếp&lt;/li>
&lt;/ol>
&lt;p>Ưu điểm: code và content &lt;strong>deploy và rollback cùng nhau&lt;/strong> — quay về revision cũ là quay về đúng bài viết cũ.&lt;/p>
&lt;p>Nhược điểm: package hiện tại &lt;strong>26 MB&lt;/strong> (120 file), mà ảnh chiếm khoảng &lt;strong>96%&lt;/strong> (PNG ~17 MB, JPEG ~8 MB), còn HTML/CSS/XML chưa tới 1 MB 😅. Sửa một dấu phẩy cũng phải upload lại cả 26 MB. Bước tiếp theo chắc sẽ là convert ảnh sang WebP, hoặc tách &lt;code>posts/images/&lt;/code> ra S3 sau cùng một CloudFront.&lt;/p>
&lt;h2 id="-deploy">🚀 Deploy&lt;/h2>
&lt;p>Lần đầu deploy từ máy local, một lệnh là đủ:&lt;/p>
&lt;div class="highlight">&lt;pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-bash" data-lang="bash">aws lambda-web deploy &lt;span style="color:#ae81ff">\
&lt;/span>&lt;span style="color:#ae81ff">&lt;/span> --name blog-hungran-cloud &lt;span style="color:#ae81ff">\
&lt;/span>&lt;span style="color:#ae81ff">&lt;/span> --code .lambda-web-build &lt;span style="color:#ae81ff">\
&lt;/span>&lt;span style="color:#ae81ff">&lt;/span> --endpoint-name prod &lt;span style="color:#ae81ff">\
&lt;/span>&lt;span style="color:#ae81ff">&lt;/span> --create
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Lệnh này tự tạo S3 bucket (versioning), IAM execution role (chỉ có quyền ghi CloudWatch Logs), Web Function, revision đầu tiên và một endpoint &lt;code>HomeRegion&lt;/code> với &lt;code>autoDeploymentMode=LatestRevision&lt;/code> — tức là revision mới &lt;code>Active&lt;/code> xong là endpoint tự chuyển traffic sang.&lt;/p>
&lt;p>Tất cả được gói trong &lt;code>lambda-web/deploy.sh&lt;/code> (build Hugo → đóng gói → deploy → in ra domain của endpoint). CI chạy đúng script đó trên GitHub Actions, đăng nhập AWS bằng &lt;strong>GitHub OIDC&lt;/strong>, không lưu access key nào trong repo.&lt;/p>
&lt;h2 id="-cloudfront-phía-trước">🌐 CloudFront phía trước&lt;/h2>
&lt;p>Vì endpoint không gắn custom domain được nên cần thêm CloudFront:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Origin&lt;/strong>: domain của endpoint, HTTPS only&lt;/li>
&lt;li>&lt;strong>Cache policy&lt;/strong>: &lt;code>CachingOptimized&lt;/code> — tôn trọng &lt;code>Cache-Control&lt;/code> từ server&lt;/li>
&lt;li>&lt;strong>Origin request policy&lt;/strong>: &lt;code>AllViewerExceptHostHeader&lt;/code> — ⚠️ &lt;strong>không forward header &lt;code>Host&lt;/code>&lt;/strong>, nếu không endpoint sẽ không nhận ra request&lt;/li>
&lt;li>&lt;strong>Certificate&lt;/strong>: ACM ở &lt;strong>us-east-1&lt;/strong> (bắt buộc với CloudFront), validate bằng CNAME trên Cloudflare DNS&lt;/li>
&lt;li>&lt;strong>Cloudflare DNS&lt;/strong>: &lt;code>blog&lt;/code> → CNAME tới &lt;code>dxxxx.cloudfront.net&lt;/code>, để &lt;strong>grey cloud (DNS only)&lt;/strong> — bật proxy cam lên là chồng 2 CDN, rắc rối thêm phần certificate và header&lt;/li>
&lt;/ul>
&lt;p>Với cache như trên, phần lớn request được CloudFront trả về luôn, Lambda chỉ nhận cache miss → với blog cá nhân thì chi phí compute gần như không đáng kể.&lt;/p>
&lt;h2 id="-test-local-thế-nào">🧪 Test local thế nào?&lt;/h2>
&lt;p>Không có &lt;code>sam local&lt;/code> hay emulator nào cho Web Functions cả. Nhưng vì code chỉ là một HTTP server, cách AWS khuyến nghị là &lt;strong>chạy đúng cái package sẽ deploy&lt;/strong> với biến môi trường được set sẵn:&lt;/p>
&lt;div class="highlight">&lt;pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4">&lt;code class="language-bash" data-lang="bash">AWS_LAMBDA_HTTP_ENDPOINT&lt;span style="color:#f92672">=&lt;/span>127.0.0.1:3000 node index.js
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Mình gói lại thành &lt;code>lambda-web/local-test.sh&lt;/code>:&lt;/p>
&lt;ol>
&lt;li>Build Hugo, đóng gói và zip &lt;strong>y hệt&lt;/strong> &lt;code>deploy.sh&lt;/code>&lt;/li>
&lt;li>Giải nén vào một thư mục trống — không có repo, không có dev dependency&lt;/li>
&lt;li>Chạy trong container &lt;code>node:24&lt;/code> &lt;code>linux/arm64&lt;/code> (cùng Node version và CPU arch với Lambda) nếu có Docker/Colima&lt;/li>
&lt;li>Fail nếu server không listen trong 10 giây (giống giới hạn của Lambda)&lt;/li>
&lt;li>Bắn 17 HTTP check (trang chủ, bài viết, phân trang, tag, RSS, sitemap, favicon, CV PDF, 404, path traversal, &lt;code>/healthz&lt;/code>&amp;hellip;) + kiểm tra favicon có trong &lt;code>&amp;lt;head&amp;gt;&lt;/code>&lt;/li>
&lt;li>200 request với concurrency 50 để chắc một environment chịu được nhiều request cùng lúc&lt;/li>
&lt;/ol>
&lt;pre>&lt;code>==&amp;gt; Result: 19 passed, 0 failed
&lt;/code>&lt;/pre>&lt;p>Những thứ local không test được — IAM, trạng thái revision/endpoint, scaling, CloudFront — thì để smoke test trong CI kiểm sau khi deploy.&lt;/p>
&lt;h2 id="-những-cái-hố">🕳️ Những cái hố&lt;/h2>
&lt;p>&lt;strong>1. Hugo version.&lt;/strong> Blog được viết cho &lt;strong>Hugo 0.74&lt;/strong> (2020). Máy mình cài Hugo mới nhất qua Homebrew (0.158+) và build&amp;hellip; nổ ngay:&lt;/p>
&lt;pre>&lt;code>ERROR ... rss.xml:20:50: executing &amp;quot;rss.xml&amp;quot; at &amp;lt;.Site.Author.email&amp;gt;:
can't evaluate field Author in type page.Site
&lt;/code>&lt;/pre>&lt;p>Hugo mới đã bỏ &lt;code>.Site.Author&lt;/code>. Tệ hơn, có một lỗi &lt;strong>im lặng&lt;/strong>: Hugo mới không còn đọc key &lt;code>paginate&lt;/code> nữa → &lt;code>/page/2/&lt;/code> biến mất mà không báo lỗi gì 😱.&lt;/p>
&lt;p>Cách xử lý:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Pin version&lt;/strong>: &lt;code>lambda-web/hugo.sh&lt;/code> tự tải đúng Hugo 0.74.0 vào &lt;code>.bin/&lt;/code> (gitignore) và mọi script đều đi qua nó → máy local, CI và site đang chạy dùng cùng một version. Hugo 0.74 chỉ có bản Intel cho macOS nên trên Apple Silicon cần Rosetta.&lt;/li>
&lt;li>&lt;strong>Forward-compat&lt;/strong>: override &lt;code>layouts/_default/rss.xml&lt;/code> ở project (không đụng vào theme submodule) để đọc &lt;code>params.author&lt;/code> thay cho &lt;code>.Site.Author&lt;/code>, và thêm &lt;code>[pagination] pagerSize = 5&lt;/code> bên cạnh &lt;code>paginate&lt;/code>. Giờ build được cả trên 0.74 lẫn 0.168.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>2. &lt;code>Active&lt;/code> chưa chắc đã chạy.&lt;/strong> Lambda &lt;strong>không kiểm tra&lt;/strong> server có thực sự listen hay không khi tạo revision → revision và endpoint vẫn có thể hiện &lt;code>Active&lt;/code> trong khi mọi request đều lỗi. Vì vậy CI luôn có bước smoke test sau deploy.&lt;/p>
&lt;p>&lt;strong>3. Package to vì ảnh.&lt;/strong> Như đã nói ở trên — 26 MB mà 96% là ảnh.&lt;/p>
&lt;h2 id="-so-sánh-nhanh">⚖️ So sánh nhanh&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tiêu chí&lt;/th>
&lt;th>Cloudflare Pages&lt;/th>
&lt;th>CloudFront + Lambda Web Functions&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Độ đơn giản&lt;/td>
&lt;td>Rất đơn giản, connect repo là xong&lt;/td>
&lt;td>Nhiều mảnh hơn: Lambda, CloudFront, ACM, IAM, CI&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Custom domain &amp;amp; TLS&lt;/td>
&lt;td>Có sẵn&lt;/td>
&lt;td>Qua CloudFront + ACM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Build&lt;/td>
&lt;td>Cloudflare build hộ&lt;/td>
&lt;td>Tự build trong GitHub Actions&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Logic phía server&lt;/td>
&lt;td>Pages Functions (Workers runtime)&lt;/td>
&lt;td>Node.js server đầy đủ, framework tuỳ chọn&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Rollout&lt;/td>
&lt;td>Deploy mới thay thế bản cũ&lt;/td>
&lt;td>Revision + traffic weight (canary, blue-green)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Chi phí cho blog cá nhân&lt;/td>
&lt;td>Free tier rộng rãi&lt;/td>
&lt;td>Gần như không đáng kể nhờ CloudFront cache, nhưng không phải free hoàn toàn&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Nói thật lòng: với một blog tĩnh thì &lt;strong>Cloudflare Pages vẫn là lựa chọn đơn giản hơn&lt;/strong>. Mình chuyển chủ yếu để học và thử một compute primitive mới, gom mọi thứ về AWS, và có sẵn nền tảng nếu sau này muốn thêm phần dynamic (search, API, SSR&amp;hellip;) mà không phải đổi hosting lần nữa.&lt;/p>
&lt;h2 id="-bonus">🌩️ Bonus&lt;/h2>
&lt;p>Nhân tiện đổi luôn favicon sang 🌩️ cho hợp với tên blog — để ý tab trình duyệt nhé 😄.&lt;/p>
&lt;h2 id="-tham-khảo">📚 Tham khảo&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/10/aws-lambda-web-functions/">AWS What&amp;rsquo;s New: AWS Lambda announces Lambda Web Functions&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://aws.amazon.com/blogs/aws/announcing-aws-lambda-web-functions-for-fast-consistent-web-app-performance-anywhere/">AWS News Blog: Announcing AWS Lambda Web Functions&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/web-functions.html">Lambda Web Functions developer guide&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/web-functions-testing.html">Testing Lambda Web Functions&lt;/a>&lt;/li>
&lt;/ul></content></item></channel></rss>