Bab 41 menutup dengan sebuah janji: repository Git yang sudah jadi fondasi workflow modern developer akan disambungkan ke server yang baru saja kita bangun lewat pipeline CI/CD. Instance vm-cloud01 sendiri sudah dibersihkan mengikuti kebiasaan jujur soal biaya di Bagian 41.4.4, jadi bab ini menepati janji tersebut di server yang tetap hidup sepanjang series ini, yaitu app01 (192.168.1.40) yang sudah kita kenalkan sebagai managed node Ansible sejak Bagian 36.2.2 dan sudah menjalankan aplikasi demo-node sebagai service systemd sejak Bagian 15.6. Seluruh pola yang dipraktikkan di bab ini, mulai dari pipeline, kunci deploy terbatas, sampai rollback, berlaku identik seandainya target deployment-nya adalah instance cloud publik seperti vm-cloud01, disertai satu tambahan pertimbangan jaringan yang disinggung di Bagian 42.4.2.
Skenario yang mendasari bab ini sangat umum terjadi begitu tim mulai bertambah. Developer sudah bisa menulis kode aplikasi Node.js sejak Bab 15, dan Sysadmin sudah bisa mengotomasi konfigurasi server lewat Ansible sejak Bab 36, tetapi kedua kemampuan ini masih dihubungkan secara manual. Developer selesai coding, mengabari lewat chat bahwa "sudah siap deploy", lalu Sysadmin login ke server, menjalankan script atau playbook, dan berharap tidak ada langkah yang lupa dijalankan. Proses ini rawan tertunda jika Sysadmin sedang sibuk atau tidak online, serta rawan human error karena bergantung pada seseorang yang harus mengetik urutan perintah yang benar setiap kali. CI/CD menutup celah ini dengan menjadikan git push itu sendiri sebagai pemicu otomatis, bukan pesan chat yang menunggu direspons manusia.
Bab ini dimulai dari konsep inti Continuous Integration dan Continuous Deployment, dilanjutkan peran Git sebagai fondasi yang memicu seluruh proses, lalu menyiapkan app01 dengan struktur direktori berbasis release dan akses yang sengaja dibatasi untuk otomasi. Bagian praktik intinya mempraktikkan tiga cara mengirim rilis dari repository ke server, yaitu script manual, GitHub Actions dengan self-hosted runner, dan playbook Ansible yang dipanggil dari pipeline, sebelum ditutup strategi rollback yang memanfaatkan pola symlink yang sama.
42.1 Konsep Continuous Integration/Continuous Deployment
42.1.1 Continuous Integration: Menggabungkan dan Menguji Kode Secara Otomatis
Continuous Integration (CI) adalah praktik menggabungkan perubahan kode dari banyak Developer ke satu branch bersama secara sering, biasanya beberapa kali sehari, dan setiap penggabungan langsung diuji otomatis lewat proses build dan test tanpa menunggu campur tangan manusia. Sebelum CI populer, tim sering menunda proses penggabungan kode sampai mendekati jadwal rilis, sehingga konflik antarperubahan baru ketahuan belakangan dan jauh lebih sulit diurai. CI membalik urutan itu: penggabungan dilakukan sesering mungkin, dan setiap penggabungan langsung diverifikasi lewat pipeline otomatis, sehingga masalah ketahuan dalam hitungan menit, bukan minggu.
Praktisnya, sebuah repository yang menerapkan CI selalu punya definisi "berhasil" yang jelas dan bisa dijalankan mesin, misalnya "semua unit test lulus" atau "proses build tidak menghasilkan error". Bagian 42.2.3 mempraktikkan langsung definisi ini pada aplikasi demo-node.
42.1.2 Continuous Delivery vs Continuous Deployment
Dua istilah ini sering dianggap sama padahal bedanya cukup penting bagi Sysadmin yang bertanggung jawab atas server produksi.
- Continuous Delivery berarti setiap perubahan yang lulus tahap CI otomatis disiapkan sampai benar-benar siap dirilis, tetapi langkah terakhir untuk benar-benar mendorongnya ke server produksi masih menunggu persetujuan manusia, biasanya lewat satu klik tombol "deploy" atau approval di pull request.
- Continuous Deployment melangkah lebih jauh: setiap perubahan yang lulus tahap CI langsung dirilis ke produksi tanpa ada gerbang persetujuan manual sama sekali. Kecepatan ini hanya masuk akal jika cakupan test dan mekanisme rollback-nya sudah cukup dipercaya, sesuatu yang baru kita bangun sendiri di Bagian 42.5.
Bab ini mempraktikkan pola Continuous Deployment penuh, sebab lab dengan satu server tunggal dan trafik terbatas adalah tempat paling aman untuk belajar tanpa risiko besar. Di lingkungan produksi dengan trafik nyata, kombinasi Continuous Delivery untuk rilis besar dan Continuous Deployment untuk perbaikan kecil yang sudah teruji baik justru lebih umum ditemui.
42.1.3 Anatomi Pipeline CI/CD dan Posisi Sysadmin di Dalamnya
Sebuah pipeline CI/CD pada dasarnya adalah rangkaian tahap otomatis yang dijalankan berurutan setiap kali ada trigger, umumnya git push. Empat tahap yang paling umum ditemui, meski penamaannya bisa berbeda-beda antar-tool, adalah build (menyiapkan aplikasi agar bisa dijalankan, misalnya npm install), test (menjalankan pengujian otomatis), deploy (mengirim hasilnya ke server target), dan verify (memastikan hasil deploy benar-benar sehat sebelum dianggap selesai).
Developer biasanya paling peduli pada dua tahap pertama, memastikan kode yang ditulis benar secara fungsional. Sysadmin justru paling banyak berperan di dua tahap terakhir: menyiapkan server target agar bisa menerima deployment dengan aman, membatasi akses yang dipakai pipeline seketat mungkin, dan menyiapkan jalan keluar jika deployment ternyata bermasalah. Bagian 42.3 sampai 42.5 bab ini seluruhnya berada di wilayah tanggung jawab Sysadmin tersebut.
42.2 Git sebagai Fondasi Workflow Deployment
42.2.1 Kenapa Commit Git Jadi Satu-Satunya Sumber Kebenaran Rilis
Bagian 36.1.1 sudah memperkenalkan masalah configuration drift pada konfigurasi server, dan masalah yang sama persis terjadi pada kode aplikasi jika tidak ada satu sumber kebenaran yang jelas. Tanpa Git, "versi yang berjalan di server" bisa jadi hasil scp manual yang sudah beberapa kali ditimpa file tambal sulam langsung di server, tanpa riwayat yang jelas mengenai siapa yang mengubah apa dan kapan. Git menutup celah ini dengan menjadikan setiap commit sebagai unit perubahan yang tercatat permanen dan punya identitas unik (hash SHA), sehingga "versi yang sedang berjalan di server" selalu bisa dijawab tegas dengan satu commit hash tertentu, bukan tebakan.
Bab ini mengasumsikan pembaca sudah familier dengan perintah Git dasar seperti git add, git commit, dan git push, karena fokus bab ini adalah bagaimana Git terhubung ke pipeline deployment, bukan tutorial Git dari nol.
42.2.2 Trunk-Based Development vs Git-Flow untuk Deployment Otomatis
Cara tim mengatur branch Git secara langsung memengaruhi seberapa lancar CI/CD bisa berjalan. Dua pola yang paling sering dibandingkan adalah trunk-based development dan Git-Flow.
| Aspek | Trunk-Based Development | Git-Flow |
|---|---|---|
| Branch utama | Satu branch (main), jadi sumber deploy langsung | Beberapa branch permanen (main, develop) plus branch rilis terpisah |
| Umur feature branch | Pendek, idealnya kurang dari sehari sebelum di-merge | Bisa berumur panjang sampai fitur benar-benar selesai |
| Kecocokan dengan Continuous Deployment | Tinggi, tiap merge ke main langsung berarti kandidat rilis | Rendah, perlu tahap merge tambahan ke branch rilis dulu |
| Risiko utama | Kode belum matang bisa ikut ter-deploy jika tidak dibantu feature flag | Merge konflik besar menumpuk karena branch berumur panjang |
Pipeline yang dibangun di bab ini memakai pola trunk-based, dengan setiap push ke branch main langsung memicu deployment. Pola ini sengaja dipilih karena selaras dengan Continuous Deployment yang dibahas di Bagian 42.1.2; branch fitur yang berumur panjang ala Git-Flow justru menunda nilai utama CI/CD, yaitu umpan balik cepat begitu kode digabungkan.
42.2.3 Menyiapkan Repository demo-node dan Push ke GitHub
Aplikasi demo-node yang sudah berjalan sebagai service di app01 sejak Bagian 15.6.2 belum pernah tersimpan di Git sama sekali; source code-nya hanya ada di /opt/demo-node/server.js milik server itu sendiri. Bagian ini membuat repository-nya, sekaligus menambahkan sedikit struktur pada server.js supaya ada logika yang benar-benar bisa diuji otomatis pada tahap CI.
Langkah Praktik
- Di komputer kerja kita (bukan di
app01), buat direktori project baru dan inisialisasi Git.mkdir ~/demo-node && cd ~/demo-node git init - Buat
server.js, melanjutkan kode dari Bagian 15.6.1, ditambah sedikit refactor agar fungsi pembentuk response bisa diimpor dan diuji terpisah dari proseslisten.nano server.js - Isi dengan kode berikut.
Blokconst http = require('http'); const os = require('os'); const port = process.env.PORT || 3000; function buildResponseText(hostname, date) { return `Node.js aktif di ${hostname}, waktu server: ${date.toISOString()}\n`; } const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(buildResponseText(os.hostname(), new Date())); }); if (require.main === module) { server.listen(port, () => { console.log(`Server berjalan di port ${port}`); }); } module.exports = { buildResponseText };if (require.main === module)memastikanserver.listenhanya berjalan saat file ini dieksekusi langsung lewatnode server.js, persis seperti dipakaiExecStartpada unit systemd Bagian 15.6.2, tetapi tidak ikut berjalan saat file ini diimpor sebagai module oleh test. - Buat
package.jsonminimal, cukup untuk mendaftarkan perintah test.nano package.json - Isi dengan konfigurasi berikut.
Perintah{ "name": "demo-node", "version": "1.0.0", "private": true, "scripts": { "test": "node --test" } }node --testmemakai test runner bawaan Node.js (modulenode:test), stabil sejak beberapa versi LTS terakhir, sehingga tidak perlu memasang dependency test framework pihak ketiga sama sekali untuk contoh sesederhana ini. - Buat direktori test dan file pengujian pertama.
mkdir test nano test/server.test.js - Isi dengan pengujian sederhana terhadap
buildResponseText.const test = require('node:test'); const assert = require('node:assert'); const { buildResponseText } = require('../server'); test('buildResponseText menyertakan hostname dan format ISO date', () => { const sample = new Date('2026-08-28T00:00:00.000Z'); const text = buildResponseText('app01', sample); assert.match(text, /app01/); assert.match(text, /2026-08-28T00:00:00\.000Z/); }); - Jalankan test secara lokal sebelum di-commit, memastikan tahap CI nanti punya dasar yang sudah terbukti jalan.
node --test - Commit seluruh file, lalu buat repository baru di GitHub (privat, karena Bagian 42.4.2 nanti menjalankan self-hosted runner yang menurut dokumentasi resmi GitHub sebaiknya tidak dipasangkan ke repository publik), dan push.
git add server.js package.json test/ git commit -m "Inisialisasi demo-node dengan test dasar" git branch -M main git remote add origin [email protected]:contoh-user/demo-node.git git push -u origin main
Verifikasi dan Troubleshooting
- Output
node --testyang sehat menampilkan ringkasan# pass 1dan# fail 0. Kegagalan pada barisassert.matchkedua biasanya berarti argumen tanggal yang dikirim kebuildResponseTextbukan objekDateyang valid. - Ganti
[email protected]:contoh-user/demo-node.gitdengan URL repository milik kita sendiri. Perintahgit pushyang gagal denganPermission denied (publickey)berarti akun GitHub kita belum terdaftar SSH key, langkah yang polanya sama seperti autentikasi berbasis key di Bagian 3.2.2, hanya saja key-nya didaftarkan ke pengaturan akun GitHub, bukan keauthorized_keyssebuah server.
42.3 Menyiapkan app01 sebagai Target Deployment
42.3.1 Migrasi ke Struktur Direktori Berbasis Release
Struktur /opt/demo-node/server.js yang dipakai sejak Bab 15 cukup untuk belajar systemd, tetapi tidak cocok untuk deployment berulang: menimpa server.js langsung saat rilis baru sedang berjalan berarti ada jendela waktu singkat ketika proses lama masih membaca file yang sudah setengah tertimpa. Pola yang jauh lebih aman, dipakai luas oleh tool deployment seperti Capistrano dan dipraktikkan di bagian ini secara manual, adalah menaruh setiap rilis di direktori sendiri lalu mengarahkan satu symlink bernama current ke rilis yang aktif.
Langkah Praktik
Jalankan seluruh langkah berikut di app01.
- Hentikan sementara service yang sedang berjalan sebelum strukturnya diubah.
sudo systemctl stop demo-node.service - Buat direktori
releases, lalu pindahkanserver.jsyang sudah ada ke dalam rilis pertama, diberi nama timestamp saat ini.sudo mkdir -p /opt/demo-node/releases RELEASE_ID=$(date +%Y%m%d%H%M%S) sudo mkdir "/opt/demo-node/releases/$RELEASE_ID" sudo mv /opt/demo-node/server.js "/opt/demo-node/releases/$RELEASE_ID/" - Buat symlink
currentmenunjuk ke rilis pertama ini.sudo ln -sfn "/opt/demo-node/releases/$RELEASE_ID" /opt/demo-node/current - Samakan kepemilikan seluruh struktur baru ke user
nodeappyang sudah dibuat di Bagian 15.6.2.sudo chown -R nodeapp:nodeapp /opt/demo-node - Sesuaikan unit systemd agar menunjuk ke symlink
current, bukan lagi ke path tetap.sudo nano /etc/systemd/system/demo-node.service - Ubah baris
WorkingDirectorydanExecStartmenjadi seperti berikut, baris lain tetap sama seperti Bagian 15.6.2.WorkingDirectory=/opt/demo-node/current ExecStart=/usr/bin/node /opt/demo-node/current/server.js - Muat ulang systemd dan jalankan kembali service-nya.
sudo systemctl daemon-reload sudo systemctl start demo-node.service
Verifikasi dan Troubleshooting
- Uji dengan
curlseperti biasa untuk memastikan aplikasi tetap berjalan normal setelah migrasi struktur.curl http://localhost:3000 - Perintah
readlink -f /opt/demo-node/currentsemestinya menampilkan path lengkap ke direktori rilis pertama tadi, bukti symlink sudah terpasang benar. - Kegagalan
systemctl startdengan pesan menyinggungMODULE_NOT_FOUNDatau path tidak ditemukan hampir selalu berarti langkah 6 salah ketik path, atau langkah daemon-reload di langkah 7 terlewat sehingga systemd masih memakai definisi unit lama dari cache-nya.
42.3.2 Membuat Deploy User dengan SSH Key Terbatas (Forced Command)
Pipeline CI butuh cara masuk ke app01 tanpa campur tangan manusia. Menyerahkan key SSH pribadi Sysadmin ke sebuah pipeline otomatis, seperti sempat dilakukan untuk kebutuhan manusia di Bagian 41.2.3, bukan praktik yang aman untuk konteks ini: jika credential pipeline bocor, seluruh akses yang dimiliki key itu ikut bocor. Solusinya adalah user dan key khusus yang aksesnya dibatasi ketat hanya untuk satu tugas: menerima rilis baru.
Langkah Praktik
- Buat system user
deploydiapp01, tanpa hak login shell interaktif.sudo useradd --system --create-home --shell /usr/sbin/nologin deploy - Buat direktori
.sshmilik user ini dengan permission yang benar.sudo -u deploy mkdir -m 700 /home/deploy/.ssh - Di komputer kerja kita (atau di control node yang dipakai di Bagian 42.4.2), buat pasangan key baru khusus untuk deployment, terpisah dari key pribadi Sysadmin.
ssh-keygen -t ed25519 -f ~/.ssh/deploy_demo_node -C "deploy-ci@demo-node" -N "" - Salin isi
~/.ssh/deploy_demo_node.pub, lalu diapp01tulisauthorized_keysmilik userdeploydengan opsi forced command yang membatasi key ini hanya boleh menjalankan satu script tertentu.sudo -u deploy nano /home/deploy/.ssh/authorized_keys - Isi dengan satu baris berikut, ganti bagian
ssh-ed25519 AAAA...dengan isi public key sesungguhnya.command="/opt/demo-node/deploy.sh",no-agent-forwarding,no-port-forwarding,no-pty,no-X11-forwarding ssh-ed25519 AAAA... deploy-ci@demo-node - Kunci permission file dan direktori ini sesuai syarat OpenSSH.
sudo chmod 600 /home/deploy/.ssh/authorized_keys sudo chown -R deploy:deploy /home/deploy/.ssh
Verifikasi dan Troubleshooting
- Opsi
command=padaauthorized_keysini didokumentasikan resmi di halaman manualsshd(8)bagian AUTHORIZED_KEYS FILE FORMAT: perintah apa pun yang dikirim client lewat SSH akan diabaikan, dan server selalu menjalankan/opt/demo-node/deploy.shsebagai gantinya. Artinya, sekalipun key ini bocor, penyerang paling jauh hanya bisa memicu script deploy itu sendiri, bukan mendapat shell bebas diapp01. Opsino-ptyaman dipakai di sini karena proses pengiriman rilis nanti murni streaming data lewat stdin, tidak butuh terminal interaktif sama sekali. - Script
/opt/demo-node/deploy.shyang dirujuk baris ini belum dibuat sampai Bagian 42.3.3 selesai; percobaan koneksi sebelum script itu ada akan gagal dengan pesanNo such file or directory, wajar untuk sementara.
42.3.3 Menulis deploy.sh dan Sudoers untuk Restart Service
Bagian ini menulis script yang benar-benar dijalankan setiap kali user deploy menerima koneksi SSH. Script ini sengaja tidak ikut disimpan di repository demo-node dan tidak pernah ikut tertimpa oleh isi rilis yang dikirim CI, karena payload rilis nanti hanya diekstrak ke dalam subdirektori releases/ yang baru, bukan menimpa /opt/demo-node itu sendiri. Pemisahan ini penting: perubahan pada script deploy tetap sepenuhnya berada di kendali Sysadmin lewat akses SSH langsung ke app01, bukan lewat commit siapa pun ke repository aplikasi.
Langkah Praktik
- Siapkan direktori
releasesdengan bit setgid aktif dan grupnodeapp, supaya setiap rilis baru yang dibuat userdeployotomatis bisa dibaca usernodeappyang menjalankan service, tanpa perlu perintahchownterpisah setiap deploy.
Bit setgid (sudo chown deploy:nodeapp /opt/demo-node/releases sudo chmod 2775 /opt/demo-node/releases2pada awal mode) membuat subdirektori baru yang dibuat di dalamreleasesotomatis mewarisi grupnodeappmilik direktori induknya, bukan grup utama user yang membuatnya, perilaku standar filesystem Linux untuk direktori dengan bit ini aktif. - Buat file
deploy.sh, dimiliki root supaya tidak bisa diubah oleh userdeploysendiri.sudo nano /opt/demo-node/deploy.sh - Isi dengan script berikut.
Perintah#!/bin/bash set -euo pipefail RELEASES_DIR=/opt/demo-node/releases CURRENT_LINK=/opt/demo-node/current KEEP_RELEASES=5 RELEASE_DIR="$RELEASES_DIR/$(date +%Y%m%d%H%M%S)" mkdir -p "$RELEASE_DIR" tar -xzf - -C "$RELEASE_DIR" ln -sfn "$RELEASE_DIR" "$CURRENT_LINK.tmp" mv -T "$CURRENT_LINK.tmp" "$CURRENT_LINK" sudo /usr/bin/systemctl restart demo-node.service cd "$RELEASES_DIR" ls -1 | sort -r | tail -n +$((KEEP_RELEASES + 1)) | xargs -r rm -rf --tar -xzf -membaca arsip terkompresi langsung dari stdin, cocok dipasangkan dengan CI yang mengirim isi repository lewat pipa SSH yang sama seperti dipraktikkan di Bagian 42.4.1. Pergantian symlink sengaja ditulis dua langkah, membuat symlink sementaracurrent.tmplalu memindahkannya menimpacurrentlewatmv -T, karenarename()pada filesystem yang sama dijamin atomik oleh POSIX, sementara membuat ulang symlink langsung di atas nama yang sama (ln -sfntanpa langkah pemindahan) masih menyisakan jendela waktu singkat tanpa symlink sama sekali. Nama direktori rilis sengaja berformat timestampYYYYMMDDHHMMSSsupaya bisa diurutkan leksikografis lewatsort -r, tidak bergantung pada metadata waktu modifikasi filesystem yang bisa saja meleset. - Beri izin eksekusi. Grup
deployyang dirujuk di sini tidak perlu dibuat terpisah, sebabuseradddi Bagian 42.3.2 langkah pertama sudah otomatis membuat grup baru bernama sama dengan usernya sebagai grup utama, perilaku default Ubuntu selama pengaturanUSERGROUPS_ENABdi/etc/login.defstidak diubah dariyes.sudo chmod 750 /opt/demo-node/deploy.sh sudo chown root:deploy /opt/demo-node/deploy.sh - Baris
sudo systemctl restartdi dalam script butuh privilege yang belum dimiliki userdeploy. Buat rule sudoers baru, mengikuti konvensisudoers.d/yang sudah dipakai sejak Bagian 4.2.2, dibatasi hanya untuk satu perintah spesifik ini.sudo visudo -f /etc/sudoers.d/deploy-demo-node - Isi dengan satu baris berikut.
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart demo-node.service
Verifikasi dan Troubleshooting
- Rule sudoers di atas sengaja tidak memakai wildcard seperti
systemctl *atauALL, mengikuti prinsip least privilege yang sudah dibahas di Bagian 34.2.1: userdeployhanya boleh me-restart satu service tertentu, tidak lebih, sekalipun kredensialnya bocor. sudo visudo -fpada langkah 5 memvalidasi sintaks file sebelum menyimpannya, mencegah kesalahan ketik yang bisa merusak seluruh mekanismesudodi server, kebiasaan yang sama pentingnya dengan validasicloud-init schemadi Bagian 41.2.3.- Bagian ini belum bisa diuji end-to-end sampai ada CI yang benar-benar mengirim rilis; pengujian penuh dilakukan di Bagian 42.4.1.
42.4 Deployment Sederhana: dari Repository ke Server
42.4.1 Uji Manual: Mengirim Rilis Lewat Tar over SSH
Sebelum menyerahkan proses ini ke pipeline otomatis, kita uji dulu secara manual dari komputer kerja sendiri, persis pola yang sama dipakai Bagian 36.3.2 untuk menguji konektivitas Ansible lewat ad-hoc command sebelum menulis playbook sungguhan.
Langkah Praktik
- Dari direktori
~/demo-nodeyang sudah dibuat di Bagian 42.2.3, kemas seluruh isi project jadi arsip tar terkompresi, kecualikan direktori.gityang tidak perlu ikut terdeploy, lalu alirkan langsung lewat SSH keapp01memakai key deploy dari Bagian 42.3.2.tar czf - --exclude=.git . | ssh -i ~/.ssh/deploy_demo_node [email protected]
Verifikasi dan Troubleshooting
- Perintah di atas tidak menampilkan output apa pun jika berhasil, sesuai isi
deploy.shyang memang tidak mencetak apa-apa ke stdout. Verifikasi keberhasilan lewatcurlseperti biasa.curl http://192.168.1.40:3000 - Periksa juga bahwa direktori rilis baru benar-benar terbentuk dan symlink berpindah.
Perhatikan bahwa argumenssh -i ~/.ssh/deploy_demo_node [email protected] readlink -f /opt/demo-node/currentreadlink -f /opt/demo-node/currentyang dikirim di sini sebenarnya diabaikan sepenuhnya oleh server, sesuai perilakucommand=yang sudah dijelaskan di Bagian 42.3.2; perintah ini tetap hanya menjalankandeploy.shdan mengirim rilis kosong (karena kita tidak mengalirkan tar apa pun kali ini). Untuk sekadar mengecek symlink tanpa memicu deploy baru, jalankanreadlink -f /opt/demo-node/currentlangsung dari sesi SSH biasa milik Sysadmin, bukan lewat key deploy yang memang dibatasi khusus untuk deploy saja. - Error
Permission deniedberarti public key belum cocok dengan barisauthorized_keysdi Bagian 42.3.2. Errortar: This does not look like a tar archivedi sisi server biasanya berarti koneksi terputus di tengah jalan sebelum seluruh stream terkirim.
42.4.2 Mengotomasi Trigger dengan GitHub Actions Self-Hosted Runner
Perintah manual di Bagian 42.4.1 sudah bekerja, tetapi tetap butuh seseorang mengetiknya setiap kali ada rilis baru, persis masalah yang sama dibahas di pembuka bab ini. Bagian ini memasang self-hosted runner GitHub Actions di control node yang sama dipakai untuk Ansible sejak Bab 36, sehingga runner ini bisa langsung menjalankan perintah tar dan ssh di atas begitu ada push ke branch main.
Langkah Praktik
- Di halaman repository
demo-nodepada GitHub, buka Settings > Actions > Runners > New self-hosted runner, pilih OS Linux dan arsitektur x64. Salin persis perintahcurldan./config.shyang ditampilkan halaman tersebut, jangan disalin dari sumber lain, karena perintah itu menyertakan nomor versi runner terbaru dan token registrasi sekali pakai yang unik untuk repository serta sesi kita sendiri. - Jalankan perintah yang disalin tadi di control node, pola umumnya seperti berikut (nilai
VERSIONdanTOKENharus diganti sesuai yang ditampilkan GitHub).mkdir actions-runner && cd actions-runner curl -o actions-runner-linux-x64.tar.gz -L \ https://github.com/actions/runner/releases/download/vVERSION/actions-runner-linux-x64-VERSION.tar.gz tar xzf ./actions-runner-linux-x64.tar.gz ./config.sh --url https://github.com/contoh-user/demo-node --token TOKEN - Pasang runner sebagai service systemd, sesuai script bawaan yang disediakan paket runner itu sendiri, mengikuti prinsip yang sama seperti unit service kustom di Bagian 5.2.
sudo ./svc.sh install sudo ./svc.sh start - Salin isi private key deploy dari Bagian 42.3.2 (
~/.ssh/deploy_demo_node, bukan file.pub-nya), lalu tambahkan sebagai secret repository bernamaDEPLOY_SSH_KEYlewat Settings > Secrets and variables > Actions > New repository secret. Key ini sengaja tidak pernah tersimpan sebagai file permanen di control node maupun di mana pun selain GitHub; step workflow pada langkah berikutnya merekonstruksinya jadi file sementara hanya selama satu kali eksekusi, lalu menghapusnya lagi begitu selesai. - Kembali di komputer kerja, buat file workflow.
mkdir -p .github/workflows nano .github/workflows/deploy.yml - Isi dengan definisi berikut.
Urutan step ini sengaja menjalankan test lebih dulu sebelum langkah kirim rilis; jikaname: Deploy demo-node ke app01 on: push: branches: [main] jobs: deploy: runs-on: self-hosted steps: - name: Checkout kode uses: actions/checkout@v4 - name: Jalankan test run: node --test - name: Kirim rilis lewat SSH env: DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }} run: | install -m 600 /dev/null /tmp/deploy_key echo "$DEPLOY_KEY" > /tmp/deploy_key tar czf - --exclude=.git . | \ ssh -i /tmp/deploy_key -o StrictHostKeyChecking=accept-new [email protected] rm -f /tmp/deploy_keynode --testgagal, workflow berhenti di step itu dan step pengiriman rilis tidak pernah dieksekusi sama sekali, inilah bentuk konkret Continuous Integration yang dibahas di Bagian 42.1.1 benar-benar mencegah kode bermasalah sampai ke tahap deploy. - Commit dan push file workflow ini ke branch
main.git add .github/workflows/deploy.yml git commit -m "Tambahkan pipeline deploy ke app01" git push
Verifikasi dan Troubleshooting
- Tab Actions pada repository GitHub menampilkan riwayat jalannya workflow setiap kali ada push ke
main, lengkap dengan log tiap step. Status hijau berarti seluruh step termasuk pengiriman rilis berhasil; klik run yang gagal untuk melihat step mana persisnya yang berhenti. - Catatan keamanan yang jujur perlu disampaikan di sini. Runner self-hosted menjalankan isi workflow langsung di sistem operasi control node dengan privilege user yang menjalankan service runner, bukan di lingkungan sandbox terpisah seperti runner yang disediakan GitHub sendiri. Dokumentasi resmi GitHub secara eksplisit tidak merekomendasikan self-hosted runner untuk repository publik, karena siapa pun yang bisa membuka pull request berpotensi menyisipkan perintah berbahaya ke dalam workflow dan menjalankannya di mesin kita. Repository
demo-nodedi bab ini privat, tetapi kombinasi ini tetap jadi alasan kenapa Bagian 42.3.2 dan 42.3.3 sengaja membatasi apa yang bisa dilakukan credential deploy sendiri lewat forced command dan sudoers sempit, bukan sekadar mengandalkan repository yang privat saja sebagai satu-satunya lapisan pertahanan. - Jika control node berada di jaringan yang berbeda dari
192.168.1.0/24atau target deployment-nya adalah instance cloud publik sepertivm-cloud01di Bab 41, dua opsi umum dipakai di lapangan: memasang self-hosted runner yang sama ini di jaringan yang bisa menjangkau server target lewat SSH biasa (persis pola bab ini), atau memakai runner terkelola milik GitHub sendiri (runs-on: ubuntu-latest) yang berjalan di infrastruktur cloud GitHub dan butuh security group cloud (Bagian 41.3.2) dibuka untuk rentang IP publik GitHub Actions yang dipublikasikan lewat API meta GitHub, sesuatu yang lebih merepotkan dijaga tetap akurat dibanding cukup mengizinkan satu IP tetap milik control node internal.
42.4.3 Alternatif: Memanggil Playbook Ansible dari Pipeline
Pendekatan tar-over-SSH di Bagian 42.4.2 cukup untuk satu server, tetapi begitu jumlah managed node bertambah seperti skenario pembuka Bab 36, menulis ulang logika symlink dan cleanup rilis di setiap server jadi pekerjaan berulang. Karena Ansible sudah terpasang di control node yang sama sejak Bab 36, cara paling wajar untuk berkembang bukan menulis ulang deployment dari nol, melainkan menyuruh CI memanggil playbook yang polanya sama persis dengan install-nginx.yml di Bagian 36.4.1.
Langkah Praktik
- Di direktori
~/ansible-projectyang sudah ada dari Bab 36, buat playbook baru.cd ~/ansible-project nano deploy-demo-node.yml - Isi dengan definisi berikut.
Module--- - name: Deploy rilis terbaru demo-node hosts: webservers become: true vars: release_id: "{{ lookup('pipe', 'date +%Y%m%d%H%M%S') }}" release_path: "/opt/demo-node/releases/{{ release_id }}" app_src: "{{ playbook_dir }}/../demo-node/" tasks: - name: Buat direktori rilis baru ansible.builtin.file: path: "{{ release_path }}" state: directory owner: deploy group: nodeapp mode: "2775" - name: Salin source code ke direktori rilis ansible.posix.synchronize: src: "{{ app_src }}" dest: "{{ release_path }}/" rsync_opts: - "--exclude=.git" - name: Arahkan symlink current ke rilis baru ansible.builtin.file: src: "{{ release_path }}" dest: /opt/demo-node/current state: link force: true - name: Restart service demo-node ansible.builtin.systemd: name: demo-node.service state: restarted - name: Bersihkan rilis lama, sisakan 5 terbaru ansible.builtin.shell: | cd /opt/demo-node/releases && ls -1 | sort -r | tail -n +6 | xargs -r rm -rf -- changed_when: falseansible.posix.synchronizemembungkusrsync, bagian dari collectionansible.posixyang sudah ikut terpasang lewat metapackageansiblesejak Bagian 36.2.1. Playbook ini butuhbecome: truepenuh di managed node, cakupan privilege yang lebih luas dibanding sudoers sempit khusus restart pada Bagian 42.3.3, konsekuensi wajar dari memakai user Ansible yang sama dipakai untuk seluruh konfigurasi server, bukan user deploy yang sengaja dipersempit. Variableapp_srcsengaja dipisah jadi default yang bisa ditimpa, sebab lokasi source code yang disalin task ini berbeda tergantung siapa yang menjalankan playbook: dijalankan manual dari control node, default{{ playbook_dir }}/../demo-node/sudah tepat karena mengasumsikan direktori~/demo-nodesejajar dengan~/ansible-project; dipanggil dari GitHub Actions, checkout milik runner tidak pernah berada di path itu, melainkan di direktori workspace milik job itu sendiri. - Ganti step "Kirim rilis lewat SSH" pada
deploy.ymlBagian 42.4.2 dengan pemanggilan playbook ini, dijalankan dari checkout kodedemo-nodeyang sudah ada di workspace runner. Timpaapp_srclewat flag-emengarah ke$GITHUB_WORKSPACE, environment variable bawaan GitHub Actions yang selalu menunjuk ke direktori checkout job yang sedang berjalan, supaya playbook tidak keliru mengambil source code dari~/demo-nodemilik sesi manual Sysadmin.- name: Deploy dengan Ansible run: | ansible-playbook -i ~/ansible-project/inventory.ini \ ~/ansible-project/deploy-demo-node.yml \ -e "app_src=$GITHUB_WORKSPACE/"
Verifikasi dan Troubleshooting
- Jalankan playbook ini manual sekali dari control node sebelum mengandalkannya lewat CI, mengikuti kebiasaan uji ad-hoc yang sama seperti Bagian 36.4.2. Perintah manual ini sengaja tidak menyertakan flag
-e app_src=..., karena nilai defaultapp_srcpada playbook sudah tepat untuk kondisi ini.ansible-playbook -i inventory.ini deploy-demo-node.ymlPLAY RECAPyang sehat menampilkanchanged=4pada eksekusi pertama (empat task benar-benar mengubah kondisi server), dan sebagianchangedturun jadi lebih kecil pada eksekusi berikutnya untuk task yang sifatnya idempotent seperti pembuatan direktori. - Dua pendekatan pada Bagian 42.4.2 dan 42.4.3 ini bukan soal mana yang "lebih benar", melainkan trade-off yang jujur perlu disampaikan: script tar-over-SSH dengan forced command punya blast radius paling kecil jika credential-nya bocor, cocok untuk satu atau dua server penting. Ansible lebih mudah diperluas ke banyak server sekaligus tanpa menulis ulang logika deploy, tetapi menyerahkan privilege
becomeyang jauh lebih luas ke tangan pipeline CI. Pilih sesuai jumlah server dan tingkat kepercayaan terhadap pipeline itu sendiri, bukan sekadar preferensi tool.
42.5 Rollback Strategy Dasar
42.5.1 Symlink Swap Atomik sebagai Dasar Rollback Cepat
Rollback adalah proses mengembalikan aplikasi ke versi rilis sebelumnya begitu rilis terbaru ternyata bermasalah. Pola direktori berbasis release yang sudah dibangun sejak Bagian 42.3.1 membuat rollback jadi operasi yang sangat murah: karena setiap rilis lama tetap tersimpan utuh di releases/ sampai giliran dihapus lewat cleanup di Bagian 42.3.3, rollback cukup mengarahkan ulang symlink current ke direktori rilis sebelumnya lalu restart service, tanpa perlu proses build ulang atau menarik ulang kode dari Git sama sekali.
Bandingkan dengan pola deployment naif yang menimpa file langsung di tempat tanpa struktur release: rollback pada pola itu berarti harus menyiapkan ulang seluruh file versi lama dari awal, proses yang jauh lebih lambat justru di saat kecepatan paling dibutuhkan, yaitu ketika produksi sedang bermasalah.
42.5.2 Script rollback.sh dan Menjaga Riwayat Release
Berbeda dari deployment rutin yang cocok sepenuhnya otomatis, rollback biasanya tetap melibatkan keputusan sadar seorang Sysadmin, sebab rollback berarti mengakui rilis terbaru bermasalah dan perlu ditangani lebih lanjut di sisi kode. Karena itu, rollback.sh pada bagian ini dijalankan manual lewat akses sudo biasa, bukan lewat key deploy yang dibatasi forced command di Bagian 42.3.2.
Langkah Praktik
- Di
app01, buat filerollback.sh.sudo nano /opt/demo-node/rollback.sh - Isi dengan script berikut.
Logika pemilihan#!/bin/bash set -euo pipefail RELEASES_DIR=/opt/demo-node/releases CURRENT_LINK=/opt/demo-node/current CURRENT_RELEASE=$(basename "$(readlink -f "$CURRENT_LINK")") PREVIOUS_RELEASE=$(cd "$RELEASES_DIR" && ls -1 | sort -r | grep -v "^${CURRENT_RELEASE}$" | head -n1) if [ -z "$PREVIOUS_RELEASE" ]; then echo "Tidak ada rilis sebelumnya untuk rollback." >&2 exit 1 fi echo "Rollback dari $CURRENT_RELEASE ke $PREVIOUS_RELEASE" ln -sfn "$RELEASES_DIR/$PREVIOUS_RELEASE" "$CURRENT_LINK.tmp" mv -T "$CURRENT_LINK.tmp" "$CURRENT_LINK" systemctl restart demo-node.servicePREVIOUS_RELEASEmemakai teknik pengurutan nama direktori yang sama seperti cleanup di Bagian 42.3.3, mengambil entri paling baru yang bukan rilis yang sedang aktif sekarang. - Beri izin eksekusi, dimiliki root karena script ini dijalankan lewat
sudo.sudo chmod 750 /opt/demo-node/rollback.sh sudo chown root:root /opt/demo-node/rollback.sh
Verifikasi dan Troubleshooting
- Jalankan rollback dan konfirmasi hasilnya.
sudo /opt/demo-node/rollback.sh curl http://localhost:3000 - Pesan
Tidak ada rilis sebelumnya untuk rollbackyang keluar dengan exit code bukan nol adalah perilaku yang disengaja, bukan bug, mencegah script mencoba rollback ke rilis yang tidak ada saat baru pertama kali deploy. - Karena cleanup di Bagian 42.3.3 hanya menyisakan lima rilis terakhir, rollback lewat script ini hanya bisa mundur satu langkah ke rilis tepat sebelumnya, bukan ke rilis mana pun di masa lalu. Kebutuhan rollback ke rilis yang lebih lama dari itu berarti kembali mengandalkan Git: checkout commit lama, lalu jalankan ulang proses deploy di Bagian 42.4 seperti rilis baru biasa.
42.5.3 Menutup Celah dengan Health Check Sebelum Rollback Otomatis
Bab ini konsisten menjalankan rollback secara manual, tetapi pipeline production yang matang umumnya melangkah satu tahap lebih jauh: menambahkan step health check tepat setelah deploy, memanggil endpoint aplikasi lewat curl persis seperti langkah verifikasi yang sudah berulang kali dipraktikkan sepanjang series ini, lalu memanggil rollback.sh secara otomatis jika health check gagal dalam beberapa kali percobaan. Pola inilah yang sesungguhnya membedakan Continuous Deployment yang dipercaya penuh dari sekadar "auto deploy tanpa jaring pengaman", perbedaan yang sudah disinggung di Bagian 42.1.2.
Bab ini sengaja berhenti di rollback manual sebagai fondasi, karena health check otomatis yang baik butuh definisi "sehat" yang spesifik untuk tiap aplikasi, di luar cakupan pengenalan CI/CD. Prinsip dasarnya tetap sama seperti pengecekan systemctl status dan log yang sudah kita kuasai sejak Bab 5, hanya saja pemicunya kini otomatis lewat pipeline, bukan diketik manual oleh Sysadmin.
Sampai di sini, aplikasi demo-node di app01 sudah punya alur deployment yang utuh: kode tersimpan di Git sebagai satu sumber kebenaran, pipeline CI menguji setiap perubahan sebelum sempat menyentuh server produksi, dua jalur deployment (script terbatas dan Ansible) siap dipilih sesuai kebutuhan, dan rollback bisa dilakukan dalam hitungan detik lewat pola symlink yang sama dipakai saat deploy. Pipeline ini masih sesederhana mungkin, cukup untuk satu aplikasi dan satu server target, belum menyentuh isu seperti deployment tanpa downtime ke banyak server sekaligus atau strategi blue-green yang jadi standar di lingkungan produksi besar. Bab 43 menutup series ini dengan sisi yang selalu menyertai sistem otomatis mana pun, yaitu troubleshooting, termasuk saat pipeline yang sudah kita bangun di bab ini sendiri suatu saat gagal di tengah malam tanpa ada Sysadmin yang menungguinya secara langsung.

