Deployment ke VPS sering dianggap menyeramkan karena melibatkan SSH, kunci, dan mesin produksi. Episode ini menunjukkan alur CD yang aman dan berulang: menyiapkan kunci SSH sebagai variabel file, mengonfigurasi ssh-agent dan ssh-keyscan di pipeline, menjalankan perintah remote dan sinkronisasi rsync, lalu mengotomasi semuanya dengan playbook Ansible.

Di episode 14 sebelumnya kita membahas environment dan deployment gates — target tempat aplikasi dikirim, riwayat deployment, dan persetujuan manusia. Sekarang tibalah bagian yang paling dinanti: mengirim aplikasi secara nyata ke server. Tidak semua perusahaan berjalan di Kubernetes; sebagian besar memulai dari sebuah VPS Linux dengan Nginx, yang paling simpel adalah menjalankan deployment lewat SSH.
Di episode 6 kita sudah membangun image Docker, dan di episode 7 mengelola artefak build. Episode ini menggabungkan semuanya ke target paling klasik dalam dunia DevOps: server Linux. Kalian akan belajar menyimpan kunci SSH dengan aman sebagai File type variable di GitLab, mengonfigurasi ssh-agent dan ssh-keyscan di dalam pipeline, mengeksekusi perintah remote serta sinkronisasi file dengan rsync, dan terakhir mengotomasi deployment penuh menggunakan playbook Ansible.
Ini adalah pola yang langsung bisa kalian pakai di pekerjaan nyata — dan justru di sinilah banyak pipeline pemula gagal, biasanya karena perintah SSH yang meminta interaksi (prompt host key atau passphrase) sehingga pipeline menggantung selamanya. Mari kita bangun alur yang benar dari awal.
Skenario kita sederhana namun nyata: aplikasi web dibangun di pipeline, lalu hasilnya (folder dist/) dikirim ke server produksi myapp.example.com dan service-nya dinyalakan ulang — pipeline men-build artefak, job deployment mengirimnya lewat SSH, dan remote command menjalankan reload. Dua bagian yang wajib dipersiapkan sebelum pipeline bisa berjalan: kunci SSH dan host key server.
Pertama, generate pasangan kunci di mesin lokal. Kunci ed25519 direkomendasikan — lebih pendek, lebih cepat, dan modern:
ssh-keygen -t ed25519 -C "gitlab-cicd-deploy" -f ~/.ssh/gitlab_deploy_key -N ""Public key (~/.ssh/gitlab_deploy_key.pub) ditambahkan ke ~/.ssh/authorized_keys pada user deployment di server target. Kunci privat (~/.ssh/gitlab_deploy_key) disimpan ke GitLab sebagai File type variable:
SSH_PRIVATE_KEY, pilih Type: File, lalu tempel isi kunci privat.Note
Untuk File type variable, nilai yang disimpan GitLab ditulis ke sebuah file di runner, dan variabel SSH_PRIVATE_KEY memuat path ke file tersebut — bukan isinya. Karena itu ssh-add bisa langsung memakainya tanpa menyalin isi ke variabel shell. Variabel tambahan seperti DEPLOY_HOST dan DEPLOY_USER bisa disimpan sebagai regular variables.
Warning
Jangan pernah men-generate kunci tanpa passphrase (-N "") lalu menyebarkannya ke sembarang tempat. Kunci deployment adalah kunci untuk server produksi — simpan hanya sebagai protected variable, beri user deployment di server hanya akses ke direktori yang dibutuhkan, dan gunakan kunci terpisah untuk tujuan berbeda.
before_scriptMasalah klasik ketika menjalankan SSH dari pipeline: perintah SSH meminta konfirmasi host key (Are you sure you want to continue connecting?) yang tidak bisa dijawab di CI, sehingga job menggantung hingga timeout. Solusinya adalah ssh-keyscan — mengambil host key server dan menaruhnya ke ~/.ssh/known_hosts sebelum SSH dipanggil.
Kunci privat juga sebaiknya dimuat ke ssh-agent — daemon yang menyimpan kunci di memori, sehingga ssh dan rsync tidak perlu membaca ulang file kunci setiap koneksi. Setup lengkapnya diletakkan di before_script agar semua job deployment memakainya:
variables:
DEPLOY_HOST: myapp.example.com
DEPLOY_USER: deploy
.deploy-setup: &deploy-setup
before_script:
- eval "$(ssh-agent -s)"
- ssh-add "$SSH_PRIVATE_KEY"
- mkdir -p ~/.ssh
- ssh-keyscan "$DEPLOY_HOST" >> ~/.ssh/known_hostsPerhatikan detail penting: ssh-keyscan menambahkan host key ke known_hosts, sehingga koneksi SSH berjalan tanpa interaksi. Menggunakan eval "$(ssh-agent -s)" memastikan agent berjalan di shell job ini, dan ssh-add memuat kunci ke memori agent.
Dengan setup di atas, dua pola eksekusi paling umum adalah:
Contoh job deployment lengkap yang menggabungkan keduanya:
deploy-production:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client rsync
- eval "$(ssh-agent -s)"
- ssh-add "$SSH_PRIVATE_KEY"
- mkdir -p ~/.ssh
- ssh-keyscan "$DEPLOY_HOST" >> ~/.ssh/known_hosts
environment:
name: production
url: https://myapp.example.com
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
script:
- rsync -avz --delete dist/ "$DEPLOY_USER@$DEPLOY_HOST:/var/www/myapp/"
- ssh "$DEPLOY_USER@$DEPLOY_HOST" "sudo systemctl reload nginx"Dua hal yang perlu diperhatikan di job ini:
--delete pada rsync membuat direktori di server persis sama dengan artefak — file yang tidak ada di build ikut dihapus. Ini mencegah file usang menumpuk.rules + when: manual mengingatkan kita pada episode 14: deployment produksi menunggu persetujuan manusia dan hanya berjalan di branch main.Tip
Untuk kecepatan, tambahkan opsi rsync seperti --compress dan --partial. Untuk keamanan, jangan pernah menjalankan job deployment sebagai root di server — gunakan user deploy dengan sudo terbatas hanya pada perintah yang dibutuhkan (misal systemctl reload nginx).
Ketika deployment sudah lebih dari sekadar menyalin file — misal butuh mengelola beberapa server, mengubah konfigurasi Nginx, atau menjalankan langkah kondisional — script shell yang panjang mulai sulit dipelihara. Di sinilah Ansible masuk: playbook mendeklarasikan keadaan akhir server, dan GitLab CI cukup memanggil ansible-playbook.
Buat playbook dan inventory di repository:
[production]
myapp ansible_host=myapp.example.com ansible_user=deploy- name: Deploy aplikasi ke production
hosts: production
become: true
tasks:
- name: Pastikan direktori aplikasi ada
file:
path: /var/www/myapp
state: directory
- name: Sinkronkan artefak build ke server
synchronize:
src: dist/
dest: /var/www/myapp/
- name: Reload Nginx
systemd:
name: nginx
state: reloadedModul synchronize di Ansible memanfaatkan rsync di balik layar, sehingga pola sinkronisasi yang sama tetap berlaku. Job CI-nya menjadi sangat bersih:
deploy-ansible:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client ansible
- eval "$(ssh-agent -s)"
- ssh-add "$SSH_PRIVATE_KEY"
- mkdir -p ~/.ssh
- ssh-keyscan "$DEPLOY_HOST" >> ~/.ssh/known_hosts
environment:
name: production
url: https://myapp.example.com
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
script:
- ansible-playbook -i inventory/production.ini deploy.ymlSeluruh logika deployment kini tinggal di playbook Ansible — bisa diuji lokal, bisa di-versioning, dan tidak terkunci di dalam YAML pipeline. Job GitLab hanya bertugas: menyiapkan SSH, lalu menyerahkan eksekusi ke Ansible.
| Kesalahan | Gejala | Solusi |
|---|---|---|
Tidak ada ssh-keyscan | Job menggantung saat prompt host key | Seed known_hosts sebelum koneksi |
| Kunci dengan passphrase | Job meminta interaksi yang tak bisa dijawab | Generate tanpa passphrase untuk CI |
| Mengirim kunci privat sebagai regular variable | Isi kunci terlihat di log jika echo | Gunakan File type variable + Masked |
rsync tanpa --delete | File usang menumpuk di server | Tambahkan --delete untuk sinkron penuh |
| Menjalankan SSH sebagai root | Risiko keamanan besar | Gunakan user khusus dengan sudo terbatas |
| Playbook Ansible tanpa uji lokal | Error baru muncul di produksi | Jalankan ansible-playbook --check dulu |
Important
Selalu uji koneksi dengan mode check sebelum deployment sungguhan. Ansible menyediakan ansible-playbook --check untuk dry-run, dan perintah SSH bisa diuji manual dengan ssh -o StrictHostKeyChecking=no pada mesin developer. Semakin banyak yang diuji sebelum produksi, semakin sedikit yang meledak setelah produksi.
Pada episode ini kita telah membahas alur Continuous Deployment ke Linux server melalui SSH: menyiapkan pasangan kunci ed25519 dan menyimpannya sebagai File type variable SSH_PRIVATE_KEY di GitLab; mengonfigurasi ssh-agent dan ssh-keyscan di before_script agar koneksi berjalan tanpa interaksi; mengeksekusi deployment dengan remote commands via SSH dan sinkronisasi artefak dengan rsync; serta mengotomasi seluruh deployment dengan playbook Ansible yang dipanggil langsung dari pipeline.
Inti dari episode ini: deployment yang andal adalah deployment yang bisa diulang tanpa kejutan. Dengan kunci yang disimpan aman, setup SSH yang deterministik, dan playbook yang mendeklarasikan keadaan akhir, men-deploy ke server menjadi tindakan yang biasa — bukan momen yang menegangkan.
Di episode 16 selanjutnya kita membawa deployment ke level berikutnya: Continuous Deployment ke Kubernetes — menghubungkan GitLab ke cluster secara aman dengan GitLab Agent for Kubernetes, men-deploy manifest dan Helm chart dengan kubectl dan helm upgrade --install, serta memperkenalkan GitOps dengan ArgoCD dan Flux. Sampai jumpa!