Provision Cluster

Provision Cluster #

Membangun kluster Kubernetes dari nol sering kali dianggap sebagai salah satu ritus peralihan paling menantang bagi seorang DevOps Engineer maupun Administrator Sistem. Proses ini melibatkan serangkaian langkah tingkat sistem yang presisi dan berurutan di banyak server: menonaktifkan swap, memuat modul kernel, menyetel parameter sysctl, menginstal container runtime, menginstal binari Kubernetes, melakukan inisialisasi control plane, mengatur konfigurasi jaringan kluster (CNI), hingga akhirnya menggabungkan worker nodes.

Melakukan langkah-langkah ini secara manual pada 3 atau 10 server tidak hanya melelahkan, tetapi juga sangat rentan terhadap kesalahan manusia (human error). Satu kesalahan kecil seperti lupa mengaktifkan SystemdCgroup pada runtime kontainer dapat menyebabkan kluster gagal berfungsi secara misterius beberapa hari kemudian. Di sinilah Ansible hadir sebagai solusi penyelamat. Dengan mengotomasi seluruh alur bootstrap kluster menggunakan playbook Ansible yang idempoten, kita dapat melahirkan kluster Kubernetes yang konsisten, terstandardisasi, dan siap produksi hanya dengan satu perintah tunggal.


Mengapa Memilih Ansible untuk Provisioning Kluster? #

Ketika kita memutuskan untuk mengotomasi siklus hidup infrastruktur Kubernetes, kita memiliki beberapa pilihan alat. Namun, Ansible menawarkan keunggulan unik untuk proses bootstrapping sistem operasi tingkat rendah.

Tantangan Bootstrap Manual:
  ✗ Kita harus SSH ke setiap server satu per satu untuk menjalankan perintah setup OS.
  ✗ Mengedit file konfigurasi sensitif seperti /etc/fstab atau config.toml secara manual rawan typo.
  ✗ Token inisialisasi kubeadm memiliki masa berlaku terbatas (24 jam) — menyalin token ini secara manual antar-server sangat merepotkan.
  ✗ Sulit untuk mereplikasi konfigurasi yang sama persis jika kita ingin membuat kluster Staging dan Produksi yang identik.

Kemudahan dengan Automasi Ansible:
  ✓ Satu Playbook untuk Semua Node — jalankan semua tugas persiapan OS secara paralel di seluruh server.
  ✓ Templating Dinamis — gunakan Jinja2 untuk menghasilkan file konfigurasi containerd dan sysctl yang bersih dan akurat.
  ✓ Orkestrasi Alur Kerja — Ansible dapat mendaftarkan token kubeadm di control plane, menyimpannya sebagai variabel memori, lalu meneruskannya ke worker nodes secara otomatis.
  ✓ Version Control — seluruh infrastruktur kluster kita didefinisikan sebagai kode (IaC) yang dapat disimpan di repositori Git.

Desain Arsitektur Jaringan dan Inventory Kluster #

Sebelum mulai menulis task Ansible, kita harus merancang topologi kluster kita terlebih dahulu. Kita akan membangun kluster dengan satu Control Plane (Master Node) dan dua Worker Nodes. Topologi komunikasi antar node dapat kita visualisasikan melalui diagram berikut:

flowchart TD
    subgraph ControlPlane["Control Plane Node"]
        M1["k8s-master-01 (10.0.1.10)"]
    end

    subgraph WorkerNodes["Worker Nodes"]
        W1["k8s-worker-01 (10.0.2.10)"]
        W2["k8s-worker-02 (10.0.2.11)"]
    end

    M1 <-->|"Join Token & API Control (Port 6443)"| W1
    M1 <-->|"Join Token & API Control (Port 6443)"| W2
    W1 <-->|"Pod-to-Pod CNI Overlay Network"| W2

Untuk merepresentasikan arsitektur di atas ke dalam Ansible, kita membuat file inventory terstruktur. File ini membagi server berdasarkan perannya agar kita dapat mengarahkan tugas-tugas tertentu ke kelompok server yang tepat.

# inventory/k8s-cluster/hosts.ini
[control_plane]
k8s-master-01 ansible_host=10.0.1.10 ansible_user=ubuntu

[worker_nodes]
k8s-worker-01 ansible_host=10.0.2.10 ansible_user=ubuntu
k8s-worker-02 ansible_host=10.0.2.11 ansible_user=ubuntu

[k8s_cluster:children]
control_plane
worker_nodes

Kita juga mendefinisikan variabel global kluster untuk mengatur versi paket dan konfigurasi jaringan internal kluster di dalam file group_vars:

