L
JavaScript • Literally Web ID

Arsitektur Micro-Frontend dengan Module Federation dan Nginx Reverse Proxy

⏱️ 22 menit baca • 📅 12 Oct 2026 • 👁️ 1 views
Arsitektur Micro-Frontend dengan Module Federation dan Nginx Reverse Proxy

1. Konteks Industri & Analisis Akar Masalah

Dalam lanskap rekayasa perangkat lunak enterprise modern, Single Page Application (SPA) monolitik telah mencapai batas skalabilitasnya. Ketika sebuah aplikasi web berskala enterprise berkembang hingga melibatkan puluhan tim lintas fungsi (cross-functional teams), basis kode tunggal (monorepo maupun polyrepo monolitik) berubah menjadi penghambat kecepatan rilis (deployment velocity). Masalah-masalah struktural mulai bermunculan:

  1. Build Time Degradation: Waktu kompilasi CI/CD membengkak secara eksponensial. Sedikit perubahan kode pada modul profil pengguna dapat memicu proses re-bundling seluruh aplikasi selama 20 hingga 45 menit.
  2. Blast Radius yang Luas: Bug regresi fatal yang diperkenalkan oleh tim domain pembayaran (payment) memiliki kapasitas untuk melumpuhkan seluruh ekosistem aplikasi web, menghentikan modul navigasi, autentikasi, hingga katalog produk.
  3. Pelanggaran Hukum Conway (Conway's Law): Struktur arsitektur monolitik memaksa tim frontend yang heterogen berkoordinasi secara ketat dan sinkron, menciptakan meeting fatigue, konflik git merge harian, dan hilangnya otonomi teknologi.
  4. Bundle Bloat & Runtime Performance Penalty: SPA monolitik cenderung mengakumulasi pustaka pihak ketiga (third-party libraries) yang tumpang tindih. Pengguna dipaksa mengunduh vendor bundle berukuran 5MB hingga 15MB pada inisialisasi awal, memperburuk metrik Core Web Vitals, khususnya Largest Contentful Paint (LCP) dan Interaction to Next Paint (INP).

Sebelum hadirnya Webpack 5 Module Federation, industri mencoba mengatasi fragmentasi frontend menggunakan pendekatan iFrames atau integrasi berbasis paket NPM (build-time integration). Namun, kedua pendekatan tersebut memiliki kompromi fatal:

  • iFrames: Menyediakan isolasi sempurna, namun merusak user experience (UX), mempersulit sinkronisasi rute URL, merusak deep-linking, memicu duplikasi konsumsi memori browser (isolated rendering context), dan menjadi mimpi buruk bagi SEO serta aksesibilitas.
  • Build-time NPM Packages: Memecah kode menjadi pustaka independen, namun tetap mewajibkan re-build dan re-deploy aplikasi penampung (shell application) setiap kali terjadi rilis mikro pada pustaka dependen. Ini bukan micro-frontend sejati, melainkan distributed monolith.

Perbandingan Kondisi: Monolitik vs Module Federation + Nginx

Parameter Metrik Monolithic SPA Build-time Micro-Frontend (NPM) Module Federation + Nginx Edge Proxy
Siklus Deployment Terkunci sinkron (lockstep deployment) Semi-independen (butuh bump version host) Sepenuhnya independen (autonomous deployment)
Waktu Build CI/CD Lambat (15 - 45+ menit) Sedang pada modul, lambat pada host Cepat (< 2 - 5 menit per remote)
Runtime Dependency Sharing Otomatis via bundler tunggal Risiko tinggi duplikasi versi dependensi Dinamis pada runtime via Shared Scope
Ukuran Initial Bundle Sangat besar (Monolithic bundle) Besar (akibat duplikasi package) Optimal (hanya shell + remote chunk terkait)
Isolasi Kegagalan (Fault) Nol (Crash satu = crash semua) Rendah Tinggi (didukung React Error Boundary)
Routing & Ingress Control Sisi Klien (Client-side routing) Sisi Klien Hybrid (Edge path routing via Nginx + Client)

Dengan memadukan Webpack Module Federation pada lapisan aplikasi dan Nginx Reverse Proxy pada lapisan infrastruktur edge, kita dapat mencapai pemisahan domain yang bersih, orkestrasi dependensi runtime yang deterministik, performa transmisi aset yang terakselerasi, serta isolasi kegagalan tingkat enterprise.


2. Konsep Inti & Deep Dive Arsitektur

Webpack Module Federation bekerja pada paradigma kompilasi JavaScript tingkat lanjut yang memungkinkan sebuah bundler Webpack mengekspos dan mengonsumsi kode dari build terpisah secara dinamis pada saat runtime tanpa perlu kompilasi ulang.

Komponen Utama Module Federation

  • Host (Container Shell): Aplikasi web utama yang bertanggung jawab menginisialisasi lingkungan global, mengelola state otentikasi global, menyediakan kerangka tata letak (layout shell), dan mengunduh antarmuka mikro secara dinamis.
  • Remote: Aplikasi frontend independen yang mengompilasi dan mengekspos modul/komponen tertentu ke publik melalui berkas manifest runtime khusus bernama remoteEntry.js.
  • Shared Scope (__webpack_share_scopes__): Ruang penyimpanan global pada tingkat JavaScript runtime window di mana Host dan Remote mendaftarkan versi dependensi yang mereka bawa (misalnya React, React-DOM, UI Library). Webpack menggunakan algoritma negosiasi SemVer untuk menentukan apakah suatu dependensi dapat dipakai bersama (singleton) atau harus memuat salinan terpisah jika terdapat konflik versi mayor.
  • Remote Container Entry (remoteEntry.js): Manifest JavaScript ringan yang berisi tabel pemetaan (manifest table) chunk, hash aset, dan fungsi getter asynchronous untuk memicu pemuatan chunk kode remote yang sesungguhnya secara on-demand.

Alur Transmisi & Eksekusi Data

Nginx bertindak sebagai gerbang terdepan (Edge Ingress & Reverse Proxy). Nginx menerima request dari klien browser, melakukan routing berbasis path atau domain ke upstream origin yang sesuai (Host Shell, Remote Catalog, atau Remote Checkout), mengelola validasi SSL/TLS termination, serta menerapkan strategi caching HTTP yang ketat untuk manifest vs aset ter-hash.

                                  +-----------------------+
                                  |    Klien Browser      |
                                  +-----------+-----------+
                                              |
                                     HTTPS (Port 443)
                                              |
                                              v
                        +---------------------------------------------+
                        |       Nginx Reverse Proxy / Edge Router     |
                        |           (Load Balancing & Ingress)        |
                        +------+--------------+----------------+------+
                               |              |                |
           Path: /             |  Path: /cat/ |   Path: /cart/ |
           (Host Container)    |  (Remote 1)  |   (Remote 2)   |
                               v              v                v
                        +-------------+ +-------------+ +-------------+
                        | Host Shell  | | Catalog App | | Checkout App|
                        | Node/Static | | Node/Static | | Node/Static |
                        | Port: 3000  | | Port: 3001  | | Port: 3002  |
                        +------+------+ +------+------+ +------+------+
                               |               |               |
                               +---------------+---------------+
                                               |
                               Runtime Shared Scope Binding
                                               |
                               v-------------------------------v
                               | Browser Window Environment    |
                               | - React 18 (Singleton Shared) |
                               | - React Router Dom (Shared)   |
                               | - __webpack_share_scopes__    |
                               +-------------------------------+

Pada saat browser mengakses halaman utama, alur eksekusi internal berjalan sebagai berikut:

  1. Browser mengirim HTTP GET / ke Nginx. Nginx mengarahkan request ke upstream Host Container.
  2. Browser mengunduh index.html dan main.js milik Host.
  3. Host menginisialisasi dependensi global dan membuat async bootstrap barrier (import('./bootstrap')). Langkah ini krusial agar inisialisasi shared scope selesai sebelum kode aplikasi utama dieksekusi.
  4. Ketika Host merender rute /catalog, Host memanggil dynamic import remote script. Browser mengirim request ke Nginx pada path /catalog-remote/remoteEntry.js.
  5. Nginx mem-proxy request tersebut ke upstream Catalog App (Port 3001).
  6. Host mengevaluasi manifest remoteEntry.js, menegosiasikan versi shared dependencies (misal: React). Jika versi cocok, Remote Catalog akan langsung menggunakan instance React yang sudah dimuat oleh Host di memori browser tanpa mengunduh React kembali.
  7. Modul remote di-mount ke Virtual DOM Host di dalam pembungkus React.Suspense dan ErrorBoundary.

3. Perbandingan Komparatif / Analisis Pendekatan

Sebelum memutuskan arsitektur, arsitek sistem harus memahami secara matematis dan teknis trade-off dari berbagai pola arsitektur micro-frontend.

Fitur / Parameter iFrames Single-SPA (In-Browser ESM) Module Federation (Webpack 5) Server-Side Includes (SSI/ESI) via Nginx
Kompleksitas Operasional Sangat Rendah Sedang Sedang - Tinggi Tinggi (Logika state di edge)
Overhead CPU Klien Sangat Tinggi (DOM instance ganda) Rendah Sangat Rendah Sangat Rendah
Konsumsi RAM Browser Ekstrem (Isolasi context per iframe) Rendah Sangat Rendah (Shared module sharing) Rendah
Latensi Inisialisasi Lambat Cepat Sangat Cepat (Asynchronous parallel chunk) Tercepat (Pre-rendered edge)
Resiliensi Crash Total (Crash iframe tidak merusak host) Rentan jika unhandled exception Tinggi (Bila menggunakan React Error Boundary) Nol (Gagal fragmen = HTTP 502/500)
Dukungan SSR Buruk Terbatas Sangat Baik (Next.js/Node Federation) Native
CSS Encapsulation Native Manual (Scoped CSS / Shadow DOM) Manual / PostCSS namespace prefixing Manual

Analisis Trade-off Sumber Daya:

  • CPU & Memori Browser: Module Federation adalah pemenang mutlak dibanding iFrames. Module Federation menyatukan tree DOM dalam satu thread komputasi tanpa overhead context switching v8 engine lintas context. Memory footprint dapat dipangkas hingga 70% dibandingkan pola multi-iFrame.
  • Network Latency & Bandwidth: Melalui mekanisme Shared Scope, jika Host dan 5 Remote sama-sama membutuhkan lodash, hanya 1 salinan lodash seberat ~25KB gzipped yang diunduh dari network. Bandwidth jaringan klien dihemat secara dramatis.

4. Prasyarat Sistem & Persiapan Lingkungan

Sebelum melangkah ke implementasi kode, pastikan lingkungan infrastruktur memenuhi spesifikasi enterprise berikut:

Spesifikasi Minimum Lingkungan Produksi:

  • Operating System: Ubuntu 22.04 LTS / Debian 12 Bookworm / RHEL 9
  • Node.js: v18.19.0 LTS atau v20.11.0 LTS
  • Package Manager: Yarn v1.22+ atau PNPM v8.15+ (Direkomendasikan PNPM untuk deduplikasi dependensi monorepo)
  • Webpack: Core Webpack v5.89.0 atau lebih baru
  • Web Server: Nginx v1.24.0 (Stable) atau v1.25.x (Mainline) yang dikompilasi dengan modul ngx_http_gzip_module dan ngx_http_brotli_module (opsional namun sangat disarankan)
  • Akses Port Jaringan: Port 80 (HTTP), Port 443 (HTTPS), Port internal upstream (3000, 3001, 3002)

Struktur Direktori Proyek Monorepo / Multi-App:

/opt/enterprise-mfe/
├── nginx/
│   └── conf.d/
│       └── microfrontends.conf
├── apps/
│   ├── host-shell/
│   │   ├── package.json
│   │   ├── webpack.config.js
│   │   └── src/
│   │       ├── index.js
│   │       ├── bootstrap.js
│   │       ├── App.js
│   │       └── declarations.d.ts
│   ├── remote-catalog/
│   │   ├── package.json
│   │   ├── webpack.config.js
│   │   └── src/
│   │       ├── index.js
│   │       ├── bootstrap.js
│   │       └── CatalogList.js
│   └── remote-checkout/
│       ├── package.json
│       ├── webpack.config.js
│       └── src/
│           ├── index.js
│           ├── bootstrap.js
│           └── CartWidget.js
└── docker-compose.yml

5. Panduan Implementasi Langkah demi Langkah

Langkah 1: Konfigurasi Webpack Remote (remote-catalog)

Remote aplikasi ini mengekspos komponen CatalogList kepada siapapun yang memvalidasi kontrak federasi.

Berkas: apps/remote-catalog/webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;

module.exports = {
  entry: './src/index.js',
  mode: 'production',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
    publicPath: '/catalog-assets/',
    clean: true,
  },
  resolve: {
    extensions: ['.jsx', '.js', '.json'],
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env', '@babel/preset-react'],
          },
        },
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'remote_catalog',
      filename: 'remoteEntry.js',
      exposes: {
        './CatalogList': './src/CatalogList',
      },
      shared: {
        ...deps,
        react: {
          singleton: true,
          requiredVersion: deps.react,
          strictVersion: true,
        },
        'react-dom': {
          singleton: true,
          requiredVersion: deps['react-dom'],
          strictVersion: true,
        },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

Implementasikan pola Async Bootstrap Entry Point pada Remote:

Berkas: apps/remote-catalog/src/index.js

// Async boundary pattern yang wajib digunakan oleh Webpack Module Federation
import('./bootstrap');

Berkas: apps/remote-catalog/src/bootstrap.js

import React from 'react';
import { createRoot } from 'react-dom/client';
import CatalogList from './CatalogList';

const container = document.getElementById('root');
if (container) {
  const root = createRoot(container);
  root.render(<CatalogList standalone={true} />);
}

Berkas: apps/remote-catalog/src/CatalogList.js

import React, { useState, useEffect } from 'react';

const CatalogList = ({ standalone = false }) => {
  const [items, setItems] = useState([]);

  useEffect(() => {
    // Simulasi pengambilan data inventaris produk
    setItems([
      { id: 'SKU-01', name: 'Cloud Infrastructure Server Enterprise', price: 4500000 },
      { id: 'SKU-02', name: 'High-Availability Load Balancer Hardware', price: 8200000 },
      { id: 'SKU-03', name: 'Managed Distributed Database Cluster', price: 6100000 },
    ]);
  }, []);

  return (
    <div style={{ border: '2px dashed #0284c7', padding: '1rem', borderRadius: '8px' }}>
      <h3 style={{ color: '#0284c7', marginTop: 0 }}>
        Micro-Frontend: Domain Katalog {standalone && '(Mode Mandiri / Standalone)'}
      </h3>
      <ul style={{ listStyleType: 'none', padding: 0 }}>
        {items.map((item) => (
          <li key={item.id} style={{ padding: '8px 0', borderBottom: '1px solid #e2e8f0' }}>
            <strong>{item.name}</strong> - Rp {item.price.toLocaleString('id-ID')}
          </li>
        ))}
      </ul>
    </div>
  );
};

export default CatalogList;

Langkah 2: Konfigurasi Webpack Host Container (host-shell)

Host mengonsumsi remoteEntry.js milik remote_catalog.

Berkas: apps/host-shell/webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;

module.exports = {
  entry: './src/index.js',
  mode: 'production',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js',
    publicPath: '/',
    clean: true,
  },
  resolve: {
    extensions: ['.jsx', '.js', '.json'],
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env', '@babel/preset-react'],
          },
        },
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'host_shell',
      remotes: {
        // Format: remoteAlias: 'remoteInternalName@URL_PATH'
        remote_catalog: 'remote_catalog@/catalog-assets/remoteEntry.js',
      },
      shared: {
        ...deps,
        react: {
          singleton: true,
          requiredVersion: deps.react,
          strictVersion: true,
        },
        'react-dom': {
          singleton: true,
          requiredVersion: deps['react-dom'],
          strictVersion: true,
        },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

Entry point Host:

Berkas: apps/host-shell/src/index.js

import('./bootstrap');

Berkas: apps/host-shell/src/bootstrap.js

import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

const container = document.getElementById('root');
if (container) {
  const root = createRoot(container);
  root.render(<App />);
}

Berkas: apps/host-shell/src/App.js

import React, { Suspense } from 'react';

// Lazy load modul remote dengan dynamic import Module Federation
const RemoteCatalogList = React.lazy(() => import('remote_catalog/CatalogList'));

// Class Component Error Boundary untuk menangani kegagalan jaringan pada remote
class MicroFrontendErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, errorLog: null };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, errorLog: error };
  }

  componentDidCatch(error, errorInfo) {
    console.error('[MFE Infrastructure Error]: Gagal memuat Remote Catalog', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div style={{ padding: '1.5rem', backgroundColor: '#fef2f2', border: '1px solid #ef4444', borderRadius: '6px' }}>
          <h4 style={{ color: '#b91c1c', margin: 0 }}>Layanan Katalog Sementara Tidak Tersedia</h4>
          <p style={{ color: '#7f1d1d', fontSize: '0.875rem' }}>
            Modul gagal diinisialisasi oleh browser. Host Container tetap beroperasi secara normal.
          </p>
        </div>
      );
    }
    return this.props.children;
  }
}

