Cache pre cargo v kontajneroch
Po úspechu s cache pre apt-get v kontajneroch som chcel urobiť to isté s cargo a rustup. Konkrétne som chcel vytvoriť lokálnu HTTP cache, ktorú by kontajnery mohli používať namiesto oficiálnych serverov. Cargo aj rustup sa bránia cachovaniu, takže cesta k úspechu nebola priamočiara, ale nakoniec sa mi našťastie podarilo vytvoriť účinnú cache pre cargo aj rustup.
Prečo nie zväzky (volumes)?
Obvyklá rada, ako lokálne cachovať stiahnuté cargo a rustup súbory, je zdieľať zväzky (volumes) medzi všetkými kontajnermi. To však úplne zničí akúkoľvek izoláciu. Pokiaľ viem, v cargo ani v rustupe momentálne nie je žiadne hašovanie ani podpisovanie, takže napadnutý kontajner bude schopný infikovať všetky ostatné kontajnery cez zdieľanú cache. Náhodné poškodenie cache sa tiež rozšíri do ostatných kontajnerov. Takže hoci je zdieľanie zväzkov skutočne jednoduché a účinné, oslabená bezpečnosť a izolácia ho robia dosť neatraktívnym.
Štandardný mirror softvér
Panamax aj ROMT mirrorujú tak cargo crate súbory, ako aj binárky rustupu. Problém je, že tieto nástroje očakávajú, že budete mirrorovať všetko. Nedokážu sťahovať potrebné súbory lenivo. To obmedzuje ich použitie na niekoľko obrovských spoločností a oficiálne verejné mirrory. Pre lokálnu cache sú nepoužiteľné. ROMT sa dá technicky nakonfigurovať tak, aby mal v cache len podmnožinu crate súborov, ale jeho nastavenie je pracné a musíte ho opakovať zakaždým, keď sa vaša podmnožina zmení.
Počiatočný odpor
Pred cargo sa nedá jednoducho postaviť reverzná HTTP proxy. Staršie verzie cargo používali ako index git, čo nie je práve typický obsah pre CDN. Zdá sa, že cargo stále používa git pre súkromné archívy, ale ten verejný prešiel na niečo, čo nazývajú riedky index (sparse index), čo je len sada cachovateľných súborov. To však problém s cache úplne nevyriešilo. Existujú dve samostatné subdomény, index.crates.io pre index a static.crates.io pre crate súbory. Subdoména s indexom má URL subdomény s crate súbormi napevno zakódovanú vo svojom /config.json takto:
{ "dl": "https://static.crates.io/crates", "api": "https://crates.io" }
Cachovanie cargo crate súborov lokálne pod jedným portom si preto vyžaduje tri pravidlá pre URL: jedno pre config.json, jedno pre index a jedno pre crate súbory. Fungujúca konfigurácia je uvedená neskôr v tomto článku, ale dosť ma prekvapilo, že pred to celé nemôžem len tak postaviť generickú HTTP proxy.
S rustup je to ešte horšie. Rustup vám umožňuje špecifikovať mirror pomocou premenných RUSTUP_DIST_SERVER a RUSTUP_UPDATE_ROOT, ale predvolený inštalačný skript z rustup.rs trvá na HTTPS prístupe k mirroru, čo je samozrejme pre lokálnu cache veľmi nepraktické. Panamax aj ROMT vám preto odporúčajú, aby ste obišli sh.rustup.rs a stiahli si priamo rustup-init pre vašu platformu. To však vyzerá škaredo a zbytočne to viaže váš Containerfile na jednu platformu.
Tento odpor voči cachovaniu mi príde prekvapujúci. Možno má projekt Rust veľa bezplatných a rýchlych mirrorov? Aj keď kapacita serverov nie je problém, stále chcem, aby bolo cargo rýchle a spoľahlivé. Chcem tiež odolnosť voči výpadkom siete.
Cache pre rustup
Predtým, než opíšem cache pre cargo, stručne sa pozrime na rustup, čo je jednoduchší prípad. Vyriešil som to jednoducho inštaláciou Ubuntu balíčkov pre rust a cargo, ktoré sa sťahujú z predtým nakonfigurovanej cache pre apt-get. Inštalačné súbory z Ubuntu majú nevýhodu, že sú niekoľko mesiacov pozadu za aktuálnou verziou. Keďže ja sám nepoužívam najnovšie funkcie rustu, jediná nepríjemnosť, s ktorou sa pri tomto nastavení stretávam, je, že musím nastaviť hornú hranicu verzie pre knižnice, ktoré bezhlavo pridávajú požiadavky na najnovší rust bez toho, aby zvýšili hlavnú (major) alebo vedľajšiu (minor) verziu.
Ďalšou možnosťou je spustiť skript rustup.rs dostatočne vysoko v Containerfile, aby sa maximalizovala pravdepodobnosť, že jeho vrstva v podman cache bude zdieľaná medzi súvisiacimi kontajnerovými imidžmi. Rust sa v bežiacom kontajneri aktualizuje zriedka, takže ak dokážete udržať konzistentný úvod v Containerfile pre všetky vaše kontajnerové imidže, cache vrstiev bude stačiť. Toto je dobrá voľba, ak bezpodmienečne musíte mať najnovšiu verziu rust a cargo.
Treťou a poslednou možnosťou je nasmerovať jednoduché reverzné HTTP proxy na static.rust-lang.org a zahrnúť tento kód do vášho Containerfile:
ARG RUSTUP_DIST_SERVER=https://static.rust-lang.org ENV RUSTUP_UPDATE_ROOT=https://static.rust-lang.org/rustup RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sed "s/--proto '=https' //g" | sh
Všimnite si, ako pomocou sed odstraňujeme požiadavku na HTTPS z parametrov curl vnútri skriptu. Potom môžete RUSTUP_DIST_SERVER nasmerovať na vaše reverzné proxy. Túto tretiu možnosť som však netestoval. Možno ju budete musieť trochu doladiť.
S vyriešenou cache pre rustup sa teraz môžeme pozrieť na cache pre cargo.
Výber HTTP cache servera
V dnešnej dobe je prekvapivo ťažké nájsť dobrý HTTP cache server. Squid v akceleračnom režime sa ťažko konfiguruje. Varnish nemá vstavanú podporu pre HTTPS backendy. Caddy má experimentálny cache modul, ktorý je slabo zdokumentovaný a pokiaľ viem, neukladá index cache na disk. Preto som sa rozhodol pre nginx, ktorý sa zdá byť jedinou rozumnou voľbou pre naše potreby. Nie som zvlášť nadšený zo softvéru s ruským pôvodom a množstvom zraniteľného C kódu, ale riziká sú v jednoúčelovej kontajnerizovanej cache minimálne.
Konfigurácia nginx
Nakonfigurujme teda nginx. Konfigurácia je trochu dlhšia, pretože pokrýva vyššie spomínané tri cesty (config.json, index a crate súbory) plus štandardné nastavenia nginx. Definuje lenivú cache pre cargo, ktorá počúva na porte 3263. Revalidácia musí byť povolená, aby si cargo všimlo nové verzie crate súborov. Dal som si tú námahu a pridal som logovanie úspešnosti (hit/miss) pre všetky dotazy, aby ste si mohli skontrolovať, že cache funguje.
# Vypnutie predvoleného logu prístupu, aby sa predišlo duplicitnému logovaniu. access_log off; # Cache pre crate súbory. proxy_cache_path /var/cache/nginx/crates levels=1:2 keys_zone=crates_cache:10m max_size=10g inactive=400d use_temp_path=off; # Cache pre index crate súborov. proxy_cache_path /var/cache/nginx/index levels=1:2 keys_zone=index_cache:10m max_size=10g inactive=400d use_temp_path=off; # Konfigurácia DNS resolvera, keďže IP adresa sa môže zmeniť. # Vynútiť len IPv4, aby sa predišlo pokusom o IPv6 pripojenie na hostoch bez IPv6. resolver 1.1.1.1 9.9.9.9 ipv6=off valid=300s; # Mapovanie stavu cache na textový reťazec (predvolene "-"). map $upstream_cache_status $cache_status { default "-"; HIT "HIT"; MISS "MISS"; BYPASS "BYPASS"; EXPIRED "EXPIRED"; STALE "STALE"; UPDATING "UPDATING"; REVALIDATED "REVALIDATED"; } # Mapovanie stavu upstream servera na textový reťazec (predvolene "-"). map $upstream_status $upstream_code { default "-"; ~^[0-9]+$ $upstream_status; } # Formát logu prístupu, ktorý zahŕňa stav cache a stav upstream servera. log_format cargo_cache '$status $upstream_code $cache_status "$request" $body_bytes_sent'; # Posielanie logov na stdout/stderr kontajnera, aby ich zachytil Podman/Journald. error_log /dev/stderr warn; server { listen 3263; # Zapnutie nášho vlastného logu prístupu len v rámci tohto server bloku. access_log /dev/stdout cargo_cache; # Spoločné nastavenia proxy. proxy_http_version 1.1; proxy_set_header Connection ""; proxy_ssl_server_name on; proxy_ssl_verify on; proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt; add_header X-Proxy-Cache $upstream_cache_status always; # Povolenie podmienenej revalidácie pomocou If-Modified-Since a ETag. proxy_cache_revalidate on; # Cachovať len odpovede s kódom 200. Ostatné odpovede nebudú v cache. proxy_cache_valid 200 400d; # Vlastný config.json, ktorý nasmeruje cargo pri sťahovaní na túto cache. location = /config.json { add_header Content-Type application/json; return 200 '{"dl": "http://127.0.0.1:3263/crates", "api": "https://crates.io"}\n'; } # Presmerovanie požiadaviek na crate súbory na static.crates.io. location /crates/ { proxy_ssl_name static.crates.io; proxy_set_header Host static.crates.io; proxy_cache crates_cache; # Použitie premennej na vynútenie DNS dotazu počas behu. set $upstream https://static.crates.io; proxy_pass $upstream; } # Presmerovanie požiadaviek na index na index.crates.io. location / { proxy_ssl_name index.crates.io; proxy_set_header Host index.crates.io; proxy_cache index_cache; # Použitie premennej na vynútenie DNS dotazu počas behu. set $upstream https://index.crates.io; proxy_pass $upstream; } }
Toto pridáme do štandardného nginx imidžu:
# Prispôsobenie štandardného nginx imidžu. FROM docker.io/library/nginx:stable # Kopírovanie konfiguračného súboru. COPY nginx.conf /etc/nginx/conf.d/default.conf # Port, na ktorom cache počúva. EXPOSE 3263
Pred použitím musíme imidž skompilovať:
podman build -t localhost/cargo-cache
Konfigurácia systemd
Na spustenie imidžu pod systemd použijeme integráciu podmana so systemd. Všimnite si, že sú tu dva zväzky (volumes), jeden pre cache crate súborov a jeden pre cache indexu. Nechceme vytvárať zväzok pre celý adresár /var/cache/nginx, pretože štandardný nginx imidž tam ukladá aj iné veci, ktoré nechceme zachovať.
[Unit] Description=Nginx-based cache for crates.io After=network-online.target Wants=network-online.target [Container] Image=localhost/cargo-cache ContainerName=cargo-cache LogDriver=journald PublishPort=127.0.0.1:3263:3263 Volume=cargo-cache-crates:/var/cache/nginx/crates:Z Volume=cargo-cache-index:/var/cache/nginx/index:Z [Service] Restart=always [Install] WantedBy=default.target
Uložte vyššie uvedenú konfiguráciu služby ako ~/.config/containers/systemd/cargo-cache.container a spustite ju pomocou príkazov nižšie. Uprednostňujem spúšťanie všetkého pomocou rootless podman kontajnera pod neprivilegovaným používateľom. Aby sa kontajner spustil ešte pred prihlásením používateľa, povolíme pre používateľa lingering.
systemctl --user daemon-reload systemctl --user restart cargo-cache sudo loginctl enable-linger $USER
Konfigurácia aplikačného kontajnera
Pripravíme Containerfile aplikácie na použitie cache pridaním krátkeho nastavenia pre cargo:
ARG CARGO_MIRROR="" RUN if [ -n "$CARGO_MIRROR" ]; then \ mkdir -p .cargo && \ echo '[source.crates-io]' > ~/.cargo/config.toml && \ echo "registry = 'sparse+$CARGO_MIRROR/'" >> ~/.cargo/config.toml; \ fi
Všimnite si, že toto podporuje kompletné mirrory aj lenivé cache. Ak parameter CARGO_MIRROR nie je počas kompilácie imidžu špecifikovaný, kontajner bude sťahovať crate súbory z oficiálnych cargo serverov. Týmto spôsobom môže byť Containerfile umiestnený vo verejných zdrojákoch bez toho, aby sa pokazila kompilácia ľuďom, ktorí nemajú našu cache.
Pre zapnutie cache nasmerujeme CARGO_MIRROR na našu lokálnu cache. Okrem parametra musíme tiež prepojiť port cache do kontajnera, a to počas kompilácie aj počas behu:
podman build \ -t localhost/cargo-cache-test \ --build-arg CARGO_MIRROR=http://127.0.0.1:3263 \ --network=pasta:-T,3263 podman run -it --rm \ --network=pasta:-T,3263 \ localhost/cargo-cache-test
Ak používate aj moju cache pre apt-get, potom kompletné nastavenie vyzerá takto:
podman build \ -t localhost/cargo-cache-test \ --build-arg APT_PROXY=http://127.0.0.1:3142 \ --build-arg CARGO_MIRROR=http://127.0.0.1:3263 \ --network=pasta:-T,3142,-T,3263 podman run -it --rm \ --network=pasta:-T,3142,-T,3263 \ localhost/cargo-cache-test
Teraz môžete používať cargo počas kompilácie imidžu aj počas behu kontajnera. Sťahovanie v cargo bude presmerované cez nginx, ktorý bude ukladať súbory do cache. Po spustení journalctl --user -u cargo-cache by ste mali vidieť logy, ktoré vyzerajú takto:
200 - - "GET /config.json HTTP/1.1" 67 200 - MISS "GET /3/p/png HTTP/1.1" 4104 200 200 MISS "GET /crates/home/0.5.11/download HTTP/1.1" 9926 ... 200 304 REVALIDATED "GET /3/p/png HTTP/1.1" 4104 304 - HIT "GET /ho/me/home HTTP/1.1" 0 200 - HIT "GET /crates/home/0.5.11/download HTTP/1.1" 9926
Revalidácia (len hlavičky) je to najlepšie, čo sa dá dosiahnuť pre index, ak má cargo stále vidieť nové verzie. Čistý hit (bez revalidácie) je to, čo by ste mali vidieť pre nemenné crate súbory.