# inventory/k8s-cluster/group_vars/k8s_cluster.yml
---
k8s_version: "1.29.2"
k8s_minor_version: "1.29"
pod_network_cidr: "192.168.0.0/16" # CIDR default untuk CNI Calico
control_plane_endpoint: "10.0.1.10:6443"

Langkah 1: Persiapan Sistem Operasi dan Kernel Linux #

Kubernetes membutuhkan konfigurasi sistem operasi tingkat rendah yang sangat spesifik sebelum komponen utamanya dapat berjalan. Dua konfigurasi paling kritis adalah menonaktifkan swap secara permanen dan mengonfigurasi modul kernel untuk menjembatani jaringan kontainer.

Mengapa Kubernetes Melarang Swap? #

Secara default, kubelet tidak akan mau berjalan jika swap aktif di server. Alasan utamanya adalah jaminan alokasi resource dan performa. Kubernetes didesain untuk menjadwalkan aplikasi berdasarkan batas resource yang ketat. Jika sistem operasi memindahkan memori Pod ke swap disk, performa aplikasi akan menurun drastis secara tidak terduga, dan Kubernetes tidak dapat menghitung penggunaan memori nyata kluster secara akurat.

Modul Kernel dan Sysctl #

Agar lalu lintas jaringan dari container di dalam Pod dapat dijembatani dengan benar di tingkat host, kita harus memuat modul kernel overlay dan br_netfilter, serta mengonfigurasi parameter sysctl agar iptables dapat melihat lalu lintas bridge jaringan Linux.

Berikut adalah implementasi tugas persiapan OS dalam role Ansible kita:

# roles/k8s-common/tasks/main.yml
---
- name: Nonaktifkan swap untuk sesi saat ini
  command: swapoff -a
  changed_when: false

- name: Pastikan swap dinonaktifkan secara permanen di fstab
  replace:
    path: /etc/fstab
    regexp: '^([^#].*?\sswap\s+sw\s+.*)$'
    replace: '# \1'

- name: Buat file konfigurasi load module kernel
  copy:
    dest: /etc/modules-load.d/k8s.conf
    content: |
      overlay
      br_netfilter      
    owner: root
    group: root
    mode: '0644'

- name: Muat modul kernel overlay secara manual
  modprobe:
    name: overlay
    state: present

- name: Muat modul kernel br_netfilter secara manual
  modprobe:
    name: br_netfilter
    state: present

- name: Konfigurasi parameter sysctl untuk jaringan Kubernetes
  sysctl:
    name: "{{ item.key }}"
    value: "{{ item.value }}"
    state: present
    sysctl_file: /etc/sysctl.d/99-kubernetes-cri.conf
    reload: true
  loop:
    - { key: "net.bridge.bridge-nf-call-iptables",  value: "1" }
    - { key: "net.bridge.bridge-nf-call-ip6tables", value: "1" }
    - { key: "net.ipv4.ip_forward",                 value: "1" }

Langkah 2: Konfigurasi Container Runtime (containerd) #

Sejak Kubernetes mendepresiasi Docker Shim pada versi 1.20 dan menghapusnya sepenuhnya di versi 1.24, kita harus menggunakan container runtime mandiri yang kompatibel dengan CRI (Container Runtime Interface). containerd adalah pilihan standar industri yang sangat stabil dan ringan.

Tantangan terbesar saat mendeploy containerd untuk Kubernetes adalah konfigurasi driver cgroup. Secara default, Linux menggunakan systemd sebagai sistem inisialisasi proses. Jika containerd dikonfigurasi menggunakan driver cgroup bawaannya sendiri (cgroupfs), sistem akan memiliki dua manajer cgroup yang berbeda. Hal ini dapat menyebabkan ketidakstabilan sistem ketika server kehabisan resource. Kita harus memaksa containerd untuk menggunakan driver cgroup milik systemd (SystemdCgroup = true).

# roles/containerd/tasks/main.yml
---
- name: Install dependencies awal untuk containerd
  apt:
    name:
      - apt-transport-https
      - ca-certificates
      - curl
      - gnupg
    state: present
    update_cache: true

- name: Buat direktori keyrings untuk repositori Docker
  file:
    path: /etc/apt/keyrings
    state: directory
    mode: '0755'

- name: Tambahkan GPG Key resmi Docker
  apt_key:
    url: https://download.docker.com/linux/ubuntu/gpg
    state: present
    keyring: /etc/apt/keyrings/docker.gpg

- name: Tambahkan repositori Docker untuk containerd
  apt_repository:
    repo: "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
    state: present
    filename: docker

