i{F. 節t,Ryiqd8 7fD $H@Iy}%EjUk?+bͳg3.i8ک\~];U[nl)oh)!z7$]k7O9+Uq*Wnug\l/r$A]OҮ_V[ l(/GK!{#X&vB\_W9)okyj]SS5qW] oWsAZ䞼AZO~)< 7}T>x,.M™Ym\ӸT3JǺtLi%Nʶb/>W]Ǯ0WY$נJqX|Om~ϟ^>Tx4_ހr } ~s]eVhև#[m7A|?}>4|xy~zz^ZoJzZ|B3'o_Z7^퓗?ek|!l_'gM+yjI7hnj |v6`ΣwEQ&׋G7MƯiDBijJbYz^ɫgfykPݽv0uwW'|I‘vvl6 >_}u<-|iZ_2~5,ÿq#|xEXo[}ۛij?_5>Y_im%ׯe/X؏k_^o˛@ͼ7?py7oDϟ?uQW=X[ԛoσAQ6V)7)e;I@OA' nod)?"ap,p#:(\צO|J9ѓpa_/߿]۠l2h)wt__/Q\U7Vzp_ȕu34+(`~~ N3pK~ hokMY΍io_o?DdDzۘ.nO(TUDPמh]›|HoOC~>wխԋ]SU&W W,mG {'_1}ѧݾoP[Ho_K3z|]wg~KV6rr\Ewp퇏yaDDOOo?Ձ𥂛rҊ.k-&m:'GT:T^cݓʂM!$6xcD ×ɿӭh.ۛϷ]$~k4x܌O~.-Tˏo$~>ib/h>-k{K)^R_fyK׏7A+d{qׯTۓU7]^: <]VNM5*6L"Lb"l/xZ>ۏayy[_^KDrwAR_&ɷo( ת)_~|;|l|&_~O屺]{P)/e W_3~U2ߟ?G}=CoAܔ7-) }#>{7.n军hZG~~}LJ'oߣ\% @]ؿ,_~Ŗ~y kg|`~.Fm0H?X)4_נ%rKAO^H?8^Da1?Tp9D$}W8٣Nßz6]w1z ٠kH-V_7^?~|  6kܾrqO/s;|K_Bk_9$ߟo^/O?痿OP6sd.VJz?1՚| ۷oQ%B/oW ^[?~<|(~kGȴ@e߀syޗU8)^<={ti7n eqṕolkPp:} `ZS̾)Qk@مgD3[Hsp{x د~?W6?+Nu͛W]=#R3篊7#~y~3~GN{ԯ-\Es=<7F3:"tdz ̯d~ nG~u7@ !O:_~`@?@ .cۛ@M-tu {an ؓ7K4b /!R_YnR[GDl7I1qdH!j^@%XA?͛~3[dm}F֭U<._j7OqBFH+ҡC9ᯗQg>~G80-2x $ؗϏAm@'䢉#^^o_‹\߫_-uΘD׍|0?"hxo= ȲN]/@eON'Y3(7c!9¼˷?i?ޜ?a/P{z˷oXȭa7'"V?_EWq—7+/_܄-FO=y3+r|=yI'c>o2&!{7No} B_YojTXo}wP_@O MYs$ 틯0an{s;춸r9:臘Et1cය?XqHOӃmp#jO [szyo/؏?~_> ~z ^yP+٧'+R`) .ۦc!Hq/{AY09'D\F^0)@o5W3`?[z .OMGooUH ^y N&^ݽr}@Sc&_̛e$ոiTY3[[S3@Znjo~<@tipnU*C_|,fu/o??)U Lq}|({FC?P_"xA @<0%0ݿeB>~A8)^" (V@ Dճ[ .t_6m Y`={޽u}ef!XN+<}|5"|Q>#pOB*9>`bQBc(g γ RZ3VРp {:3EaTNژ3| D.xTƉBͣ7t|?_ү2y5cඁ|3jq=VO_ӯ~_K"Oo&m" c+ï>a~&o_+8}~z&܂~?_߂z~yz6+isXG6=+/`-c ~ʯ߀_9.`o~?ƀݷkءG_!=8]W?g g~{ ~i_ruJoYegs,fKVwE^]˹i<*^]'Y|<(z_0F{hb~h`o \N~Mk#Klջn6j㵊W4wymDUhIGKJtOmdS8eõb4pTHpثfM)U$yJWk+DWئIjI}`2s+h9g_vUgp:xݬfq{ã).@^~3NF"FzFϳon߹gA>Jvm|^!5a`絿>qR*xi˼e -@}fBMA(UOq&NZN[  zzÛW5J`*P_kQeOx_(lټNk8JdzXf8UOI1G3!/.fn`ۓt aǚxA'`ZbT6Ev'Yn"Jt8`;I|K83;'oֲd]64/ ! 'AY\ۦ*ڦ߁{/w7~v7?Î<.~{Wo?/ծ-VRrdC>j<2K~hDxʋ8 'ݭ``k9MW綷cݱ>8NW*MU\&]Iq<+vU~&;.<&~cZEyE(. K1LsHºd#jMRͧǻү*A۠Ks(}pk@1\P*~G".LKpHS71[m"Y*J0{9h5 mc@ ٟ8bHgcĹvJZz;q N!B%C \h+Bz~>{HTw)Ɋ-ð[`&^h2'otlwd :pd^'oj6VR;"XV]Cb1NJXR#Q2#T8g! v@%=N5Uƌ:tMA>\ϣX }${$2]UĆ+&G+a܌uO,^3PuM$v&k*~kh'P@ZPuzՆL!/EУIVxuT~@mC m\KorxmN.c;aw/`&Ww4!=/"D o H.@pL}eu3Fp%56Z ֹ_~5cNN &5I61}69+pL6I5$S(_6~!F*vkYމd ϊN`{z rs`q8}kc ,6/k|U2My-߾׃jt2Љ>vV 'n9aVȑ0;GQ:ޙwUd6[ӭj6Նa}jD7W·.4U7Ppº =0/ 2Eȥ} ,EV넬~b`EHV7<:cSmYy1yY:t8Bh…jҐ;9cˣ4ÚGl\%y(IyYp [ax%KLݸetJKc ]*H{ЛS; , >T[X/ٌ=\y #rg/@I8$Gw ڣ~(Uj"HH~Mր4@›IKn}*cbv۬KMg, +I!m\H2;~BaT1xq\qGE*O˰I f}̛y V(僵nW&0U$kךoJzb\wW l|“_d8P epN(]h9d+1!pǷ <ÃM8hd**BxqryPXzw[[tuD5ॅGalxRq}PgYd** ҫJ,-uNΌ|׎*<"dnGN_;Dy,s-S[;lQ5[a`Rdqg0;W+(ymsK8f-%b\äs"\XS U  Mm9+2%TjrLS~5Z_HK dej uѫuFlL녹qfL::9NDukQ5'e6lFs 00 NpLiz O]SiN]Jvͯ,9 uԤNfljyIw9s2r]iQS4 68ϳ.sb b)se.wte,af s*:ނP;cMĎ`K ǚP,{Ep_oB0}J?~w~jЯ [5onSe[yGi }E"wQ5`{Iϡ[ yaNu9ZMȮ"Ah`3/O%Aiܭ_S5B|&)(vtr6>UIߣɵ?eiycE !kYfND :8ch'E}dX+a{C m1(^Uv#5w˻k5c ՘1,)#.vkpU;C?1g.x$!WDvSQS #ҍ6>`L d L2S +|vͦX:Lg 8& `iCZ]ױByhmwIԙuݚC;xD.B %̓fōJו%!&o!yw!וOPN]z$uA mp\iN:ڂ_NL Ղ{(/UҠ%G鎗L] X8 -ԈBWwy#w<徣L*&P7n0tILwQQ`yMFlp2$9y,G_w?*V?j;l *1y~i@8y{Qmwm<p!t&cnuw[D[ ~Zd/J޿Ɔ7A/rP8=a}{7وq. s6 Rgll 8C!";1vqyq%ާ| /sg6[EKNF|kRc ik}}(~ Z*(Ek֩HK26S"gna *=Y0` 8h]+0>EV0TO0np#PniNOj`3N"= :u!"`I@*Ǐ :'76| "O0 !'/~_h*F'%2/*qD($ Ϙ;t踗A@׬4'-TќASu~I)"Gn\D~yU5~2oMX&<{TRSG5:0nX#dCzRf+16~V7e%&k:\REYe~UűPKzmܘ/ZvNSEĸ,~ ^9ËюL!B܆'cwݢA8DɅH L4o =odN.b1rkZ@gη~FMg˸vG~SׅhP~ǃKSO Uh u#o\lSZ5/ $* C ($3 q@O\^:7xsprG<xW$ƅVY9bɁ̄):眇 m FdIE4m/8+p{vm1[ N kWuP*;49^ŤhnV5I+ֆ{O t.3A⠜&Fdp<#[Q]N݇4QG ׻/ŎZ2IK'U e BT̾ @{cڲԿ ?3te0a 1",R0KHF-׋;gRRCY?~H$ )@qJ:0z:Rܱ{ԥHwM-M.^_;Rr}F GL_1v4. J!dkć eDCb _\Ithc霉WIl <`sd=̫ח="kr|NSe;|+u~}rU_bRxPo҆*q^V~rCzJcQ}V~йtwz2^ൽ=agp 63a^hs.7D0ܗZ~F六JЪw$m(yl*3*5򠷁/ingn ` ٨d7.] \!_:|r5* ΖOW~7oic0.>*WբNdꄩy$] %3SnԗSN# Lq.ı`#@ Iݸvy@~Jצ'h5(Tk̳Bjx zV"[%l/nF4-hvFo5%?v67Ac`C屘f@311j=}-H3x d`);dyʑ=RrƛEsfq;4Bͱ񭝣m<^/f~ij(֎_t.㔛4x }^Ő| _A˞&%ۀ[O\@BL#rz{L]N4IϮ:7TRC!Y:&vyܐ D䈉')5M|wZ^' /WQa%Tb W:/ Y!)p kzk9 #Bk#a$1Q.sk2o499Z+k'r>oyT qF@G&E|1?7W(H&n`TTwJ(Dʫ'sEr_;g3cA 8 lh`LT:˧٨QePwo.罪 J ;ѡXg~[Ż1=&ĺ8 TJIVFPgbg$jp ZB"-ukZS Has7;H蕄@ȺbjOGp=9(Hnsy\ (/{Tx,Ϳo߼}۷.R<04윁zev܏3U||pa`Ry 7jL_AGv^}}nJIm 1ZϞHu4ϰX-0zlk!kμVw`vʝI@OJ{Ռ E dsJ80[}!s72w1ԒNb) RWEO? O;EZ^Sz2-]*Xķ䁖 ];2q.`ME@0^C*ZժFjj,0+Ăta{1 n|M ׬ChQzxo!=tϰ%:iyc]](䆀FVL(+ *㰩N}Q's @{=I|^By^ISE0&lЏe:59GgEWl6 !&=Į t )k|q;%a(nnYu|",{kfڮA/:b"kM|$i`lm&0wP-lEb#f RgGaU '9<塠mQwġP(]򍐒٘+Lʔבꬮ`a@Bl M N @CeDppϼhzp JxN|?ھrH'Jy"%&>Fu/h9nnMs` juY0{b "q]82W9¨^_EpP$3%1 On0ΠrC=aulX$hZd&{/3s`!I5Ho <顝4b^ZfQ woe%2#anfQ.6q1J 2Z U ^Kԍ+YEh0xİdӰ()Rzk;6(W}ȠFcy$L W̥eq_{L>Aq)}ETLv; zw*o">mudf B$d&զ!͎}.ۈl;'=>xN5UjvZ(+b/opN`q)hM>R; RKvʢ bS ԝU{HuS&5 1vƴS Q#do{J X'f l.H0cdRَ#^\sW~}1Cuia4%W7j?Qt0~|vN9; 麍VAޱAȯ}&~41p?=030U.`0? *&cz6I&řwZ͓G=wAB4y&w7vЙnHH4Pb!"O8qW632f.6sb?pS0 Cg O83LlxlQf}X`3*+?C:9qUBSswux0[QmgHK@r{GR\+͜XV;MTֺz6v$`k]K"94lIpr"9oQ- 0 Nmlg .Z4<;R?kc ֱvb8rlmuQ*\xyKDXq 4͏jQzd űȕ@tT g Kt;]ƿy*vJ< Mנ'jл܊@ag4YPhYx<&wal yNQ!eiMy51z~xY +ˎwQ:4>y(PFoʿ2!c~;c_$AX)AȻTa"Xb*d݋"oW@P.%PKR1Q>'kzCh§{?b)ze!h3nbd>p} <uy*i$ a zu=OW2- p2уު+Π;U,@4HÆ )bP~8ǮГ+`F0o0Gg^ r0Cݔj&iH)L!| u&qxJ 9H)DIZC\ϋf[%9eV]%tDl{݆>p> sCHu! WQ7c 9BS;t\>,T=X[D;0§eGuI{" =R?\)M: BZk&E D:myҤYEeaƆ]2t^;;5hĝL8:jbl;s&`H4o*–yP+zmꧠNw'7t=wz#'锭5.Tz(`˻S$GK٢t:,`|(v>73ux\ׇsp6c3'o|G^'c'pOG:q(-pz~wJ$Q' 9'-腀DNwpr5ƑQN0DH \睤5)5g\M+e:~'X4abs^[a* G!qȠɊ×~B僬}$Q~ԩ빗u˱أx%ʭj;8)E`eMFOOSQ=``D4ʀVҘOh [&Ѧ`_Lr [wx `)MHR!(Gib6=CDO( f c>Fq1'ZbM7PDt1k18ܭS"q6 ~E~\WHxX_7jPׇ˝usWfTlK4Asul+Nf8!YZyl 2܋`a MoBGyZm\ˊ#ƴKgĤwzlL;᭯Ecp~aHTes&;Ӕu \7*k[*5rjX iJ@70Ob!n-M >ēAZ˲Giܒb6$2τ8M;} 1k0ݪmcl zW%wR@me%RH`T!oeO #YVbfգ4W'p: b|g`25BOޔZ ٥QBf2_=hIsv ]ͫ{IeC7Y&1Bwot*M5WdQ ƯkxR;YL_bz"1dYwN>3Zn@ 9|[7,̡X@HdJag.|<ޜERt8TfNۀCO=![nQd>)56WQm^Nl/$2XSWlиD"t,# +c* ޽BIAm8 gsGrL+n h^ڡ^kwrcr0@plG|HsHk:Ho8W pB;ۦKzeL$5nf^`GY$IKz4ԕdS; ?c)60l_BGEyӑ0 =F[V{de*XMNi?Пɗ &*ώbJBď;n9P Rv0 ]4# $#0qiC4QTҝq~B'|'Ϋ΀;i<β%u (=:/8?w6?{= nzO#g3׺*&Uum=k&?2@d qnL[r#UL)Txe5$[U +dl;'@~B ;OLy$<]hnStwz#l:g4#jhx 62 19ۑzl,MO: 6"XPYwԖ>&X9rsAv굸)#]#b5k.ZT)sip<)98aHf "C݉0.8Ov|p+ ,LglyGtkkWAwyȍ8lSPST/XL>22ME/$ ;kYY0Bs<|Yg4=)'NrBgcy+CkqZjNhDfi 6&yB~t ءq1"d ܖdHi"@,w\? eL>ۻ鏉#6&cDh1wۄNq];8'*DM }gaL,B[7]]0 byau${:5( ,-pMʹeEG%zfsU7Ut4|VKs7ӫ:r=}Wfw a-@D 5T;@ۜ-Qq1 U5w[ge('ȧFO>U G4'(_@q_=2t!FzHDŽMͲSQVMn*{CpEۿmX: ;e{Nz펻6砫=pp 5#'vy.bi'>ڵzh猥8Mʠo}N9.h$$x@v)0{eΙ&7.9& ?Ϥ4=`%#~=(V^U)*=/NEo XI h6V?J9ĬŇ&+434_T+ܝ:jd%Uu"F6NUs7DbǞ xX̌Gx +/S!|,lDZ[Mc Zm sĖ|A>8=14 hośV5FY%5lmX{Td/IѮn(.=s ;tL3o%yRlFL9F 9EU0͇^Qi:blME0¨=VJ9\`ڌ=z?;|,#$=7iYl/XpD0mk9 " b/ +{. z555R+V:d󆛯 &14Metf*`7Ȱf ႛvt4S taJU&rD6PXQ8`ʦ7$MD)68@q?O 2w,gC8# S׫?hpl;Ti'H@ٔPf{=&QY>#rUmjҐf¼@w4ۼTy7r$VIN띿xأ cB%sus PQ-UYZ3^1#,< ;}%>Q$\V.+ ̎)ԷYQZ̀w8úH.< xt=kf@,+ݰzvp0w[ NCG\:hٺ0|QDi3a &Q2sL>`}|EG1Ǔ(K x1t14 4݆z.*$sB;;-@\cWEyQ Ovd3>:|yzl$W7n^4*<]񝸦Yg$5w[gG1 P{{M_,eD>b'Х1WdB*Lwui+F?\*Aɉ \ V'ڝ(uJ,A\#H'[k[a^J [9 Z{@oÁ\tUӃX_?]kD}VH,$Si2v$>2OFf#3t@,?n٘$5X1x][:ΨtJ!]-E'q= Ƞ2h4\II :˜)y8x͘Tig#5]IF$DSR. v &:a EIv:7iH%( ! .ԁ4hN2[aH;RȽ:iλ1!cP.$*E(̺t][jL $ c ҷiAN>NHP6dAK6KC%%}%=]R Riw}i:QcKSCp$[~8d8 OGgeA/ e^nqgE^2 J-n\j'Dw ? W0e$Kl\&|^!naphV10C&/ޒcdQuӇap.$j '=uov˂鞦|{#*0 6wbP@ւ:P S0}=gEH*ueGg P3l:"vǭ]k}MvSoAb^--7_i[jm7m>/93>] I` Ĭ>sP$ FVĈ! C' q L}Dg!RKUCkЎy7F3rZ1#_G$# xp?ps|tkάKuC?x2w4,#l uW KHCX"zd=pUȊ81a|BI8CC% k~:3fshϺ΅ul8C xScl\%1EVxCӻz<>9k- xPrOjdaI^Hq9cڠbbL0tʡbn#XXԂCuH?Sr)\p.ݽ4{۝ A\d :ޏNSjq xq@tX򌊚7[({x&b'vײoX 6{h"ފyLq ]|uzax`#Fp*){|l{$X.U{9AN}aVt$ɡC ^Op}I{nU ?c4o8}R"FvY씢ޯRKYq9)-Wp? L,Q3 H`8@~40cĠ8 k291Fmw1n Ȼc2; &BKCE2IgY QXj6<yt 4I;Iǻ 5yzБ{{s/ lcFscqip;u26wy Bv`CI"%N4{CQ45F ^c[;kxѱ*1v *-v!Kl^˼]WQ}{l+J&UOU1;pF{S, $at٧a8N}Ap{pl6WލMo6kiH \@) 'y͘HV]湳m6mۮm(t`]P:3nf]Тh?4ɮYK;t@ڬ-Xy31/{L ~GӇ HQy2$I߉FAV'Bر1o:c}+&fEw;ӥB&u[ TDfh|w0+` *phԬ6H ܸtBv _va.hyE\M\7b]o0`?tϻ#>gk7P.^^IrT(1.P@G(21@6U'@jc[`0칚qU[\gpYl]nwȴ)(YSW,m-3]YPEy`i4¤rSi\dfQG2މ̾eP rW{0vR' S#c\|7ĄG0 8~ Ȼ;3tSlLL0,y&Dn221m_89D?'(|:̧vjr%y2!\̐#GA懭|Ƃa=8ʈi<1TA݅I;UTZlض 6uJ.ل?@SDfVHPDCZ8". ?pɨ˿t&IE&vUMQ1`4! 04s}WM1 ԋw"Be:(HfyF9J. m\$M&=ӯ'S Z?$huu!\]صtWĆtin} `{쀸-}JH]i0Ыp0E5RtF{jk 侽sv3C,XZw+^I 9Wz 'ޮIGp{*)̷K -pQaP3b#2kSL=\Q%x2 #kX;8:tΦ}1}Ȯ-؍FRGoijqe6WI8 D+NOi0.CvPQWlx| 8c̶bWkG+]oc.lEџ{ !Þns<!/l闀wQžI1uCElrȊl,UbLT|t36 J&P߯OH|J0ygg Sj 4u 2`a\!em F1dgݜܻ;,/%f4%^4!kە(F+amw2<c:s=.4FH*2h3ƥs"g:ztmvW)ezҹyYD(ڠ͓nI\sT1bx?c[qr>$Zbt"Sݔo.ΣI2ϭ?eY_H( =<}Nާl$m"L-zB-Sq<taNc{EMQx)êʢfCh>ۀۉf$Mr\7ZzEI5H"ql>?:-Hp\tFwłq5${B/)<3A&LK6b!0qgQƷ{ԃǵtGSp1 `xb8lV`pFH"MR4R UyG"pXAG BiǎXGC;@'~MwK\ܓQ1Pki뙥*D5yӔ`M)V 9ƕÍ]AdRoFي. +ԁ\Rf9B WzM7 I8uÑ S2zK0U’"2{iU~9W;#Cv}3r|4{ >vh3ǿ=f `3D c/G|r9X -6hf+؁ a|")jXǚFb = :K_ J5b!3dhBLΝ)&iWP-b⤘L UX MN.V4ÌqOGds1O6v-3] jR|X J;'ƁgF+^94(D7lt}SF, >wg]Hv.&?M OlC?Cu\ 1Y?u":eis@[4c7:`:N)_{Y{rsiv 93; qcwX;]4Uq՚ *3 )1l8k瑣$H&Nz1;(~JG~1=(:82ӂka Oכ"pTL8s<n%Ykدo8!éf/c>$ Y ^LD.\Kݧ2 @:3jcž%`ւ67\#)[#-޳'Xs=?NzsjYt:w6ZQ[K,uJPdu|-Iz٢߆oWzg] J`[ܜ'pq㻤$ACV5qM 2?2ߡRtPmVi"sFMFRʑG 8{?mv@(w%@ tgeR 5..BP>ɩ=_` ^&V=Y{dp9qHH ܀A0LH{ӄp;Yɝ.1izkQڅ>:I*ݪ{TTj"`fhVxh0&[9Z)mqwckgrpao2m{ґ'+d("sʀmcAxE\ˏN'˟=tqEćfs0g]Mv~af hʚ1FNwd =],Z6RQ = 얪D/]TK,3OSS:syDx76pyU|+cF%C FRnWd%+<;SZW,QX.R7+hߧݭXv|[%k:qE10JӼQCOlcu7>gxz^юˠӫќw{Cc(:Ū|F;2Ⱦs@גoM$ˆ鴚  uM64:g>3Cq8ޠJbO>l'-] ❟La=|kg]J (>(W}N8o`jZdN,Lis I qyc9R5l?RמHѱ4$PTO$opͫ/zEä(>>indq)Ƣ'%OtR54ø;];(nrvg&E"NwSgz9tizE~Ì"n"f9ϼM5 m ʀv<{;&{iɩ wᘡ1C{6Wb*L* x>vAUv'#6Qw٪Sm;169fT=KUuwY\;@Avg,J-]ul'꧵[o“Bҽc <ȸa529|EѨY$=\ƬJo:NKp1C, 1> FnY~brw(: B$xBT@Y$ tjZݞ:I6ݺIn+Y-@So<9M!y!r U?O$ڠҏ1۞  RGw  52 IidN ^J(&qqq !yJ*ʦn2M#|f(?"^f"tSR춥LQQE]+IG+{_GQM$xFz^"3t%G;MvWO;AD@%Vtlw+MKNe%r#K0d^TSvu}&=ʹfFH$o陗j/OggJi (]6;j@"s>}d! ueJ)Jt>\=b_,-bkTy'tRBG'=wܱ-dKEA9`B4IrNL~|p*9IЕ>bsnS¼U ض8Q疺K-a< ?=py<^GㄊP6P JLA^I3p669J'O.E okrRa(>;T:7#HFp-RsW"#P'(tFA}X3nO$i1:oՌs@3'Z]"s]B?2ip -!.1&z֧/ -E[,"y4b9H<*l(OwS>szO~<@랒Oa} (!D\"lםFҤaþ*<.?RL6 N4u'oBe0!s/E!$L_ϓ6Z%vMA%hHPq,X2mF'1%206 my&Nf?_yzI!UbEl7txRi"OC xs's"y8QNSFI>h R CvAveeY|0r%mxƜySEgxrEWAi~Kd 'nJ%?@$Nqα.{P>Bl][$>ao\. wRhjrsk< `D7q&1[4>KlEayt|.35CYK~ʞ_إB\UP\9DrwÜD C'7qJj&4B)NqA=%'r0" =ovq'{z0, =GGE+9D;* lAC tBR]#*ҿm.n9`pQnBtO~edxhTVY1ϟףo@![ , F{K'(`˰˜VpS'OpEnfsEScS5{pJ L'iH [_\%1ȄUF6n˵Y$ce:%}/(&Irw#x{هB1Uz5(T F>#Ɗ>LBus A fq EzƁ2{q\#ܙ>0s#^\sW~}ѭhm~钧X"Ů4tw.yOyI'm{vJ%?uƵz@TL'Yƍ#{8̣Mc7ը:{vfF+Yx6x2 %9'ֈVyxk#ys#0٠8Y9v0Xk&Aݑc pKhgm\\A-p0Ǜ"yY Nj݆Y:wNgW$TO^tj ψxxXfAaeX IV]i4G8þCC`x.GX |/!B3kY܋2GPT]Bb޷=AE6]3Nod%_H9t8DN!ʠlİǵnL>AAEh1|Pw(Aqg3]&[MiRDـF]cT|tq=$(l+ Na項8A\T lNB)ӊ%8'5([q>5$ B:}H :/pՃM#g%[X_0􏁣9A fW8ChH RWtmdW/Ǝ ¦ Q1>O6-\\+t*"FG3F@^i"n$剩?d&u2;Ni\՘ 'wL~Xgh>+j&vJDfLwjU^3|ßV5gm_&+4ZmX%-<HWa; py2*:wI,Cȁ&[26;ԇۏ3䮂4&[46g`FH*r?zRuMvn"af{bMG0P9,[@q2x7/v' L]"Jx\/yPG!cVY]P/)iH+a {7ehG}D<+m3~<.\3ODD*sbXegYLWEs^="k󅜱S |XzMC^ !e2_(&Jב  ~ijQIg'z6ظYγXWmzaP&&a6$h&S25}k66'Z N=OrZLPQ'98/=ˌ;Fa;goxob8X"F蒑q@N,h,ЛqAOt|ٺ-"p(3JmeRCYw|eh%t\ K6%Jf678IoD:ﮚq4KρpJx#w%P7 innf|6EsF 'tLp*0lItw6d.>Bȷn çGzl*I5eC`/[ʪ$KČ)J!- qU",z$u# 6ZgAa6HٻTc J$5-P>, EaB'ޯs7T0d%Q>P>Ye?ԸD3쉃}vNDh0p@oBu}͓fHR#R>ZUt]BO7F"Q(ZmC)㥘ę$^Irb 1_ɧ1/R9z"KR.$pߕ} L,658:O v't|Bއ&f $Myyd=a f^N_i鋧557u&07$&1CbTUtRUҺwFf6NҼsr9hr:bDNt Fvbh.ӥH.)۵iE|\*μ}WC2ZI>əVaTӬ$K6Z䧰(3}S88pע)}DN8# arX -&& ޔjxH⌂RS4L9wҲ:78qi4M)GcL.Ƭ$^`ܶ]8;2:|ãW >avi"8t5N T\>vL dG|&c<$,dkeʺw+cw+g`Hv" =#-Q6Awk#8cbPd,41``^N_9YsX3= BI F;Ai6h`2jLqڨF)vG-L);[sZ*Eׇp'l]\t=JHG.L gu($o?}`;wR)gcyI&AM4adBI8 +҄=^k7-sh();Cs۪Y Rn*Wnv}F#\>'MV.j vg!*jX=6F  H%px߃\"sGi zQ.!ty|t"^i~ w l׆0CBBUx|٠C , ԡ]@qT2 #c['CS^ȶTik/^g(ڃ*tw%A )Qp˩=Bձe@(mZc8$1"F$qyޥin#,'i-ˇjނ H9LLކ8:uC&30{|o&4qfo4 TY5ԙ7neҍGGMe$U:_;6aZP4"zNjTsDa=M#dgUL qq;}{E'Q(Ա=$~OLInѡ~.y}/# X\wTL nHI ֠>bJQ9ɁVCQz 4@.,;nCcdd)<8pZ rgf3c!Y慳gAZE,7:l.pdO hY&ٝ5LS`Q,dD@NtuQWSLm68Cz[bgxW00V ]d:"V]]*&wyQ9KS6+ԩq-Ǿa]<!4 3tDu`dž} 5v/zd=qWsUsl}M ĤÈ)J 9}{ Љ2CY+uF=`C"ZKѧscXNE15/ \wy9i@N%`'W5 }>R֖ ,~լcPtwyJxn[Ćr"ؠ F03&<\gRQp ί+t<\ l0C o i*Bbq52f j$ ĝzs`clDGnϧ_GV˸3{tt'uHBҐ:W|u9{STq܉3BAfϨ Hx'+idĚb:8Mf:e>żl8Ehe Fg뷠^inUnlr~u|ܟ :'&lýރb@.5ŻUpQ^\0#MP 0ռA.J%H>0jQ@ F}䭆_ ;=5ZeT{sӔh# $yܔ|z+I|)<6Ί{v͇T8A(̓uUǩ?pKCΎ}Z&'~rYUO5>:lՌ1%f!g*ƒ}SJ1[a朓\%t=#xco0Hj%jXBw|QoBO'r@bQc2kFaE!/<2i/ѕ{&Tl. 6] ĪA3$?Y &c0; wS0`0 ZBwt 5@T~sLFC>2Z7'wۂKڂ(|,){Q Aw B4akZT&ԤNg \/kTzOU%U?g*Q\` +yC¶(Lp:$~MRP9L8vCT5ϑֆ;(lEe\huzSsb-sg&2ŮN'+n6Lbu0 rLj_yqxpx]缳JB[ H32y7FD<(10a8!ZE\,֌v͂:܍ A)&!/t* ƘrX#SCF*%xTbq [A/%qhx%;WŔVMOu5 фSӉi&YRK. txC\mVIțWYoj;n|:j<V=n6-G 属23jIÙ3#7W>zl0'=ZJD<-&elqjL9?:#MzA|s6MٸxKz[y"!PFq܈hy#?[<O1{lΚ `3vͭwebg,s mUkMnqX6Z[`KqnYTaٻa?E4aeպ5 8riܪ5˵V/]LBKgF{}1doL^UUtSկl6b1c EaRjOĽfe o;*C㎜_6B9aةLM)d uDqsc6Z)!eqj$+݄WxNۮKMv9RLmlAa[Fot|l3=% .mwC$Yؼ*VF\/F=brOܪ@m{vF;hO{sl#h~$:UiXTBצPC%⛷ҐBztPLΙ >+';ݻ>:j Hkþ+m:bEN P$$ !Z߉݈&ws:/wy6Zjz!6gH_]:gJ0BԝRd%juRh{'ٜ ]q- &q]H!`A\uOaGboAM UH]c`Bۛ~3yh4f%f%YI@W43v4"^1%4sLYo^Iˎf۸ۑ!f2gF6"3Auh~*>i绝ok]oV|47) >f]G*զowa{uWM٠)pٔ =ɫ.|lʢrA/\s.w o;On^ y$%BG^2u\ [DW2k` hÅɜs&7Ttc27t9ɇ\G~CGF6y-nOv5%0y!H/kOSϩ7RzΥy1g)3yLL֞<Ū;"E8Yi/TvGŅhK*j YJG%MlۚhP}`6[uOa/IUrսZP,Iїwzz݆k+lY'RPUNtS2T"+5=E-%xJSqӬVmj2 hK^n9&e.k'f,C@f:{M X~xN84؉fҿ %msogP/͆ s=,8IBt_`XRpkΆi<덺9VcTwÉ :]X_|< >CAN]&Oׄp4I[Xc~9C.[-P"cǃBo {1Q@|BD=ȰSOwa1OW%"}4 ZDy"ƕqom0 /^:DYǰ=k9 5EmxPj/t611M^{~1wjrFX{ɹqe7BH![$+ilWAf!qc*(iG8 ;ʩ=f5A aΝV?V*2ZKZ}߼vÛWo_ NMTm:\%bG,EjlŗC 4Im#_c2A lØ s0k?`t#!.ݸ`ڥ;\YҮYmvTo$q^*(YtkY/ Q[j"MSN" uczu SL]zﭡO~OMkkĉԤư:.&z*ܼyo>hXoo5Yݿ_~=Qb;aomU܃mqD8oN7r018Rj]x-Bah{*)>V{wPuLC(b{ Z8a׹p7R903@|$ؖT$Z}FP:rpзI_D6Coմ<#zFVJ0Tq5x=:z(ݖ"Oȥl.juSPp"~EܽV`OFצ׶=ѝ̾h%kg0M ==Zlv- Engineering Team – Capten https://capten.ai Thu, 15 May 2025 12:03:27 +0000 en-US hourly 1 https://wordpress.org/?v=7.0.3 https://capten.ai/wp-content/uploads/2024/11/Capten-Outline-Logo-Icon-100x100.png Engineering Team – Capten https://capten.ai 32 32 Understanding Software Supply Chain Security https://capten.ai/blog/understanding-software-supply-chain-security/ https://capten.ai/blog/understanding-software-supply-chain-security/#respond Thu, 08 May 2025 07:07:10 +0000 https://capten.ai/?p=25331

Nowadays, software companies tend to use/borrow a lot of code that has been created by third-parties. Usually, this code is taken from open-source codebases. In fact, around 85% of enterprise codebases contain code that has been taken from open-source. Each addition of this open-source code counts as a dependency for the enterprise codebase, and each dependency adds to the list of possible vulnerabilities in the software product. This is where software supply chain security comes into play. This is a concept not understood often by fledgling developers, so let’s get into what this is, and which best practices to follow.

The What & Why Of Software Supply Chain Security

The software supply chain is defined by anything and everything that takes part in your software product. This includes everything from binaries and open source libraries, to whoever wrote the code and known vulnerabilities. We must care about this security as developers because, as mentioned before, software products carry a lot of dependencies from open-source. If any of these dependencies have vulnerabilities, then those vulnerabilities extend into your software product as well. This also means that all parts of a supply chain can affect our software, and can contribute to vulnerabilities. This is extremely important, as keeping track of vulnerabilities in our supply chain can get tedious if your software product has hundreds of open source codebases to keep track of.

Software Supply Chain Attacks

An attack occurs on the software supply chain when malicious code is added to a component of software, and spread using that same software’s supply chain. These are very real attacks that can be executed very easily. The reasoning behind which is that anyone can contribute to open source code. If the code is not well maintained and/or the pull request is not reviewed thoroughly, malicious code can be inserted into the codebase. This will then be used by whatever software supply chains the open source codebase is part of. The now inserted malicious code can execute crypto mining, or create a backdoor for bots to access. Fewer than 10 attacks occur each year, and they are extremely targeted.

Did You Know? – Log4Shell Attack

Log4Shell is a vulnerability within a popular Java library used for logging errors in applications.
This vulnerability allows attackers to take control of a device (through the internet) if the device is using a certain version of this library. Since this library was ubiquitous, this in turn resulted in millions of attempted exploits. This snafu serves as a reminder to developers and enterprises to maintain our software supply chain security.

Current Software Supply Chain Security

Currently, major threats to software supply chains are still present within open source codebases. More specifically, vulnerabilities that have been identified within these codebases tend to persist without being fixed. Of course, it’s most likely that these vulnerabilities will eventually be fixed with patches. However, the identification of and patching these vulnerabilities tends to make software supply chain attacks easier to execute.

Software Supply Chain Best Practices

Keeping your software supply chain secure goes beyond just constantly applying patches. In order to fully embrace a secure software supply chain, you must adopt the DevSecOps philosophy. This idea aims to integrate security into every aspect of development and deployment for an app.
There are three key best practices:

  • Keep track of dependencies – this includes transitive dependencies (a dependency of a dependency). You must also keep track of vulnerabilities in your dependencies.
  • Manage your dependencies – if/when any new vulnerabilities are discovered, your dependencies need to be patched accordingly. During this step, you must keep track of updating your dependencies.
    Note: At times, when a dependency updates, it can add a transitive dependency to your supply chain.
  • Monitor your supply chain – inspect and enforce your dependencies often, to prevent a drift in your software supply chain.

Of course, these practices can be combined with other, more basic practices. You can try to use signed builds of dependencies, or verified components such that you maintain the integrity of your supply chain. An important word to note is “provenance”, which is knowing where a component came from, and verifying/trusting its source. There are also certain tools that can make following these practices easy. For example, identifying dependencies is easy on GitHub using the dependency graph . Other tools such as GitHub’s Dependabot can help you by notifying you of vulnerabilities within your codebase’s dependencies.

Conclusion

In conclusion, the software supply chain is composed of any and all parts that are required to make software. Meanwhile, software supply chain security deals with protecting our software from all of the vulnerabilities that our dependencies bring to the table. In order to keep software products up-to-date and secure, we must keep track and manage all of our dependencies.

]]>
https://capten.ai/blog/understanding-software-supply-chain-security/feed/ 0
Stable Diffusion for the Win https://capten.ai/blog/stable-diffusion-for-the-win/ https://capten.ai/blog/stable-diffusion-for-the-win/#respond Wed, 07 May 2025 09:15:28 +0000 https://capten.ai/?p=25277

Just within the past year, we have seen an explosion in the release of new machine learning models that utilize novel techniques and methods to achieve specific and/or general tasks. Models such as OpenAI’s ChatGPT have taken front-and-center stage, pushing other models to the back. Within these other, still reputable models, exists stable diffusion. Let’s take a shallow dive into what this model is, how it works, and why it’s so contested.

What Is Stable Diffusion?

Initially released on August 22, 2022 , stable diffusion is a deep learning product of Stability AI used to generate images from text input. A user simply has to give a prompt about the image to generate, and the diffusion model will generate the image over a series of steps. Each step will create a better version of the previous image. Sometimes, objects within the image might change, however, the images themselves will improve in quality. These ‘improvements in quality’ can be attributed to less noise in the images.

How It Works

Stable diffusion works on the principle of diffusion.

Diffusion: Literally means to ‘spread something widely’.

However, in our case, it means gradually adding random noise to data over a series of steps. For instance, imagine a process where noise is incrementally introduced step by step until the original content becomes completely unrecognizable and consists only of noise. 

Now, consider building a model that learns to reverse this process. Such a model would start with a noisy input and attempt to recover the original, clear version. This reverse process is essentially what models like stable diffusion are designed to do. If the model is trained effectively, it doesn’t even need to know the exact noise pattern that was used originally, it can start from completely random noise and still produce a coherent output. 

Let’s explore this intuitively. If the model’s goal is to work backward, then at each step it removes most of the noise from the input, keeps just a small portion (as if rewinding by one step), and reintegrates it to simulate the previous state. Iteratively, this results in the data becoming less noisy and progressively more structured, until a clear result is achieved. 

Benefits Of Stable Diffusion

Stable diffusion made waves when it came out, because of its sheer capability, and all of the benefits associated with it:

  • Open Source – the source code for this model is available online . This also means that the model is modifiable based on an individual’s use cases.
  • No fees – there is no cost/licensing fees associated with using this model.
  • Low compute resources – there are surprisingly low compute resources for running this model, given the task that it accomplishes.

Opposition – Why Do People Hate This Technology?

As there always is with a new technology, there exists opposition to the stable diffusion model. The outcry in the cases listed below might be a bit more justified, however. Since this model is used to generate pictures, obviously there will be some instances where people will use the ‘art’ generated by the model to gain money/fame:

Both instances have got artists fuming, as most people (especially the judges) could not tell if an image was generated or not. Of course, they have a right to be concerned about AI intervening in the art department.

Conclusion

Stable Diffusion makes it easy to create images from text, with no cost and low system requirements. But its use has raised broader concerns about trust, originality, and how AI-generated content is treated. As this technology grows, it’s important to think about how it should be used and where it fits in. 

]]>
https://capten.ai/blog/stable-diffusion-for-the-win/feed/ 0
Containers Containers Containers https://capten.ai/blog/containers-containers-containers/ https://capten.ai/blog/containers-containers-containers/#respond Tue, 06 May 2025 17:36:14 +0000 https://capten.ai/?p=25173

Containers seem to be a buzzword in the industry nowadays. As more and more companies are undergoing cloud transformation, they are hoping to convert their legacy applications into containerized ones. This is being done all while trying to adapt to a microservice architecture, in lieu of a monolith one. But what exactly is a container? What are its use cases, and what benefits does it provide? Let’s take a dive into these questions, to see why companies are so forward about adopting containerized applications.

What Are Containers?

Containers are a type of virtualization technology that allow you to run portable software on any machine. In essence, a container will have the code for a software product, and all of it dependencies and requirements, such that the code can run anywhere the container can. This means that containerized code can run on numerous different machines, even after it has only been developed using a specific environment.

What Is A Container image?

A container image is the package of the application code, its dependencies, and more. Images are usually executable, and they contain everything required to run the application code. This will include stuff like system tools, libraries, settings, and even the runtime. The instructions for how to build the image are usually within a document called the DOCKERFILE. We can modify/create a custom dockerfile, in order to install our own dependencies into the container image.

What Is The Difference Between An Image And A Container?

An image is the first step in the process of creating a container. During runtime, container images become containers.

What Problem Do Containers Solve?

Normally, when you want to run your application on a server (i.e. a host), you would have to customize the entire server to be able to serve the needs of your software product. This can get very expensive very quickly.
In order to take this a step further, we can create multiple virtual machines (VMs) on a single server. This VM is a type of virtualization technology that allows you to run one operating system on another (linux on windows, for example). This way, we don’t need to customize each server, but rather each VM to accommodate the software product. This allows for less overhead. Each VM, however, still creates their own operating systems.
What if your software doesn’t need access to the entire OS, and the OS just serves as extra baggage?
Containers aim to solve this by taking the VM concept to the next step. Suppose we take VMs and reduce their overhead. This can be accomplished by each container sharing the host’s operating system. This way, we can get rid of the extra baggage. If a container needs some extra components for your software to run, then we can install those within the container itself, instead of on the host. Their small size also allows you to put numerous containers on a single host.

Containers VS Virtual Machines

Containers can accomplish the same things as VMs, all while:

  • Reducing overhead
  • Reducing disk size taken
  • Reducing startup time (seconds instead of minutes)
  • And Using fewer resources

Container Orchestration kubernetes

Since each host can hold multiple containers, and organizations usually have numerous hosts, we need a way to manage containers, a process known as orchestration. For example, if you need to allocate 5 containers, then you would allocate 5 hosts to hold each container. This is to ensure that if a host fails, your containers are safe, and your software product does not experience downtime. In this case, it is best to use a container orchestration tool.
Container orchestration can be used to build, manage, and automate the management of containers and container infrastructure.
This is where tools like Kubernetes comes into play. These tools help you automate containers to the point of being able to offer zero-downtime deployment (apps can run even while being upgraded). Self-healing apps, and automated scaling of resources. Sometimes, cloud providers include container management tools out of the box, such as Google Cloud offering the Google Kube engine. Docker swarm (offered by Docker), Nomad, and Marathon are both good alternatives to Kubernetes.

Benefits Of Containers

Building upon the benefits that containers offer over VMs. They also offer:

  • Software portability – your app can run on any machine.
  • Isolation – software can be split into separate, individual pieces.
  • Scaling – can increase/decrease resources as needed, saving money.
  • Automation – saves time and money for your organization.

Limitations Of Containers

Containers do have their own limitations, even when compared to VMs. These are:

  • Less flexibility than VMs – currently, you can create a Windows VM on a Linux machine. However, you cannot create a Windows container on a Linux machine, yet.
  • Orchestration challenges – due to their large numbers, managing containers can be a hassle. This is made easier through container management software such as Kubernetes.

So, What Is Docker? 

Docker is, officially, a container runtime. It is a piece of software that allows you to build/create/run containers and their images.

There are other options besides Docker, rkt (project has ended), and containerd.

Use Cases Of Containers

  • Microservices – normally, apps are written using a monolithic architecture, where each component of the app is typically part of one service (think of this as one process doing everything the app needs to have done).
    If that same app is written using a microservice architecture, each component of the app is written using a separate service (with its own endpoint). This allows for each service to be built/modified independently of other services.
    We can then deploy these microservices each within its own container to allow for easy and efficient scaling, whichever service is being used more often, will be scaled up.
  • Cloud transformation – this is the process of bringing an organization’s existing IT infrastructure and codebase to the cloud. This can be accomplished easily with containers.
  • Automated scaling – container orchestration tools can allow for increased stability and decreased running costs of the app you have deployed. This is achieved through automated scaling. More copies of the app are created when load is high, and less are created when load is low.
  • Continuous deployment – containers allow for the easy deployment of new code automatically and frequently. This increases the stability of your app through easy/quick bug fixes.
  • Self-healing apps – these can detect when problems have occurred, and take steps necessary to rectify. For example, suppose a container has run into a problem, the server can be set to automatically reboot. OR, since containers start up so quickly, you can just replace the bad container with a brand new, working container on a completely separate host. This will reduce the startup time immensely compared to the first method. It will also allow that host to be restarted, all while still maintaining access to the container (now on the new host).
  • Developer visibility – there typically are problems between development and production teams. These are due to an app working in development, but not while in production. With containers, however, the development team can use the same container image the production team is using. This completely gets rid of this issue.

Containers In/Versus The Cloud?

A lot of times, there seems to be confusion about the relationship between the Cloud and containers. Let’s provide some disambiguation:
Cloud providers allow their customers access to their servers. These same servers can be thought of as hosts for containers to run on. Therefore, you can run your containers in the cloud. Cloud provider-offered containers are super cheap, include extra tools for orchestration, and have good support right out of the box.

Conclusion

In this blog, we learned about what containers are, their benefits, how they are built, and their use cases. Using the basic concepts you have learned here, you can try to containerize your first app using Docker’s tutorial, as the next step in your journey with containers.

]]>
https://capten.ai/blog/containers-containers-containers/feed/ 0
K8s Hello World https://capten.ai/blog/k8s-hello-world/ https://capten.ai/blog/k8s-hello-world/#respond Tue, 06 May 2025 16:59:19 +0000 https://capten.ai/?p=25166

Imagine embarking on a journey across uncharted seas—the world of software deployment. Navigating these waters is akin to steering a ship, facing challenges that range from smooth sailing to avoiding treacherous downtime, all while adapting to ever-changing conditions.

Just as a seasoned captain guides a ship through unpredictable waters, Kubernetes emerges as the expert navigator of the digital realm. Think of it as an automated navigation system, expertly guiding your application through the complexities, ensuring a steady course regardless of the turbulence.

Kubernetes, often referred to as K8s, is more than just a tool—it’s an open-source container orchestration platform. It simplifies the intricate tasks of deploying, scaling, and managing applications packaged in containers. Containers are like compact, self-sufficient units containing everything an application needs, promoting consistency across diverse environments.

Through this blog, we’ll uncover Kubernetes’ key role in orchestrating deployments. We’ll explore from setting the stage (environment) to crafting your app’s performance (building container image) and culminating in the main event (deploying your app).

Prerequisites

Before embarking on the process of deploying your first application on Kubernetes, make sure you have the following tools and accounts ready:

  1. Docker: Install Docker to create container images for your application. Refer to the official Docker documentation for installation instructions.
  2. Image Registry Account: Sign up for an account on GitHub , DockerHub , or any other container image registry. You’ll use this account to store and manage your container images.

With these tools and accounts in place, you’re equipped to begin your journey into Kubernetes deployment. Let’s begin!

Prepare the application

Clone the Repository

In this guide, we’re using hello-Kubernetes simple web-based application written in Go. You can find the source code here .

git clone https://github.com/pratikjagrut/hello-kubernetes.git
cd hello-kubernetes

Understanding the Code

package main

import (
 "fmt"
 "log"
 "net/http"
 "os"
)

func handler(w http.ResponseWriter, r *http.Request) {
 log.Printf("Received request from %s", r.RemoteAddr)
 fmt.Fprintf(w, "Hello, Kubernetes!")
}

func main() {
 port := os.Getenv("PORT")
 if port == "" {
  port = "8080"
 }

 http.HandleFunc("/", handler)

 go func() {
  log.Printf("Server listening on port %s...", port)
  err := http.ListenAndServe(":"+port, nil)
  if err != nil {
   log.Fatal("Failed to start the server")
  }
 }()

 log.Printf("Click on http://localhost:%s", port)

 done := make(chan bool)
 <-done
}

In this code snippet, the main function sets up an HTTP server to handle requests. The handler function responds to requests with a “Hello, Kubernetes!” message and logs request details. By launching the server in a separate goroutine, the program continues executing, allowing you to interact with the server via http://localhost:8080. A channel is used to keep the main function from exiting immediately. Understanding this code gives you insight into how the application handles requests and concurrently manages server operations.

Understanding the Dockerfile

The repository also includes a Dockerfile that employs a multi-stage build process to craft a streamlined container image for a GoLang application.

FROM cgr.dev/chainguard/go:latest as builder

# Set the working directory inside the container
WORKDIR /app

COPY . .

# Download dependencies
RUN go mod download

# Build the Go application
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

# Create a minimal final image
FROM scratch

# Copy the compiled application binary from the builder image
COPY --from=builder /app/main /app/main

# Expose port 8080 to the outside world
EXPOSE 8080

# Command to run the executable
CMD ["/app/main"]

Let’s deconstruct each segment of the Dockerfile to grasp its purpose:

FROM cgr.dev/chainguard/go:latest as builder

In this line, we’re leveraging the Go images provided by Chainguard, based on Wolfi. These images are tailored for constructing Go workloads and follow a “Distroless” approach. Distroless images encapsulate your application and its runtime dependencies exclusively, omitting package managers and extraneous components found in typical Linux distributions. This practice, endorsed by tech giants like Google, refines the signal-to-noise ratio of security scanners and streamlines establishing provenance.

Distroless images exhibit remarkable compactness. The smallest one, gcr.io/distroless/static-debian11 , weighs in at around 2 MiB—roughly half the size of Alpine (~5 MiB) and less than 2% of the heft of Debian (124 MiB). Chainguard offers both a minimal runtime image for executing Go workloads and a development image that encompasses a shell and standard Go build tooling.

# Set the working directory inside the container
WORKDIR /app
COPY . .

In this portion, we establish the working directory as /app within the container. Subsequently, the COPY . . command duplicates all files from the host directory (where the Dockerfile resides) into the /app directory of the container.

# Download dependencies
RUN go mod download

# Build the Go application
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

This sequence initiates by fetching the Go module dependencies outlined in the go.mod file. Following that, it proceeds to build the Go application, meticulously configuring the compilation process.

# Create a minimal final image
FROM scratch

# Copy the compiled application binary from the builder image
COPY --from=builder /app/main /app/main

This section introduces a fresh base image called scratch. This image serves as a blank canvas upon which to construct. Scratch finds its utility in crafting base images (like debian and busybox) or extremely minimal images (housing only one binary and its prerequisites, such as “hello-world”).

Subsequently, the COPY directive transports the compiled application binary (main) from the builder stage (the part marked by FROM cgr.dev/chainguard/go:latest as builder) to the /app directory in the ultimate image.

# Expose port 8080 to the outside world
EXPOSE 8080

The EXPOSE command signifies that the container’s enclosed application listens on port 8080. Yet, it doesn’t publish this port to the host—it necessitates specification during container execution.

# Command to run the executable
CMD ["/app/main"]

The final line defines the default command executed when the container commences. It launches the main executable—the Go application built earlier.

Building the Container Image

  1. Open the terminal and navigate to the repository directory.
  2. Build the container image using the following command:
docker build -t ghcr.io/pratikjagrut/hello-kubernetes .

This command builds the container image using the Dockerfile from current directory. The -t flag specifies the image name.

Running the Container

  1. Once the image is built, run a Docker container from the image:
➜ docker run -p 8080:8080 ghcr.io/pratikjagrut/hello-kubernetes
2023/08/08 13:25:24 Click on the link http://localhost:8080
2023/08/08 13:25:24 Server listening on port 8080...

2. This command maps port 8080 of your host machine to port 8080 in the container.

3. Open a web browser and navigate to http://localhost:8080 . You should see the Hello, Kubernetes! message.

Pushing the Docker Container Registry

For our blog, we’ve opted for the GitHub container registry. However, feel free to select a registry that aligns with your preferences.

  1. Log in to Docker using the GitHub Container Registry:
docker login ghcr.io

2. When you run the command, it will ask for your username and password. Enter these credentials to log into your container registry.

3. Push the tagged image to the GitHub Container Registry:

docker push ghcr.io/pratikjagrut/hello-kubernetes

4. Verify that the image is in your GitHub Container Registry by visiting the Packages section of your GitHub repository.

With our application now prepared and containerized, the subsequent phase involves provisioning a Kubernetes cluster and orchestrating the deployment of this containerized application onto it.

Setup Kubernetes cluster

In this section, we’ll walk you through setting up a Kubernetes cluster to begin your deployment journey. We’ll use KIND (Kubernetes in Docker) as our chosen tool, which provides an easy way to create a local Kubernetes cluster. However, we’ll also mention alternative options for local and cloud-based clusters, ensuring you find the setup that suits you best.

Installing KIND and Kubectl

Before we dive into setting up the Kubernetes cluster, you’ll need to install both KIND and kubectl on your machine.

  • KIND (Kubernetes in Docker): KIND allows you to run Kubernetes clusters as Docker containers, making it perfect for local development. Follow the official KIND installation guide to install it on your system.
  • kubectl: This command-line tool is essential for interacting with your Kubernetes cluster. Follow the Kubernetes documentation to install kubectl on your machine.

Creating Your KIND Cluster

Once KIND and Kubectl are set up, let’s create your local Kubernetes cluster:

  1. Open your terminal.
  2. Run the following command to create a basic KIND cluster:
kind create cluster

3. Check if the cluster is properly up and running using kubectl get ns

It should get all the namespaces present in the cluster.

➜ kubectl get ns
NAME                 STATUS   AGE
default              Active   3m13s
kube-node-lease      Active   3m14s
kube-public          Active   3m14s
kube-system          Active   3m14s
local-path-storage   Active   3m9s
Alternative Setup Options
  • Minikube: If you prefer another local option, Minikube provides a hassle-free way to run a single-node Kubernetes cluster on your local machine.
  • Docker Desktop: For macOS and Windows users, Docker Desktop offers a simple way to set up a Kubernetes cluster.
  • Rancher DesktopRancher Desktop is another choice for a local development cluster that integrates with Kubernetes, Docker, and other tools.
  • Cloud Clusters: If you’d instead work in a cloud environment, consider platforms like Google Kubernetes Engine (GKE) or Amazon EKS for managed Kubernetes clusters.

With your Kubernetes cluster up and running, you’re ready to sail ahead with deploying your first application.

Deploy application on Kubernetes

Now, we’ll deploy our application onto the Kubernetes cluster.

Create a Kubernetes Deployment

Deployment in Kubernetes serves as a manager for your application’s components, known as Pods. Think of it like a supervisor ensuring that the right number of Pods are running and matching your desired configuration.

In more technical terms, a Deployment lets you define how many Pods you want and how they should be set up. If a Pod fails or needs an update, the Deployment Controller steps in to replace it. This ensures that your application remains available and runs smoothly.

To put it simply, a Deployment takes care of keeping our application consistent and reliable, even when Pods face issues. It’s a fundamental tool for maintaining the health of your application in a Kubernetes cluster.

Here’s how we can create a Deployment for our application:

Create a YAML file named hello-k8s-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-k8s-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello-k8s
  template:
    metadata:
      labels:
        app: hello-k8s
    spec:
      containers:
        - name: hello-k8s-container
          image: ghcr.io/pratikjagrut/hello-kubernetes
          ports:
            - containerPort: 8080

This YAML defines a Deployment named hello-k8s-deployment that runs two replicas of our application.

Apply the Deployment to your Kubernetes cluster:

kubectl apply -f hello-k8s-deployment.yaml

Now, if you’re using a GitHub registry just like me then you’ll see an error(ImagePullBackOff or ErrImagePull) in deploying your application. By default the images on the GitHub container registry are private.

When you describe the pods you’ll see warning messages in the events section such as Failed to pull image "ghcr.io/pratikjagrut/hello-kubernetes ".

➜ kubectl describe pods hello-k8s-deployment-54889c9777-549rn
...
Events:
  Type     Reason     Age                  From               Message
  ----     ------     ----                 ----               -------
  Normal   Scheduled  2m40s                default-scheduler  Successfully assigned default/hello-k8s-deployment-54889c9777-549rn to kind-control-plane
  Normal   Pulling    75s (x4 over 2m39s)  kubelet            Pulling image "ghcr.io/pratikjagrut/hello-kubernetes"
  Warning  Failed     74s (x4 over 2m39s)  kubelet            Failed to pull image "ghcr.io/pratikjagrut/hello-kubernetes": rpc error: code = Unknown desc = failed to pull and unpack image "ghcr.io/pratikjagrut/hello-kubernetes:latest": failed to resolve reference "ghcr.io/pratikjagrut/hello-kubernetes:latest": failed to authorize: failed to fetch anonymous token: unexpected status: 401 Unauthorized
  Warning  Failed     74s (x4 over 2m39s)  kubelet            Error: ErrImagePull
  Warning  Failed     50s (x6 over 2m39s)  kubelet            Error: ImagePullBackOff
  Normal   BackOff    36s (x7 over 2m39s)  kubelet            Back-off pulling image "ghcr.io/pratikjagrut/hello-kubernetes"

This happened because Kubernetes is trying to pull the private image and it does not have permission to do so.

When a container image is hosted in a private registry, we need to provide Kubernetes with credentials to pull the image. Create an Image Pull Secret to store these credentials:

Create a Docker registry secret:

kubectl create secret docker-registry my-registry-secret \
  --docker-username=<your-username> \
  --docker-password=<your-password> \
  --docker-server=<your-registry-server>

Attach the secret to your Deployment:

spec:
  template:
    spec:
      imagePullSecrets:
        - name: my-registry-secret

Apply the changes to the Deployment:

kubectl apply -f hello-k8s-deployment.yaml

After applying the updated deployment you can see that all the pods are running.

➜ kubectl get pods
NAME                                    READY   STATUS    RESTARTS   AGE
hello-k8s-deployment-669788ccd6-4dbb6   1/1     Running   0          22s
hello-k8s-deployment-669788ccd6-k5gfg   1/1     Running   0          37s

Access Your Application

With the Deployment in place, we can access our application externally. Since we’re using KIND, we can use port-forwarding to access the application:

Find the name of one of the deployed Pods:

kubectl get pods -l app=hello-k8s

Forward local port 8080 to the Pod:

kubectl port-forward <pod-name> 8080:8080

Now, if you open a web browser and navigate to http://localhost:8080 or use curl http://localhost:8080 you should see “Hello, Kubernetes!” displayed, indicating your application is running successfully.

➜ curl http://localhost:8080
Hello, Kubernetes!%

NOTE: Port forwarding isn’t the optimal method for accessing applications within a production cluster. In such scenarios, it’s recommended to establish a Kubernetes service and employ Ingress for handling traffic.

Conclusion

To wrap up our beginner’s guide, we’ve navigated through the steps of deploying your very first application on Kubernetes. However, this journey is only the initial leg of a much larger expedition. In Kubernetes, a world of opportunities awaits, allowing you to optimize and fine-tune your application’s performance, scalability, and resilience. From advanced networking and load balancing to automated scaling and self-healing, Kubernetes offers many tools to ensure your applications run seamlessly in any environment. So, while this guide concludes here, your exploration of Kubernetes is just beginning – embark on this adventure with confidence and curiosity!

]]>
https://capten.ai/blog/k8s-hello-world/feed/ 0
Your First Docker Container https://capten.ai/blog/your-first-docker-container/ https://capten.ai/blog/your-first-docker-container/#respond Tue, 06 May 2025 13:56:08 +0000 https://capten.ai/?p=25159

In last week’s blog , we had learned what containers were, the benefits they offered, how they are built, and their use cases. This week, let’s apply what we have learned on a small sample project, to containerize it, and run it.

A Quick Refresher

As a quick recap of what we learned last week, containers are a type of virtualization technology that allow you to run portable software on any machine. Containerized code can run on different machines, even if it was coded in a certain environment. On top of this, containers are much lighter on the CPU, and faster than virtual machines.
A container image is a package of the application code, its dependencies, and everything needed to run the application. Creating a container image is the first step in creating a container. You can use one image to make numerous containers, and container images are usually created using a DOCKERFILE.

Docker is a software that allows you to build/run containers (a.k.a. a container runtime).
Now that we have our basics covered, let’s jump right into the tutorial.

Installing Docker On Windows

  1. Install Windows Subsystem Linux via the admin powershell command: *wsl –install
  2. Install Docker Desktop
  3. Restart your computer.
  4. Open Docker Desktop and go through the tutorial.
  5. Once finished, you should see your deployed website on localhost:80.
  6. You should also run the docker version command to see if you have both client and server side Docker installed.

The Python App

The app we will be running is a simple script. It is a variation of the popular ‘FizzBuzz’ challenge found in interviews. To add some complexity (dependencies), we will be creating a pandas dataframe, and filling a column with the result of the fizzbuzz routine. The code will then print the data frame.
The source code can be found here . Go ahead and clone the app.py file into your own project directory that you have created, so we can turn it into a Docker container.

Dependencies/Requirements

You should create/copy the above script into a directory called “docker_container_example”. Since Python allows for easy managing of requirements, all we have to do is create a requirements.txt within our project directory. Within that, we can specify the pandas library to be used.

  • pandas

The Dockerfile

Within that same project directory, we will define a dockerfile with the name .DOCKERFILE. This will tell Docker which container to use. The dockerfile source code can be found here .

Building An Image

Now that we have everything we will need in our directory, we can build our container image using our directory contents and Dockerfile.
Open a cmd within your projects directory, and issue the following command:

docker build --tag docker_example .

This builds an image with the tag docker_example.

Running Your Container

Now that an image has been created (think of it as a template for creating containers from), you can finally create and run a container using that image. To run, it’s as simple as:

docker run docker_example

This will print out the FizzBuzz dataframe that our program created. The output should look similar to this:

Keep in mind that if the application was a web-based application, your container would be published at some address which you can access. In fact, we will do that in the next section.

Creating A Simple Flask Web-App To Be Deployed

In our project directory, we will create another python script for our web app. This script will be called web_app.py. It will do the same thing as our previous command line app. The full contents of the script can be found here .

Updating Your Dockerfile & Creating A New Image

We need to update the dockerfile to include our new script only. This includes the new flask command, otherwise our script will not run/publish correctly. We need to update the requirements file to include everything from all scripts. This includes flask. We need to build another image, to allow for our dockerfile changes to be reflected correctly.

docker build –tag docker_example_2 .

Finally, we can run our image docker_example_2:

docker run –publish 8888:8888 docker_example_2

Your app can now be accessed here , and your output should look something like this:

Note: If you cannot see an app deployed, make sure the ports you specify in your Dockerfile are the same as the ports you specified in your docker run command.

Conclusion

In this blog, we created our first DOCKERFILE, container image, and container. We also learned what steps to take in order to update our code, and recreate our image and container. Hopefully this tutorial has served its purpose of getting you accustomed to working with DOCKERFILES, images, and containers.

Full source code located here

]]>
https://capten.ai/blog/your-first-docker-container/feed/ 0
What is gRPC https://capten.ai/blog/what-is-grpc/ https://capten.ai/blog/what-is-grpc/#respond Tue, 06 May 2025 13:42:55 +0000 https://capten.ai/?p=25153

Most times, when we want to bridge the communication between two services, developers opt to use REST API. This however, presents a problem when trying to communicate between two services using different networks or languages. This is where gRPC comes in.
gRPC is a high performance Remote Procedure Call (RPC) framework. gRPC offers many advantages to REST. Both are API standards, allowing for communication between systems. However, they differ in the message formats, and error codes they use. In order to understand the benefits/advancements of gRPC, let’s take a look at remote procedure calls.

Remote Procedure Calls – What Are They

A Remote Procedure Call (RPC) is when one computer causes a process/subroutine to run on another computer. Usually this call is written as if it were to occur on the first computer. Remote calls are usually slower and less reliable than local calls and are treated as a form of inter-process communication, the only difference being that the processes have different address-spaces ( they each have access to physically separate memory addresses).
RPCs are a request-response protocol. They follow the standard of clients requesting access to resources, and the server side responding to those requests. RPC calls can be blocking (client stops its program until the server response is received), or asynchronous (client continues execution, and processes the server response in the background when it arrives).

Typical RPC Flow

Below is how RPC events typically take place between a client and a server:

  1. The client calls the client stub (a client-side object that is responsible for communication).
  2. The client stub takes the parameters from the client (received in the above call) and creates a message with them in it.
  3. It then calls upon the system to send the message.
  4. The client OS handles the sending of the message from the client to the server machine.
  5. The server OS receives the message, and unpacks the parameters.
  6. The server stub calls a server procedure to process the remote call, and sends a response. The response is sent in reverse steps.

Since this process is very standard, many RPC systems have been created to allow for inter-platform RPCs. These typically use interface description language, which is used to generate code on each client and server platforms.

So What Is gRPC?

gRPC is an open source RPC framework created by Google. It is based on “Stubby”, the proprietary RPC framework previously developed and used by Google. gRPC allows the definition of methods, along with their parameters and return types. Clients using gRPC can access these methods using their stub, and treat them as a local object. Servers using gRPC implement these methods, and handle client calls via a separate gRPC server. gRPC supports many languages, and the client/server can use the supported language of their choosing. In order to do so, gRPC needs to convert the data being transferred into a format that can be understood by both languages. This is where protocol buffers (also known as protobufs or protobuf) come in.

Protocol Buffers

Before objects in one language can be used in another, it needs to be converted into a format which is interpretable by the second language. Objects are usually converted into bytes in a process called serialization. Serialized objects can theoretically be used by any language, as the object itself can be recreated from the serialized form whenever necessary. This is what protocol buffers are used to do.

Advantages

There are also many advantages to using protocol buffers instead of other formats like JSON. For example, protocol buffers offer:

  1. Compact storage
  2. Fast parsing
  3. Cross-language compatability
  4. Automatically generated classes/code

Another major advantage to protocol buffers is that proto definitions can be updated without damaging old code. The old code can simply run by ignoring the newly added data.

A Small Example On Protocol Buffers

You can define the structure of your serializable data using a ‘.proto’ file. Each object is stored as a message. Think of the message as a struct, filled with multiple variable-value fields. Here is an example:

message Car {
  string make = 1;
  string model = 2;
  int32 year = 3;
  bool is_hybrid = 4;
  int32 mpg = 5;
  int32 tank_gallons = 6;
}

You can then use a compiler to make a code file filled with getters/setters for each field in every message of your proto file. The compiler, protoc, can be used to compile proto files into the language of your choice. The compiler will also create methods to both serialize and recreate the object from the gRPC messages. Lastly, the code generated from the proto file will include both client and server code. This includes services. Below is another example of the service we can use with our car example.

service CalculateTotalMilesOnFullTank {
  rpc GetTotalMilesOnFullTank (MPG) returns (TotalMiles) {}
}
message MPG {
  int32 mpg = 1;
  int32 tank_gallons = 2;
}
message TotalMiles {
  int32 total_miles = 1;
}

After serialization, the clients and servers will be sending these “proto requests” and “proto response” instead of regular requests/responses. The current protocol buffers version is proto3, and this is the version recommended by gRPC. It can be installed in each language individually, however, I’ve found that Python and GoLang implementations are the quickest/easiest to install.

So Then What’s The Difference Between gRPC & REST?

Both are APIs used to create a standardized interface for systems to communicate with each other. And both types of API standards have their own separate benefits. Using a specific standard depends on the project at hand. If your project is meant to handle high loads (due to lower latency), two-way communication, and you also want simplicity in your code, you should use the gRPC framework. However, if your project will require statelessness, self-contained messages, and language agnosticity, then REST is the better framework for it. Keep in mind that gRPC can still allow for communication between systems with different languages, it’s just that some features offered by gRPC are language dependent. Lastly, gRPC uses HTTP/2 which offers many more benefits than REST APIs HTTP/1.1.

Conclusion

In this blog, we learned what Remote Procedure Calls are, what gRPC is and its advantages over REST API. We even went a little bit into protocol buffers with some example code, to see how gRPC code is set up by the developer to be generated by the compiler protoc. Lastly, once we had understood a bit more about gRPC, we went back into how REST API compares to gRPC.
You can read more about gRPC here , and even get started with some sample code in the langauge of your choosing here.

]]>
https://capten.ai/blog/what-is-grpc/feed/ 0
Enhancing Your Pixie PXL Script by Manipulating Data https://capten.ai/blog/enhancing-your-pixie-pxl-script-by-manipulating-data/ https://capten.ai/blog/enhancing-your-pixie-pxl-script-by-manipulating-data/#respond Tue, 06 May 2025 13:22:34 +0000 https://capten.ai/?p=25146

Pixie can be utilized for gathering monitoring metrics from your clusters. It offers you the benefit of having pre-written scripts, as well as custom scripts that extrapolate data. The custom scripts are written in Pixie’s very own language, PXL, which is similar to Python. Last week, we had been working on creating our first custom PXL script to gather data . In this tutorial, lets focus on customizing the data we have gathered and tuning it to our own preferences.

PXL Uses DataFrames

Within the PXL language, we can see that we use dataframes to interact with our data. For those of you familiar with Python, specifically pandas, this blog post will come as second nature to you. Dataframes are just tabular representations of data. You can think of a dataframe as a spreadsheet, but way more powerful.
You can tell from the script we wrote last week that the columns included were from the process_stats table. (see script below)

# We import px, which is the library we will be using to add extra data to our table.
import px
# We gather data from the last 5 minutes, from the `process_stats` table, and create a dataframe from it.
df = px.DataFrame(table='process_stats', start_time='-5m')
# Below, we are adding extra data to our table, using `context` or `execution_time_functions`
df.pod_id = df.ctx['pod_id']
df.pod_name = px.upid_to_pod_name(df['upid'])
df.pod_id = px.pod_name_to_pod_id(df['pod_name'])
df.cmd = df.ctx['cmdline']
df.pid = df.ctx['pid']
df.container_name = df.ctx['container_name']
df.container_id = df.ctx['container_id']
# We group the dataframe based on certain attributes, and aggregate the data.
df = df.groupby(['pid', 'cmd', 'upid', 'container_name']).agg()
# We display the dataframe.
px.display(df, 'processes_table')

This script used basic functions on the dataframe, such as adding new columns. It also used slightly more advanced functions such as the groupby function, and the aggregation function .agg().
Let’s get right into how we can enhance our PXL scripts by manipulating data.

Joining Tables Using PXL

On top of just adding a few extra columns, we can also join two tables together based on common columns shared by the two tables. This process is called merging. Take a look at the code below for an example/explanation.

# We import px, which is the library we will be using to add extra data to our table.
import px
# We gather data from the last 5 minutes, from the `conn_stats` table,
# and create a dataframe from it.
df = px.DataFrame('conn_stats', start_time='-5m')
# We also gather data from the `http_events` table.
http_e_df = px.DataFrame('http_events', start_time='-5m')
# We can now combine the two tables, using the merge function.
df = df.merge(http_e_df, how='left', left_on=['time_', 'upid'], right_on=['time_', 'upid'], suffixes=['', '_x'])

px.display(df, 'conn_stats_and_http_events_table')

In the script above, we are using the merge function to join columns from the http_events table to the conn_stats table. Here is a brief explanation of what the parameters in this function mean:

  • how: how we are going to be joining one table to another.
    'left' means we will keep all data from the left table.
    'right' means we keep all data from the right table.
    'inner' means we will only be keeping the data that is present in both tables. 'outer' means that we will be keep all data present in both tables.
  • left_on<</code style="color:#ee449d" >/right_on: These define the columns which we will compare between the two tables to align the data correctly. In the code above, we are aligning data based on the time_ and upid columns.
  • suffixes: defines what strings to attach to the duplicate columns in the resulting table. At the end of the merging done in this script, you will notice that we have columns from both tables. Yet, we will only have observations (rows) from the conn_stats table, since it is the left table.

Dropping Columns

We can drop certain columns that we would not like from a table. For example, if there is a column that is duplicated from the previous merge we have done, we can drop it after merging. Take a look below:

...

# We can now combine the two tables, using the merge function.
df = df.merge(http_e_df, how='left', left_on=['time_', 'upid'], right_on=['time_', 'upid'], suffixes=['', '_x'])
# we get rid of duplicate values such as `time__x` and `upid_x`
df = df.drop(['time__x', 'upid_x'])

...

Notice that the colums we are dropping have the duplicate suffixes attached to their names. This ensures that the original columns are still present, so that we do not lose the data.

Adding A Custom Column

We can add custom columns to our data based on calculations we have done ourselves, or calculations based on other columns. This process is called mapping. For example, we might want to convert bytes to megabytes. This can be done via:

df['req_body_size'] = df['req_body_size']/1.0e6

We can also add custom columns with whatever data we would like. If I wanted a column named foo, with the attribute bar added to each observation, I could do that using the following:

df['foo'] = "bar"

Filtering Data

We can filter data within our script using PXL’s filter function. This functionality is similar to what is done in Python’s pandas package. In the example below, I am filtering to include the rows that have their bytes_sent value higher than 65399738:

df = df[df['bytes_sent'] > 65399738]

Other Useful Functions

Pixie docs list a whole bunch of useful functions that can be applied to PXL dataframes . Some of my favorites are:

  • Dataframe.head(): For when you need only a certain number of rows to be received from Pixie. This is extremely helpful in debugging while you are writing PXL scripts.
  • Dataframe.groupby(): As we have used in our previous PXL blog .
  • Dataframe.stream(): For when you have so much data that you need it on a streaming basis.

Conclusion

In this blog, we have understood what PXL dataframes are, and the special dataframe functions we can use to enhance our PXL script and manipulate our data. Feel free to look at the PXL Docs to learn more.

]]>
https://capten.ai/blog/enhancing-your-pixie-pxl-script-by-manipulating-data/feed/ 0
Advanced PXL Script Functions https://capten.ai/blog/advanced-pxl-script-functions/ https://capten.ai/blog/advanced-pxl-script-functions/#respond Tue, 06 May 2025 13:02:52 +0000 https://capten.ai/?p=25139

Over the past couple of weeks, we have been covering the powerful cluster monitoring tool, Pixie. We have covered how to get custom data by writing your own PXL script . We have also covered how to enhance your custom script by using data manipulation functions provided by Pixie. Let’s wrap up our coverage of Pixie’s custom data collection by diving into an example of an advanced PXL script.

A Quick Refresher

Pixie is a cloud observation tool used for gathering monitoring metrics from your clusters. It provides a Live UI which is a dashboard that can be accessed from anywhere. It also provides users the ability to execute custom scripts to gather custom data.

These scripts can be executed from the Live UI or even from the Pixie API. The language these scripts are written in is PXL, which is similar in syntax to Python. In fact, users of the Python library pandas will notice many similarities between PXL and pandas, such as the fact that they both use dataframes (which are like spreadsheets but faster).

What We Covered In The First Blog

In the first blog about PXL, we ran the script below.

# We import px, which is the library we will be using to add extra data to our table.
import px

# We gather data from the last 5 minutes, from the `process_stats` table, and create a dataframe from it.
df = px.DataFrame(table='process_stats', start_time='-5m')
# Below, we are adding extra data to our table, using `context` or `execution_time_functions`
df.pod_id = df.ctx['pod_id']
df.pod_name = px.upid_to_pod_name(df['upid'])
df.pod_id = px.pod_name_to_pod_id(df['pod_name'])
df.cmd = df.ctx['cmdline']
df.pid = df.ctx['pid']
df.container_name = df.ctx['container_name']
df.container_id = df.ctx['container_id']
# We group the dataframe based on certain attributes, and aggregate the data.
df = df.groupby(['pid', 'cmd', 'upid', 'container_name']).agg()
# We display the dataframe.
px.display(df, 'processes_table')

This script covered the following:

  • The syntax of a PXL script.
  • Retrieving a table’s data (process_stats) for the past 5 minutes.
  • Adding new columns containing context data.
  • Grouping by certain columns and aggregating the data.

What We Covered In Last Week’s Blog

In last week’s blog, we covered some advanced PXL functions used for manipulating data. These included:

  • Joining Tables – so we can add data from other tables based on common attributes between the two tables.
  • Dropping/Creating Columns – so we can add more context to our data.
  • Filtering Data – based on columnar values.

This week, let’s dive into using all of these at once to create an advanced PXL script that gives us the exact data that we want!

Creating Our Script

Let’s set a goal for this script. I would like to get the conn_stats table data and merge it with the network_stats table data. To do this, I would need to merge both tables on as many unique identifiers as possible. In this case, that would constitute the time_ and pod_id columns.
There is only one problem though. The network_stats table is an aggregate table. This means that it collects data on an interval basis, and sums up the collected data, so it can be added to the table. This also means that the time_ column in the network_stats table will not always align with the time_ column in the conn_stats table. In fact, in order to align the two, we will need to manipulate the data in the time_ column to match both tables, which we will do later on.
Let’s start our script by setting up the base structure. We will get both of the tables, and display one for now.

import px

conn_stats_df = px.DataFrame('conn_stats', start_time='-5m')
net_stats_df = px.DataFrame('network_stats', start_time='-5m')

px.display(conn_stats_df, 'conn_stats_df')

Setting Up More Variables (For Context/Merging)

Now, let’s add more variables to our script to get even more data out of each run/call. Let’s start by adding the following contextual columns to the conn_stats_df dataframe:

...

conn_stats_df.pod = conn_stats_df.ctx['pod']
conn_stats_df.pod_id = px.upid_to_pod_id(conn_stats_df.upid)
conn_stats_df.container_name = px.upid_to_container_name(conn_stats_df.upid)
conn_stats_df.container_id = px.upid_to_container_id(conn_stats_df.upid)
conn_stats_df.namespace = px.pod_id_to_namespace(conn_stats_df.pod_id)
conn_stats_df.node = px.pod_id_to_node_name(conn_stats_df.pod_id)

...

We will also add a column that will help us merge with the network_stats table later:

...

conn_stats_df.time_aligned = px.bin(conn_stats_df.time_, 1000000000)

...

Note that the above function is advanced. Here is what is does:

  • Creates a new column called time_aligned
  • Bins the original time_ column.
    • The binning is done in one second increments (equivalent to 1000000000 nanoseconds)
    • You can think of this as ‘rounding’ in a way.

We will create a similar binned time value in the net_stats_df by using the following:

...

net_stats_df.time_aligned = px.bin(net_stats_df.time_, 1000000000)

...

Notice that we are using the same exact values for binning. This is important, as this is what will allow the dataframes to be aligned correctly.

Merging The Two Tables

Now that we have everything set up correctly, we can start merging (joining) the two tables. This is a similar step to the joining we have done before. Let’s add this snippet to the code, right before the px.display() call:

...

df = conn_stats_df.merge(net_stats_df, how='left', left_on=['time_aligned', 'pod_id'], right_on=['time_aligned', 'pod_id'], suffixes=['', '_x'])

...

This will result in one big table with all of the columns of conn_stats and network_stats. This includes duplicate columns (which will now have the suffix ‘_x’ added on to them). Let’s take care of cleaning this data.
Also, take note that this is a left join, meaning that all the observations from the conn_stats table will be present.

Cleaning Up The Joined Table

Let’s drop the columns that are duplicated:

...

df = df.drop(['time__x', 'time_aligned_x', 'pod_id_x'])

...

Now let’s rename some columns that used to be in network_stats for more clarity:

...

df['received_bytes'] = df['rx_bytes']
df['received_packets'] = df['rx_packets']
df['received_errors'] = df['rx_errors']
df['received_drops'] = df['rx_drops']
df['transmitted_bytes'] = df['tx_bytes']
df['transmitted_packets'] = df['tx_packets']
df['transmitted_errors'] = df['tx_errors']
df['transmitted_drops'] = df['tx_drops']

...

Of course, since we renamed the columns, we will have to drop the old columns:

...

df = df.drop(['rx_bytes', 'rx_packets', 'rx_errors', 'rx_drops', 'tx_bytes', 'tx_packets', 'tx_errors', 'tx_drops'])

...

Full Code

You can find the full code for this script below. To give it a quick test-run, you can try it out using the ‘Scratch Pad’ function of the Pixie Live UI.

import px

# get the conn_stats data.
conn_stats_df = px.DataFrame('conn_stats', start_time='-5m')
# add contextual data about the cluster.
conn_stats_df.pod = conn_stats_df.ctx['pod']
conn_stats_df.pod_id = px.upid_to_pod_id(conn_stats_df.upid)
conn_stats_df.container_name = px.upid_to_container_name(conn_stats_df.upid)
conn_stats_df.container_id = px.upid_to_container_id(conn_stats_df.upid)
conn_stats_df.namespace = px.pod_id_to_namespace(conn_stats_df.pod_id)
conn_stats_df.node = px.pod_id_to_node_name(conn_stats_df.pod_id)
# we convert the time value from nanoseconds and bin it to the nearest second (since there are 1000000000 ns in 1 s)
conn_stats_df.time_aligned = px.bin(conn_stats_df.time_, 1000000000)
# get the network_stats data.
net_stats_df = px.DataFrame('network_stats', start_time='-5m')
# we convert the time value from nanoseconds and bin it to the nearest second (since there are 1000000000 ns in 1 s)
net_stats_df.time_aligned = px.bin(net_stats_df.time_, 1000000000)
# merging the two dataframes based on the time_aligned and pod_id attributes.
df = conn_stats_df.merge(net_stats_df, how='left', left_on=['time_aligned', 'pod_id'], right_on=['time_aligned', 'pod_id'], suffixes=['', '_x'])
# drop the duplicate time_ column
df = df.drop(['time__x', 'time_aligned_x', 'pod_id_x'])
# rename some columns
df['received_bytes'] = df['rx_bytes']
df['received_packets'] = df['rx_packets']
df['received_errors'] = df['rx_errors']
df['received_drops'] = df['rx_drops']
df['transmitted_bytes'] = df['tx_bytes']
df['transmitted_packets'] = df['tx_packets']
df['transmitted_errors'] = df['tx_errors']
df['transmitted_drops'] = df['tx_drops']
# get rid of the old named columns
df = df.drop(['rx_bytes', 'rx_packets', 'rx_errors', 'rx_drops', 'tx_bytes', 'tx_packets', 'tx_errors', 'tx_drops'])
# display the merged dataframe
px.display(df, 'df')

Conclusion

In this blog, we have understood how to create an advanced PXL script for use with clusters that have Pixie deployed on them. We have combined all the ideas from the previous blogs into a final, advanced tutorial script. You are now a PXL script expert! 🙌
Keep in mind that there are a number of other functions you can use in your advanced PXL scripts. These can be found on Pixie’s documentation page .

]]>
https://capten.ai/blog/advanced-pxl-script-functions/feed/ 0
Anomalies in Graphs https://capten.ai/blog/anomalies-in-graphs/ https://capten.ai/blog/anomalies-in-graphs/#respond Tue, 06 May 2025 12:53:32 +0000 https://capten.ai/?p=25132

Graph

Graphs are a kind of data structure that models a set of data objects and their relationships. The data objects are typically known as Nodes and their relationship as Edges. In a simpler way, one can visualize an edge as a connection between two nodes and that connection implies the relationship between the nodes. Graphs are utilized as unique non-Euclidean data structures for machine learning across various domains such as the denotation of a large number of systems across various areas including social sciences, natural sciences, knowledge graphs, protein-protein interaction networks, and various other researches.

Anomalies

The term ‘Anomaly’ basically signifies abnormal pattern which is significantly different from normal pattern. Different types of anomalies can exist even in graphs. Those anomalies are discussed in the following section.

Types of Graph Anomalies

  • Node Level Anomaly:

    Each of the node in the graph contains certain features which are known as node attributes. On the other hand, all nodes follow some structural pattern in a graph. Node level anomalies can happen if anomalies present in node attributes or in structural patterns or in both.

In the figure, nodes are represented by round-shaped objects with different colors. Node attributes are represented by vertical bars with white and black colors. According to attributes, node 'A' and node 'C' are anomalous. But according to structure, node 'A' and 'B' are anomalous as they do not belong to any communities (see the right and left cluster of nodes). Hence, node 'A' is indeed  anomalous both attribute-wise and structure-wise.
  • Edge Level Anomaly:

Edge represents the relationship between nodes. Hence, the presence of fake edge or absence of any edge which contradicts the relationship between nodes in reality is known as edge level anomaly. Moreover, features connected with edges are known as edge attributes. If edge attributes are anomalous, then it is also termed as edge level anomaly.

  • Sub-graph Level Anomaly:

A small portion of the graph is known as sub-graph. If a sub-graph shows anomalous behavior compared to the other portions of the graph, then it is called as sub-graph anomaly.

  • Graph Level Anomaly:

Graph level anomaly indicates the presence of abnormal patterns in a graph among a set of graphs.

Conclusion

The graph and different types of anomalies in graph are discussed here. Knowledge of these anomalies is quite important as the information is needed to detect anomalies in graphs. Graph anomaly detection has been drawing much attention over the past few years. It has important applications in many real-world problems such as fraud detection in banking and social networks, threat detection in cyber security etc. With the advancement of Graph Neural Network and Reinforcement Learning models the interest in this field has grown exponentially. It is now a very vibrant and active field of research and lot of new techniques are sure to come in coming years to solve the problems more effectively.

References

  1. J. Zhou, et.al., AI Open 1 (2020) 57-81.
  2. J. Hwan Kim, et al., arxiv 2209.14930 (2022).
  3. Picture Courtesy: J. Hwan Kim, et al., arxiv 2209.14930 (2022).
]]>
https://capten.ai/blog/anomalies-in-graphs/feed/ 0
Evans CLI – A Go gRPC Client https://capten.ai/blog/evans-cli-a-go-grpc-client/ https://capten.ai/blog/evans-cli-a-go-grpc-client/#respond Tue, 06 May 2025 11:29:27 +0000 https://capten.ai/?p=25124

Evans CLI is a command-line tool designed to facilitate interaction with Go gRPC services. It offers a range of features that simplify the testing and debugging of gRPC services. Here are some of the advantages of using Evans CLI:

Advantages of Evans CLI

Evans CLI stands out from other tools for interacting with gRPC services due to the following advantages:

  • Ease of use: Evans CLI provides a simple command-line interface, making it incredibly user-friendly. It allows you to effortlessly send requests and receive responses from gRPC services.
  • Powerful features: Evans CLI boasts several powerful features that streamline the testing and debugging process. These features include automatic service discovery, an interactive mode, and code generation capabilities.
  • Open source: Evans CLI is an open-source tool, which means it is free to use and modify. This makes it a flexible and cost-effective choice for developers.

Installation of Evans CLI

To install Evans CLI, follow these steps:

Sample gRPC Server Project

For the purpose of demonstrating the usage of Evans CLI, let’s consider a sample gRPC server that implements CRUD (Create, Read, Update, Delete) operations based on an office protocol file.

You can find the source code for this sample project here .

How Evans CLI Solves Client-Side Issues for the gRPC Server

Evans CLI addresses client-side issues for the gRPC server through its REPL (Read-Eval-Print Loop) mode and CLI (Command Line Interface) mode. In this example, we will focus on the REPL mode, as it offers a more user-friendly way to interact with the server.

To start the Evans CLI client in REPL mode, execute the following command in the root directory of your project:

evans -r repl -p <your-gRPC-server-port>

Here are some useful commands you can utilize with Evans CLI:

  • To view the available packages on your server, use the command:
show package
  • To select a specific package from the available options, use the command:
package <Package_Name>

For example:

package gen
  • To view the services within the selected package, use the command:
show service
  • To select a specific service from the available options, use the command:
service <Service_Name>

For example:

service OfficeService
  • To view the messages within the services.package, use the command:
show message
  • To obtain more information about a particular message and its fields, use the command:
desc <Message_Name>

For example:

desc Office
  • To view the RPC methods

available in the services, use the command:

show rpc
  • To call a method from a service, use the command:
call <rpc-method-name>

For example:

call CreateOffice

Conclusion

Evans CLI is a powerful tool that simplifies the testing and debugging of Go gRPC services. With its easy-to-use interface, rich set of features, and open-source nature, Evans CLI provides developers with an efficient way to interact with gRPC servers.

By installing Evans CLI and utilizing its REPL mode, developers can seamlessly explore, inspect, and test the different APIs exposed by their gRPC servers. This interactive approach enhances the development workflow, improves efficiency, and boosts confidence in the reliability of gRPC-based systems.

Embrace Evans CLI as your go-to tool for testing and debugging Go gRPC services, and experience the benefits of its simplicity, power, and open-source nature. Happy coding!

]]>
https://capten.ai/blog/evans-cli-a-go-grpc-client/feed/ 0