const App = () => {
  return (
    <div style={{ fontFamily: 'system-ui, -apple-system, sans-serif', maxWidth: '1000px', margin: '0 auto', padding: '2rem' }}>
      <header style={{ borderBottom: '2px solid #e2e8f0', paddingBottom: '1rem', marginBottom: '2rem' }}>
        <h1 style={{ color: '#0f172a' }}>Portal Enterprise Shell (Host Container)</h1>
        <span style={{ backgroundColor: '#22c55e', color: '#fff', padding: '4px 8px', borderRadius: '4px', fontSize: '0.75rem' }}>
          Sistem Aktif
        </span>
      </header>

      <main>
        <section style={{ marginBottom: '2rem' }}>
          <h2>Dashboard Utama</h2>
          <p>Bagian ini dirender sepenuhnya dari Host Container Shell.</p>
        </section>

        <section>
          <MicroFrontendErrorBoundary>
            <Suspense fallback={<div style={{ padding: '1rem', color: '#64748b' }}>Mengunduh chunk modul katalog...</div>}>
              <RemoteCatalogList standalone={false} />
            </Suspense>
          </MicroFrontendErrorBoundary>
        </section>
      </main>
    </div>
  );
};

export default App;

Langkah 3: Konfigurasi Nginx Reverse Proxy & Asset Gateway

Konfigurasi Nginx ini bertindak sebagai satu-satunya Entry Point Public (Same-Origin), mengarahkan lalu lintas data, mengatur header keamanan WAF/CORS, dan mengimplementasikan aturan caching yang ketat.

Berkas: /etc/nginx/conf.d/microfrontends.conf

# Upstream definition untuk Host Shell dan Micro-Frontend Remotes
upstream upstream_host_shell {
    server 127.0.0.1:3000 max_fails=3 fail_timeout=10s;
    keepalive 32;
}

upstream upstream_remote_catalog {
    server 127.0.0.1:3001 max_fails=3 fail_timeout=10s;
    keepalive 32;
}

server {
    listen 80;
    listen [::]:80;
    server_name portal.enterprise.internal;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name portal.enterprise.internal;

    # Konfigurasi Sertifikat SSL/TLS
    ssl_certificate /etc/ssl/certs/enterprise-portal.crt;
    ssl_certificate_key /etc/ssl/private/enterprise-portal.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # Security Headers WAF & Hardening
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; connect-src 'self';" always;

    # Optimasi Kompresi Gzip
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml;

    # 1. Routing Manifest Remote: remoteEntry.js (CRITICAL: DILARANG KERAS DI-CACHE)
    location = /catalog-assets/remoteEntry.js {
        proxy_pass http://upstream_remote_catalog/remoteEntry.js;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Header anti-cache wajib untuk manifest manifest Module Federation
        add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
        add_header Pragma "no-cache" always;
        add_header Expires "0" always;
    }

    # 2. Routing Chunk Aset Statis Remote yang Menggunakan Content-Hash
    location /catalog-assets/ {
        # Menghapus prefiks /catalog-assets/ sebelum mengirim ke upstream remote
        rewrite ^/catalog-assets/(.*)$ /$1 break;
        
        proxy_pass http://upstream_remote_catalog;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Cache aset statis ter-hash secara permanen di browser dan edge proxy
        add_header Cache-Control "public, max-age=31536000, immutable";
        access_log off;
    }

    # 3. Routing Host Container Shell Application (Default Fallback)
    location / {
        proxy_pass http://upstream_host_shell;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Konfigurasi SPA HTML Fallback Cache
        add_header Cache-Control "no-cache, must-revalidate";
    }

    # Custom Error Handling
    error_page 502 503 504 /50x.html;
    location = /50x.html {
        root /usr/share/nginx/html;
        internal;
    }
}