- name: Install paket containerd.io
  apt:
    name: containerd.io
    state: present
    update_cache: true

- name: Buat direktori konfigurasi containerd
  file:
    path: /etc/containerd
    state: directory
    mode: '0755'

- name: Generate konfigurasi default containerd
  command: containerd config default
  register: containerd_config_raw
  changed_when: false

- name: Tulis file konfigurasi default containerd
  copy:
    content: "{{ containerd_config_raw.stdout }}"
    dest: /etc/containerd/config.toml
    owner: root
    group: root
    mode: '0644'

- name: Konfigurasi containerd agar menggunakan SystemdCgroup driver
  replace:
    path: /etc/containerd/config.toml
    regexp: 'SystemdCgroup = false'
    replace: 'SystemdCgroup = true'
  notify: Restart containerd

- name: Pastikan containerd aktif dan berjalan saat booting
  systemd:
    name: containerd
    state: started
    enabled: true

Kita juga membuat handler untuk merestart layanan containerd jika ada perubahan konfigurasi:

# roles/containerd/handlers/main.yml
---
- name: Restart containerd
  systemd:
    name: containerd
    state: restarted

Langkah 3: Instalasi Komponen Kubeadm, Kubelet, dan Kubectl #

Setelah container runtime siap, langkah selanjutnya adalah memasang komponen inti Kubernetes:

  1. kubelet: Layanan agen utama yang bertanggung jawab menjalankan container di setiap node.
  2. kubeadm: Alat CLI untuk mendeploy dan melakukan inisialisasi kluster Kubernetes sesuai dengan best practice keamanan.
  3. kubectl: Alat CLI untuk berinteraksi dengan API Server Kubernetes.

[!IMPORTANT] Mulai pertengahan tahun 2023, Google secara resmi menutup repositori APT warisan (apt.kubernetes.io). Kita wajib beralih menggunakan repositori komunitas baru yang beralamat di pkgs.k8s.io dengan format penamaan direktori versi minor yang spesifik.

# roles/k8s-packages/tasks/main.yml
---
- name: Buat direktori keyring untuk Kubernetes
  file:
    path: /etc/apt/keyrings
    state: directory
    mode: '0755'

- name: Unduh GPG Key repositori Kubernetes baru
  apt_key:
    url: "https://pkgs.k8s.io/core:/stable:/v{{ k8s_minor_version }}/deb/Release.key"
    state: present
    keyring: /etc/apt/keyrings/kubernetes-apt-keyring.gpg

- name: Tambahkan repositori Kubernetes baru ke sumber apt
  apt_repository:
    repo: "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v{{ k8s_minor_version }}/deb/ /"
    state: present
    filename: kubernetes

- name: Install kubelet, kubeadm, dan kubectl secara bersamaan
  apt:
    name:
      - "kubelet={{ k8s_version }}-*"
      - "kubeadm={{ k8s_version }}-*"
      - "kubectl={{ k8s_version }}-*"
    state: present
    update_cache: true

- name: Pin versi paket agar tidak terupdate tidak sengaja saat apt upgrade
  dpkg_selections:
    name: "{{ item }}"
    selection: hold
  loop:
    - kubelet
    - kubeadm
    - kubectl

Langkah 4: Inisialisasi Control Plane (Bootstrap Master Node) #

Sekarang, semua dependencies telah terinstal di semua node. Saatnya kita menginisialisasi node Control Plane pertama kita menggunakan perintah kubeadm init. Tugas ini hanya boleh dijalankan di server master pertama (control_plane[0]).

Setelah inisialisasi selesai, kita harus:

  1. Menyalin file administrasi kluster (admin.conf alias kubeconfig) ke direktori home pengguna kita agar kita dapat menjalankan perintah kubectl tanpa hak akses root.
  2. Membuat token join baru yang dinamis untuk digunakan oleh worker nodes agar dapat bergabung ke kluster secara aman.
# playbooks/init-control-plane.yml
---
- name: Inisialisasi Kubernetes Control Plane
  hosts: control_plane[0]
  become: true
  tasks:
    - name: Cek apakah Control Plane sudah pernah diinisialisasi sebelumnya
      stat:
        path: /etc/kubernetes/admin.conf
      register: k8s_init_check

    - name: Jalankan inisialisasi kubeadm di node master pertama
      command: >
        kubeadm init
        --pod-network-cidr={{ pod_network_cidr }}
        --control-plane-endpoint={{ control_plane_endpoint }}
        --kubernetes-version={{ k8s_version }}        
      register: kubeadm_init_output
      when: not k8s_init_check.stat.exists

