Second Brain

Sebuah catatan perjalanan menekuni bidang IT Infra, Cloud, DevOps, dan Security

Membangun Platform Aplikasi Self-Hosted dengan Klaster Kubernetes Multi-Node dan Pipeline CI/CD Otomatis

Tujuan: Kita akan menyimulasikan lingkungan enterprise (perusahaan) di mana kita membangun server DevOps sendiri tanpa bergantung pada platform lain, sehingga kita bisa membangunnya secara mandiri sesuai dengan skala perusahaan.

Arsitektur:

  1. Infrastruktur: 2 Server VPS (Virtual Private Server).

  2. Orkestrasi: K3s (Versi ringan dari Kubernetes).

  3. Penyimpanan: Longhorn (Agar data tidak hilang saat restart).

  4. Git Server: Gitea (Alternatif GitHub/GitLab).

  5. CI/CD: Drone CI (Otomatisasi untuk build & deploy).

Sebelum memulai, berikut pembagian fungsi kedua VPS tersebut. Kita akan menyebutnya:

  • VPS A (Master): Otak dari cluster, mengatur semua proses.

  • VPS B (Worker): Otot dari cluster, tempat aplikasi berjalan.

Catatan: Ganti tulisan seperti <IP_PRIVATE_VPS_A> dengan alamat IP asli VPS.

Fase 1: Membangun Fondasi Klaster Kubernetes

Di fase ini, kita akan menggabungkan dua server terpisah menjadi satu kesatuan sistem (Cluster).

Langkah 1.1: Persiapan Sistem (Lakukan di KEDUA VPS)

Kita perlu membuka “pintu” (port) agar kedua server bisa saling berkomunikasi dan menginstal driver penyimpanan.

Update dependensi dan install open iscsi

sudo apt update && sudo apt upgrade -y
sudo apt-get install -y open-iscsi # Dibutuhkan oleh Longhorn nanti

Instalasi K3s (Kubernetes Ringan)

Instalasi di VPS A (Master): Perintah ini akan menginstal K3s sebagai server.

curl -sfL https://get.k3s.io | sh -s - server \
  --node-ip <IP_PRIVATE_VPS_A> \
  --flannel-iface <NAMA_INTERFACE_PUBLIK> \
  --resolv-conf /etc/resolv.conf

NAMA_INTERFACE_PUBLIK biasanya eth0 atau ens3. Cek dengan perintah ip a

Agar VPS B bisa bergabung, ia butuh “password” (token) dari Master.

sudo cat /var/lib/rancher/k3s/server/node-token

Instalasi di VPS B (Worker): Perintah ini menginstal K3s sebagai agen yang melapor ke Master.

curl -sfL https://get.k3s.io | K3S_URL=https://<IP_PRIVATE_VPS_A>:6443 K3S_TOKEN=<TOKEN_DARI_MASTER> sh -s - agent \
  --node-ip <IP_PRIVATE_VPS_B> \
  --flannel-iface <NAMA_INTERFACE_PUBLIK> \
  --resolv-conf /etc/resolv.conf

Kembali ke VPS A, cek apakah kedua node sudah siap. Pastikan statusnya Ready.

kubectl get nodes

 

Fase 2: Menyiapkan Layanan Inti

Sekarang kita akan men-deploy semua layanan pendukung ke dalam klaster. Pada tutorial ini saya menyimpan semua file konfigurasi .yaml di VPS A.

Langkah 2.1: Deploy Longhorn untuk Penyimpanan

Jalankan perintah ini dari komputer lokal Anda (yang memiliki helm terinstal ).
helm repo add longhorn https://charts.longhorn.io
helm repo update
helm install longhorn longhorn/longhorn --namespace longhorn-system --create-namespace

Tunggu beberapa menit. Verifikasi bahwa semua pod di namespace longhorn-system berjalan dengan command berikut:

kubectl get pods -n longhorn-system

Langkah 2.2: Deploy Registry Docker Lokal

Buat file bernama local-registry.yaml. Ini akan menjadi tempat kita menyimpan image Docker.
# local-registry.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: local-registry
  namespace: default
  labels:
    app: local-registry
spec:
  replicas: 1
  selector:
    matchLabels:
      app: local-registry
  template:
    metadata:
      labels:
        app: local-registry
    spec:
      containers:
      - name: registry
        image: registry:2
        ports:
        - containerPort: 5000
---
apiVersion: v1
kind: Service
metadata:
  name: local-registry-svc
  namespace: default
spec:
  selector:
    app: local-registry
  ports:
  - protocol: TCP
    port: 5000
    targetPort: 5000

Selanjutnya jalankan command berikut:

kubectl apply -f local-registry.yaml

Langkah 2.3: Deploy Gitea (Git Server )

Buat file bernama gitea-manual.yaml.
# gitea-manual.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: gitea-pvc
  namespace: default
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: longhorn
  resources:
    requests:
      storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: gitea
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gitea
  template:
    metadata:
      labels:
        app: gitea
    spec:
      containers:
      - name: gitea
        image: gitea/gitea:latest
        ports:
        - containerPort: 3000
          name: http
        - containerPort: 22
          name: ssh
        volumeMounts:
        - name: gitea-data
          mountPath: /data
      volumes:
      - name: gitea-data
        persistentVolumeClaim:
          claimName: gitea-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: gitea-svc
  namespace: default
spec:
  type: NodePort
  selector:
    app: gitea
  ports:
  - name: http
    protocol: TCP
    port: 3000
    targetPort: 3000
  - name: ssh
    protocol: TCP
    port: 22
    targetPort: 22

Kemudian jalankan command berikut:

kubectl apply -f gitea-manual.yaml

Setelah Gitea berjalan, akses melalui http://<IP_PUBLIK_VPS>:<NODE_PORT> untuk menyelesaikan instalasi awal dan membuat akun admin.

Langkah 2.4: Deploy Drone CI (Server & Runner )

Buat Izin RBAC: Buat file drone-cluster-rbac.yaml.

# drone-cluster-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: drone-runner-sa
  namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: drone-runner-cluster-role
rules:
- apiGroups: [""]
  resources: ["pods", "pods/log", "secrets", "events", "services", "persistentvolumeclaims"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: drone-runner-cluster-role-binding
subjects:
- kind: ServiceAccount
  name: drone-runner-sa
  namespace: default
roleRef:
  kind: ClusterRole
  name: drone-runner-cluster-role
  apiGroup: rbac.authorization.k8s.io

Terapkan file ini:

kubectl apply -f drone-cluster-rbac.yaml

Buat Aplikasi OAuth di Gitea:

Di UI Gitea, pergi ke Settings -> Applications -> Manage OAuth2 Applications. Buat aplikasi baru dan masukkan http://<IP_PUBLIK_VPS>:<NODE_PORT_DRONE>/login sebagai Redirect URI. Salin Client ID dan Client Secret yang dihasilkan.

Buat Manifes Drone: Buat file drone-manual.yaml

# drone-manual.yaml
apiVersion: v1
kind: Secret
metadata:
  name: drone-secret
  namespace: default
type: Opaque
stringData:
  DRONE_RPC_SECRET: "GantiDenganPasswordDroneYangSangatPanjangDanAcak"
  DRONE_GITEA_CLIENT_ID: "PASTE_CLIENT_ID_DARI_GITEA"
  DRONE_GITEA_CLIENT_SECRET: "PASTE_CLIENT_SECRET_DARI_GITEA"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: drone-server
  namespace: default
spec:
  replicas: 1
  selector: { matchLabels: { app: drone-server } }
  template:
    metadata: { labels: { app: drone-server } }
    spec:
      containers:
      - name: drone
        image: drone/drone:2
        ports:
        - containerPort: 80
        env:
        - name: DRONE_SERVER_PROTO
          value: "http"
        - name: DRONE_SERVER_HOST
          value: "<IP_PUBLIK_VPS_A>:<NODE_PORT_DRONE>"
        - name: DRONE_GITEA_SERVER
          value: "http://<IP_PUBLIK_VPS_A>:<NODE_PORT_GITEA>"
        - name: DRONE_RPC_SECRET
          valueFrom: { secretKeyRef: { name: drone-secret, key: DRONE_RPC_SECRET } }
        - name: DRONE_GITEA_CLIENT_ID
          valueFrom: { secretKeyRef: { name: drone-secret, key: DRONE_GITEA_CLIENT_ID } }
        - name: DRONE_GITEA_CLIENT_SECRET
          valueFrom: { secretKeyRef: { name: drone-secret, key: DRONE_GITEA_CLIENT_SECRET } }
        - name: DRONE_USER_CREATE
          value: "username:<NAMA_ADMIN_GITEA_ANDA>,admin:true"
---
apiVersion: v1
kind: Service
metadata:
  name: drone-svc
  namespace: default
spec:
  type: NodePort
  selector: { app: drone-server }
  ports:
  - name: http
    port: 80
    targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: drone-runner
  namespace: default
spec:
  replicas: 1
  selector: { matchLabels: { app: drone-runner } }
  template:
    metadata: { labels: { app: drone-runner } }
    spec:
      serviceAccountName: drone-runner-sa
      containers:
      - name: runner
        image: drone/drone-runner-kube:latest
        ports:
        - containerPort: 3000
        env:
        - name: DRONE_RPC_HOST
          value: drone-svc
        - name: DRONE_RPC_PROTO
          value: http
        - name: DRONE_RPC_SECRET
          valueFrom: { secretKeyRef: { name: drone-secret, key: DRONE_RPC_SECRET } }

Terapkan file ini:

kubectl apply -f drone-manual.yaml

Fase 3: Menjalankan Pipeline CI/CD

Langkah 3.1: Persiapan Kode Aplikasi

Di komputer lokal, buat sebuah repositori Git baru dengan file-file berikut:

  • main.go
    package main
    import ("fmt"; "net/http" )
    func main() {
        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request ) {
            fmt.Fprintf(w, "Hello, Anda telah berhasil men-deploy aplikasi dengan CI/CD!")
        })
        http.ListenAndServe(":8080", nil )
    }
    
  • deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: hello-kubernetes
    spec:
      replicas: 1
      selector: { matchLabels: { app: hello-kubernetes } }
      template:
        metadata: { labels: { app: hello-kubernetes } }
        spec:
          containers:
          - name: app
            image: IMAGE_PLACEHOLDER
            ports:
            - containerPort: 8080
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: hello-kubernetes-svc
    spec:
      type: NodePort
      selector: { app: hello-kubernetes }
      ports:
      - port: 80
        targetPort: 8080
    
  • .drone.yml
    kind: pipeline
    type: kubernetes
    name: default
    
    steps:
    - name: build-and-push-with-kaniko
      image: gcr.io/kaniko-project/executor:v1.9.1-debug
      commands:
      - mkdir -p /kaniko/build
      - cp -r ./* /kaniko/build/
      - /kaniko/executor
        --context dir:///kaniko/build/
        --dockerfile /kaniko/build/Dockerfile
        --destination local-registry-svc:5000/hello-kubernetes:${DRONE_COMMIT_SHA:0:7}
        --skip-pull
        --insecure
    - name: deploy-to-kubernetes
      image: bitnami/kubectl
      commands:
      - sed -i "s|IMAGE_PLACEHOLDER|local-registry-svc:5000/hello-kubernetes:${DRONE_COMMIT_SHA:0:7}|g" deployment.yaml
      - kubectl apply -f deployment.yaml
    
  • Dockerfile
    FROM golang:1.19-alpine AS builder
    WORKDIR /app
    COPY . .
    RUN go build -o main .
    
    FROM alpine:latest
    WORKDIR /app
    COPY --from=builder /app/main .
    EXPOSE 8080
    CMD ["./main"]
    

     

Langkah 3.2: Proses “Image Pre-warming”

Untuk menghindari ketergantungan pada internet saat pipeline berjalan, kita akan “menyelundupkan” image yang dibutuhkan ke dalam klaster. Jalankan command berikut di VPS A dan VPS B.

docker pull gcr.io/kaniko-project/executor:v1.9.1-debug

docker pull bitnami/kubectl:latest

docker pull golang:1.19-alpine

Langkah 3.3: Eksekusi!

  1. Push repositori lokal Anda ke Gitea.
  2. Buka dashboard Drone CI.
  3. Aktifkan repositori tersebut di Drone.
  4. Lakukan git push sekali lagi untuk memicu build pertama.
Kita akan melihat pipeline berjalan dengan sukses, dari clone, build, hingga deploy. Untuk mengakses aplikasi, cari NodePort dari service hello-kubernetes-svc dan buka http://<IP_PUBLIK_VPS>:<NODE_PORT_APLIKASI>.