Pada tanggal 27 Mei, di aula utama konferensi DevOpsConf 2019, diadakan sebagai bagian dari festival
RIT ++ 2019 , sebagai bagian dari bagian Pengiriman Berkelanjutan, sebuah laporan dibuat βbukan alat kami untuk CI / CD di Kubernetesβ. Ini berbicara tentang
masalah dan tantangan yang dihadapi semua orang ketika menggunakan Kubernetes , serta tentang nuansa yang mungkin tidak segera terlihat. Menganalisis solusi yang mungkin, kami menunjukkan bagaimana ini diimplementasikan dalam
alat Open Source
werf .
Sejak pertunjukan, utilitas kami (sebelumnya dikenal sebagai dapp) telah mengatasi batas historis
1000 bintang di GitHub - kami berharap bahwa komunitas penggunanya yang berkembang akan menyederhanakan kehidupan banyak insinyur DevOps.

Jadi, kami menyajikan
video dengan laporan (~ 47 menit, jauh lebih informatif daripada artikel) dan ekstrak utama darinya dalam bentuk teks. Ayo pergi!
Pengiriman Kode dalam Kubernetes
Pembicaraan tidak lagi tentang werf, tetapi tentang CI / CD di Kubernetes, menyiratkan bahwa perangkat lunak kami dikemas dalam wadah Docker
(saya berbicara tentang ini dalam laporan 2016 ) , dan K8 akan digunakan untuk meluncurkannya dalam produksi
(tentang ini - tahun 2017 ) .
Seperti apa bentuk pengiriman Kubernet?
- Ada repositori Git dengan kode dan instruksi untuk membangunnya. Aplikasi ini dikompilasi ke dalam gambar Docker dan diterbitkan ke Docker Registry.
- Dalam repositori yang sama ada instruksi tentang cara menggunakan dan menjalankan aplikasi. Pada tahap penerapan, instruksi ini dikirim ke Kubernetes, yang menerima gambar yang diinginkan dari registri dan memulainya.
- Plus, biasanya ada tes. Beberapa di antaranya dapat dilakukan saat menerbitkan gambar. Anda juga dapat (dengan instruksi yang sama) menggunakan salinan aplikasi (dalam ruang nama K8 yang terpisah atau dalam kelompok yang terpisah) dan menjalankan tes di sana.
- Akhirnya, kita membutuhkan sistem CI yang menerima acara dari Git (atau klik tombol) dan memanggil semua tahapan yang ditunjukkan: bangun, publikasikan, sebarkan, uji.

Ada beberapa catatan penting di sini:
- Karena kita memiliki infrastruktur yang tidak dapat diubah, citra aplikasi yang digunakan di semua tahap (pementasan, produksi, dll.) Harus menjadi satu . Saya berbicara lebih banyak tentang ini dan dengan contoh di sini .
- Karena kita mengikuti pendekatan infrastruktur sebagai kode (IaC) , kode aplikasi dan instruksi untuk membangun dan menjalankannya harus berada dalam satu repositori . Untuk lebih lanjut tentang ini, lihat laporan yang sama .
- Kami biasanya melihat rantai pengiriman (pengiriman) seperti ini: aplikasi dirakit, diuji, dirilis (tahap rilis) dan itu saja - pengiriman telah terjadi. Tetapi pada kenyataannya, pengguna menerima apa yang Anda keluarkan, bukan ketika Anda mengirimkannya ke produksi, tetapi ketika ia bisa pergi ke sana dan produksi ini berhasil. Oleh karena itu, saya percaya bahwa rantai pengiriman berakhir hanya pada tahap operasional (berjalan) , dan lebih tepatnya, bahkan pada saat kode dihapus dari produksi (menggantinya dengan yang baru).
Mari kita kembali ke skema pengiriman Kubernet yang ditunjukkan di atas: skema itu diciptakan tidak hanya oleh kita, tetapi juga oleh setiap orang yang menangani masalah ini. Intinya, pola ini sekarang disebut GitOps
(lebih lanjut tentang istilah dan ide-ide di baliknya dapat ditemukan di sini ) . Mari kita lihat tahapan skema.
Membangun panggung
Tampaknya pada 2019 Anda bisa memberi tahu tentang perakitan gambar Docker, ketika semua orang tahu cara menulis Dockerfiles dan menjalankan
docker build
? .. Berikut adalah nuansa yang ingin saya perhatikan:
- Bobot masalah gambar , jadi gunakan multi-stage untuk hanya menyisakan aplikasi yang benar-benar diperlukan untuk gambar.
- Jumlah layer harus diminimalkan dengan menggabungkan rantai perintah
RUN
dalam artinya. - Namun, ini menambah masalah debugging , karena ketika perakitan crash, Anda harus menemukan perintah yang diperlukan dari rantai yang menyebabkan masalah.
- Kecepatan build itu penting karena kami ingin segera melakukan perubahan dan melihat hasilnya. Misalnya, saya tidak ingin memasang kembali dependensi di perpustakaan bahasa dengan setiap versi aplikasi.
- Seringkali, banyak gambar diperlukan dari satu repositori Git, yang dapat diselesaikan dengan satu set Dockerfiles (atau tahap bernama dalam satu file) dan skrip Bash dengan rakitan berurutan mereka.
Itu hanya puncak gunung es yang semua orang hadapi. Tetapi ada masalah lain, dan khususnya:
- Seringkali pada tahap perakitan kita perlu me - mount sesuatu (misalnya, cache hasil dari perintah seperti apt ke direktori pihak ketiga).
- Kami ingin Ansible daripada menulis di shell.
- Kami ingin membangun tanpa Docker (mengapa kami membutuhkan mesin virtual tambahan di mana Anda perlu mengonfigurasi semuanya untuk ini ketika sudah ada kluster Kubernet di mana Anda dapat menjalankan kontainer?).
- Perakitan paralel , yang dapat dipahami dengan cara yang berbeda: perintah berbeda dari Dockerfile (jika multi-stage digunakan), beberapa komit dari satu repositori, beberapa Dockerfiles.
- Perakitan terdistribusi : kami ingin mengumpulkan sesuatu di pod yang βephemeralβ, karena cache mereka menghilang, yang berarti perlu disimpan di suatu tempat secara terpisah.
- Akhirnya, saya menyebut puncak dari hasrat otomatis: akan ideal untuk masuk ke dalam repositori, mengetik beberapa tim dan mendapatkan gambar yang sudah jadi, berkumpul dengan pemahaman tentang bagaimana dan apa yang harus dilakukan dengan benar. Namun, saya pribadi tidak yakin bahwa semua nuansa dapat diramalkan dengan cara ini.
Dan inilah proyeknya:
- moby / buildkit - pembangun dari perusahaan Docker Inc (sudah terintegrasi ke dalam versi Docker saat ini), yang mencoba menyelesaikan semua masalah ini;
- kaniko - seorang kolektor dari Google, yang memungkinkan Anda membangun tanpa Docker;
- Buildpacks.io - upaya oleh CNCF untuk melakukan auto- magis dan, khususnya, solusi menarik dengan rebase untuk layer;
- dan banyak utilitas lain seperti buildah , genuinetools / img ...
... dan lihat berapa banyak bintang yang mereka miliki di GitHub. Yaitu, di satu sisi,
docker build
adalah dan dapat melakukan sesuatu, tetapi dalam kenyataannya,
masalah tersebut belum sepenuhnya diselesaikan - ini dibuktikan dengan pengembangan paralel dari pembangun alternatif, yang masing-masing memecahkan beberapa masalah.
Bangun di werf
Jadi kita harus
werf (sebelumnya dikenal sebagai dapp) - utilitas Open Source dari Flant, yang telah kita lakukan selama bertahun-tahun. Semuanya dimulai sekitar 5 tahun yang lalu dengan skrip Bash yang mengoptimalkan perakitan Dockerfiles, dan 3 tahun terakhir, pengembangan penuh telah berlangsung dalam kerangka satu proyek dengan repositori Git sendiri
(pertama di Ruby, dan kemudian ditulis ulang untuk Go, dan pada saat yang sama diganti namanya) . Apa masalah pembangunan yang diselesaikan di werf?

