Panduan lengkap dan runut untuk men-deploy situs Hugo secara otomatis ke AWS S3 + CloudFront menggunakan GitHub Actions, lengkap sampai konfigurasi domain kustom ber-HTTPS. Semua langkah di sini sudah teruji di dunia nyata — termasuk bagian troubleshooting dari semua error yang biasanya muncul.
Kalau kamu membangun situs dengan Hugo , kamu butuh tempat hosting yang cepat, murah, dan aman. Kombinasi S3 (penyimpanan) + CloudFront (CDN global) adalah salah satu yang terbaik: biaya rendah, performa tinggi, dan otomatis HTTPS. Di artikel ini kamu akan belajar:
- Membuat pipeline GitHub Actions yang build Hugo dan deploy ke S3 setiap kali push.
- Menyiapkan bucket S3 dengan benar (termasuk soal region dan ACL).
- Membuat distribusi CloudFront dengan OAC + sertifikat ACM (HTTPS).
- Mengarahkan domain kustom ke CloudFront lewat DNS.
- Memperbaiki pretty URL Hugo agar
/about/tidakAccessDenied. - Troubleshooting 8 error nyata yang sering bikin pusing.
Mari mulai.
Mengapa S3 + CloudFront?
Untuk situs statis seperti Hugo, kombinasi S3 + CloudFront sulit ditandingi:
- Murah & bayar sesuai pemakaian — tidak ada server yang nyala 24/7 yang harus dibayar. Situs kecil bahkan bisa nyaris gratis.
- Cepat secara global (CDN) — CloudFront menyajikan konten dari edge location terdekat dengan pengunjung, jadi TTFB rendah di mana pun mereka berada.
- HTTPS otomatis & gratis — sertifikat dari AWS Certificate Manager (ACM) gratis dan diperpanjang otomatis.
- Skalabilitas & ketahanan — S3 punya durability 99,999999999% (11 nine) dan otomatis menahan lonjakan traffic tanpa kamu utak-atik kapasitas.
- Minim perawatan — tidak ada OS, web server, atau patch keamanan yang harus diurus. Tidak ada yang bisa “down” karena salah konfigurasi Nginx.
Singkatnya: untuk situs statis, kamu membayar penyimpanan + bandwidth saja, bukan menyewa mesin yang harus dirawat.
Prasyarat
Sebelum mulai, pastikan kamu punya:
- Situs Hugo yang sudah ada di repository GitHub.
- Akun AWS (S3, CloudFront, ACM, IAM).
- Domain yang kamu kendalikan (bisa atur DNS-nya).
- Versi Hugo lokal kamu — cek dengan
hugo version(kita akan pin versi ini di CI).
Langkah 1: Workflow GitHub Actions (Build & Deploy)
Buat file .github/workflows/deploy.yml di repository kamu:
1name: Deploy Hugo to S3 + CloudFront
2on:
3 push:
4 branches:
5 - main
6jobs:
7 deploy:
8 runs-on: ubuntu-latest
9 steps:
10 - name: Checkout (with submodules)
11 uses: actions/checkout@v2
12 with:
13 submodules: recursive
14 token: ${{ secrets.SUBMODULE_ACCESS_TOKEN }} # hanya jika tema kamu submodule privat
15
16 - name: Setup Hugo
17 uses: peaceiris/actions-hugo@v2
18 with:
19 hugo-version: '0.145.0' # pin ke versi LOKAL kamu (jalankan: hugo version)
20 extended: true
21
22 - name: Build
23 run: hugo --gc --minify
24
25 - name: Sync to S3
26 uses: jakejarvis/s3-sync-action@master
27 with:
28 args: --follow-symlinks --delete
29 env:
30 SOURCE_DIR: 'public'
31 AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
32 AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
33 AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
34 AWS_REGION: ${{ secrets.AWS_S3_REGION }} # WAJIB sama dengan region bucket
35
36 - name: Invalidate CloudFront
37 uses: chetan/invalidate-cloudfront-action@master
38 env:
39 DISTRIBUTION: ${{ secrets.AWS_CF_DISTRIBUTION_ID }} # ID E..., bukan domain d....cloudfront.net
40 PATHS: '/*'
41 AWS_REGION: ${{ secrets.AWS_S3_REGION }}
42 AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
43 AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}Lalu tambahkan GitHub Secrets (Settings → Secrets and variables → Actions):
AWS_S3_BUCKET— nama bucket (mis.www.your-domain.com)AWS_S3_REGION— region bucket (mis.ap-southeast-1)AWS_ACCESS_KEY_ID&AWS_SECRET_ACCESS_KEY— kredensial IAMAWS_CF_DISTRIBUTION_ID— Distribution ID (E...)SUBMODULE_ACCESS_TOKEN— hanya jika tema kamu submodule privat
hugo-version: 'latest'. Rilis Hugo baru bisa mengubah aturan dan mematahkan build kamu tiba-tiba. Pin ke versi yang sama dengan lokal supaya hasil build deterministik.Langkah 2: Membuat Bucket S3
- Buka AWS Console → S3 → Create bucket.
- Beri nama bucket (mis.
www.your-domain.com) dan pilih Region — ingat baik-baik region ini, harus sama denganAWS_S3_REGIONdi workflow. - Untuk akses, ada dua pendekatan:
Pilihan A — Bucket privat + CloudFront OAC (direkomendasikan). Bucket tetap tertutup, hanya CloudFront yang boleh baca. Lebih aman. Bucket policy diisi nanti setelah CloudFront dibuat (lihat Langkah 3).
Pilihan B — Bucket public-read via bucket policy. Lebih sederhana tapi bucket terbuka ke publik. Matikan “Block all public access”, lalu pasang policy:
1{
2 "Version": "2012-10-17",
3 "Statement": [
4 {
5 "Sid": "PublicReadGetObject",
6 "Effect": "Allow",
7 "Principal": "*",
8 "Action": "s3:GetObject",
9 "Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*"
10 }
11 ]
12}--acl public-read saat sync (akan error AccessControlListNotSupported). Berikan akses baca lewat bucket policy atau OAC, bukan ACL. Workflow di Langkah 1 sudah benar — tanpa --acl.Langkah 3: Membuat Distribusi CloudFront
Minta sertifikat TLS di ACM — region
us-east-1(N. Virginia). CloudFront hanya menerima sertifikat dari region ini, apa pun region bucket kamu.- ACM → Request a public certificate → domain
www.your-domain.com→ validasi DNS → tambahkan record CNAME validasi → tunggu status Issued.
- ACM → Request a public certificate → domain
CloudFront → Create distribution.
- Origin domain: pilih bucket S3 kamu.
- Origin access: pilih Origin access control (OAC) → buat OAC baru. CloudFront akan menampilkan bucket policy yang harus kamu tempel ke bucket:
1{ 2 "Version": "2012-10-17", 3 "Statement": [ 4 { 5 "Sid": "AllowCloudFrontServicePrincipal", 6 "Effect": "Allow", 7 "Principal": { "Service": "cloudfront.amazonaws.com" }, 8 "Action": "s3:GetObject", 9 "Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*", 10 "Condition": { 11 "ArnLike": { 12 "AWS:SourceArn": "arn:aws:cloudfront::YOUR_ACCOUNT_ID:distribution/YOUR_DISTRIBUTION_ID" 13 } 14 } 15 } 16 ] 17}- Viewer protocol policy: Redirect HTTP to HTTPS.
- Alternate domain name (CNAME):
www.your-domain.com. - Custom SSL certificate: pilih sertifikat ACM dari langkah 1.
- Default root object:
index.html.
Create → tunggu status Deployed. Catat Distribution domain name (
dXXXX.cloudfront.net) dan Distribution ID (E...).
Langkah 4: Arahkan Domain Kustom (DNS)
Di penyedia DNS kamu, tambahkan record CNAME:
| Type | Name (label saja!) | Value / Data | TTL |
|---|---|---|---|
| CNAME | www | dXXXXXXXX.cloudfront.net | 300 |
www), bukan www.your-domain.com. Hampir semua panel DNS otomatis menambahkan nama zona; kalau kamu ketik nama lengkap, record-nya jadi www.your-domain.com.your-domain.com dan tidak akan resolve.Verifikasi setelah beberapa menit:
1# DNS sudah mengarah ke CloudFront?
2dig +short www.your-domain.com # -> dXXXXXXXX.cloudfront.net.
3
4# Sudah HTTPS, homepage + halaman dalam?
5curl -I https://www.your-domain.com/
6curl -I https://www.your-domain.com/about/Langkah 5: Perbaiki Pretty URL (CloudFront Function)
Hugo memakai pretty URL seperti /about/, yang sebenarnya adalah file /about/index.html. Dengan OAC + origin S3 REST, CloudFront meminta objek about ke S3 — objek itu tidak ada, dan karena bucket privat S3 membalas AccessDenied (bukan melayani index.html). Akibatnya semua halaman selain root error.
Solusinya: CloudFront Function yang menambahkan index.html.
CloudFront → Functions → Create function (mis.
rewrite-index).Isi kode:
1function handler(event) { 2 var request = event.request; 3 var uri = request.uri; 4 5 if (uri.endsWith('/')) { 6 request.uri += 'index.html'; // /about/ -> /about/index.html 7 } else if (!uri.includes('.')) { 8 request.uri += '/index.html'; // /about -> /about/index.html 9 } 10 11 return request; 12}Publish.
Distribusi → tab Behaviors → Default (*) → Edit → Function associations → Viewer request = function tadi → Save.
Buat Invalidation
/*agar response error lama tidak ter-cache.
/css/style.css, /sitemap.xml, /robots.txt) punya titik → tidak diutak-atik, dilayani langsung.Langkah 6: Deploy & Verifikasi
- Commit & push file workflow ke branch
main. - Buka tab Actions di GitHub → pastikan job hijau (build → sync → invalidate).
- Cek hasilnya:
https://www.your-domain.com/→ homepage tampil.https://www.your-domain.com/about/→ halaman dalam tampil (berkat CloudFront Function).
Selesai — setiap push ke main kini otomatis men-deploy situs kamu. 🎉
Estimasi Biaya & Perbandingan dengan Hosting/VPS
Mari hitung biaya untuk blog menengah: ±5 GB penyimpanan, ±100.000 kunjungan/bulan, ±50 GB transfer/bulan. Harga mengacu region ap-southeast-1 (Singapore), kurs ±Rp16.000/USD (perkiraan pertengahan 2026).
Rincian biaya S3 + CloudFront (harga normal / list price)
| Komponen | Pemakaian | Biaya (USD) | Estimasi (IDR) |
|---|---|---|---|
| S3 Standard storage | 5 GB | ~$0.13 | ~Rp2.000 |
| S3 requests | deploy + minimal | ~$0.02 | ~Rp300 |
| CloudFront transfer keluar | 50 GB | ~$6.00 | ~Rp96.000 |
| CloudFront requests (HTTPS) | ~1,2 juta | ~$1.44 | ~Rp23.000 |
| Sertifikat SSL (ACM) | 1 | $0.00 | Rp0 |
| Total | ~$7.6/bulan | ~Rp121.000/bulan |
Dengan CloudFront Free Tier (selamanya gratis)
CloudFront punya always-free tier: 1 TB transfer keluar + 10 juta request per bulan, gratis selamanya. Karena blog menengah kita (50 GB + 1,2 juta request) masih jauh di bawah batas itu, biaya CloudFront menjadi $0. Yang tersisa hanya storage S3:
| Komponen | Biaya (USD) | Estimasi (IDR) |
|---|---|---|
| S3 storage + requests (~5 GB) | ~$0.15 | ~Rp2.400 |
| CloudFront (dalam free tier) | $0.00 | Rp0 |
| Total realistis | ~$0.15/bulan | ~Rp2.400/bulan |
Artinya blog kecil–menengah sering nyaris gratis di S3 + CloudFront. 🎉
Perbandingan dengan Shared Hosting & VPS
| Opsi | Biaya/bulan | Kelebihan | Kekurangan |
|---|---|---|---|
| S3 + CloudFront | ~Rp2.400–121.000 (sering masuk free tier) | CDN global, HTTPS otomatis, nyaris tanpa maintenance, skalabel & durable, bayar sesuai pakai | Setup awal lebih teknis (dipandu artikel ini); bukan untuk konten dinamis |
| Shared Hosting | ~Rp15.000–50.000 | Murah, mudah (cPanel), cocok pemula | Resource dibagi, server 1 lokasi (lambat global), fitur PHP/MySQL mubazir untuk situs statis, bisa lemot saat ramai |
| VPS | ~Rp80.000–150.000+ | Kontrol penuh, fleksibel | Kamu urus sendiri OS, web server, patch keamanan, perpanjang SSL; tanpa CDN global bawaan; butuh effort maintenance |
Kesimpulan biaya: untuk situs statis dengan traffic rendah–menengah, S3 + CloudFront biasanya termurah sekaligus tercepat (sering gratis di free tier), tanpa repot maintenance server. Shared hosting menang di kemudahan awal; VPS baru masuk akal kalau kamu butuh backend dinamis (database, server-side rendering).
Troubleshooting (error nyata + solusinya)
| # | Gejala | Penyebab | Solusi |
|---|---|---|---|
| 1 | Job nyangkut “Waiting for a runner to pick up this job” berjam-jam | Image runner lama (mis. ubuntu-20.04) sudah di-retire GitHub | Pakai runs-on: ubuntu-latest |
| 2 | “Unrecognized named-value: ‘secrets’” di if: step | Context secrets tidak boleh dipakai langsung di if: step | Map secret ke env di level job, lalu uji env.X di if: |
| 3 | Build error: The “_build” front matter key … was removed | Hugo 0.145+ menghapus key _build | Ganti _build: jadi build: di front matter |
| 4 | Build kadang sukses kadang gagal tanpa ubah kode | hugo-version: 'latest' → versi berubah-ubah | Pin hugo-version ke versi lokal kamu |
| 5 | IllegalLocationConstraintException saat sync | Region di workflow beda dengan region bucket | Set AWS_REGION sama persis dengan region bucket |
| 6 | AccessControlListNotSupported / “bucket does not allow ACLs” | Bucket punya ACL dinonaktifkan (default modern) | Hapus --acl public-read; pakai bucket policy / OAC |
| 7 | staging/subdomain tidak resolve sama sekali | Salah isi kolom Name DNS dengan FQDN → dobel domain | Isi Name dengan label saja (www, staging) |
| 8 | AccessDenied (XML) di /about, root OK | OAC + origin S3 REST tidak auto index.html | Pasang CloudFront Function (Langkah 5) |
(Opsional) Lingkungan Staging
Kalau ingin lingkungan pratinjau terpisah (mis. staging.your-domain.com), buat bucket + distribusi CloudFront sendiri, dengan pipeline yang trigger dari branch staging. Tambahkan juga noindex + robots.txt disallow agar tidak terindeks Google, dan pastikan terisolasi penuh dari produksi (bucket/distribusi berbeda). Ini persis yang kami lakukan untuk staging Santekno.
Kesimpulan
Kamu sudah punya pipeline deploy Hugo end-to-end: GitHub Actions → S3 → CloudFront → domain kustom HTTPS, plus CloudFront Function untuk pretty URL dan daftar solusi error yang umum. Pendekatan ini cepat, murah, dan aman untuk situs statis apa pun.
Mau menyembunyikan URL bawaan CloudFront dan memaksa hanya domain kustom yang dipakai? Lanjut baca: Cara Menyembunyikan URL CloudFront dan Menggunakan Domain Kustom dengan Aman .