- name: Konfigurasi Akses kubectl untuk User Biasa
  hosts: control_plane[0]
  become: false
  tasks:
    - name: Buat direktori .kube di home user
      file:
        path: "{{ ansible_env.HOME }}/.kube"
        state: directory
        mode: '0700'

    - name: Salin file konfigurasi admin.conf ke direktori home user
      copy:
        src: /etc/kubernetes/admin.conf
        dest: "{{ ansible_env.HOME }}/.kube/config"
        remote_src: true
        owner: "{{ ansible_user }}"
        group: "{{ ansible_user }}"
        mode: '0600'
      become: true

- name: Ambil Perintah Join Secara Dinamis
  hosts: control_plane[0]
  become: true
  tasks:
    - name: Generate join command baru yang valid
      command: kubeadm token create --print-join-command
      register: temp_join_command
      changed_when: false

    - name: Simpan join command ke variabel global localhost
      set_fact:
        k8s_join_command: "{{ temp_join_command.stdout }}"
      delegate_to: localhost
      delegate_facts: true

Langkah 5: Setup Container Network Interface (CNI) #

Kubernetes mengasumsikan bahwa setiap Pod memiliki IP address unik sendiri yang dapat berkomunikasi langsung dengan Pod lain di kluster, bahkan jika Pod tersebut berada di host fisik yang berbeda. Untuk mewujudkan model jaringan ini, kita harus menginstal plugin CNI (Container Network Interface).

Dua opsi CNI paling populer di industri:

  1. Flannel: CNI yang sangat sederhana dan ringan. Ia menggunakan enkapsulasi VXLAN untuk membuat jaringan overlay dasar. Flannel sangat cocok untuk kluster kecil atau lingkungan development karena konfigurasinya yang minimal. Namun, ia tidak mendukung fitur keamanan tingkat lanjut seperti Network Policies.
  2. Calico: CNI tingkat lanjut yang sangat populer di tingkat produksi. Calico menggunakan protokol BGP murni tanpa enkapsulasi tambahan (jika dikonfigurasi demikian) untuk memberikan performa jaringan berkecepatan tinggi. Selain itu, ia menyediakan mesin penegak aturan keamanan (Network Policies) yang sangat kuat.
flowchart TD
    subgraph Node_Master["Node Master"]
        API_CNI["kube-apiserver"]
    end

    subgraph Node_Worker1["Node Worker 1"]
        PodA["Pod A (192.168.1.10)"]
        CNI1["Calico Agent (CNI)"]
        PodA --> CNI1
    end

    subgraph Node_Worker2["Node Worker 2"]
        PodB["Pod B (192.168.2.20)"]
        CNI2["Calico Agent (CNI)"]
        PodB --> CNI2
    end

    CNI1 <-->|"Tunnel Overlay VXLAN / BGP"| CNI2

Kita akan menginstal CNI Calico dengan mendeploy manifest resminya langsung dari control plane master setelah inisialisasi selesai:

# playbooks/install-cni.yml
---
- name: Deploy Jaringan CNI Calico
  hosts: control_plane[0]
  become: false
  tasks:
    - name: Unduh manifest resmi Calico Operator
      get_url:
        url: https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/tigera-operator.yaml
        dest: /tmp/tigera-operator.yaml
        mode: '0644'

    - name: Terapkan operator Tigera Calico ke kluster
      command: kubectl create -f /tmp/tigera-operator.yaml
      register: apply_operator
      failed_when: 
        - apply_operator.rc != 0 
        - "'already exists' not in apply_operator.stderr"
      changed_when: "'created' in apply_operator.stdout"

    - name: Unduh manifest konfigurasi Custom Resources Calico
      get_url:
        url: https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/custom-resources.yaml
        dest: /tmp/custom-resources.yaml
        mode: '0644'

    - name: Sesuaikan CIDR Pod di manifest Calico dengan variabel kita
      replace:
        path: /tmp/custom-resources.yaml
        regexp: 'cidr: 192.168.0.0/16'
        replace: "cidr: {{ pod_network_cidr }}"

    - name: Terapkan konfigurasi jaringan Calico
      command: kubectl create -f /tmp/custom-resources.yaml
      register: apply_resources
      failed_when:
        - apply_resources.rc != 0
        - "'already exists' not in apply_resources.stderr"
      changed_when: "'created' in apply_resources.stdout"

Langkah 6: Join Worker Nodes ke Kluster #

Setelah jaringan Control Plane dan CNI terpasang dengan baik, saatnya kita menggabungkan server-server pekerja (Worker Nodes) agar mereka dapat menerima instruksi penjadwalan Pod. Kita akan menggunakan variabel join command yang telah disimpan sebelumnya di host localhost.

