Introduction
Dalam dunia modern berbasis data, proses pengolahan data dari berbagai sumber ke dalam bentuk yang siap dianalisis sangatlah krusial. Salah satu pendekatan populer adalah ETLT (Extract → Load → Transform → Visualize), yang memungkinkan data mentah dari berbagai sumber dikumpulkan, disimpan, diproses, lalu divisualisasikan untuk pengambilan keputusan.
Pada artikel ini, kita akan membahas bagaimana membangun sebuah pipeline ETLT menggunakan tools open source seperti:
- Airbyte untuk mengekstrak dan memuat data dari database mentah (seperti PostgreSQL dan MySQL),
- BigQuery sebagai data warehouse,
- dbt (Data Build Tool) untuk proses transformasi data,
- dan Metabase untuk visualisasi data.
Menariknya, seluruh proses ini akan dijalankan dengan bantuan Docker, sehingga memudahkan proses deployment, pengujian, dan replikasi pipeline di berbagai environment.
Langkah Setup Airbyte dan Metabase dengan Docker
1. Prepare Docker Airbyte
a. Git clone
git clone https://github.com/airbytehq/airbyte
cd airbyte
b. Jalankan Airbyte dengan Docker Compose
- Persiapkan beberapa tools berikut dan juga edit ENV sesuai kebutuhan : .env
- database via docker kebetulan menggunakan Postgre
version: '3.9'
services:
postgres:
image: postgres:12-alpine
ports:
- 5432:5432
volumes:
- ./apps/postgres:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=pass
- POSTGRES_USER=postgre
- POSTGRES_DB=airbyte
- POSTGRES_ROOT_PASSWORD=akshenrke
- ENV ( karena di kita perlu set supaya storage server kita tidak full)
DATABASE_USER=airbyte
DATABASE_PASSWORD=pass
DATABASE_HOST=host.docker.internal
DATABASE_PORT=5432
DATABASE_DB=airbyte
# translate manually
DATABASE_URL=jdbc:postgresql://${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_DB} (do not include the username or password here)
DATABASE_URL=jdbc:postgresql://host.docker.internal:5432/airbyte
# DATABASE_URL=jdbc:postgresql://host.docker.internal:30
### JOBS ###
# Relevant to scaling.
SYNC_JOB_MAX_ATTEMPTS=2
SYNC_JOB_MAX_TIMEOUT_DAYS=2
JOB_MAIN_CONTAINER_CPU_REQUEST=1
JOB_MAIN_CONTAINER_CPU_LIMIT=1.50
JOB_MAIN_CONTAINER_MEMORY_REQUEST=6144MB
JOB_MAIN_CONTAINER_MEMORY_LIMIT=8192MB
NORMALIZATION_JOB_MAIN_CONTAINER_MEMORY_LIMIT=8192MB
NORMALIZATION_JOB_MAIN_CONTAINER_MEMORY_REQUEST=6144MB
NORMALIZATION_JOB_MAIN_CONTAINER_CPU_LIMIT=1.50
NORMALIZATION_JOB_MAIN_CONTAINER_CPU_REQUEST=1
# Relevant to scaling.
MAX_SYNC_WORKERS=2
MAX_SPEC_WORKERS=2
MAX_CHECK_WORKERS=2
MAX_DISCOVER_WORKERS=2
MAX_NOTIFY_WORKERS=2
SHOULD_RUN_NOTIFY_WORKFLOWS=false
- Docker-compose airbyte
version: "3.8"
#https://github.com/compose-spec/compose-spec/blob/master/spec.md#using-extensions-as-fragments
x-logging: &default-logging
options:
max-size: "100m"
max-file: "5"
driver: json-file
services:
# hook in case we need to add init behavior
# every root service (no depends_on) should depend on init
init:
image: airbyte/init:${VERSION}
logging: *default-logging
container_name: init
command: /bin/sh -c "./scripts/create_mount_directories.sh /local_parent ${HACK_LOCAL_ROOT_PARENT} ${LOCAL_ROOT}"
environment:
- LOCAL_ROOT=${LOCAL_ROOT}
- HACK_LOCAL_ROOT_PARENT=${HACK_LOCAL_ROOT_PARENT}
volumes:
- ${HACK_LOCAL_ROOT_PARENT}:/local_parent
bootloader:
image: airbyte/bootloader:${VERSION}
logging: *default-logging
container_name: airbyte-bootloader
environment:
- AIRBYTE_VERSION=${VERSION}
- DATABASE_PASSWORD=${DATABASE_PASSWORD}
- DATABASE_URL=${DATABASE_URL}
- DATABASE_USER=${DATABASE_USER}
- LOG_LEVEL=${LOG_LEVEL}
networks:
- airbyte_internal
depends_on:
init:
condition: service_completed_successfully
# db:
# image: airbyte/db:${VERSION}
# logging: *default-logging
# container_name: airbyte-db
# restart: unless-stopped
# environment:
# - CONFIG_DATABASE_PASSWORD=${CONFIG_DATABASE_PASSWORD:-}
# - CONFIG_DATABASE_URL=${CONFIG_DATABASE_URL:-}
# - CONFIG_DATABASE_USER=${CONFIG_DATABASE_USER:-}
# - DATABASE_PASSWORD=${DATABASE_PASSWORD}
# - DATABASE_URL=${DATABASE_URL}
# - DATABASE_USER=${DATABASE_USER}
# - POSTGRES_PASSWORD=${DATABASE_PASSWORD}
# - POSTGRES_USER=${DATABASE_USER}
# volumes:
# - db:/var/lib/postgresql/data
# networks:
# - airbyte_internal
worker:
image: airbyte/worker:${VERSION}
logging: *default-logging
container_name: airbyte-worker
restart: unless-stopped
environment:
- AIRBYTE_VERSION=${VERSION}
- AUTO_DISABLE_FAILING_CONNECTIONS=${AUTO_DISABLE_FAILING_CONNECTIONS}
- CONFIG_DATABASE_PASSWORD=${CONFIG_DATABASE_PASSWORD:-}
- CONFIG_DATABASE_URL=${CONFIG_DATABASE_URL:-}
- CONFIG_DATABASE_USER=${CONFIG_DATABASE_USER:-}
- CONFIGS_DATABASE_MINIMUM_FLYWAY_MIGRATION_VERSION=${CONFIGS_DATABASE_MINIMUM_FLYWAY_MIGRATION_VERSION:-}
- CONFIG_ROOT=${CONFIG_ROOT}
- DATABASE_PASSWORD=${DATABASE_PASSWORD}
- DATABASE_URL=${DATABASE_URL}
- DATABASE_USER=${DATABASE_USER}
- DEPLOYMENT_MODE=${DEPLOYMENT_MODE}
- INTERNAL_API_HOST=${INTERNAL_API_HOST}
- JOBS_DATABASE_MINIMUM_FLYWAY_MIGRATION_VERSION=${JOBS_DATABASE_MINIMUM_FLYWAY_MIGRATION_VERSION:-}
- JOB_MAIN_CONTAINER_CPU_LIMIT=${JOB_MAIN_CONTAINER_CPU_LIMIT}
- JOB_MAIN_CONTAINER_CPU_REQUEST=${JOB_MAIN_CONTAINER_CPU_REQUEST}
- JOB_MAIN_CONTAINER_MEMORY_LIMIT=${JOB_MAIN_CONTAINER_MEMORY_LIMIT}
- JOB_MAIN_CONTAINER_MEMORY_REQUEST=${JOB_MAIN_CONTAINER_MEMORY_REQUEST}
- LOCAL_DOCKER_MOUNT=${LOCAL_DOCKER_MOUNT}
- LOCAL_ROOT=${LOCAL_ROOT}
- LOG_LEVEL=${LOG_LEVEL}
- LOG_CONNECTOR_MESSAGES=${LOG_CONNECTOR_MESSAGES}
- MAX_CHECK_WORKERS=${MAX_CHECK_WORKERS}
- MAX_DISCOVER_WORKERS=${MAX_DISCOVER_WORKERS}
- MAX_SPEC_WORKERS=${MAX_SPEC_WORKERS}
- MAX_SYNC_WORKERS=${MAX_SYNC_WORKERS}
- MAX_NOTIFY_WORKERS=${MAX_NOTIFY_WORKERS}
- SHOULD_RUN_NOTIFY_WORKFLOW=${SHOULD_RUN_NOTIFY_WORKFLOW}
- NORMALIZATION_JOB_MAIN_CONTAINER_MEMORY_LIMIT=${NORMALIZATION_JOB_MAIN_CONTAINER_MEMORY_LIMIT}
- NORMALIZATION_JOB_MAIN_CONTAINER_MEMORY_REQUEST=${NORMALIZATION_JOB_MAIN_CONTAINER_MEMORY_REQUEST}
- NORMALIZATION_JOB_MAIN_CONTAINER_CPU_LIMIT=${NORMALIZATION_JOB_MAIN_CONTAINER_CPU_LIMIT}
- NORMALIZATION_JOB_MAIN_CONTAINER_CPU_REQUEST=${NORMALIZATION_JOB_MAIN_CONTAINER_CPU_REQUEST}
- SECRET_PERSISTENCE=${SECRET_PERSISTENCE}
- SYNC_JOB_MAX_ATTEMPTS=${SYNC_JOB_MAX_ATTEMPTS}
- SYNC_JOB_MAX_TIMEOUT_DAYS=${SYNC_JOB_MAX_TIMEOUT_DAYS}
- TEMPORAL_HOST=${TEMPORAL_HOST}
- TRACKING_STRATEGY=${TRACKING_STRATEGY}
- WEBAPP_URL=${WEBAPP_URL}
- WORKSPACE_DOCKER_MOUNT=${WORKSPACE_DOCKER_MOUNT}
- WORKSPACE_ROOT=${WORKSPACE_ROOT}
- METRIC_CLIENT=${METRIC_CLIENT}
- OTEL_COLLECTOR_ENDPOINT=${OTEL_COLLECTOR_ENDPOINT}
- JOB_ERROR_REPORTING_STRATEGY=${JOB_ERROR_REPORTING_STRATEGY}
- JOB_ERROR_REPORTING_SENTRY_DSN=${JOB_ERROR_REPORTING_SENTRY_DSN}
- ACTIVITY_MAX_ATTEMPT=${ACTIVITY_MAX_ATTEMPT}
- ACTIVITY_INITIAL_DELAY_BETWEEN_ATTEMPTS_SECONDS=${ACTIVITY_INITIAL_DELAY_BETWEEN_ATTEMPTS_SECONDS}
- ACTIVITY_MAX_DELAY_BETWEEN_ATTEMPTS_SECONDS=${ACTIVITY_MAX_DELAY_BETWEEN_ATTEMPTS_SECONDS}
- WORKFLOW_FAILURE_RESTART_DELAY_SECONDS=${WORKFLOW_FAILURE_RESTART_DELAY_SECONDS}
- AUTO_DETECT_SCHEMA=${AUTO_DETECT_SCHEMA}
- USE_STREAM_CAPABLE_STATE=${USE_STREAM_CAPABLE_STATE}
- MICRONAUT_ENVIRONMENTS=${WORKERS_MICRONAUT_ENVIRONMENTS}
- APPLY_FIELD_SELECTION=${APPLY_FIELD_SELECTION}
- FIELD_SELECTION_WORKSPACES=${FIELD_SELECTION_WORKSPACES}
#test env external database
# - CONFIG_DATABASE_PASSWORD=${CONFIG_DATABASE_PASSWORD:-}
# - CONFIG_DATABASE_URL=${CONFIG_DATABASE_URL:-}
# - CONFIG_DATABASE_USER=${CONFIG_DATABASE_USER:-}
# - DATABASE_PASSWORD=${DATABASE_PASSWORD}
## - DATABASE_URL=${DATABASE_URL}
# - DATABASE_USER=${DATABASE_USER}
# - POSTGRES_PASSWORD=${DATABASE_PASSWORD}
# - POSTGRES_USER=${DATABASE_USER}
configs:
- flags
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- workspace:${WORKSPACE_ROOT}
- ${LOCAL_ROOT}:${LOCAL_ROOT}
ports:
- "9000"
networks:
- airbyte_internal
depends_on:
bootloader:
condition: service_completed_successfully
server:
image: airbyte/server:${VERSION}
logging: *default-logging
container_name: airbyte-server
restart: unless-stopped
environment:
- AIRBYTE_ROLE=${AIRBYTE_ROLE:-}
- AIRBYTE_VERSION=${VERSION}
- CONFIG_DATABASE_PASSWORD=${CONFIG_DATABASE_PASSWORD:-}
- CONFIG_DATABASE_URL=${CONFIG_DATABASE_URL:-}
- CONFIG_DATABASE_USER=${CONFIG_DATABASE_USER:-}
- CONFIGS_DATABASE_MINIMUM_FLYWAY_MIGRATION_VERSION=${CONFIGS_DATABASE_MINIMUM_FLYWAY_MIGRATION_VERSION:-}
- CONFIG_ROOT=${CONFIG_ROOT}
- DATABASE_PASSWORD=${DATABASE_PASSWORD}
- DATABASE_URL=${DATABASE_URL}
- DATABASE_USER=${DATABASE_USER}
- JOB_MAIN_CONTAINER_CPU_LIMIT=${JOB_MAIN_CONTAINER_CPU_LIMIT}
- JOB_MAIN_CONTAINER_CPU_REQUEST=${JOB_MAIN_CONTAINER_CPU_REQUEST}
- JOB_MAIN_CONTAINER_MEMORY_LIMIT=${JOB_MAIN_CONTAINER_MEMORY_LIMIT}
- JOB_MAIN_CONTAINER_MEMORY_REQUEST=${JOB_MAIN_CONTAINER_MEMORY_REQUEST}
- JOBS_DATABASE_MINIMUM_FLYWAY_MIGRATION_VERSION=${JOBS_DATABASE_MINIMUM_FLYWAY_MIGRATION_VERSION:-}
- LOG_LEVEL=${LOG_LEVEL}
- NEW_SCHEDULER=${NEW_SCHEDULER}
- SECRET_PERSISTENCE=${SECRET_PERSISTENCE}
- TEMPORAL_HOST=${TEMPORAL_HOST}
- TRACKING_STRATEGY=${TRACKING_STRATEGY}
- JOB_ERROR_REPORTING_STRATEGY=${JOB_ERROR_REPORTING_STRATEGY}
- JOB_ERROR_REPORTING_SENTRY_DSN=${JOB_ERROR_REPORTING_SENTRY_DSN}
- WEBAPP_URL=${WEBAPP_URL}
- WORKER_ENVIRONMENT=${WORKER_ENVIRONMENT}
- WORKSPACE_ROOT=${WORKSPACE_ROOT}
- GITHUB_STORE_BRANCH=${GITHUB_STORE_BRANCH}
- MICRONAUT_ENVIRONMENTS=${WORKERS_MICRONAUT_ENVIRONMENTS}
- AUTO_DETECT_SCHEMA=${AUTO_DETECT_SCHEMA}
- JAVA_OPTS=-Xms512m -Xmx12g
ports:
- "8001"
configs:
- flags
volumes:
- workspace:${WORKSPACE_ROOT}
- data:${CONFIG_ROOT}
- ${LOCAL_ROOT}:${LOCAL_ROOT}
networks:
- airbyte_internal
depends_on:
bootloader:
condition: service_completed_successfully
webapp:
image: airbyte/webapp:${VERSION}
logging: *default-logging
container_name: airbyte-webapp
restart: unless-stopped
ports:
- "80"
environment:
- AIRBYTE_ROLE=${AIRBYTE_ROLE:-}
- AIRBYTE_VERSION=${VERSION}
- API_URL=${API_URL:-}
- CONNECTOR_BUILDER_API_URL=${CONNECTOR_BUILDER_API_URL:-}
- INTERNAL_API_HOST=${INTERNAL_API_HOST}
- CONNECTOR_BUILDER_API_HOST=${CONNECTOR_BUILDER_API_HOST}
- OPENREPLAY=${OPENREPLAY:-}
- PAPERCUPS_STORYTIME=${PAPERCUPS_STORYTIME:-}
- TRACKING_STRATEGY=${TRACKING_STRATEGY}
networks:
- airbyte_internal
depends_on:
bootloader:
condition: service_completed_successfully
airbyte-temporal:
image: airbyte/temporal:${VERSION}
logging: *default-logging
container_name: airbyte-temporal
restart: unless-stopped
environment:
- DB=postgresql
- DB_PORT=${DATABASE_PORT}
- DYNAMIC_CONFIG_FILE_PATH=config/dynamicconfig/development.yaml
- LOG_LEVEL=${LOG_LEVEL}
- POSTGRES_PWD=${DATABASE_PASSWORD}
- POSTGRES_SEEDS=${DATABASE_HOST}
- POSTGRES_USER=${DATABASE_USER}
volumes:
- ./temporal/dynamicconfig:/etc/temporal/config/dynamicconfig
networks:
- airbyte_internal
airbyte-cron:
image: airbyte/cron:${VERSION}
logging: *default-logging
container_name: airbyte-cron
restart: unless-stopped
environment:
- AIRBYTE_VERSION=${VERSION}
- CONFIGS_DATABASE_MINIMUM_FLYWAY_MIGRATION_VERSION=${CONFIGS_DATABASE_MINIMUM_FLYWAY_MIGRATION_VERSION}
- DATABASE_PASSWORD=${DATABASE_PASSWORD}
- DATABASE_URL=${DATABASE_URL}
- DATABASE_USER=${DATABASE_USER}
- DEPLOYMENT_MODE=${DEPLOYMENT_MODE}
- LOG_LEVEL=${LOG_LEVEL}
- REMOTE_CONNECTOR_CATALOG_URL=${REMOTE_CONNECTOR_CATALOG_URL}
- TEMPORAL_HISTORY_RETENTION_IN_DAYS=${TEMPORAL_HISTORY_RETENTION_IN_DAYS}
- UPDATE_DEFINITIONS_CRON_ENABLED=${UPDATE_DEFINITIONS_CRON_ENABLED}
- WORKSPACE_ROOT=${WORKSPACE_ROOT}
- MICRONAUT_ENVIRONMENTS=${CRON_MICRONAUT_ENVIRONMENTS}
configs:
- flags
volumes:
- workspace:${WORKSPACE_ROOT}
networks:
- airbyte_internal
depends_on:
bootloader:
condition: service_completed_successfully
airbyte-connector-builder-server:
image: airbyte/connector-builder-server:${VERSION}
logging: *default-logging
container_name: airbyte-connector-builder-server
restart: unless-stopped
ports:
- 80
environment:
- AIRBYTE_VERSION=${VERSION}
networks:
- airbyte_internal
depends_on:
bootloader:
condition: service_completed_successfully
airbyte-proxy:
image: airbyte/proxy:${VERSION}
container_name: airbyte-proxy
restart: unless-stopped
ports:
- "8000:8000"
- "8001:8001"
- "8003:8003"
environment:
- BASIC_AUTH_USERNAME=${BASIC_AUTH_USERNAME}
- BASIC_AUTH_PASSWORD=${BASIC_AUTH_PASSWORD}
- BASIC_AUTH_PROXY_TIMEOUT=${BASIC_AUTH_PROXY_TIMEOUT}
networks:
- airbyte_internal
- airbyte_public
depends_on:
- webapp
- server
volumes:
workspace:
name: ${WORKSPACE_DOCKER_MOUNT}
# the data volume is only needed for backward compatibility; when users upgrade
# from an old Airbyte version that relies on file-based configs, the server needs
# to read this volume to copy their configs to the database
data:
name: ${DATA_DOCKER_MOUNT}
db:
name: ${DB_DOCKER_MOUNT}
configs:
flags:
file: ./flags.yml
networks:
airbyte_public:
airbyte_internal:
Akan ada beberapa container yang ter create

docker-compose up -d

Perintah ini akan menjalankan Airbyte dan UI-nya yang bisa diakses di http://localhost:8000. Anda bisa mulai menambahkan sumber data seperti PostgreSQL atau MySQL, dan destinasi seperti BigQuery melalui antarmuka web ini.
2. Menyiapkan Metabase dengan Docker
Metabase adalah alat Business Intelligence (BI) open-source yang memudahkan dalam membuat dashboard dan visualisasi data.
a. Buat file docker-compose.metabase.yml
Buat file dengan nama docker-compose.metabase.yml dan isi seperti berikut:
NB : Siapkan database postgres untuk self hosted si metabase nanti
version: "3"
services:
metabase:
image: metabase/metabase:v0.54.4.2
container_name: metabase-prod
restart: always
volumes:
- /root/metabase-docker/storage/metabase-data:/metabase-data
ports:
- 3002:3000
environment:
- MB_DB_TYPE=postgres
- MB_DB_DBNAME=metabase
- MB_DB_PORT=5432
- MB_DB_USER=postgres
- MB_DB_PASS=ahitudia
- MB_DB_HOST-127.0.0.1
healthcheck:
test: curl --fail -I http://localhost:3002/api/health || exit 1
interval: 15s
timeout: 5s
retries: 5
b. Jalankan Metabase
docker-compose up -d
Akses Metabase melalui http://localhost:3000 dan ikuti panduan awal untuk menghubungkannya dengan BigQuery sebagai sumber data.

3. Install DBT
- Install DBT Core
- Contact admin to get your GCP and BigQuery account
- IAM account to access BigQuery
- Service account key file
- Setup your local ENV
- DBT_GOOGLE_PROJECT={gcp-project-id}
- DBT_GOOGLE_BIGQUERY_DATASET={bigquery-dataset-name}
- DBT_GOOGLE_BIGQUERY_KEYFILE={service-account-filepath}
Enjoy work on

Admin website igunawan.com, System Administrator, DevOps Engineer