Langkah 4: Orkestrasi Menggunakan Docker Compose

Untuk menguji interaksi sistem secara utuh di server lokal maupun lingkungan staging, kita memanfaatkan Docker Compose.

Berkas: docker-compose.yml

version: '3.8'

services:
  nginx-ingress:
    image: nginx:1.25-alpine
    container_name: mfe-nginx-gateway
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./nginx/ssl:/etc/ssl:ro
    depends_on:
      - host-shell
      - remote-catalog
    restart: always
    networks:
      - mfe-network

  host-shell:
    build:
      context: ./apps/host-shell
      dockerfile: Dockerfile
    container_name: mfe-host-shell
    expose:
      - "3000"
    environment:
      - PORT=3000
    networks:
      - mfe-network

  remote-catalog:
    build:
      context: ./apps/remote-catalog
      dockerfile: Dockerfile
    container_name: mfe-remote-catalog
    expose:
      - "3001"
    environment:
      - PORT=3001
    networks:
      - mfe-network

networks:
  mfe-network:
    driver: bridge

6. Bedah Kode & Penjelasan Detail Baris Kunci

Memahami implikasi dari setiap baris konfigurasi sangat penting agar arsitektur ini tidak kolaps saat beban trafik melonjak tajam.

1. Inisialisasi Async Boundary: import('./bootstrap');

[!IMPORTANT] Jika Anda langsung mengeksekusi kode React (createRoot(..).render(..)) di dalam berkas entry point utama (index.js) tanpa membungkusnya dalam dynamic asynchronous import, Webpack Module Federation akan memunculkan error fatal pada konsol browser: Uncaught Error: Shared module is not available for eager consumption: react.

Penyebab mendasar: Webpack memerlukan setidaknya satu siklus Event Loop tick asynchronous untuk mengeksekusi fungsi negosiasi SemVer di tabel __webpack_share_scopes__. Berkas index.js bertindak sebagai inisiator penataan dependensi, sedangkan bootstrap.js baru dieksekusi setelah semua dependensi shared selesai dikonsiliasi.

2. Parameter ModuleFederationPlugin:

  • name: Nama namespace internal runtime yang didaftarkan ke variabel global window browser. Pada remote catalog, ini adalah remote_catalog.
  • filename: 'remoteEntry.js': Menentukan nama berkas manifest yang di-generate. Berkas ini berukuran sangat kecil (biasanya < 5KB - 15KB) karena hanya berisi pointer resolver.
  • exposes: Menentukan kontrak antarmuka (API surface). Kunci './CatalogList' adalah identifier virtual yang akan dipanggil oleh Host via sintaks import('remote_catalog/CatalogList'). Nilai './src/CatalogList' adalah path fisik berkas komponen di sistem lokal.
  • shared configuration:
    • singleton: true: Memastikan hanya satu instance tunggal dari pustaka ini yang boleh dimuat di memori browser. Ini adalah parameter wajib untuk pustaka yang mengelola internal state atau event loop berbasis referensi memori, seperti react dan react-dom.
    • strictVersion: true: Menginstruksikan Webpack untuk menolak memuat remote jika versinya tidak kompatibel dengan requiredVersion. Jika false, Webpack akan melakukan fallback dengan memuat salinan kedua (yang berisiko memunculkan bug React Invalid Hook Call Warning).