Setelah menjalankan perintah join, tugas operasional terbaik adalah memastikan dan menunggu hingga status node baru tersebut dilaporkan sebagai Ready oleh API Server sebelum kita mengakhiri playbook.

# playbooks/join-workers.yml
---
- name: Gabungkan Worker Nodes ke Cluster
  hosts: worker_nodes
  become: true
  tasks:
    - name: Cek apakah node ini sudah terhubung sebelumnya
      stat:
        path: /etc/kubernetes/kubelet.conf
      register: kubelet_conf_check

    - name: Jalankan perintah join ke Control Plane
      command: "{{ hostvars['localhost']['k8s_join_command'] }}"
      when: not kubelet_conf_check.stat.exists

    - name: Tunggu status node menjadi 'Ready' di Control Plane
      command: >
        kubectl get node {{ ansible_hostname }}
        --kubeconfig /etc/kubernetes/admin.conf
        -o jsonpath='{.status.conditions[-1].type}'        
      register: node_status_output
      until: node_status_output.stdout == "Ready"
      retries: 30
      delay: 10
      delegate_to: "{{ groups['control_plane'][0] }}"
      changed_when: false

Langkah 7: Pengambilan Kubeconfig ke Control Node Lokal #

Langkah terakhir dari provisioning kluster adalah menyalin file admin.conf yang aman dari server master pertama ke mesin lokal (control node) kita. Ini memungkinkan kita untuk mengelola kluster Kubernetes dari jarak jauh menggunakan tool local seperti kubectl, k9s, atau playbook Ansible kita selanjutnya tanpa harus login SSH terus-menerus ke server master.

# playbooks/fetch-kubeconfig.yml
---
- name: Amankan dan Fetch Kubeconfig ke Lokal
  hosts: control_plane[0]
  become: true
  tasks:
    - name: Buat direktori penampung kubeconfig lokal di Control Node
      file:
        path: "{{ playbook_dir }}/kubeconfig"
        state: directory
        mode: '0700'
      delegate_to: localhost
      become: false

    - name: Salin file admin.conf ke direktori playbook lokal
      fetch:
        src: /etc/kubernetes/admin.conf
        dest: "{{ playbook_dir }}/kubeconfig/admin.conf"
        flat: true

    - name: Sesuaikan alamat endpoint API di file kubeconfig lokal
      replace:
        path: "{{ playbook_dir }}/kubeconfig/admin.conf"
        regexp: 'server: https://127.0.0.1:6443'
        replace: "server: https://{{ control_plane_endpoint.split(':')[0] }}:6443"
      delegate_to: localhost
      become: false

Sekarang kita memiliki file konfigurasi cluster lokal yang aman di path kubeconfig/admin.conf. Kita dapat menggunakannya dengan mudah dengan mengeset variabel lingkungan export KUBECONFIG=./kubeconfig/admin.conf di terminal lokal kita.


Ringkasan #

  • Menonaktifkan swap wajib dilakukan secara permanen di seluruh node kluster agar kubelet dapat berjalan stabil dan menghitung resource secara akurat.
  • Modul kernel overlay dan br_netfilter serta parameter sysctl jaringan bridge wajib aktif agar lalu lintas komunikasi kontainer dapat menjembatani host Linux dengan benar.
  • Gunakan containerd dengan konfigurasi SystemdCgroup = true untuk menyelaraskan manajemen cgroup sistem operasi dengan container runtime demi menghindari crash OOM.
  • Pindahkan konfigurasi repositori Kubernetes lama ke repositori komunitas baru pkgs.k8s.io untuk mendapatkan pembaruan versi minor di atas versi 1.28+.
  • Gunakan dpkg_selections hold pada paket kubelet, kubeadm, dan kubectl agar versi komponen kluster terkunci dan tidak ter-upgrade otomatis secara tidak sengaja.
  • Terapkan CNI (seperti Calico) segera setelah control plane siap untuk mengaktifkan komunikasi jaringan dinamis antar-Pod lintas host.
  • Ambil join command secara dinamis menggunakan memori host fakta (set_fact pada localhost) daripada meng-hardcode token statis yang cepat kedaluwarsa di file inventory.
  • Ambil file admin.conf ke lokal dengan penyesuaian endpoint IP agar kluster dapat dikendalikan dari jarak jauh tanpa perlu selalu melakukan SSH ke server master.

← Sebelumnya: Apa itu Kubernetes?   Berikutnya: Deploy Manifest →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact