Case Study • AWS / Identity • 2026

Từ một static website đến internal portal có SSO

Một bài toán tưởng như chỉ cần host vài file HTML, nhưng cuối cùng lại dẫn tới những quyết định về ownership, deployment, identity và access control.

Có những hệ thống lúc mới nhìn vào tưởng khá đơn giản.

Một bộ HTML đã có sẵn. Dashboard cũng đã chạy được. Ở giai đoạn đầu, cách sử dụng vẫn còn khá thủ công: nội dung có lúc được mở từ máy cá nhân hoặc qua một đường tunnel, và khi cần chia sẻ thì file HTML lại được gửi qua email hoặc chat.

Cách đó đủ để thử nghiệm, nhưng không phải cách mình muốn giữ khi portal bắt đầu được nhiều người sử dụng hơn.

Không có một URL cố định 24/7. Việc phân quyền khó kiểm soát. Và quan trọng hơn, một bundle HTML không nhất thiết chỉ chứa “giao diện”. Phía sau dashboard vẫn có thể là dữ liệu nội bộ mà mình không muốn bị phát tán chỉ vì một file được forward nhầm.

Vì vậy câu hỏi không còn đơn giản là “Đưa mấy file HTML này lên đâu?”

Mà trở thành: “Làm sao biến nó thành một internal portal mà business vẫn publish được nội dung, người dùng có một URL ổn định và quyền truy cập vẫn được kiểm soát?”

Từ đó mới kéo theo những câu hỏi khác: Business sẽ publish bằng cách nào? Có cần cấp cho người làm nội dung quyền vào AWS không? Nếu S3 không public thì CloudFront lấy file bằng cách nào? Người dùng đã có Microsoft 365 và Entra ID rồi, tại sao lại phải tạo thêm một bộ account? Và nếu một người không còn thuộc nhóm được sử dụng portal thì quyền nên được gỡ ở đâu?

Rồi khi portal lớn dần, lại có thêm một câu hỏi khác: nếu bundle từ vài trăm MB tăng lên hơn một GB với hàng chục nghìn file, có thật mỗi lần deploy đều nên tải lại tất cả?

Project SGD Portal bắt đầu từ những câu hỏi như vậy. Và phần đáng nhớ nhất cuối cùng lại không nằm ở HTML.

Lúc đầu, architecture thực ra khá ngắn

Nếu chỉ nhìn bài toán ban đầu, luồng có thể hình dung khá đơn giản: business chuẩn bị nội dung trên SharePoint, deploy host lấy bundle xuống, đưa lên S3 rồi CloudFront phân phối cho người dùng.

Version đầu của SGD Portal: Content-to-Delivery từ Business, SharePoint, Deploy Host, Amazon S3, CloudFront đến User
Version đầu — khi bài toán chủ yếu vẫn là đưa một published bundle từ SharePoint lên AWS.

Ở thời điểm đó, câu chuyện vẫn khá gần với một static website thông thường.

Nhưng khi bắt đầu nghĩ đến cách hệ thống sẽ được vận hành sau go-live, mình nhận ra một câu hỏi quan trọng hơn việc chọn AWS service nào:

Ai nên chịu trách nhiệm cho việc gì?

Business nên publish nội dung, không nên vận hành AWS

Portal có khá nhiều dashboard, report, HTML và asset do phía business tạo ra. Những nội dung đó sẽ tiếp tục thay đổi sau khi hệ thống đi vào sử dụng.

Một phương án kỹ thuật rất dễ nghĩ tới là cho người phụ trách nội dung upload trực tiếp lên S3.

Làm được.

Nhưng mình không thích boundary đó.

Business không cần biết bucket nào đang phục vụ production, không cần AWS credential, không cần biết CloudFront invalidation là gì và cũng không nên phải hiểu deployment pipeline chỉ để publish một phiên bản nội dung mới.

Trong khi đó, SharePoint đã là công cụ quen thuộc trong môi trường Microsoft 365 của công ty.

Vì vậy mình tách hai việc ra.

Business quản lý nội dung trên SharePoint.

Infrastructure quản lý deployment sang AWS.

Một vùng Published Bundle / Current được dùng như nguồn nội dung đã sẵn sàng để publish. Khi bundle ở đó được xác nhận ready, Infrastructure mới thực hiện deployment.

SharePoint giữ trạng thái business muốn publish. AWS giữ trạng thái đang phục vụ người dùng.

Có một boundary khác cũng cần rõ: publish dashboard không có nghĩa là publish toàn bộ source phía sau dashboard.

Production bundle chỉ nên chứa những gì runtime thật sự cần. Source workbook, file làm việc, archive, backup hay artifact dùng trong quá trình build không nên tự động đi theo chỉ vì chúng nằm gần nhau trong cùng một thư mục.

Với mình, đây cũng là một phần của content ownership: business quyết định nội dung nào đã ready, còn deployment process phải đảm bảo chỉ đúng runtime bundle đó được đi tiếp tới production.

Hai trạng thái không cần phải là một. Và người làm nội dung cũng không cần được cấp quyền AWS chỉ vì portal cuối cùng được host trên AWS.

Nhìn lại, đây có lẽ là quyết định ảnh hưởng đến phần còn lại của architecture nhiều nhất.

Mình không bắt đầu bằng câu hỏi “Dùng AWS service nào?”. Câu hỏi quan trọng hơn là “Ai nên chịu trách nhiệm cho việc gì?”

Business cần một nơi quen thuộc để chuẩn bị nội dung. Infrastructure cần một deployment path có thể kiểm soát và kiểm tra. Người dùng cần tiếp tục sử dụng corporate identity họ đã có.

Khi những boundary đó rõ rồi, việc chọn Graph, S3, CloudFront hay Cognito trở thành phần implementation phía sau.

Đọc SharePoint nhưng không cần account cá nhân

Khi đã chọn SharePoint làm source, câu hỏi tiếp theo là deploy host lấy dữ liệu xuống bằng cách nào.

Mình không muốn một production workflow phụ thuộc vào account của một cá nhân. Cũng không muốn script unattended phải giữ username/password của user.

Phương án cuối cùng sử dụng Microsoft Graph với app-only authentication bằng certificate.

Nhưng app-only vẫn chưa đủ. Một application có thể dùng certificate mà vẫn được cấp quyền rộng hơn mức cần thiết.

Deploy process chỉ cần đọc site phục vụ portal. Không cần đọc cả SharePoint tenant. Vì vậy app được giới hạn bằng Sites.Selected.

Đây là một control khá nhỏ nếu nhìn trên architecture diagram. Nhưng mình thấy nó phản ánh đúng một thói quen quan trọng khi làm Infrastructure:

Mỗi lần cấp quyền, thử hỏi thêm một lần: có thật nó cần nhiều đến vậy không?

Khi full sync bắt đầu không còn hợp lý

Ở giai đoạn đầu, deployment khá thẳng. Deploy host lấy bundle từ SharePoint xuống local, validate các file cần thiết, sync sang S3 rồi tạo CloudFront invalidation.

Một deployment ở thời điểm đầu ghi nhận bundle khoảng 1.903 file, 652 MB. Ở quy mô đó, full sync chưa phải vấn đề lớn.

Nhưng portal tiếp tục phát triển. Nội dung nhiều hơn. Asset nhiều hơn. Đến một thời điểm, bundle đã tăng lên khoảng 27.641 file và hơn 1,6 GB.

Lúc này câu hỏi trở nên khá rõ:

Nếu lần này chỉ thay vài file, tại sao deployment phải tải lại hơn 27.000 file?

Pipeline được chuyển sang hướng incremental. Deploy host giữ local cache và manifest. File mới hoặc thay đổi thì download. File không đổi thì skip. Stale file được xử lý. Validation và guard vẫn chạy trước khi content được sync lên AWS.

Implementation đầu tiên cũng không chạy hoàn hảo ngay. Có một lần Smart Incremental V3 fail vì gặp empty path trong bước normalize relative path. Pipeline dừng lại thay vì tiếp tục deploy một bundle chưa chắc chắn.

Sau khi xử lý lỗi đó, lần chạy tiếp theo cho một kết quả khá đẹp:

Initial Bundle1.903 files652 MB
Current Bundle27.641 files1,6 GB
Incremental DeploymentDownloaded: 0Skipped: 27.641

Required-file validation và final bundle guard vẫn tiếp tục chạy trước khi sync sang S3 và tạo CloudFront invalidation.

Với mình, điều đáng chú ý nhất không phải pipeline chạy nhanh hơn. Mà là deployment bắt đầu hiểu được state.

Skip những gì không cần làm lại, nhưng đừng skip những bước giúp mình biết bundle vẫn đúng.

S3 vẫn private, dù đây chỉ là static website

Ở phía AWS, một boundary khác mình muốn giữ khá rõ: S3 không trở thành public website origin.

Nếu mục tiêu cuối cùng là internal portal có authentication, mình không muốn storage layer phải public chỉ vì content là static.

S3 được giữ private. CloudFront là delivery layer phía trước. Origin Access Control cho phép CloudFront đọc object từ S3 nhưng browser không cần truy cập bucket trực tiếp.

Nhìn từ phía người dùng, họ chỉ biết portal URL. Nhìn từ phía storage, biết S3 URL không có nghĩa là có thể bypass CloudFront để lấy content.

Authentication và authorization là hai chuyện khác nhau

Một internal portal cũng không nên bắt nhân viên nhớ thêm username/password nếu công ty đã có identity platform.

Người dùng đã đăng nhập Microsoft 365 bằng Entra ID. Vì vậy mục tiêu là tận dụng corporate identity đó.

Ở phía AWS, Cognito đảm nhiệm federation. Lambda@Edge nằm trên request path của CloudFront để kiểm tra authentication/session trước khi request đi tiếp tới content.

Nhưng login thành công chưa phải câu hỏi cuối cùng.

Có account công ty không đồng nghĩa với việc được phép sử dụng mọi internal application.

Điều thú vị là ngay trong proposal ban đầu, access vẫn còn được nghĩ theo hướng một allow-list giới hạn. Cách đó phù hợp cho pilot, nhưng khi chuẩn bị vận hành lâu dài mình bắt đầu thấy một vấn đề: nếu Entra đã là corporate identity source thì tại sao application lại cần thêm một danh sách user thứ hai ở AWS?

Cách allow-list vẫn chạy được, nhưng nó tạo ra nhiều nơi cùng quản lý một quyền. Khi user thay đổi access, phải kiểm tra nhiều chỗ. Khi audit, lại xuất hiện câu hỏi: “Source of truth cho quyền truy cập thực sự nằm ở đâu?”

Mình không muốn câu trả lời là: “Phải kiểm tra cả Entra lẫn một danh sách ở AWS.”

Vì vậy architecture được chỉnh lại.

Authorization cuối cùng được đưa về Microsoft Entra Enterprise Application, sử dụng Assignment required cùng Users and groups.

Cognito và Lambda@Edge tiếp tục xử lý authentication flow, token/session và bảo vệ request path. Entra trả lời câu hỏi còn lại: Identity này có được phép sử dụng application hay không?

Từ đó việc cấp hoặc thu hồi quyền không cần sửa Lambda, không cần update một per-user allow-list khác ở AWS và cũng không cần deploy application chỉ để thêm một người.

Điều mình thích ở thay đổi này không phải vì architecture có thêm một control. Mà vì nó bớt đi một nơi phải quản lý quyền.

Authentication chứng minh bạn là ai. Authorization quyết định bạn có được vào hay không. Và với một application, câu trả lời thứ hai nên có một nơi rõ ràng để quản lý.

Deploy vẫn là một hành động có chủ đích

Có một chỗ mình không cố automation bằng mọi giá: thời điểm publish production.

