Skip to content
Santekno.com | Tech Tutorials and Trends
ID
📖 0%

Cara Deploy Hugo ke AWS S3 + CloudFront dengan GitHub Actions dan Domain Kustom

· 9 mnt baca ·Ihsan Arif

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/ tidak AccessDenied.
  • 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:

yaml
 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 IAM
  • AWS_CF_DISTRIBUTION_ID — Distribution ID (E...)
  • SUBMODULE_ACCESS_TOKEN — hanya jika tema kamu submodule privat
Danger
Penting soal versi Hugo: jangan pakai 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

  1. Buka AWS Console → S3 → Create bucket.
  2. Beri nama bucket (mis. www.your-domain.com) dan pilih Region — ingat baik-baik region ini, harus sama dengan AWS_S3_REGION di workflow.
  3. 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:

json
 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}
Danger
Catatan ACL: bucket S3 modern (dibuat sejak 2023) punya ACL dinonaktifkan secara default (“Bucket owner enforced”). Karena itu jangan memakai flag --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

  1. 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.
  2. 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:
    json
     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.
  3. 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:

TypeName (label saja!)Value / DataTTL
CNAMEwwwdXXXXXXXX.cloudfront.net300
Danger
⚠️ Pitfall paling sering: kolom Name diisi label subdomain saja (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:

bash
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.

  1. CloudFront → FunctionsCreate function (mis. rewrite-index).

  2. Isi kode:

    javascript
     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}
  3. Publish.

  4. Distribusi → tab BehaviorsDefault (*)EditFunction associationsViewer request = function tadi → Save.

  5. Buat Invalidation /* agar response error lama tidak ter-cache.

Danger
File dengan ekstensi (/css/style.css, /sitemap.xml, /robots.txt) punya titik → tidak diutak-atik, dilayani langsung.

Langkah 6: Deploy & Verifikasi

  1. Commit & push file workflow ke branch main.
  2. Buka tab Actions di GitHub → pastikan job hijau (build → sync → invalidate).
  3. 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).

Danger
Angka di bawah adalah estimasi dan bisa berubah tergantung region, pola traffic, dan kebijakan harga AWS. Selalu cek kalkulator harga AWS untuk angka terbaru.

Rincian biaya S3 + CloudFront (harga normal / list price)

KomponenPemakaianBiaya (USD)Estimasi (IDR)
S3 Standard storage5 GB~$0.13~Rp2.000
S3 requestsdeploy + minimal~$0.02~Rp300
CloudFront transfer keluar50 GB~$6.00~Rp96.000
CloudFront requests (HTTPS)~1,2 juta~$1.44~Rp23.000
Sertifikat SSL (ACM)1$0.00Rp0
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:

KomponenBiaya (USD)Estimasi (IDR)
S3 storage + requests (~5 GB)~$0.15~Rp2.400
CloudFront (dalam free tier)$0.00Rp0
Total realistis~$0.15/bulan~Rp2.400/bulan

Artinya blog kecil–menengah sering nyaris gratis di S3 + CloudFront. 🎉

Perbandingan dengan Shared Hosting & VPS

OpsiBiaya/bulanKelebihanKekurangan
S3 + CloudFront~Rp2.400–121.000 (sering masuk free tier)CDN global, HTTPS otomatis, nyaris tanpa maintenance, skalabel & durable, bayar sesuai pakaiSetup awal lebih teknis (dipandu artikel ini); bukan untuk konten dinamis
Shared Hosting~Rp15.000–50.000Murah, mudah (cPanel), cocok pemulaResource dibagi, server 1 lokasi (lambat global), fitur PHP/MySQL mubazir untuk situs statis, bisa lemot saat ramai
VPS~Rp80.000–150.000+Kontrol penuh, fleksibelKamu 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)

#GejalaPenyebabSolusi
1Job nyangkut “Waiting for a runner to pick up this job” berjam-jamImage runner lama (mis. ubuntu-20.04) sudah di-retire GitHubPakai runs-on: ubuntu-latest
2“Unrecognized named-value: ‘secrets’” di if: stepContext secrets tidak boleh dipakai langsung di if: stepMap secret ke env di level job, lalu uji env.X di if:
3Build error: The “_build” front matter key … was removedHugo 0.145+ menghapus key _buildGanti _build: jadi build: di front matter
4Build kadang sukses kadang gagal tanpa ubah kodehugo-version: 'latest' → versi berubah-ubahPin hugo-version ke versi lokal kamu
5IllegalLocationConstraintException saat syncRegion di workflow beda dengan region bucketSet AWS_REGION sama persis dengan region bucket
6AccessControlListNotSupported / “bucket does not allow ACLs”Bucket punya ACL dinonaktifkan (default modern)Hapus --acl public-read; pakai bucket policy / OAC
7staging/subdomain tidak resolve sama sekaliSalah isi kolom Name DNS dengan FQDN → dobel domainIsi Name dengan label saja (www, staging)
8AccessDenied (XML) di /about, root OKOAC + origin S3 REST tidak auto index.htmlPasang 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 .

IH
Ihsan Arif
Backend engineer & penulis di Santekno. Aktif menulis tentang Go, Laravel, dan arsitektur backend modern.
Follow

💬 Komentar