Masalah berbayang biru telah diimplementasikan, perakitan paralel telah dilakukan dalam host yang sama, dan kami berencana untuk menyelesaikan pertanyaan kuning pada akhir musim panas.
Tahap publikasi dalam registri (terbitkan)
Kami mengetikkan
docker push
... - apa yang bisa sulit dalam mengunggah gambar ke registri? Dan kemudian muncul pertanyaan: "Tag apa untuk meletakkan gambar?" Itu muncul karena kami memiliki
Gitflow (atau strategi Git lain) dan Kubernetes, dan industri berkomitmen untuk memastikan bahwa apa yang terjadi di Kubernetes mengikuti apa yang sedang dilakukan di Git. Git adalah satu-satunya sumber kebenaran kami.
Apa yang rumit?
Pastikan reproduktifitas : dari komit di Git, yang secara inheren
tidak dapat
diubah , ke gambar Docker yang harus dijaga tetap sama.
Penting juga bagi kita untuk
menentukan asal , karena kita ingin memahami dari mana komit aplikasi yang diluncurkan di Kubernetes dibangun (maka kita dapat melakukan hal-hal yang berbeda dan serupa).
Strategi penandaan
Yang pertama adalah
tag git sederhana. Kami memiliki registri dengan gambar yang ditandai sebagai
1.0
. Kubernetes memiliki tahap dan produksi, tempat gambar ini dipompa. Di Git, kami membuat komitmen dan pada titik tertentu memasang tag
2.0
. Kami mengumpulkannya sesuai dengan instruksi dari repositori dan memasukkannya ke dalam registri dengan tag
2.0
. Kami meluncurkannya di atas panggung dan, jika semuanya baik-baik saja, maka pada produksi.

Masalah dengan pendekatan ini adalah bahwa kita pertama-tama mengatur tag, dan baru kemudian menguji dan meluncurkannya. Mengapa Pertama, ini sama sekali tidak masuk akal: kami memberikan versi perangkat lunak yang bahkan belum kami uji (kami tidak bisa melakukan sebaliknya, karena untuk mengecek, Anda perlu memberi tag). Kedua, cara ini tidak kompatibel dengan Gitflow.
Opsi kedua adalah
git commit + tag . Ada tag
1.0
di cabang master; baginya dalam registri - gambar yang digunakan untuk produksi. Selain itu, kluster Kubernetes memiliki pratinjau dan pementasan loop. Selanjutnya kami mengikuti Gitflow: di cabang utama untuk pengembangan kami
develop
fitur-fitur baru, sebagai akibatnya ada komit dengan pengenal
#c1
. Kami mengumpulkannya dan menerbitkannya di registri menggunakan pengenal ini (
#c1
). Kami meluncurkan pratinjau dengan pengidentifikasi yang sama. Kami melakukan hal yang sama dengan komit
#c2
dan
#c3
.
Ketika kami menyadari bahwa ada cukup fitur, kami mulai menstabilkan semuanya. Di Git, buat cabang
release_1.1
(berdasarkan
#c3
dari
develop
). Mengumpulkan rilis ini tidak diperlukan, karena Ini dilakukan pada langkah sebelumnya. Karena itu, kita bisa langsung menggelarnya untuk pementasan. Kami memperbaiki bug di
#c4
dan juga melakukan pementasan. Pada saat yang sama, pengembangan sedang berlangsung di
develop
, di mana perubahan dari
release_1.1
dilakukan secara berkala. Pada titik tertentu, kita mendapatkan komitmen dan dorongan untuk melakukan komitmen, yang kita
#c25
(
#c25
).
Lalu kami melakukan penggabungan (dengan fast-forward) dari cabang rilis (
release_1.1
) di master. Kami memberi tag dengan versi baru (
1.1
) di komit ini. Tetapi gambar ini sudah dirangkai dalam registri, jadi agar tidak mengumpulkannya lagi, kami hanya menambahkan tag kedua ke gambar yang ada (sekarang memiliki tag
#c25
dan
1.1
di registri). Setelah itu, kami meluncurkannya untuk produksi.
Ada kekurangan bahwa satu gambar (
#c25
)
#c25
saat pementasan, dan yang lain (
1.1
)
#c25
saat produksi, tetapi kita tahu bahwa "secara fisik" itu adalah gambar yang sama dari registri.

