30. června 2018

Správa proprietárních závislostí v Golang

Se změnou zaměstnání přišly nové výzvy - prototypujeme teď nový produkt a jako primární technologie byl zvolen Golang. Myslím si, že vzhledem k povaze produktu (smečka mikro-servis) a cílové infrastruktuře (IaaS) je to dobrá volba.

Golang není zas až tak nový jazyk (je tady s námi nějakých 9 let), a ačkoliv se v něm příjemně píše, má vývoj v Golangu určitá úskalí a výzvy - to buď v případě, že vám úplně nevyhovuje, jak v Googlu vymysleli vývojový proces, anebo pokud očekáváte vlastnosti běžné na některé zralejší platformě.

Jak já, tak celý tým máme silné Java zázemí a je tedy možné, že náš přístup ke Golang infrastruktuře není úplně adekvátní. Nicméně poslední tři měsíce jsem se tématu verzování, závislostí, reprodukovatelnosti a automatizaci Go buildů intenzivně věnoval a řekl bych, že to soudruzi z U.S.A "nedotáhli úplně dokonce".

Požadavky

Pro náš projekt jsme měli, řekl bych, celkem běžné nároky:
  1. Privátní Git repository za firemní proxy. Kvůli legálním záležitostem a firemním politikám nemůžeme dát projektový kód na GitHub.
  2. Samostatná projektová repozitory. OK, chápu, že v Googlu milují monorepo, ale většina z nás v Googlu nepracuje...
  3. Reprodukovatelný build. To je samozřejmost, k tomu asi není co dodat.
  4. Spolehlivá správa závislostí a jejich verzí. V Javě, nebo na Linuxu je to no-brainer - stačí mít package manager a repozitory, ne?
  5. Plně automatizované. Zase by se řeklo samozřejmost, ale pořád a pořád potkávám lidi, kteří smolí (klidně i produkční) buildy ručně.

Problémy

Pokud budete chtít výše zmíněné požadavky naimplementovat v Golangu, narazíte na problém: out-of-the-box to nejde vůbec a pokud nebudete ochotni část nároků oželet, čeká vás world-class hackování. (Teda ne, že bych byl takovej hustej hacker... spíš jsme narazili na hluché místo.)

Esenciální potíž je, že celý Golang ekosystém je navržený jako jedno velké open-source monorepo (a.k.a. GitHub). Pokud nechcete/nemůžete mít svůj kód na GitHubu, máte problém. Protože:
  • Golang nemá jednotný/dominantní nástroj na automatizaci. Většina projektů na GitHubu builduje makem. make je fajn na kompilaci, ale dělat v něm komplexnější automatizaci je masochismus.
  • Golang nemá dořešený verzování dependencí. Člověk by řekl, že po těch letech to už bude nějak usazený, ale stačí nahlédnout do oficiální wiki stránky, nazvané lapidárně Package Management Tools a zjistit, že pro správu dependencí a verzí existuje nějakejch 20-30 nástrojů. Naštěstí se od loňského léta masivně prosazuje nástroj dep a velmi slibně to vypadá s oficiálním návrhem vgo, který by se snad měl cca na podzim dostat do Go verze 1.11.
  • Oficiální Golang nástroje neumí pracovat se závislostmi, které nejsou ve veřejných repozitářích (GitHub, Bitbucket, GitLab). Jediný podporovaný způsob je nastavit na repozitory HTML meta tag go-import. Což většinou(?) nemáte pod kontrolou.

GOPATH odbočka

Malé vysvětlení pro čtenáře, kteří nejsou Gophers (fanoušci Golangu). Proč jsou výše zmíněné problémy reálnými problémy? Všechny Golang nástroje předpokládají, že vaše zdrojáky jsou soustředeny pod tzv. GOPATH - což je fakticky lokální monorepo.

Protože v Golangu se všechno linkuje a kompiluje staticky, je potřeba mít všechny zdrojové kódy na jednom místě, neexistují binární závislosti. Pro stažení externích závislostí (v Go nazývaných package import) slouží nástroj go get, který de facto naklonuje Git repository dané závislosti do adresářové struktury pod GOPATH.

Pokud bychom pominuli verzování a reprodukovatelnost a soustředili se jenom na kompilaci, máme tady dva základní konflikty:
  • go get neumí klonovat z non-public repozitářů.
  • Pokud nemáte váš projekt pod GOPATH, budete muset s GOPATH nějak manipulovat, aby Go nástroje vůbec fungovaly.


Řešení

Protože jsem husto-krutej drsňák, před kterým bledne jak Phil Marlowe, tak Dirty Harry, tak jsem se nezaleknul, zatnul zuby a všechny problémy vyřešil. Svým způsobem...

Pravdou je, že řešení, ke kterému jsme prozatím doiterovali, byl průzkum bojem - tj. paralelně řešit business požadavky (proč vlastně ten prototyp děláme) a zároveň vyřešit automatizaci buildů (nebo minimálně aspoň kompilaci Golang projektů).

Když začnu s výše popsanými požadavky od konce - pro automatizaci jsme zvolili Gradle. Původně jsme počítali s nějakým polyglot řešením (něco v Javě, něco v Golang), ale nakonec jsme skončili s čistě Golang moduly.

Ironií je, že v celém řešení víceméně žádná Java není (výjimkou jsou Gradle pluginy). Každopádně, Gradle se výborně osvědčil - buildujeme v něm Go, RPM balíčky, Docker image, stahujeme a re-packagujeme binárky z internetu, pouštíme integrační testy, deployujeme do repozitářů a do cloudu atd.

To všechno je hezké, jak ale v Gradlu skompilovat Golang zdrojáky? Samozřejmě, vezmu nějaký hotový plugin. Bohužel, výše naznačené problémy s verzováním Golang závislostí způsobily, že nám žádný plugin úplně nevyhovoval.

A tak jsem si napsal svůj plugin. Řekl jsem si, že základní nástroje v Go fungují dobře a tak je jenom zorchestruju. Takže můj plugin není nic jiného než: wrapper + orchestrátor + lifecycle. Plugin řeší následující věci:
  1. Manipulace s GOPATH, aby fungovaly nástroje, které očekávají zdrojáky v... GOPATH.
  2. Stažení externích závislostí z public repozitářů (pomocí nástroje dep).
  3. Stažení externích závislostí z non-public repozitářů (pomocí Git klonování).
  4. Spuštění testů.
  5. Kompilace binárky.

O správu verzí public závislostí se stará nástroj dep. O správu verzí non-public závislostí se stará plugin (task proprietaryVendors). V případě, že non-public závislosti mají tranzitivní závislosti na public závislostech, je to kombinace obojího. Technický popis je trochu komplexnější, takže zájemce odkazuji do dokumentace: How to handle proprietary vendors; nicméně použití by mělo být (v rámci možností) jednoduché.

Sekvenční diagram build life-cyclu godep pluginu


Problem solved?

Takže... problém vyřešen? Nový Gradle plugin splňuje všechny v úvodu uvedené požadavky a za ty tři měsíce má za sebou stovky úspěšných buildů (build se v GitLab Pipelině pouští pro každý git push). Tudíž zatím ano, funguje to k naší spokojenosti.

Otázkou ale je, jak dlouho to vydrží - verzování závislostí je žhavé téma jak Golang komunity, tak core vývojářů Go. Všichni s očekáváním vzhlížejí ke zmíněnému vgo a tak za půl roku možná bude všechno jinak a nějaký Gradle plugin už nebude potřeba.

Ale nijak se tím netrápím - pár takových Gradle pluginů už jsem napsal: v daný moment vyřešily mou situaci, avšak časem přestaly být potřeba. A to se mi líbí - nástroje, které jsou dostatečně flexibilní a zároveň mají hezký a moderní design. A dobře se v nich dělá. A to jak Golang, tak Gradle splňují.

Externí odkazy


Mind Map