SharePoint là nơi business chuẩn bị bundle. Nhưng một file xuất hiện ở source không có nghĩa production phải thay đổi ngay lập tức.

Deployment vẫn được thực hiện khi phía liên quan xác nhận bundle đã ready. Monthly release là cadence dự kiến. Emergency patch vẫn có đường xử lý manual scan + deploy riêng.

Automation tập trung vào những việc máy làm tốt: đọc đúng source, nhận biết file thay đổi, sync, loại stale file, validation, guard, upload, invalidation và logging.

Con người vẫn giữ quyết định: “Phiên bản này đã sẵn sàng lên production chưa?”

Mình không nghĩ automation càng nhiều thì hệ thống mặc nhiên càng tốt. Một pipeline tốt cũng cần biết chỗ nào không nên tự quyết định thay con người.

Deployment completed chưa phải câu trả lời cuối cùng

Một static portal vẫn có nhiều lớp có thể hỏng độc lập.

SharePoint đọc được nhưng bundle sai. Bundle đúng nhưng S3 thiếu file. S3 đủ nhưng CloudFront còn cache cũ. Content delivery đúng nhưng authentication flow lỗi. Login thành công nhưng authorization sai.

Vì vậy mình không muốn định nghĩa thành công chỉ bằng một dòng Deployment completed.

Bundle có các artifact phục vụ validation như portal register, health metadata và published bundle receipt. Pipeline chạy required-file validation và final local bundle guard trước khi đưa content sang AWS.

Ở phía delivery còn phải kiểm tra URL thực tế, redirect authentication, user được phép vào và user không có quyền phải bị từ chối.

Câu hỏi sau một deployment vì vậy không chỉ là “Script có chạy xong không?”

Một user đúng quyền có thật sự đi hết đường từ browser tới content không?

Architecture cuối cùng

Sau các vòng thay đổi, architecture cuối cùng nhìn khá gọn. Nhưng điều quan trọng hơn số lượng service là các boundary đã rõ hơn: business sở hữu content, Infrastructure sở hữu deployment, storage vẫn private, AWS xử lý delivery/authentication path và Entra giữ application authorization.

Content-to-Access Architecture của SGD Portal từ SharePoint đến Microsoft Entra ID, kèm Before vs After Metrics

Nếu chỉ nhìn diagram cuối, architecture trông khá đơn giản. Nhưng nó không cho thấy lúc pipeline ban đầu còn full sync, lúc bundle tăng từ khoảng 652 MB lên hơn 1,6 GB, lần incremental script fail vì empty path hay lúc access control còn nằm ở nhiều nơi.

Và cũng không thấy câu hỏi ban đầu chỉ đơn giản là: “Đưa mấy file HTML này lên đâu?”

Static không có nghĩa là đơn giản

Nhìn từ phía người dùng, project này cuối cùng vẫn chỉ là một URL.

Mở trình duyệt. Đăng nhập bằng account công ty. Xem nội dung.

Nhưng phía sau URL đó là những câu hỏi về ai sở hữu content, ai được phép publish, application được đọc SharePoint đến đâu, S3 có cần public hay không, corporate identity đi qua AWS như thế nào, quyền truy cập được quản lý ở đâu và một deployment được xem là thành công khi nào.

Khi bundle tăng lên hơn 27.000 file, ngay cả cách deploy cũng phải thay đổi theo thực tế.

Người dùng không cần nhìn thấy những thứ đó. Và có lẽ họ cũng không nên phải nhìn thấy.

Người dùng chỉ nhìn thấy một URL.

Nhưng phía sau URL đó là identity, authorization, deployment, validation, rollback và ownership.

Có lẽ đó là điều mình nhớ nhất từ project này.

Static không có nghĩa là đơn giản.

Ghi lại từ những trải nghiệm thực tế của mình trong quá trình làm Infrastructure — những câu chuyện vẫn còn tiếp tục.

Đọc trước ← Least Privilege trong thực tế
Đọc tiếp → Xây dựng một Enterprise NOC thực dụng