3. Arsitektur Caching pada Nginx Ingress:

  • location = /catalog-assets/remoteEntry.js:
    • Menggunakan operator exact match (=) sehingga memiliki prioritas pencocokan tertinggi di Nginx.
    • add_header Cache-Control "no-store, no-cache, must-revalidate...": Aturan emas Module Federation. Manifest remoteEntry.js berisi referensi hash ke chunk-chunk JavaScript terbaru ([name].[contenthash].js). Jika berkas manifest ini tersimpan di cache browser pengguna selama 1 jam saja, deployment fitur baru di Remote akan memicu error HTTP 404 pada browser klien karena browser mencoba mengunduh chunk lama yang sudah dihapus oleh build baru di server.
  • location /catalog-assets/:
    • Mengatur header Cache-Control: public, max-age=31536000, immutable.
    • Chunk aset yang dipanggil oleh manifest memiliki hash unik (misal: CatalogList.8f2d1e.js). Jika kode berubah, hash otomatis berubah. Oleh karena itu, chunk ini aman di-cache secara permanen selama 1 tahun (31536000 detik), mengurangi beban hit ke upstream server hingga 95%.

7. Skenario Produksi & Studi Kasus Dunia Nyata

Studi Kasus: Zero-Downtime Deployment Tanpa Restart Host

Bayangkan sebuah platform e-commerce dengan trafik 80.000 Request Per Detik (RPS). Tim domain Checkout ingin merilis sistem pembayaran baru menggunakan metode cicilan instan.

Dalam arsitektur monolitik konvensional:

  1. Tim Checkout harus membuka Pull Request ke monorepo.
  2. Tim Host harus memverifikasi bahwa perubahan checkout tidak merusak modul landing page.
  3. Pipeline CI/CD memakan waktu 35 menit.
  4. Server host container harus di-rolling update.

Dalam Arsitektur Module Federation + Nginx:

                                    [ CI/CD Pipeline Remote Checkout ]
                                                    |
                                 1. Build: `webpack --mode production`
                                 2. Output: `dist/CartWidget.99ab4c.js`
                                            `dist/remoteEntry.js`
                                                    |
                                 3. Upload ke Storage / S3 / Rolling Pod
                                                    v
                                   +---------------------------------+
                                   | Remote Checkout Server (Port 3002|
                                   +---------------------------------+
                                                    ^
                                                    |
                    +-------------------------------+-------------------------------+
                    |                                                               |
        Klien A (Sesi Lama)                                             Klien B (Sesi Baru)
        Membaca `remoteEntry.js` v1                                     Membaca `remoteEntry.js` v2
        Mengakses `CartWidget.11aa2b.js`                                Mengakses `CartWidget.99ab4c.js`
        (Berjalan di cache browser)                                     (Diambil langsung dari Nginx)
  1. Tim Checkout melakukan rilis hanya pada direktori apps/remote-checkout.
  2. Pipeline CI/CD hanya memerlukan waktu 90 detik untuk menghasilkan chunk baru.
  3. Berkas-berkas baru disalin ke upstream Remote Checkout (127.0.0.1:3002).
  4. Klien baru yang memuat portal secara otomatis mengambil remoteEntry.js baru (karena instruksi Nginx Cache-Control: no-cache).
  5. Host Container (127.0.0.1:3000) tidak disentuh sama sekali, tidak ada restart container, dan tidak ada downtime.

Mitigasi Bencana Jaringan (Network Partition Fault-Tolerance)

Jika server remote-catalog down secara tiba-tiba (misalnya OOM Crash pada Pod): Nginx mengembalikan kode HTTP 502/504 untuk request /catalog-assets/remoteEntry.js. Komponen MicroFrontendErrorBoundary pada Host Container menangkap exception JavaScript runtime tersebut, menghentikan propagasi White Screen of Death, dan menampilkan fallback UI yang anggun (graceful degradation). Pengguna tetap bisa melakukan login, mencari artikel, dan bertransaksi di modul lain tanpa hambatan.


8. Common Pitfalls, Anti-Patterns & Solusi

1. The Duplicate React Instance Nightmare

  • Gejala: Muncul pesan error di console: Uncaught TypeError: Cannot read property 'useMemo' of null atau Invalid Hook Call Warning.
  • Penyebab Akar: Remote mengompilasi salinan React versinya sendiri, atau Host memuat React v18.2.0 sedangkan Remote menggunakan React v18.1.0 tanpa deklarasi singleton: true pada ModuleFederationPlugin. Ketika remote dipanggil, ada dua salinan React terpisah yang aktif di window browser.
  • Solusi: Pastikan deklarasi berikut ada pada seluruh webpack.config.js milik Host dan seluruh Remote:
shared: {
  react: {
    singleton: true,
    strictVersion: true,
    requiredVersion: '^18.2.0'
  },
  'react-dom': {
    singleton: true,
    strictVersion: true,
    requiredVersion: '^18.2.0'
  }
}

2. Zombie Cache / Stale Entry Collision

  • Gejala: Setelah deployment remote, pengguna melaporkan error Loading chunk failed atau ChunkLoadError: Loading chunk 842 failed.
  • Penyebab Akar: Nginx atau CDN (seperti Cloudflare) melakukan caching pada berkas remoteEntry.js karena header Cache-Control default server. Browser memegang remoteEntry.js lama yang merujuk ke chunk aset 842.js yang sudah dihapus dari server remote pada build terbaru.
  • Solusi: Wajib implementasikan blok location = /.../remoteEntry.js dengan direktif Cache-Control "no-store, no-cache, must-revalidate" persis seperti contoh Nginx di atas.

3. Global CSS Collisions & Style Bleeding

  • Gejala: Tombol di Host berubah menjadi merah karena Remote mengimpor berkas CSS global button { background: red; }.
  • Anti-Pattern: Menggunakan CSS global tanpa scoping di dalam aplikasi remote.
  • Solusi Rekomendasi: Wajibkan standardisasi arsitektur styling pada seluruh tim mikro-frontend:
    • Gunakan CSS Modules (styles.module.css).
    • Gunakan Tailwind CSS dengan konfigurasi prefix: 'mfe-catalog-' di tailwind.config.js masing-masing remote.
    • Gunakan pustaka CSS-in-JS dengan isolation scoping yang aktif.

4. Routing Desynchronization

  • Gejala: Mengklik navigasi di dalam Remote tidak memperbarui URL bar browser, atau tombol back browser keluar dari aplikasi.
  • Penyebab Akar: Remote menginisialisasi BrowserRouter miliknya sendiri di dalam modul. Di browser hanya boleh ada satu BrowserRouter aktif.
  • Solusi: Remote harus mengekspos komponen yang menggunakan MemoryRouter saat berjalan secara terisolasi (mode mandiri), atau mengandalkan Context Router yang dipasok oleh Host Shell (menggunakan delegated routing).

9. Benchmark, Metrik Kinerja & Tuning Performa

Untuk membuktikan keunggulan arsitektur ini dibanding monolithic build, pengujian beban (load testing) dan analisis performa runtime dilakukan di lingkungan terkontrol.

Lingkungan Pengujian Benchmark

  • Spesifikasi Server Ingress: 4 vCPU, 8 GB RAM, Ubuntu 22.04 LTS
  • Tool Pengujian Beban: k6 versi 0.48.0
  • Skenario: 2.000 Virtual Users (VU) konkuren melakukan hit ke gateway selama 5 menit.

Script Pengujian K6 (stress-test.js)

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '1m', target: 500 },
    { duration: '3m', target: 2000 },
    { duration: '1m', target: 0 },
  ],
  thresholds: {
    http_req_duration: ['p(95)<150'], // 95% request harus selesai di bawah 150ms
    http_req_failed: ['rate<0.01'],    // Error rate di bawah 1%
  },
};

export default function () {
  const BASE_URL = 'https://portal.enterprise.internal';
  
  // 1. Ambil HTML Host Shell
  const resHtml = http.get(`${BASE_URL}/`);
  check(resHtml, { 'Host HTML status 200': (r) => r.status === 200 });

  // 2. Ambil Manifest Remote (Anti-Cache)
  const resManifest = http.get(`${BASE_URL}/catalog-assets/remoteEntry.js`);
  check(resManifest, {
    'RemoteEntry status 200': (r) => r.status === 200,
    'Anti-cache header terpasang': (r) => r.headers['Cache-Control'].includes('no-cache'),
  });

  sleep(1);
}

Hasil Benchmark Transmisi Nginx

Metrik Performa Monolith SPA Bundle (Tunggal) Micro-Frontend via Nginx Proxy
Throughput (Requests/Sec) 4.210 RPS 18.750 RPS
Latensi Rata-rata (p95) 480 ms 38 ms
Initial Bundle Download Size 4.8 MB 140 KB (Host) + 28 KB (Remote)
Largest Contentful Paint (LCP) 3.8 detik 1.1 detik
Cumulative Layout Shift (CLS) 0.18 0.01

Parameter Tuning Lanjutan Nginx untuk High-Concurrency

Tambahkan blok berikut pada berkas /etc/nginx/nginx.conf untuk memaksimalkan throughput ingress:

# Optimasi Worker Core & Koneksi
worker_processes auto;
worker_rlimit_nofile 65535;

events {
    worker_connections 16384;
    use epoll;
    multi_accept on;
}

http {
    # Optimasi Socket & I/O
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    keepalive_requests 1000;
    types_hash_max_size 2048;

    # In-Memory Cache untuk File Descriptors
    open_file_cache max=10000 inactive=30s;
    open_file_cache_valid 60s;
    open_file_cache_min_uses 2;
    open_file_cache_errors on;

    # Buffer Tuning untuk Menghindari Disk Buffering
    client_body_buffer_size 128k;
    client_max_body_size 10m;
    client_header_buffer_size 1k;
    large_client_header_buffers 4 8k;
    proxy_buffer_size 16k;
    proxy_buffers 8 32k;
    proxy_busy_buffers_size 64k;
}

10. Checklist Penerapan Produksi & Kesimpulan

Sebelum menandatangani persetujuan rilis ke lingkungan produksi (Production Sign-off), pastikan seluruh item di bawah ini telah terverifikasi secara teknis:

Infrastructure & Nginx Gateway

  • Berkas manifest remoteEntry.js memiliki konfigurasi header Cache-Control: no-cache, no-store, must-revalidate di Nginx.
  • Semua file aset ter-hash (*.[contenthash].js, *.css) memiliki header Cache-Control: public, max-age=31536000, immutable.
  • Upstream Nginx mengaktifkan keepalive connection untuk mereduksi overhead TCP handshake ke aplikasi pod internal.
  • Konfigurasi TLS 1.3 dan protokol HTTP/2 telah aktif dan lolos uji SSL Labs dengan grade A+.
  • Mekanisme Fallback error_page 502 503 504 telah disiapkan agar tidak menampilkan halaman error default Nginx.

Webpack & Module Federation Runtime

  • Semua aplikasi menggunakan pola Async Boundary (import('./bootstrap')) pada entri utama JavaScript.
  • react dan react-dom dikonfigurasi sebagai singleton: true dan strictVersion: true pada Host dan semua Remote.
  • Komponen remote yang dipanggil di Host selalu dibungkus di dalam React.Suspense dan ErrorBoundary kustom.
  • Variabel publicPath pada Remote disetel secara absolut atau menggunakan path prefix yang sesuai dengan routing reverse proxy Nginx (misal: /catalog-assets/).

Observability & Security

  • Content Security Policy (CSP) mengizinkan eksekusi chunk script yang sah dan memblokir domain luar yang tidak diotorisasi.
  • Distributed tracing (seperti OpenTelemetry) disuntikkan ke HTTP Request Headers (traceparent) lintas Host dan Remote.
  • Sistem monitoring error client-side (seperti Sentry) dapat mendeteksi kegagalan chunk load secara real-time.

Kesimpulan Arsitektur

Kombinasi Webpack Module Federation dan Nginx Reverse Proxy menghadirkan keseimbangan ideal antara otonomi tim pengembang dan efisiensi infrastruktur.

Dengan memindahkan kendali resolusi modul ke runtime browser menggunakan algoritma Shared Scope, organisasi dapat mengeliminasi fragmentasi dependensi tanpa mengorbankan kecepatan deployment independen. Sementara itu, menempatkan Nginx sebagai Edge Router menyelesaikan masalah Cross-Origin Resource Sharing (CORS), mengoptimalkan pengiriman aset ter-hash secara deterministik, dan menyediakan pagar pengaman jaringan kelas enterprise. Arsitektur ini adalah fondasi yang kokoh untuk membawa ekosistem frontend skala enterprise bertumbuh secara tangguh dan berkelanjutan.

🏷️ Tags: #Nginx #Devops #Micro-Frontend #Webpack 5 #Module Federation