Kekurangan sebenarnya adalah bahwa tidak ada dukungan untuk menggabungkan commit'ov, Anda perlu melakukan fast-forward.
Anda dapat melangkah lebih jauh dan melakukan trik ... Pertimbangkan contoh Dockerfile sederhana:
FROM ruby:2.3 as assets RUN mkdir -p /app WORKDIR /app COPY . ./ RUN gem install bundler && bundle install RUN bundle exec rake assets:precompile CMD bundle exec puma -C config/puma.rb FROM nginx:alpine COPY --from=assets /app/public /usr/share/nginx/www/public
Kami membuat file darinya sesuai dengan prinsip ini, yang kami ambil:
- SHA256 dari pengidentifikasi gambar yang digunakan (
ruby:2.3
dan nginx:alpine
), yang merupakan checksum dari isinya; - semua tim (
RUN
, CMD
, dll.); - SHA256 dari file yang ditambahkan.
... dan ambil checksum (lagi SHA256) dari file seperti itu. Ini adalah
tanda tangan dari segala sesuatu yang mendefinisikan konten gambar Docker.

Mari kita kembali ke skema dan
alih-alih melakukan kita akan menggunakan tanda tangan seperti itu , yaitu beri tag pada gambar dengan tanda tangan.

Sekarang, ketika Anda perlu, misalnya, untuk menggabungkan perubahan dari rilis menjadi master, kita dapat membuat komit gabungan: ia akan memiliki pengidentifikasi yang berbeda, tetapi tanda tangan yang sama. Dengan pengidentifikasi yang sama, kami juga akan meluncurkan gambar pada produksi.
Kerugiannya adalah bahwa sekarang tidak mungkin untuk menentukan jenis komit apa yang telah dipompa untuk produksi - checksum hanya bekerja dalam satu arah. Masalah ini diselesaikan oleh lapisan tambahan dengan metadata - saya akan memberi tahu Anda lebih banyak nanti.
Menandai di werf
Dalam werf, kami telah melangkah lebih jauh dan bersiap untuk membuat rakitan terdistribusi dengan cache yang tidak disimpan pada mesin yang sama ... Jadi, kami memiliki dua jenis gambar Docker, kami menyebutnya
tahap dan
gambar .
Repositori werf Git menyimpan instruksi pembangunan spesifik yang menjelaskan berbagai tahapan pembangunan (
sebelum menginstal ,
menginstal ,
sebelum pemasangan ,
pengaturan ). Kami mengumpulkan gambar tahap pertama dengan tanda tangan yang ditetapkan sebagai checksum dari langkah pertama. Kemudian kami menambahkan kode sumber, untuk gambar panggung baru kami menganggap checksumnya ... Operasi ini diulangi untuk semua tahap, sebagai hasilnya kami mendapatkan satu set gambar panggung. Kemudian kita membuat gambar-gambar akhir yang mengandung juga metadata tentang asalnya. Dan kami memberi tag pada gambar ini dengan berbagai cara (detail nanti).

Biarkan setelah itu komit baru muncul, di mana hanya kode aplikasi yang diubah. Apa yang akan terjadi Sebuah tambalan akan dibuat untuk perubahan kode, gambar tahap baru akan disiapkan. Tanda tangannya akan didefinisikan sebagai checksum dari gambar panggung lama dan tambalan baru. Dari gambar ini gambar-gambar akhir baru akan terbentuk. Perilaku serupa akan terjadi dengan perubahan pada tahap lain.
Dengan demikian, gambar panggung adalah cache yang dapat didistribusikan didistribusikan, dan gambar gambar yang sudah dibuat darinya dimuat ke dalam Docker Registry.

Membersihkan registri
Ini bukan tentang menghapus lapisan yang tetap menggantung setelah tag yang dihapus - ini adalah fitur standar dari Docker Registry itu sendiri. Ini adalah situasi di mana banyak tag Docker terakumulasi dan kami memahami bahwa kami tidak lagi membutuhkannya, dan tag tersebut menghabiskan ruang (dan / atau kami membayarnya).
Apa strategi pembersihannya?
- Anda tidak bisa membersihkan apa pun. Terkadang lebih mudah untuk membayar sedikit untuk ruang ekstra daripada mengungkap banyak tag. Tetapi ini hanya bekerja sampai titik tertentu.
- Reset penuh . Jika Anda menghapus semua gambar dan hanya membangun kembali yang relevan dalam sistem CI, maka masalah mungkin muncul. Jika wadah dimulai kembali pada produksi, gambar baru akan dimuat untuk itu - yang belum diuji oleh siapa pun. Ini membunuh gagasan infrastruktur yang tidak bisa diubah.
- Biru-hijau Satu registry mulai meluap - memuat gambar ke yang lain. Masalah yang sama seperti pada metode sebelumnya: pada titik apa Anda bisa membersihkan registri yang mulai meluap?
- Waktu Hapus semua gambar yang lebih dari 1 bulan? Tapi pasti ada layanan yang belum diperbarui selama sebulan ...
- Tentukan secara manual apa yang sudah bisa dihapus.
Ada dua opsi yang sangat layak: jangan bersihkan atau kombinasi biru-hijau + secara manual. Dalam kasus yang terakhir, kita berbicara tentang yang berikut: ketika Anda memahami bahwa sudah waktunya untuk membersihkan registri, membuat yang baru dan menambahkan semua gambar baru ke dalamnya untuk, misalnya, sebulan. Sebulan kemudian, lihat pod mana di Kubernet yang masih menggunakan registry lama, dan transfer juga ke registry baru.
Kemana kita
pergi ke
werf ? Kami mengumpulkan:
- Git head: semua tag, semua cabang, - dengan asumsi bahwa segala sesuatu yang diuji di Git, kita perlu dalam gambar (dan jika tidak, kita perlu menghapus di Git itu sendiri);
- semua pod yang sekarang diunduh di Kubernetes;
- ReplicaSets lama (sesuatu yang baru-baru ini dipompa keluar), serta kami berencana untuk memindai rilis Helm dan memilih gambar terbaru di sana.
... dan kami membuat daftar putih dari set ini - daftar gambar yang tidak akan kami hapus. Kami membersihkan segala sesuatu yang lain, setelah itu kami menemukan gambar panggung anak yatim dan menghapusnya juga.
Tahap penerapan (penerapan)
Deklaratif yang kuat
Poin pertama yang ingin saya perhatikan dalam penyebaran adalah untuk meluncurkan konfigurasi sumber daya yang diperbarui, dideklarasikan secara deklaratif. Dokumen YAML asli yang menggambarkan sumber daya Kubernetes selalu sangat berbeda dari hasil yang benar-benar berfungsi di cluster. Karena Kubernetes menambah konfigurasi:
- pengidentifikasi
- informasi layanan;
- banyak nilai default;
- bagian dengan status saat ini;
- perubahan yang dilakukan sebagai bagian dari webhook masuk;
- hasil kerja berbagai pengontrol (dan penjadwal).
Oleh karena itu, ketika konfigurasi baru sumber daya (
baru ) muncul, kita tidak bisa hanya mengambil dan menimpa dengan itu konfigurasi "hidup" saat ini (
hidup ). Untuk melakukan ini, kita harus membandingkan yang
baru dengan konfigurasi yang diterapkan
terakhir (yang diterapkan
terakhir ) dan roll tambalan yang dihasilkan ke
live .
Pendekatan ini disebut
penggabungan 2 arah . Ini digunakan, misalnya, di Helm.
Ada juga
penggabungan 3 arah , yang berbeda karena:
- membandingkan yang terakhir diterapkan dan baru , kami melihat apa yang telah dihapus;
- membandingkan yang baru dan yang hidup , kita melihat apa yang telah ditambahkan atau diubah;
- terapkan tambalan yang diringkas untuk hidup .
Kami menyebarkan 1000+ aplikasi dengan Helm, jadi kami benar-benar hidup dengan penggabungan 2 arah. Namun, ia memiliki sejumlah masalah yang kami selesaikan dengan tambalan kami yang membantu Helm bekerja secara normal.
Status peluncuran aktual
Setelah acara berikutnya, sistem CI kami menghasilkan konfigurasi baru untuk Kubernetes, mengirimkannya untuk
diterapkan ke cluster menggunakan Helm atau
kubectl apply
. Selanjutnya, penggabungan N-way yang sudah dijelaskan terjadi, yang disetujui API Kubernetes sistem CI, dan yang terakhir merespons pengguna.

Namun, ada masalah besar: lagipula,
aplikasi yang sukses tidak berarti peluncuran yang sukses . Jika Kubernetes memahami perubahan apa yang akan diterapkan, terapkan - kami masih tidak tahu apa hasilnya. Misalnya, memperbarui dan memulai ulang pod di frontend dapat berhasil, tetapi tidak di backend, dan kami akan mendapatkan versi berbeda dari gambar aplikasi yang sedang berjalan.
Untuk melakukan semuanya dengan benar, tautan tambahan muncul dalam skema ini - pelacak khusus yang akan menerima informasi status dari Kubernetes API dan mengirimkannya untuk analisis lebih lanjut tentang keadaan sebenarnya. Kami membuat pustaka Open Source di Go -
kubedog (lihat pengumumannya di sini ) - yang memecahkan masalah ini dan dibangun menjadi werf.
Perilaku pelacak ini di tingkat werf dikonfigurasikan menggunakan anotasi yang diletakkan di Deployment atau StatefulSets. Anotasi utama,
fail-mode
, memahami arti berikut:
IgnoreAndContinueDeployProcess
- abaikan masalah IgnoreAndContinueDeployProcess
komponen ini dan lanjutkan penerapan;FailWholeDeployProcessImmediately
- kesalahan dalam komponen ini menghentikan proses penyebaran;HopeUntilEndOfDeployProcess
- kami berharap bahwa komponen ini akan bekerja pada akhir penyebaran.
Misalnya, kombinasi sumber daya dan nilai anotasi
fail-mode
:

Saat menggunakan untuk pertama kalinya, basis data (MongoDB) mungkin belum siap - Penyebaran akan macet. Tapi Anda bisa menunggu sampai saat itu dimulai, dan penyebaran masih akan berlalu.
Ada dua penjelasan lagi untuk kubedog di werf:
failures-allowed-per-replica
- jumlah tetes yang diizinkan per replika;show-logs-until
till - menyesuaikan momen hingga saat werf menampilkan (dalam stdout) log dari semua pod yang sedang digulirkan. Secara default, ini adalah PodIsReady
(untuk mengabaikan pesan yang hampir tidak kita perlukan ketika lalu lintas mulai tiba di pod), namun, nilai ControllerIsReady
dan EndOfDeploy
juga EndOfDeploy
.
Apa lagi yang kita inginkan dari penyebaran?
Selain dua poin yang sudah dijelaskan, kami ingin:
- untuk melihat log - dan hanya diperlukan, tetapi tidak semuanya;
- melacak kemajuan , karena jika pekerjaan "diam-diam" hang selama beberapa menit, penting untuk memahami apa yang terjadi di sana;
- memiliki kemunduran otomatis jika terjadi kesalahan (dan oleh karena itu sangat penting untuk mengetahui status penyebaran yang sebenarnya). Peluncuran harus berupa atom: baik itu menuju akhir, atau semuanya kembali ke keadaan sebelumnya.
Ringkasan
Sebagai perusahaan, bagi kami, untuk mengimplementasikan semua nuansa yang dijelaskan pada berbagai tahap pengiriman (build, publish, deploy), sistem CI dan utilitas
werf sudah
cukup .
Alih-alih kesimpulan:

Dengan bantuan werf, kami telah membuat kemajuan yang baik dalam memecahkan sejumlah besar masalah insinyur DevOps dan akan senang jika masyarakat luas setidaknya mencoba utilitas ini dalam praktiknya. Mencapai hasil yang baik bersama akan lebih mudah.
Video dan slide
Video dari kinerja (~ 47 menit):
Penyajian laporan:
PS
Laporan Kubernet lainnya di blog kami: