evilfactorylabs

Cover image for Mengenal Continuous Integration (CI) untuk pengembang
Rizaldy
Rizaldy

Posted on

Mengenal Continuous Integration (CI) untuk pengembang

Sebagai seorang pengembang, pekerjaan utama kita besar kemungkinan adalah menulis kode, yang mana akan menghasilkan sebuah aplikasi. Aktivitas penulisan kode tersebut tidak jarang dilakukan bersama dalam bentuk tim yang terdiri dari berbagai sub-grup dan atau anggota yang ahli dalam bidangnya masing-masing.

Untuk membuat kode yang ditulis oleh pengembang "berguna", maka kode tersebut harus dapat berjalan. Namun ada yang lebih penting daripada hanya membuatnya berjalan saja, yakni membuatnya berjalan dan sesuai fungsi juga.

Yang menarik dari aktivitas pengembangan aplikasi ini adalah kode yang ditulis bisa dilakukan bersama, secara paralel. Mungkin bisa bayangkan seperti membuat sebuah novel yang mana setiap bab nya ditulis oleh penulis yang berbeda, secara paralel.

Yang menjadi pertanyaan adalah: bagaimana memastikan bahwa "plot" yang ditulis sesuai dengan keseluruhan "cerita" yang diharapkan?

Pertanyaan yang tidak kalah penting lagi adalah: bagaimana memastikan bahwa keseluruhan cerita ditulis seperti oleh satu orang?

Dan yang paling penting: bagaimana plot yang ditulis tidak merusak plot lain yang ditulis oleh penulis berbeda?

Silahkan ganti kata "plot" diatas dengan "fitur".

Code Review

Proses Code review ini sudah umum dilakukan di organisasi pengembang. Jika belum familiar dengan code review, mungkin memiliki pertanyaan seperti: Mengapa kode yang gue tulis ditinjau lagi, sedangkan gue pastinya menulis kode yang berjalan?.

Karena code review, sekali lagi, adalah tentang code review. Ini bukan hanya tentang memastikan kode yang ditulis berjalan, melainkan untuk memastikan bahwa kode yang ditulis sesuai dengan pedoman; konvensi, dan kualitas yang dimiliki organisasi tersebut.

Dan yang tidak kalah penting adalah tentang pemindahan informasi. Tentang membuat tim yang terlibat tetap singkron dengan perubahan yang terjadi. Juga tentang saling belajar cara berpikir dari pengembang yang berbeda-beda.

Peninjauan

Umumnya untuk memastikan bahwa kode yang ditulis berjalan adalah dengan melakukan proses "build", jika melewati fase ini, bisa dipastikan kode yang ditulis memang berjalan dan tidak memiliki kesalahan yang membuat aplikasi tersebut gagal dibuat.

Setelah itu, proses yang dilakukan adalah "test". Di fase ini, kita memastikan bahwa kode berjalan sesuai harapan yang mana salah satu harapannya adalah tidak merusak fungsi lain yang sudah/ditulis oleh pengembang lain.

Apa saja yang diuji? Tergantung. Bisa menguji fungsionalitas per-unit, menguji ketika unit-unit tersebut "berkomunikasi" dengan unit lain ataupun berbeda, sampai ke pengujian ketika unit-unit tersebut digabungkan menjadi satu-kesatuan. Misal, seperti cerita "Mengirim surel konfirmasi kata sandi ketika pengguna lupa kata sandinya". Yang pertama kita uji tentu adalah fungsi mengirim surel beserta konten yang ada, yang dilanjutkan dengan kondisi ketika menggunakan komponen eksternal (seperti protokol SMTP misalnya), yang diakhiri misal dengan alur dari klik tombol "Forgot password", mengisi alamat surel, dan mendapatkan surel yang diharapkan.

Tapi mengembangkan aplikasi tidak hanya tentang memedulikan siapa yang menggunakannya saja. Memedulikan pengembangnya juga tidak kalah penting selain demi menjaga kewarasan juga bisa membantu untuk membuat pengembang tersebut menjadi lebih produktif dan bergerak cepat. Seperti, keluhan-keluhan tentang "legacy code" sudah menjadi rahasia umum di kalangan pengembang, terlepas berada di organisasi manapun itu.

Legacy code ini sebenarnya tidak selalu buruk, dan menariknya keluhan tentang legacy code ini memiliki pola. Pengembang relatif akan mengeluhkan legacy code, jika mereka menemukan kode yang sudah ada ditulis:

  • Ribet/sulit dipahami
  • Tidak konsisten
  • Tidak rapih/berpola
  • Tidak singkron dengan kondisi pada saat ini

Dan hampir jarang—atau mungkin main gue kurang jauh—pengembang mengeluhkan "legacy code" bila kode yang sudah ada:

  • Mudah dipahami
  • Konsisten
  • Rapih & berpola
  • Singkron dengan kondisi pada saat ini

Setuju?

Nah, untuk dapat membuat kode yang ada memenuhi poin-poin diatas, organisasi setidaknya harus memiliki pedoman; konvensi, dan kualitas terkait kode yang harus ditulis oleh pengembang.

Namun yang paling berat bukanlah di pembuatan & penentuan standar terkait pedoman; konvensi, dan kualitas kode tersebut, melainkan di bagaimana memastikan pengembang mengikuti standar tersebut..

Dan proses pemastiannya, selain bukan tanggung jawab utama dari seorang pengembang juga adalah pekerjaan yang sangat membosankan.

...kecuali?

Automation

Kebanyakan para pengembang adalah pribadi yang malas dan (tidak jarang) mudah bosan juga, mereka akan mencari cara untuk mengatasi kemalasan dan kebosanan mereka tanpa mengorbankan esensi utama yang ada.

Jika tidak setuju, silahkan jawab mengapa kamu menulis test alih-alih mengujinya dengan tangan sendiri untuk memastikan kode yang kamu tulis berjalan dan juga tidak merusak kode yang lain?

Sadar-tidak sadar, pengembang berusaha sekeras mungkin untuk menggunakan solusi yang "otomatis" daripada "dengan tangan". Dari sesederhana menggunakan package manager daripada mengunduh berkas arsip dan mengekstraksinya sendiri sampai ke pembuatan "jalan pintas" untuk membuat Makefile dan mengetik make daripada gcc -std=gnu99 -Wall -O2 -I"./include" build/x86/obj/*.o build/x86/obj/entry/main.o -o build/x86/bin/app -lpthread untuk membuat kode yang ditulis menjadi sebuah aplikasi.

Jika otomatisasi ini bisa membantu membuat pekerjaan pengembang per-individu menjadi lebih mudah, bukankah seharusnya bisa membuat pekerjaan pengembang dalam konteks kelompok menjadi lebih mudah juga?

Dan ditulisan ini, kita akan membahas itu.

Tentang Continuous Integration.

Tentang menemukan bug lebih cepat, meningkatkan kualitas program, dan tentang mengurangi waktu dalam memvalidasi dan merilis pembaruan.

Secara otomatis.

CI at a glance

Komponen utama dari praktik CI ini adalah CI Platform itu sendiri. Karena, pada dasarnya, CI hanyalah sebuah proses yang biasa pengembang lakukan, di mesinnya, secara manual.

Dalam praktik CI, umumnya ada 2 fase yang harus dilewati untuk mewujudkan tujuan yang ingin dicapai:

  • Build
  • Test

Alurnya, sederhananya adalah seperti ini:

  1. Pengembang membuat perubahan
  2. Perubahan tersebut diunggah ke repository bersama
  3. CI Platform akan melakukan "build" dan "test"
  4. Jika 2 fase diatas berhasil dilewati, maka perubahan siap untuk digabungkan ke kode utama

Yang berarti, jika menerapkan praktik CI, setiap perubahan yang dibuat seharusnya hanya boleh digabungkan ke kode utama jika semua fase yang ada di platform CI sudah terlewati.

Apa yang dilakukan ketika build? Apa yang dilakukan ketika test? Sekali lagi, tergantung. Jika untuk menjalankan aplikasi tersebut pengembang harus:

  • Menggunakan sistem operasi GNU/Linux, Windows dan *BSD.
  • Memasang sqlite3, Node.js LTS, dan npm
  • Memasang dependensi yang dibutuhkan dengan npm install
  • Memastikan ukuran direktori dist kurang dari 10 MB

Maka pengembang harus menjelaskan langkah-langkah diatas ke platform CI untuk fase "build" tersebut.

Bagaimana cara pengembang menjelaskannya ke platform CI? Biasanya setiap platform CI memiliki sebuah berkas khusus yang ditulis menggunakan format tertentu yang mana berkas tersebut bisa dianggap sebagai "berkas konfigurasi pipeline".

...pipeline sederhananya hanyalah kumpulan langkah-langkah yang mana sebelumnya gue sebut sebagai "fase". Jika gue berbicara tentang pipeline, bisa diasumsikan gue sedang membicarakan tentang proses dari build-test.

Pipeline example: Build

Anggap aplikasimu ingin berjalan di 2 sistem operasi: GNU/Linux dan Windows.

Selain itu, aplikasimu bergantung dengan 2 program yang berada di level OS, yakni: imagemagick dan ffmpeg.

Dan persyaratan utama untuk bisa menjalankan programmu adalah:

  • Menggunakan Node.js versi LTS
  • Menggunakan arsitektur x86
  • Kapasitas RAM setidaknya 4GB

Dengan sudah mendefinisikan 3 hal diatas, setidaknya pengembang dan atau operator sudah mengetahui limitasi ada dan bisa mengurangi kemungkinan masalah unknown-unknown yang akan terjadi.

Sebagai contoh, disini kita akan menggunakan platform CI dari Gitlab yang bernama Gitlab CI. Dan berdasarkan kondisi diatas, fase build yang akan kita miliki kurang lebih seperti ini:

stages:
  - build

.build:
  script:
    - npm ci
    - npm run build

build:windows:
  stage: build
  extends:
    - .build
  tags:
    - windows
  before_script:
    - # download & install imagemagick
    - # download & install ffmpeg
    - # download & install node.js lts & npm

build:linux:
  image: ubuntu:focal
  stage: build
  extends:
    - .build
  before_script:
    - # download & install imagemagick
    - # download & install ffmpeg
    - # download & install node.js lts & npm
Enter fullscreen mode Exit fullscreen mode

Nah kunci utama dari fase dianggap sukses atau gagal adalah di exit code. Setiap program pasti memiliki exit code setiap kali dijalankan, misal, ketika kita (atau CI server) menjalankan npm run build, jika program npm berhasil menyelesaikan tugasnya, (run build) maka exit code nya 0, jika gagal, exit code nya pasti bukan 0 tergantung alasan error nya.

Mengapa fase "build" ini penting? Bayangkan jika pengembang harus melakukan build manual di mesinnya sendiri setiap kali pengembang lain membuat perubahan ketika melakukan code review, malesin banget gak, sih?

Selain itu juga, fase ini bisa menghindari masalah klasik "It works on my machine" karena pengembang harus mendefinisikan secara eksplisit apa saja yang dibutuhkan dan apa saja yang harus dilakukan.

Pipeline example: Test

Di fase ini semakin menarik. Terlebih platform CI terkadang bukan hanya menguji kode yang pengembang tulis, melainkan menguji kewarasan kita sebagai pengembang juga, iykyk.

Jenis test yang ada pun cukup beragam, tergantung seberapa luas cakupan yang ingin kita uji. Umumnya ada 3 jenis test yang biasa ditulis oleh pengembang:

  • Unit test
  • Integration testing
  • End-to-end test

Untuk pertanyaan "Test apa yang harus ditulis, dan kapan harus menulis Unit/Integration/E2E test?" jawabannya ada di manager kalian atau siapapun itu yang bertanggung jawab.

Di fase test ini tidak hanya sebatas menguji fungsionalitas dari kode yang ada, melainkan bisa juga melakukan pengujian-pengujian lain seperti:

  • Konvensi
  • Kualitas
  • Keamanan
  • Lisensi
  • Dsb

Dan akan lebih menarik lagi jika si "penguji" tersebut bisa membetulkan sendiri apa yang dia anggap salah, secara otomatis. Bukan hanya sebatas memberikan informasi lalu membuat pipeline tersebut gagal :))

Dengan sudah mendefinisikan jenis test berikut dengan skenarionya, setidaknya pengembang dapat memastikan bahwa perubahan yang dia buat dapat berjalan sehingga salah satunya bisa tidak membuat hidup operator menjadi lebih sulit dalam menentukan letak keberadaan kesalahan yang ada.

Sebagai contoh, disini kita akan menggunakan Gitlab CI lagi. Dan kurang lebih seperti ini untuk mendefinisikan fase test untuk aplikasi kita:

stages:
  - test

.test:
  script:
    - npm run lint
    - npm run check-deps
    - npm run test

test:windows:
  stage: test
  extends:
    - .build
    - .test
  tags:
    - windows

test:linux:
  image: ubuntu:focal
  stage: test
  extends:
    - .build
    - .test
Enter fullscreen mode Exit fullscreen mode

Yang perlu dicatat juga bahwa kita harusnya menguji kode dari hasil build yang kita lakukan di fase sebelumnya. Untuk mencapainya, kita bisa melakukan build lagi (yang tentu saja tidak efektif) lalu test, atau bisa menggunakan pendekatan "menggunakan artifact" yang salah satu contohnya berarti di fase test ini, untuk kasus yang menggunakan Node.js, node_modules dan direktori dist diambil dari fase build sebelumnya.

Mengapa fase "test" ini penting? Karena sekali lagi, bayangkan jika pengembang harus menjalankan test manual apalagi harus melakukan test manual di mesinnya sendiri setiap kali pengembang lain membuat perubahan ketika melakukan code review, mending resign gak, sih?

Tujuan menulis test pada umumnya untuk membuat pengembang lebih percaya diri ketika membuat perubahan. Meskipun terkadang gue berpikir bahwa menulis test adalah aktivitas yang membuang-buang waktu, jika mengambil sisi positifnya ini membantu:

  • Anggota baru untuk mempelajari & memahami aplikasi
  • Mengetahui known-issue yang ada
  • Memprediksi kemungkinan edge cases

Dan yang paling penting: memastikan bahwa perubahan yang dibuat tidak merusak fungsionalitas yang sudah ada, which is nice, right?

Jika fase build & test berhasil, apa lagi yang bisa membuat ragu bahwa perubahan yang kita—sebagai pengembang—buat itu sudah berjalan dan sesuai fungsi sehingga perubahan bisa menjadi lebih cepat dapat digabungkan ke kode utama?

Pipeline example bonus: Release

Berbicara tentang Continuous Integration tentu terasa kurang bila tidak menyinggung tentang Continuous Delivery juga. Continuous Delivery ini sederhanannya adalah jawab akan "What's next?" setelah perubahan berhasil digabungkan ke kode utama.

Setiap organisasi memiliki pendekatan yang beragam dalam mendistribusikan "artifact" atau hasil proses build tersebut. Umumnya artifact tersebut berbentuk:

  • Berkas arsip, seperti .tar, .zip ataupun sebuah docker image
  • Berkas paket, seperti .apk, .deb, dsb.

Jika kita mengambil konteks aplikasi untuk front-end web, besar kemungkinan artifact tersebut adalah sebuah berkas arsip yang berisi direktori node_modules dan dist.

Dan, ya, OCI compliant image seperti Docker Image pun pada dasarnya adalah sebuah berkas arsip.

Hal yang dilakukan di fase release ini pada dasarnya hanya satu: melakukan rilis, dan hal paling sederhana yang dilakukan adalah melakukan "tagging" dan menyimpan sesuatu yang sudah di tag tersebut ke sesuatu bernama "package registry" ataupun "artifact repository" tergantung buzzword mana yang ingin kamu gunakan.

Tagging di versi rilis ini umumnya berisi kumpulan-kumpulan perubahan yang besar pada sebuah aplikasi, bukan hanya sebatas 1 perubahan besar. Dan biasanya digambarkan dengan pemilihan nomor versi.

Alurnya biasanya seperti ini:

  1. Pengembang menentukan versi X ingin berisi perubahan apa saja yang dilanjutkan dengan membuat Pull/Merge Request
  2. Pull/Merge Request tersebut menggambarkan versi yang akan dirilis, baik menggunakan nama cabang ataupun tag
  3. Pipeline berjalan seperti biasa untuk (P/M)R tersebut.
  4. Jika pipeline berhasil, lakukan tagging dan unggah artifact yang ada ke somewhere

...yang tentu saja langkah nomor 4 berjalan secara otomatis :))

Misal, artifact untuk aplikasi kita adalah sebuah Docker image. Kurang lebih kita bisa menggunakan pendekatan seperti ini misalnya:

stages:
  - release

release:
  stage: release
  only:
    refs:
      - /^release\/.*/
  script:
    - docker build -f ./release.dockerfile -t $BASE_IMAGE:$CURRENT_TAG .
    - docker push $BASE_IMAGE:$CURRENT_TAG
Enter fullscreen mode Exit fullscreen mode

Mengapa fase "release" ini penting? Entahlah, karena males?

Bayangkan harus menjalankan 2 perintah diatas yang besar kemungkinan memakan waktu & sumber daya yang tidak sedikit bila menggunakan mesin sendiri dan secara manual.

Penutup

Di tulisan ini gue mencoba untuk berbagi seputar Continuous Integration (CI) dari sudut pandang pengembang. Tantangan dari praktik CI ini sejauh yang gue tahu adalah kurangnya pemahaman terkait apa yang ingin didefinisikan di setiap fase yang ada.

Yang mana jawaban singkatnya adalah: Hal-hal yang kamu lakukan di mesinmu sendiri untuk menjalankan tugas X, itulah yang dilakukan di CI pipeline juga. Bedanya, tugas-tugas tersebut didefinisikan secara deklaratif.

Gue tidak bisa berbicara benefit praktik CI ini dari perspektif bisnis, tapi kalau dari sisi pengembang, jika lo males:

  • Memastikan perubahan terkait kode yang ditulis memenuhi standar penulisan
  • Memastikan bahwa perubahan tersebut berjalan sesuai kebutuhan, spesifikasi, dan kasus
  • Memastikan bahwa perubahan tersebut tidak merusak fungsi yang sudah ada
  • Melakukan rilis

...secara manual, besar kemungkinan organisasi lo harus menggunakan sistem CI!

Sebagai penutup, otomatisasi bukanlah solusi untuk setiap masalah, pasti masih harus ada sentuhan tangan manusia, karena benefitnya adalah untuk membantu dan bukan untuk mengganti.

Jika organisasimu belum menerapkan sistem CI dan kamu belum terlalu berani untuk mengusulkannya, mungkin bisa mempertimbangkan untuk membagikan tulisan ini ke organisasimu?

Semoga tulisan ini mencerahkan & berguna, jika terdapat salah paham; konsep, penulisan atau apapun itu, tolong bisa diutarakan di kolom komentar dibawah.

Thank you!

Discussion (0)