<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[A slice of experiments]]></title><description><![CDATA[I like cloud and server stuff, and I like AI shit too. I write long form, deeply technical content with the brain rot vocabulary that only the internet can teach someone.]]></description><link>https://www.sliceofexperiments.com</link><image><url>https://substackcdn.com/image/fetch/$s_!o6dZ!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7aaed77e-5db5-4e6b-982e-1802998c069a_1024x1024.png</url><title>A slice of experiments</title><link>https://www.sliceofexperiments.com</link></image><generator>Substack</generator><lastBuildDate>Wed, 29 Apr 2026 11:13:18 GMT</lastBuildDate><atom:link href="https://www.sliceofexperiments.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Aaron Batilo]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[sliceofexperiments@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[sliceofexperiments@substack.com]]></itunes:email><itunes:name><![CDATA[Aaron Batilo]]></itunes:name></itunes:owner><itunes:author><![CDATA[Aaron Batilo]]></itunes:author><googleplay:owner><![CDATA[sliceofexperiments@substack.com]]></googleplay:owner><googleplay:email><![CDATA[sliceofexperiments@substack.com]]></googleplay:email><googleplay:author><![CDATA[Aaron Batilo]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[s6 - Run multiple processes in your container]]></title><description><![CDATA[Who said there can only be PID 1]]></description><link>https://www.sliceofexperiments.com/p/s6-run-multiple-processes-in-your</link><guid isPermaLink="false">https://www.sliceofexperiments.com/p/s6-run-multiple-processes-in-your</guid><dc:creator><![CDATA[Aaron Batilo]]></dc:creator><pubDate>Tue, 04 Jun 2024 20:15:02 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!o6dZ!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7aaed77e-5db5-4e6b-982e-1802998c069a_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You&#8217;ll see a lot of advice that says that docker containers should ideally only have a single process, and that you should strive for docker PID 1. I enjoy the aspiration, but there&#8217;s no way that this works for most legacy applications. It works great when you have a single statically linked binary like if you&#8217;re compiling the Golang stack. But what about if you&#8217;re trying to deploy a Python + Django stack? Django itself just does NOT come with a production web server. If you look at <a href="https://docs.djangoproject.com/en/5.0/howto/deployment/">Django&#8217;s own deployment documentation</a>, it tells you to run it with another program. Programs like <a href="https://gunicorn.org/">gunicorn</a> work by spawning multiple processes to run your application.</p><p>PID 1 purists crying in the club rn.</p><h1>Why do I want to run multiple processes anyways</h1><p>It&#8217;s pretty common for multi-node GPU training frameworks to communicate over something like ssh. If you try to do <a href="https://www.deepspeed.ai/getting-started/#resource-configuration-multi-node">multi-node training with DeepSpeed</a>, you need to configure a <code>hostfile</code> and then configure passwordless SSH between all of the nodes. <a href="https://docs.open-mpi.org/en/v5.0.x/launching-apps/ssh.html">OpenMPI does the same</a>.</p><p>In non-containerized environments, you might use a process manager like <a href="https://systemd.io/">systemd</a> or <a href="http://supervisord.org/">supervisord</a>. Docker actually <a href="https://docs.docker.com/config/containers/multi-service_container/">documents examples of using supervisord</a> for managing multiple processes. Which, for all intents and purposes, does exactly what we want, <strong>EXCEPT</strong> an organic way to exit when a process exits. Process managers tend to want to restart any process that have exited. Even with supervisord, you can configure <code>autorestart = false</code> but supervisord itself will not exit, meaning that your container itself will not exit.</p><h1>Why does exiting matter</h1><p>ML training jobs are not web services. A web service is intended to run forever, and will only be shutdown when it needs to be replaced. That makes sense. In theory, your website should live forever. ML training jobs are long running batch jobs. It&#8217;s fully expected that they will eventually finish running. They might run for hours, days, weeks, months or longer, but eventually you&#8217;ve gone through all of your training data and you want your process to exit. The function will eventually exit. How do we do this in a container?</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.sliceofexperiments.com/subscribe?"><span>Subscribe now</span></a></p><h1>s6 process supervision</h1><p>S6 describes itself as:</p><blockquote><p>s6 is a collection of utilities revolving around process supervision and management, logging, and system initialization.</p></blockquote><p><strong>Fascinating</strong>.</p><p>On top of this, I&#8217;ve been recently made aware about <a href="https://github.com/just-containers/s6-overlay">s6-overlay</a> which describes itself as:</p><blockquote><p>s6-overlay is an easy-to-install (just extract a tarball or two!) set of scripts and utilities allowing you to use existing Docker images while using <a href="https://skarnet.org/software/s6/overview.html">s6</a> as a pid 1 for your container and process supervisor for your services.</p></blockquote><p><strong>Fascinating</strong>&#8230; again<strong>.</strong> This would give us exactly what we want. The ability to run sshd as a long running process while still running our own application. Process signals will be managed for us, all in a relatively straightforward process manager.</p><p>In the rest of this post, I&#8217;m going to walkthrough a minimal Dockerfile that uses s6-overlay and explain the pieces of it, as I understand them.</p><div><hr></div><p>In order to make the Dockerfile easily reproducible, we&#8217;re going to embed the configuration directly into the Dockerfile so that we don&#8217;t have to worry about setting up other files locally.</p><pre><code>FROM ubuntu:22.04

# Install sshd and xz to unzip the s6-overlay tarball
RUN &lt;&lt;EOF
apt-get update;
apt-get install -yq openssh-server xz-utils;
EOF

# Install s6-overlay
ADD https://github.com/just-containers/s6-overlay/releases/download/v3.1.6.2/s6-overlay-noarch.tar.xz /tmp
RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz
ADD https://github.com/just-containers/s6-overlay/releases/download/v3.1.6.2/s6-overlay-x86_64.tar.xz /tmp
RUN tar -C / -Jxpf /tmp/s6-overlay-x86_64.tar.xz

# Define sshd as a long running s6 service
COPY &lt;&lt;EOF /etc/s6-overlay/s6-rc.d/sshd/type
longrun
EOF

# Define entrypoint for sshd
# Create sshd's required directory
# Daemon applications tend to log to stderr, so redirect it to stdout
# Start sshd in the foreground
COPY --chmod=700 &lt;&lt;EOF /etc/s6-overlay/s6-rc.d/sshd/run
#!/bin/sh
mkdir -p /var/run/sshd
/usr/sbin/sshd -D -e
EOF

# Register sshd as a service for s6 to manage
RUN touch /etc/s6-overlay/s6-rc.d/user/contents.d/sshd

# Copy my ssh public keys to the container
ADD https://github.com/abatilo.keys /root/.ssh/authorized_keys

# Define training as a one shot s6 service
WORKDIR /app
COPY --chmod=700 &lt;&lt;EOF /app/training.sh
#!/bin/sh
echo "Training started"
sleep 5
echo "Training finished"
EOF

ENTRYPOINT ["/init"]
CMD ["/app/training.sh"]</code></pre><p>Let&#8217;s break it down by section:</p><pre><code>FROM ubuntu:22.04

# Install sshd and xz to unzip the s6-overlay tarball
RUN &lt;&lt;EOF
apt-get update;
apt-get install -yq openssh-server xz-utils;
EOF

# Install s6-overlay
ADD https://github.com/just-containers/s6-overlay/releases/download/v3.1.6.2/s6-overlay-noarch.tar.xz /tmp
RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz
ADD https://github.com/just-containers/s6-overlay/releases/download/v3.1.6.2/s6-overlay-x86_64.tar.xz /tmp
RUN tar -C / -Jxpf /tmp/s6-overlay-x86_64.tar.xz</code></pre><p>Installing s6-overlay is incredibly easy. I think it&#8217;s relatively unknown that Docker&#8217;s <code>ADD</code> directive has the ability to download from the internet, so you don&#8217;t even need to have curl or wget installed. <code>ADD</code> will even unxz for you, and so all you need to do from that point is to untar the s6 utilities into the rest of the filesystem. ezpz.</p><pre><code># Define sshd as a long running s6 service
COPY &lt;&lt;EOF /etc/s6-overlay/s6-rc.d/sshd/type
longrun
EOF

# Define entrypoint for sshd
# Create sshd's required directory
# Daemon applications tend to log to stderr, so redirect it to stdout
# Start sshd in the foreground
COPY --chmod=700 &lt;&lt;EOF /etc/s6-overlay/s6-rc.d/sshd/run
#!/bin/sh
mkdir -p /var/run/sshd
/usr/sbin/sshd -D -e
EOF

# Register sshd as a service for s6 to manage
RUN touch /etc/s6-overlay/s6-rc.d/user/contents.d/sshd</code></pre><p>Okay, here&#8217;s the meat of using s6. The <code>/etc/s6-overlay/s6-rc.d</code> directory is where you can specify all of your services. By creating the sshd directory, that&#8217;s essentially declaring that you have a service that you want to run. The first file here is a file named <code>type</code> where you can define either a <code>longrun</code> service or a <code>oneshot</code> service. For sshd, that&#8217;s a long running service that I expect to never need to exit. Next, we have to create this <code>run</code> file in the <code>sshd</code> directory. This shell script is creating the required directory for sshd, and then runs sshd NOT as a daemon (via the -D flag, which I think feels backwards). The last line here is to create an empty file in <code>/etc/s6-overlay/s6-rc.d/user/contents.d/</code> which is how you actually register what services you want s6 to execute. The name of the file needs to match the name you used in the service configuration.</p><pre><code># Copy my ssh public keys to the container
ADD https://github.com/abatilo.keys /root/.ssh/authorized_keys

# Define training as a one shot s6 service
WORKDIR /app
COPY --chmod=700 &lt;&lt;EOF /app/training.sh
#!/bin/sh
echo "Training started"
sleep 5
echo "Training finished"
EOF

ENTRYPOINT ["/init"]
CMD ["/app/training.sh"]</code></pre><p>Lastly, here&#8217;s my quick way for me to add some public keys to my server by fetching them from GitHub. and then here&#8217;s my mock bash script that would initiate the actual training code.</p><p>The last section here is perhaps the most fascinating. We&#8217;re combining both <code>ENTRYPOINT</code> and <code>CMD</code>. The <code>ENTRYPOINT</code> invokes s6 itself with <code>/init</code> and then setting the <code>CMD</code> let&#8217;s us specify the entrypoint to our main service. Doing this means that we will actually instantiate s6 as our PID 1, and s6 will take the <code>CMD</code> as the service that we want to run. S6 will instantiate all of the services that we&#8217;ve defined, which in our case is only sshd, and once that&#8217;s running, will instantiate our main service. Once the training process exits, s6 will notice this and will send shutdown signals to all other services under its management, allowing for a clean exit. S6 will use the exit code of our main service as its overall exit code, so if something fails unexpectedly in the training process, we&#8217;ll be able to catch it in our upstream container orchestration layer.</p><p>Try it yourself with this Dockerfile:</p><pre><code>docker build -t s6 https://gist.githubusercontent.com/abatilo/2b0c99d8536476f0219de2b8cc038870/raw/c942a1269d8c9a18e70e540c79a3a26750f56352/Dockerfile &amp;&amp; docker run -p 2222:22 --rm -it s6</code></pre><p>Thanks for reading.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.sliceofexperiments.com/subscribe?"><span>Subscribe now</span></a></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[Fine tuning Llama 3 8B for $0.03]]></title><description><![CDATA[They say being GPU poor is the mother of innovation]]></description><link>https://www.sliceofexperiments.com/p/fine-tuning-llama-3-8b-for-003</link><guid isPermaLink="false">https://www.sliceofexperiments.com/p/fine-tuning-llama-3-8b-for-003</guid><dc:creator><![CDATA[Aaron Batilo]]></dc:creator><pubDate>Sun, 21 Apr 2024 20:48:14 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!BUV5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5e65123-222f-4b5b-ad80-511096953919_1754x1020.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I come to you today, admitting a deep problem. I can't help but waste money training language models. I don't even do anything with them. I don't publish them anywhere. I don't run evals. I just love watching the logs. That being said, I still want to do it all as cheaply as possible. I have a $5,000 credit to AWS from incorporating an LLC with Firstbase.io (not sponsored). If I want to watch more log lines, I need to make that money stretch as far as I can, until I can convince someone to give me money with the expectation that I do absolutely nothing with it.</p><p>I tweeted the other day asking people if they&#8217;d be interested in learning how I&#8217;m fine tuning models for $0.03 per million tokens, and at a whopping 7 likes, it&#8217;s my most liked tweet of all time. So here I am.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!BUV5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5e65123-222f-4b5b-ad80-511096953919_1754x1020.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!BUV5!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5e65123-222f-4b5b-ad80-511096953919_1754x1020.png 424w, https://substackcdn.com/image/fetch/$s_!BUV5!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5e65123-222f-4b5b-ad80-511096953919_1754x1020.png 848w, https://substackcdn.com/image/fetch/$s_!BUV5!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5e65123-222f-4b5b-ad80-511096953919_1754x1020.png 1272w, https://substackcdn.com/image/fetch/$s_!BUV5!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5e65123-222f-4b5b-ad80-511096953919_1754x1020.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!BUV5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5e65123-222f-4b5b-ad80-511096953919_1754x1020.png" width="1456" height="847" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d5e65123-222f-4b5b-ad80-511096953919_1754x1020.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:847,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:863739,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!BUV5!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5e65123-222f-4b5b-ad80-511096953919_1754x1020.png 424w, https://substackcdn.com/image/fetch/$s_!BUV5!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5e65123-222f-4b5b-ad80-511096953919_1754x1020.png 848w, https://substackcdn.com/image/fetch/$s_!BUV5!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5e65123-222f-4b5b-ad80-511096953919_1754x1020.png 1272w, https://substackcdn.com/image/fetch/$s_!BUV5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5e65123-222f-4b5b-ad80-511096953919_1754x1020.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This isn&#8217;t a tutorial, so don&#8217;t expect a step by step break down to do everything that I did. Although, if you&#8217;d be interested in that, let me know! I&#8217;ve been thinking about trying to do an online course or video cast or something walking you through actually setting up a Kubernetes cluster for this. For now, I&#8217;ll keep it high level with all of the infra and underlying pieces that I&#8217;m actually using.</p><div><hr></div><h1>How do I get my GPUs?</h1><p>Let&#8217;s start with the actual infrastructure that&#8217;s being used then, shall we? I&#8217;m still running my very same AWS Elastic Kubernetes cluster that <a href="https://www.sliceofexperiments.com/p/scale-from-0-saving-nickels-to-feed">I&#8217;ve talked about before</a>. But since that post, some things have stayed the same and some things have changed. I use <a href="https://karpenter.sh/">Karpenter.sh</a> as my cluster auto scaler, which will provision nodes for me. With that installed to the cluster, I use the following <code>EC2NodeClass</code> and <code>NodePool</code> definitions below.</p><p>Note: My EKS cluster is named &#8220;red&#8221; and my GPU node group is named &#8220;green&#8221;.</p><pre><code>apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
  name: green-gpu
spec:
  amiFamily: AL2
  blockDeviceMappings:
  - deviceName: /dev/xvda
    ebs:
      encrypted: true
      volumeSize: 100Gi
      volumeType: gp3
  role: green-eks-node-group-20230615015942833200000001
  securityGroupSelectorTerms:
  - tags:
      karpenter.sh/discovery: red
  subnetSelectorTerms:
  - tags:
      karpenter.sh/discovery: red
  tags:
    karpenter.sh/discovery: red
---
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
  name: green-gpu
spec:
  disruption:
    consolidationPolicy: WhenUnderutilized
    expireAfter: 1h
  limits:
    cpu: 100
    memory: 200Gi
    nvidia.com/gpu: 4
  template:
    spec:
      nodeClassRef:
        name: green-gpu
      requirements:
      - key: karpenter.sh/capacity-type
        operator: In
        values:
        - spot
      - key: kubernetes.io/arch
        operator: In
        values:
        - amd64
      - key: karpenter.k8s.aws/instance-category
        operator: In
        values:
        - g
      - key: karpenter.k8s.aws/instance-generation
        operator: Gt
        values:
        - "5"
      - key: karpenter.k8s.aws/instance-gpu-count
        operator: In
        values:
        - "1"
      - key: karpenter.k8s.aws/instance-cpu
        operator: Lt
        values:
        - "64"
      - key: kubernetes.io/os
        operator: In
        values:
        - linux
      taints:
      - effect: NoSchedule
        key: nvidia.com/gpu
        value: "true"</code></pre><p>Reminder, let me know if you&#8217;d be interested in an end to end tutorial for setting up a cluster. But for now, let me just link to the <a href="https://karpenter.sh/docs/getting-started/">Karpenter getting started docs</a>. The node requirement selectors are very deliberate. I&#8217;m requesting <a href="https://karpenter.sh/docs/reference/instance-types/#g6-family">g6 family nodes</a> as evidenced by the &#8220;instance-generation is greater&#8221; than 5 requirement which brings me Nvidia L4 GPUs. These GPUs have 24GB of GPU memory and are typically marketed more for inference, but for this broke boy, we&#8217;ll just have to make do. I choose to only provision nodes that are spot lifecycle nodes which helps with the pricing, but I&#8217;ll break that down more in a later section.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.sliceofexperiments.com/subscribe?"><span>Subscribe now</span></a></p><h1>What code do I run to train the models?</h1><p>Selecting for L4 GPUs is very intentional, as L4 GPUs are of the Ada generation architecture. This ties in to the actual software stack that I&#8217;m using. Back in December, a good friend of mine messaged me about some <a href="https://www.reddit.com/r/LocalLLaMA/comments/188197j/80_faster_50_less_memory_0_accuracy_loss_llama/">new project that got posted to /r/LocalLlama</a>. That project was called <a href="https://github.com/unslothai/unsloth">unsloth</a>, which made extraordinary claims for improving performance of training Llama2 models. &#8220;80% faster and 50% less memory&#8221; was pretty big. To be perfectly honest, I haven&#8217;t personally measured the difference between using unsloth and not using it, but I appreciate the tenacity. Since then, I&#8217;ve had a few conversations with the technical mind behind it, <a href="https://twitter.com/danielhanchen">@danielhanchen</a>, and I must say, he&#8217;s a very friendly and very brilliant guy. It&#8217;s amazing what a cold email can do!</p><p>My code is based <em>heavily </em>on Unsloth&#8217;s <a href="https://colab.research.google.com/drive/135ced7oHytdxu3N2DNe1Z0kqjyYIkDXp?usp=sharing">own Google Colab example</a>. Here&#8217;s my fine tuning code verbatim.</p><pre><code>from datasets import load_dataset
from datetime import datetime
from transformers import TrainingArguments, TrainerState
from transformers.trainer_utils import get_last_checkpoint
from trl import SFTTrainer
from unsloth import FastLanguageModel
import os
import shutil
import torch

max_seq_length = 8192

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="unsloth/llama-3-8b-bnb-4bit",
    max_seq_length=max_seq_length,
    dtype=None,
    load_in_4bit=True,
)

model = FastLanguageModel.get_peft_model(
    model,
    r=16,
    target_modules=[
        "q_proj",
        "k_proj",
        "v_proj",
        "o_proj",
        "gate_proj",
        "up_proj",
        "down_proj",
    ],
    lora_alpha=16,
    lora_dropout=0,
    bias="none",
    use_gradient_checkpointing="unsloth",
    random_state=1337,
    use_rslora=False,
    loftq_config=None,
)

alpaca_prompt = """Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.

### Instruction:
{}

### Input:
{}

### Response:
{}"""

EOS_TOKEN = tokenizer.eos_token


def formatting_prompts_func(examples):
    instructions = examples["instruction"]
    inputs = examples["input"]
    outputs = examples["output"]
    texts = []
    for instruction, input, output in zip(instructions, inputs, outputs):
        text = alpaca_prompt.format(instruction, input, output) + EOS_TOKEN
        texts.append(text)
    return {
        "text": texts,
    }


dataset = load_dataset("yahma/alpaca-cleaned", split="train")
dataset = dataset.map(
    formatting_prompts_func,
    batched=True,
)

trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=dataset,
    dataset_text_field="text",
    max_seq_length=max_seq_length,
    dataset_num_proc=2,
    packing=False,
    args=TrainingArguments(
        per_device_train_batch_size=4,
        gradient_accumulation_steps=4,
        include_num_input_tokens_seen=True,
        warmup_steps=5,
        num_train_epochs=1,
        save_steps=50,
        save_total_limit=3,
        learning_rate=2e-4,
        fp16=not torch.cuda.is_bf16_supported(),
        bf16=torch.cuda.is_bf16_supported(),
        logging_steps=5,
        optim="adamw_8bit",
        weight_decay=0.01,
        lr_scheduler_type="linear",
        seed=1337,
        output_dir="/mnt/outputs",
    ),
)


def get_checkpoints(checkpoint_dir):
    checkpoints = [
        os.path.join(checkpoint_dir, d)
        for d in os.listdir(checkpoint_dir)
        if os.path.isdir(os.path.join(checkpoint_dir, d))
    ]
    checkpoints.sort(reverse=True, key=lambda x: int(x.split("-")[-1]))
    return checkpoints


def safe_resume_training(trainer, checkpoint_dir):
    checkpoints = get_checkpoints(checkpoint_dir)
    for checkpoint in checkpoints:
        try:
            trainer_state_file = os.path.join(checkpoint, "trainer_state.json")
            TrainerState.load_from_json(trainer_state_file)
            print(f"Resuming training from {checkpoint}")
            trainer.train(resume_from_checkpoint=checkpoint)
            return
        except FileNotFoundError as e:
            print(f"Error: {e}")
            print(f"The checkpoint {checkpoint} is incomplete and will be skipped.")
            shutil.rmtree(checkpoint, ignore_errors=True)

    print("No valid checkpoint found. Starting training from scratch.")
    trainer.train()


start_time = datetime.now()
safe_resume_training(trainer, "/mnt/outputs")
print("Training took: ", datetime.now() - start_time)
trainer.save_state()
model.save_pretrained("/mnt/llama3_lora_model")</code></pre><p>I had ChatGPT generate the code for cleaning up faulty checkpoints. Since I&#8217;m running on spot nodes, I occasionally get nodes taken away from me that don&#8217;t get to fully checkpoint, so this is my solution for robustness.</p><p>Notice that I&#8217;m using the Unsloth quantized version of the 8b. The code sample is training using an Alpaca dataset and an Alpaca instruction format. In terms of training cheaply, I don&#8217;t think that will make a difference though. People can take my recipe and do other data with it.</p><h1>What do I use for file storage?</h1><p>Amazon S3 isn&#8217;t a file system, it&#8217;s an object store. Which makes it the perfect file system for me to use for storing the models. I have the <a href="https://docs.aws.amazon.com/eks/latest/userguide/s3-csi.html">Mountpoint for Amazon S3 CSI driver</a> installed on my EKS cluster, which lets me configure an S3 bucket as a PVC. I haven&#8217;t benchmarked it for I/O throughput but for the model sizes and data set sizes that I&#8217;ve been using, it&#8217;s been great so far.</p><h1>So how much does it cost?</h1><p>Let&#8217;s break down the costs of this setup. I&#8217;m not going to factor in the cost of the EKS cluster itself or any CPU nodes you need. I&#8217;m only going to focus on the cost of the actual GPUs. The node definitions that I shared earlier in this post are selecting only g6 instances. I allow for selecting to larger instance types just to deal with the availability of the instances themselves. Even though both a g6.xlarge up to the g6.8xlarge only have a single L4 GPU on them, having the different amounts of CPU and memory can make them more or less appetizing to other customers. Since the spot lifecycle type is always going to be excess compute capacity, I&#8217;ll take whatever I can get when I&#8217;m playing with my infrastructure.</p><p>At the g6.8xlarge upper end of the spectrum, that historically caps out at an hourly cost of about $0.22 per hour in the us-west-2 region.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_4De!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F442dcdf9-17aa-4da9-bba4-4596647d22a5_298x224.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_4De!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F442dcdf9-17aa-4da9-bba4-4596647d22a5_298x224.png 424w, https://substackcdn.com/image/fetch/$s_!_4De!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F442dcdf9-17aa-4da9-bba4-4596647d22a5_298x224.png 848w, https://substackcdn.com/image/fetch/$s_!_4De!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F442dcdf9-17aa-4da9-bba4-4596647d22a5_298x224.png 1272w, https://substackcdn.com/image/fetch/$s_!_4De!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F442dcdf9-17aa-4da9-bba4-4596647d22a5_298x224.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_4De!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F442dcdf9-17aa-4da9-bba4-4596647d22a5_298x224.png" width="298" height="224" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/442dcdf9-17aa-4da9-bba4-4596647d22a5_298x224.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:224,&quot;width&quot;:298,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!_4De!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F442dcdf9-17aa-4da9-bba4-4596647d22a5_298x224.png 424w, https://substackcdn.com/image/fetch/$s_!_4De!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F442dcdf9-17aa-4da9-bba4-4596647d22a5_298x224.png 848w, https://substackcdn.com/image/fetch/$s_!_4De!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F442dcdf9-17aa-4da9-bba4-4596647d22a5_298x224.png 1272w, https://substackcdn.com/image/fetch/$s_!_4De!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F442dcdf9-17aa-4da9-bba4-4596647d22a5_298x224.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>When there are g6.xlarge instances available, the prices on spot instances is a whopping $0.085 an hour. That&#8217;s less than 9 cents per hour for a 24GB GPU!</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!komN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcefae6bf-188b-4e39-81f6-79758aaedaf0_436x246.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!komN!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcefae6bf-188b-4e39-81f6-79758aaedaf0_436x246.png 424w, https://substackcdn.com/image/fetch/$s_!komN!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcefae6bf-188b-4e39-81f6-79758aaedaf0_436x246.png 848w, https://substackcdn.com/image/fetch/$s_!komN!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcefae6bf-188b-4e39-81f6-79758aaedaf0_436x246.png 1272w, https://substackcdn.com/image/fetch/$s_!komN!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcefae6bf-188b-4e39-81f6-79758aaedaf0_436x246.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!komN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcefae6bf-188b-4e39-81f6-79758aaedaf0_436x246.png" width="436" height="246" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cefae6bf-188b-4e39-81f6-79758aaedaf0_436x246.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:246,&quot;width&quot;:436,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:26026,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!komN!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcefae6bf-188b-4e39-81f6-79758aaedaf0_436x246.png 424w, https://substackcdn.com/image/fetch/$s_!komN!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcefae6bf-188b-4e39-81f6-79758aaedaf0_436x246.png 848w, https://substackcdn.com/image/fetch/$s_!komN!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcefae6bf-188b-4e39-81f6-79758aaedaf0_436x246.png 1272w, https://substackcdn.com/image/fetch/$s_!komN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcefae6bf-188b-4e39-81f6-79758aaedaf0_436x246.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Even the absolute very cheapest instance on <a href="https://cloud.vast.ai/">Vast.ai</a> is still $0.10 per hour.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!MmHw!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77aac0fa-f3b1-44a7-9df4-fc30ab3731f2_1606x226.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!MmHw!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77aac0fa-f3b1-44a7-9df4-fc30ab3731f2_1606x226.png 424w, https://substackcdn.com/image/fetch/$s_!MmHw!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77aac0fa-f3b1-44a7-9df4-fc30ab3731f2_1606x226.png 848w, https://substackcdn.com/image/fetch/$s_!MmHw!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77aac0fa-f3b1-44a7-9df4-fc30ab3731f2_1606x226.png 1272w, https://substackcdn.com/image/fetch/$s_!MmHw!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77aac0fa-f3b1-44a7-9df4-fc30ab3731f2_1606x226.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!MmHw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77aac0fa-f3b1-44a7-9df4-fc30ab3731f2_1606x226.png" width="1456" height="205" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/77aac0fa-f3b1-44a7-9df4-fc30ab3731f2_1606x226.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:205,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!MmHw!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77aac0fa-f3b1-44a7-9df4-fc30ab3731f2_1606x226.png 424w, https://substackcdn.com/image/fetch/$s_!MmHw!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77aac0fa-f3b1-44a7-9df4-fc30ab3731f2_1606x226.png 848w, https://substackcdn.com/image/fetch/$s_!MmHw!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77aac0fa-f3b1-44a7-9df4-fc30ab3731f2_1606x226.png 1272w, https://substackcdn.com/image/fetch/$s_!MmHw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F77aac0fa-f3b1-44a7-9df4-fc30ab3731f2_1606x226.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>And how do I get the $0.03 number that I click baited you with? Well, that&#8217;s about how much it costs to process over 1,000,000 tokens.</p><p>In my TrainingArguments, I set <code>include_num_input_tokens_seen=True</code>. I log every 5 steps, so I can see how long it takes to do 5 steps and how many tokens were processed over those 5 steps. Then I can extrapolate. It takes approximately 30 seconds to do 5 steps, which gets me just about ~24,512 tokens processed. That&#8217;s about ~817 tokens per second. Which comes out to about $0.02917 per 1,000,000 tokens.</p><p>Let me know if you&#8217;d be interested in tutorials for the rest of the infrastructure.</p><p>Thanks for reading.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.sliceofexperiments.com/subscribe?"><span>Subscribe now</span></a></p>]]></content:encoded></item><item><title><![CDATA[Distributing stateless network traffic across multiple clouds]]></title><description><![CDATA[I got them multi-cloud index.html pages]]></description><link>https://www.sliceofexperiments.com/p/distributing-stateless-network-traffic</link><guid isPermaLink="false">https://www.sliceofexperiments.com/p/distributing-stateless-network-traffic</guid><dc:creator><![CDATA[Aaron Batilo]]></dc:creator><pubDate>Tue, 18 Jul 2023 20:15:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!o6dZ!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7aaed77e-5db5-4e6b-982e-1802998c069a_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I started a new job recently and for the first time in my career, I&#8217;ve had to start using Google Cloud. Me being the Kubernetes shill that I am, that meant spinning up a GKE cluster as my way of learning more about the Google Cloud ecosystem. As a long time (~7 year) AWS person, honestly I kind of hate Google Cloud. They make it easier to get started than AWS, but they do so by picking a bunch of defaults for you and kind of hiding that away, and I disagree with almost all of the defaults that get set.</p><p>That being said, I can appreciate that if you just care to get something running, you can do that a lot more easily with GCP than with AWS. But if you just want to get something running, IMO you should be using a more managed platform. Anyways, that&#8217;s all probably for another day or another post.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>Thanks <a href="https://registry.terraform.io/modules/terraform-google-modules/kubernetes-engine/google/latest">terraform-google-kubernetes-engine</a> module, I was able to get a spot node GKE cluster up and running pretty easily. On top of that, I use ArgoCD for deploying my side projects and registering another cluster as a target was also really straightforward. I had to create a Kubernetes service account and pass that token to ArgoCD, and I hate static credentials, but that&#8217;s fine for now. I think one day I might change it around to do some OIDC based &#8220;impersonations&#8221; as GCP likes to call them.</p><p>Okay, cool. I have two clusters up. Now what? Do I create a federated mesh between them? Is there something easier? But let&#8217;s start with me telling you about what I actually did first that worked super poorly.</p><p>The first thing that I did was to run the container that I use for serving https://mentallyanimated.com but in both clusters. It&#8217;s just a single static HTML page, but you can replace that with any stateless service. I learned that by default, GCP will actually install an ingress controller for you that leverages a &#8220;classic&#8221; HTTP(S) load balancer for you. Okay, simple enough. I learned about things like the <code>BackendConfig</code> CRD. More notably, I learned about the <code>ManagedCertificate</code> CRD which would request an SSL certificate. I pointed my DNS for https://mentallyanimated.com and an hour later my SSL certificate was provisioned.</p><p>My SSL certificate was provisioned&#8230; wait, how? I couldn&#8217;t find any actual documentation about the details, but I&#8217;m assuming that the GCP load balancer is doing an HTTP01 ACME challenge and serving the validation HTTP request automatically for me. I couldn&#8217;t actually find any way to forcefully make the ManagedCertificate CRD do a DNS validation for the SSL certificate. Little did I know that this would bite me later, but whatever. In the moment, I was excited. I had my first application scheduled in GKE.</p><p>In order to do cross cloud, I ended up doing the most simple thing I could think of. I did 50/50 weighted DNS. Essentially, this is what I did:</p><pre><code>resource "aws_route53_record" "gcp_www" {
  weighted_routing_policy {
    weight = 10
  }
  # ...everything else
}

resource "aws_route53_record" "aws_www" {
  weighted_routing_policy {
    weight = 10
  }
  # ...everything else
}</code></pre><p>And it actually worked beautifully. I was now sometimes serving my page from AWS and sometimes from GCP. I was multi-cloud. There was just one problem. I had forgotten to add the alternative subject for my ManagedCertificate for the apex domain. https://mentallyanimated.com with no subdomain. And this is where I started to actually notice problems.</p><p>Firstly, it doesn&#8217;t seem like GCP technically supported that. Even though I was doing an A record from the domain to a static global IP address that I provisioned through GCP, the SSL certificate would just *never* provision. I waited days to see if that would help. I think because I was serving everything from two clouds, the HTTP01 challenge that I believe is happening behind the scenes was never consistently resolving, because of the DNS load balancing. So I gave up on that.</p><h2>Cloudflare tunnels</h2><p>Several years ago, an old mentor of mine told me about how he had successfully migrated to having completely and utterly private Kubernetes clusters, including the control plane. For whatever reason, my brain remembered that conversation and remembered that he had done so by using Cloudflare for ingress. Sure enough, there&#8217;s actually a <a href="https://github.com/cloudflare/cloudflare-ingress-controller">Cloudflare ingress controller</a> that leverages their tunnels for creating a connection from your environment into their environment, all through an egress only connection. The connection comes from your environment and are authenticated into their environment. Unfortunately, the ingress controller is super outdated. I started looking at their docs and found that they actually have <a href="https://developers.cloudflare.com/cloudflare-one/tutorials/many-cfd-one-tunnel/">documentation about exposing applications in Kubernetes</a>, exactly like I wanted.</p><p>I reached out to the same mentor and asked if he was using the ingress controller or if he went another route. He said that he runs the cloudflared tunnel service as its own Deployment in his clusters and forward traffic from there to a gateway/reverse proxy. I had an extremely similar but different idea. documentation about exposing applications in KubernetesWhat if I run cloudflared as a sidecar to my ingress controller? I tried it, and it worked phenomenally, and all it took was a few lines of terraform to make it work. All I had to do was define a cloudflare_tunnel resource, a cloudflare_tunnel_config, and then add my additional sidecar container. It basically all just looks like this:</p><pre><code>resource "cloudflare_tunnel" "mentallyanimated" {
  account_id = local.cloudflare_account_id
  name       = "mentallyanimated"
  secret     = data.aws_ssm_parameter.cloudflare_mentallyanimated_tunnel_secret.value
}

resource "cloudflare_tunnel_config" "traefik" {
  account_id = local.cloudflare_account_id
  tunnel_id  = cloudflare_tunnel.mentallyanimated.id

  config {
    ingress_rule {
      service = "http://localhost:8000"
    }
  }
}

resource "helm_release" "traefik" {
  # ...everything else
  values = [
    &lt;&lt;EOF
deployment
  additionalContainers:
  - name: cloudflared
    image: cloudflare/cloudflared:latest
    args:
    - tunnel
    - run
    - --token
    - ${cloudflare_tunnel.mentallyanimated.tunnel_token}
EOF
  ]
  depends_on = [module.traefik_crds]
}
</code></pre><p>Since cloudflared and traefik are in the same pod, all I have to do is have cloudflared forward traffic to http://localhost:8000 where traefik is listening. Traefik is already my ingress controller and will redirect traffic to my other applications based on the Host header. Having cloudflared as a sidecar means that when traefik scales out, so will cloudflared.</p><p>The best part is that since I&#8217;m using traefik as my ingress controller, it&#8217;s already cloud agnostic. I don&#8217;t need to expose it using a cloud specific load balancer anymore so I can get rid of my NLB and I can disable the GKE default ingress controller. My traefik setup in both clusters can be exactly the same and Cloudflare will do the round robin load balancing across different clouds. It&#8217;s &#8220;that easy&#8221;.</p><div><hr></div><p>This solves the problem of doing stateless applications. I&#8217;ll throw &#8220;how to do stateful applications&#8221; cross cloud on to my to-do list. For now, this is perfect.</p><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[I turned my gaming computer into a chatbot]]></title><description><![CDATA[But also it's still my gaming computer]]></description><link>https://www.sliceofexperiments.com/p/i-turned-my-gaming-computer-into</link><guid isPermaLink="false">https://www.sliceofexperiments.com/p/i-turned-my-gaming-computer-into</guid><dc:creator><![CDATA[Aaron Batilo]]></dc:creator><pubDate>Tue, 04 Jul 2023 20:15:58 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!zLB0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F004d13e2-bc9d-42c1-897f-34b3a2baffc4_1632x1154.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>TL;DR: I installed WSL on my gaming computer and then installed tailscale to expose it.</strong></p><div><hr></div><p>Recently, a friend of mine just started the <a href="https://sillycon.substack.com/">Sillycon Valley</a> newsletter where he shares what he learns about getting into AI and machine learning. He&#8217;s been playing with some image generation and asked me to run the code on my computer. With some of the memory optimization flags and running more on the CPU over my GPUs, I was able to get images to generate, but my image generation for my tower would take 2-3x as long as his laptop! I knew there had to be a better way.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments! New posts every other Tuesday</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>I have two computers. I have an older tower that I built in ~2015 which I built to do some machine learning experiments at the time. It has two GTX 970s SLId together. Each card has 4GB of VRAM which was pretty good at the time. I could run a bunch of the open source models that were being published. Today, this is still my main computer and the one that I do all my development on. I also have a pre-built gaming computer that I bought in 2021 that has a GTX 3090 in it. I got this computer to get more into PC gaming again after years of hiatus. Hilariously, most of the time I&#8217;m only playing Super Smash Bros Melee on an emulator and I don&#8217;t need such a powerful graphics card.</p><p>Recently, all the transformer models have been getting larger and larger and the standard for a lot of these models is to use an NVIDIA A100 which has 40GB of VRAM. The larger models built by well capitalized companies are built on hundreds or thousands and maybe even tens of thousands of these. My 970s just can&#8217;t keep up anymore, so I haven&#8217;t done any ML experimenting in a while.</p><p>Since I have the 3090, I&#8217;ve thought about using that machine as my dev machine, but my brain really wants to keep it a Windows gaming PC. I think because that&#8217;s what I bought it for, that&#8217;s what I want to keep it as. You might tell me to just set it up with a dual boot, but I don&#8217;t want to do that either, because I don&#8217;t want to have to decide between the two options. For years I just left it as is and then one morning I realized that if I setup a Linux VM then I could just access the 3090 like I would access an EC2 instance.</p><p>I installed the <a href="https://learn.microsoft.com/en-us/windows/wsl/install">Windows Subsystem for Linux</a> with the default Ubuntu, and sure enough:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zLB0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F004d13e2-bc9d-42c1-897f-34b3a2baffc4_1632x1154.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zLB0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F004d13e2-bc9d-42c1-897f-34b3a2baffc4_1632x1154.png 424w, https://substackcdn.com/image/fetch/$s_!zLB0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F004d13e2-bc9d-42c1-897f-34b3a2baffc4_1632x1154.png 848w, https://substackcdn.com/image/fetch/$s_!zLB0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F004d13e2-bc9d-42c1-897f-34b3a2baffc4_1632x1154.png 1272w, https://substackcdn.com/image/fetch/$s_!zLB0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F004d13e2-bc9d-42c1-897f-34b3a2baffc4_1632x1154.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zLB0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F004d13e2-bc9d-42c1-897f-34b3a2baffc4_1632x1154.png" width="1456" height="1030" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/004d13e2-bc9d-42c1-897f-34b3a2baffc4_1632x1154.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1030,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:233465,&quot;alt&quot;:&quot;Screenshot of terminal results of nvidia-smi command&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Screenshot of terminal results of nvidia-smi command" title="Screenshot of terminal results of nvidia-smi command" srcset="https://substackcdn.com/image/fetch/$s_!zLB0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F004d13e2-bc9d-42c1-897f-34b3a2baffc4_1632x1154.png 424w, https://substackcdn.com/image/fetch/$s_!zLB0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F004d13e2-bc9d-42c1-897f-34b3a2baffc4_1632x1154.png 848w, https://substackcdn.com/image/fetch/$s_!zLB0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F004d13e2-bc9d-42c1-897f-34b3a2baffc4_1632x1154.png 1272w, https://substackcdn.com/image/fetch/$s_!zLB0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F004d13e2-bc9d-42c1-897f-34b3a2baffc4_1632x1154.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"></figcaption></figure></div><p>Without doing anything else, the WSL VM can already access the graphics cards. Perfect. The next thing that I did was configure an ssh server on the VM and then setup a port forward from the host computer to the VM. By following <a href="https://www.hanselman.com/blog/how-to-ssh-into-wsl2-on-windows-10-from-an-external-machine">this tutorial</a>, I was able to ssh right from my dev computer to the VM and thus to the graphics card.</p><p>At this point, I went and yoinked the code from my good pal, <a href="https://sillycon.substack.com/p/run-your-own-ai-image-generation">Sillycon Valley and his tutorial for running stable diffusion for doing image generation</a>. And then to put my own flavor on the images I tested generating, I started generating some anime pictures. In real-life time, I spent a few hours experimenting with different parameters and <a href="https://huggingface.co/docs/diffusers/optimization/fp16">memory and speed optimizations</a>.</p><p>I also started doing some experimenting with some of the smaller open source chatbot models like <a href="https://www.databricks.com/blog/2023/04/12/dolly-first-open-commercially-viable-instruction-tuned-llm">Dolly from Databricks</a>.</p><h2>Serving the model</h2><p>All it takes to run one of these models is:</p><pre><code>import torch
from fastapi import FastAPI
from pydantic import BaseModel
from transformers import pipeline

class Req(BaseModel):
    text: str
app = FastAPI()
generate_text = pipeline(model="databricks/dolly-v2-7b", torch_dtype=torch.bfloat16, trust_remote_code=True, device_map="auto")

@app.post("/generate")
async def gen(req: Req):
    res = generate_text(req.text)
    return {'response': res[0]["generated_text"]}</code></pre><p>This is great. Now I can send requests from my dev machine and generate all kinds of stuff with my 3090 and put it to work. But what if I want to expose things to the internet? Or just my projects? Well, I don&#8217;t want to have to expose my home network. I don&#8217;t want to do any kind of port forwarding from my router, etc. That&#8217;s where the magic of <a href="https://tailscale.com/">tailscale</a> comes in. Tailscale is a vpn service built on top of <a href="https://www.wireguard.com/">WireGuard</a>. I installed the tailscale client in the WSL VM and now it&#8217;s on my private tailnet.</p><p>Next I had to figure out how to get a tailscale client into my Kubernetes applications, so that I could have my projects access the GPU all privately and securely. <a href="https://tailscale.com/kb/1185/kubernetes/">Tailscale themselves have some documentation for this</a>. All I had to do was create and set an auth token and then add the tailscale image as a sidecar container.</p><p>It actually worked perfectly without any problems at all. I could instantly send requests to my HTTP server from containers in my cluster. Perhaps the craziest part for me is that latency from us-west-2, Oregon, to my computer here in Colorado was only about 80 milliseconds. The internet is amazing.</p><h2>Wrapping up</h2><p>I did discover that auth tokens in Tailscale have a max life of 90 days. And there&#8217;s no API for renewing them programmatically. I&#8217;d have to replace the token manually every 90 days unless I find a way to automate that through my browser or something.</p><p>Regardless, now I have image generation and text generation with some limited capability available to my experiments, all for a fraction of the price of having to pay a provider for every request. Since the open source, single computer models aren&#8217;t as capable, I might not change any projects, but maybe I will. Who knows!</p><p>Thanks for reading. See ya&#8217;ll in the next one.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Scaling event workloads by SQS queue depth]]></title><description><![CDATA[Continuing to be more scalable than needed]]></description><link>https://www.sliceofexperiments.com/p/scaling-event-workloads-by-sqs-queue</link><guid isPermaLink="false">https://www.sliceofexperiments.com/p/scaling-event-workloads-by-sqs-queue</guid><dc:creator><![CDATA[Aaron Batilo]]></dc:creator><pubDate>Tue, 20 Jun 2023 20:15:12 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!o6dZ!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7aaed77e-5db5-4e6b-982e-1802998c069a_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>TL;DR: I use the <a href="https://keda.sh/docs/2.10/scalers/aws-sqs/">KEDA SQS scaler</a></strong></p><div><hr></div><p>As part of the project I mentioned in my <a href="https://www.sliceofexperiments.com/p/syncing-auth0-users-to-my-own-database">last post</a>, files are uploaded from the user and stored in an S3 bucket. Then I need to process them and do something with that information. The type of file that gets uploaded is probably measured in the hundreds or thousands per user. And the processing might take several seconds or more per file and ideally, the users can upload as many of them at once as they have. I don&#8217;t want users to be worried about throttling themselves when they start uploading. As part of my plans, I want a desktop side client that just finds all the files in a folder and uploads them all.</p><p>One thing that I could do is only do the processing in bulk in some batch processing workflow. That&#8217;s lame though. I don&#8217;t want users, aka I don&#8217;t want to have to wait a day or a few hours to see the results of the analysis after I upload it. Maybe one day I actually have to do that to handle the scale, but if that&#8217;s the case, that&#8217;s an amazing success with the project. For now, instead what I&#8217;m doing is I&#8217;m just queueing up the upload events and putting them into an SQS queue. In my terraform, that basically looks like this:</p><pre><code>resource "aws_s3_bucket" "project" {
  bucket_prefix = local.project_bucket_name
}

resource "aws_sqs_queue" "project" {
  name_prefix               = "project"
  message_retention_seconds = 900
}

resource "aws_s3_bucket_notification" "project" {
  bucket = aws_s3_bucket.project.id

  queue {
    events    = ["s3:ObjectCreated:*"]
    queue_arn = aws_sqs_queue.project.arn
  }
}

# And some permissions stuff that I didn't include</code></pre><p>I have my S3 bucket, and then create an SQS queue and attach an <code>s3:ObjectCreated</code> notification on it. That means that every time an object is uploaded, a message gets put into an SQS queue with the full key to that new object.</p><p>Now, in my fever dream where I get thousands of users all trying to upload their backlog of files all at the same time, I can handle <a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/quotas-queues.html#:~:text=The%20number%20of%20messages%20that,queue%20can%20store%20is%20unlimited.&amp;text=For%20most%20standard%20queues%20(depending,yet%20deleted%20from%20the%20queue).">an unlimited number of upload messages</a>. S3 shouldn&#8217;t have any problems keeping up with the direct uploads, and the SQS queue should be able to hold all the messages.</p><p>But Aaron, if you have unlimited messages in the queue, or just a ton of them, and it takes a few seconds per file to be processed, won&#8217;t that mean that your users will have to wait hours or days anyways? Just like you said you wanted to avoid with doing batch processing.</p><p>You&#8217;re right, listener person, and frankly I&#8217;m getting kind of sick of you pointing out all my flaws. Alas, we go on. I continue my adversarial relationship made up of these hallucinated conflicts.</p><h2>Scaling the consumers</h2><p>In my post about <a href="https://www.sliceofexperiments.com/p/scale-from-0-saving-nickels-to-feed">scaling from 0</a>, I setup the <a href="https://www.sliceofexperiments.com/p/scale-from-0-saving-nickels-to-feed">KEDA</a> scaler which has way more plugins other than just queuing up HTTP requests. Right now, the <a href="https://keda.sh/docs/2.10/scalers/aws-sqs/">AWS SQS Queue</a> scaler is looking mighty tasty. Unfortunately, it only supports scaling by queue depth. I really wish it would scale based on the average age of messages in the queue. I think that&#8217;s a much better metric for understanding the SLA/user experience of people who are waiting for their messages to be processed. We&#8217;ll make do though. This is already more work than I&#8217;ll probably ever need but it&#8217;s kind of the minimum need to make me feel good about the system&#8217;s design.</p><p>There&#8217;s a second order effect for why having this autoscaler is awesome. With SQS, one of the components to pricing is the number of API requests. Even if you make a request for messages and there aren&#8217;t any messages, you pay for that. <a href="https://repost.aws/knowledge-center/sqs-high-charges#:~:text=The%20most%20common%20cause%20for%20a%20high%20number%20of%20SQS%20requests%20are%20empty%20receives">The most common cause for a high number of SQS requests are empty receives</a>. The KEDA scaler lets me scale these consumers down to 0, so if there isn&#8217;t any work to do, then there are no workers that are sending long poll requests.</p><h2>Wrapping up</h2><p>The last thing I want to talk about is the IAM permissioning of the KEDA controller. According to the configuration, there&#8217;s a way to use the identity of the application that is being scaled out to read the SQS metrics that are used for scaling. In practice, this is a little inconvenient because the way it works is that you have to have the trust policy to make the KEDA controller able to do the role assumption to the role of the application. I got lazy and instead of doing the extra step with the role assumptions, I just configured this scaler to use the permissions of the KEDA controller itself and then gave the KEDA controller identity the ability to read metrics about the relevant SQS queue. I don&#8217;t like having the permissions on this controller but&#8230; it&#8217;s my personal environment that no one else uses and it&#8217;s a read only set of permissions so meh.</p><p>That&#8217;s it for this post. Thanks for reading and see ya&#8217;ll for the next one.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Syncing Auth0 users to my own database]]></title><description><![CDATA[Gotta keep my own records]]></description><link>https://www.sliceofexperiments.com/p/syncing-auth0-users-to-my-own-database</link><guid isPermaLink="false">https://www.sliceofexperiments.com/p/syncing-auth0-users-to-my-own-database</guid><dc:creator><![CDATA[Aaron Batilo]]></dc:creator><pubDate>Tue, 06 Jun 2023 20:15:08 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!o6dZ!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7aaed77e-5db5-4e6b-982e-1802998c069a_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>TL;DR: Auth0 Actions have what&#8217;s basically event triggered webhooks</strong></p><div><hr></div><p>I&#8217;m working on a small side project to automate something for myself. I think it&#8217;ll be useful to other people and so I took this as an opportunity to build it such that other people would also be able to use it if they wanted to. It&#8217;s a much larger project than my usual afternoon long or weekend long projects, which means I can milk multiple posts out of it if I wanted to. I&#8217;ll try to spread them out though if I have other things that I&#8217;ve worked on.</p><p>I&#8217;ve worked at multiple companies now that have used Auth0 for user management. Conceptually, I understand fairly well how JWT based authentication works. I also understand things like machine-to-machine, etc. I&#8217;ve built websites with user management on other platforms, but I&#8217;ve never actually built anything with Auth0 for myself from scratch. Something new for me, delightful. Ideally, I&#8217;d have done more of this in Terraform to keep multiple environments in-sync but I&#8217;ve actually been pretty lazy with this project compared to what I normally keep as my standard. I don&#8217;t even run anything locally when developing this project. I&#8217;m sure one day I should fix that but my push to live pipeline takes about 60-70 seconds. I know that can be a pretty long iteration time, but everything I have written to this point is really simple so far so&#8230; meh.</p><p>I created the Auth0 application, configured for a React Single Page Application, aka client side only JWTs. Easy enough. Now though&#8230; I want to store some user specific information. Like for example, I want user&#8217;s to be able to login with multiple different identity providers but still have the same account on my backend. If someone logs in with Twitch credentials or with Discord credentials (it&#8217;s a gaming related website), I want them to be considered the same user. So I need to track a user ID on my side of things that will be mapped from their individual identity provider logins.</p><h2>Auth0 actions</h2><p>Auth0 has a login flow where you can hook in and modify the JWT or do some other minor actions before anything ever reaches your service.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_abY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c31f054-4bcb-4b05-a929-57b0cb4500d2_971x299.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_abY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c31f054-4bcb-4b05-a929-57b0cb4500d2_971x299.png 424w, https://substackcdn.com/image/fetch/$s_!_abY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c31f054-4bcb-4b05-a929-57b0cb4500d2_971x299.png 848w, https://substackcdn.com/image/fetch/$s_!_abY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c31f054-4bcb-4b05-a929-57b0cb4500d2_971x299.png 1272w, https://substackcdn.com/image/fetch/$s_!_abY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c31f054-4bcb-4b05-a929-57b0cb4500d2_971x299.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_abY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c31f054-4bcb-4b05-a929-57b0cb4500d2_971x299.png" width="971" height="299" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6c31f054-4bcb-4b05-a929-57b0cb4500d2_971x299.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:299,&quot;width&quot;:971,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Diagram showing the Actions Login Flow.&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Diagram showing the Actions Login Flow." title="Diagram showing the Actions Login Flow." srcset="https://substackcdn.com/image/fetch/$s_!_abY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c31f054-4bcb-4b05-a929-57b0cb4500d2_971x299.png 424w, https://substackcdn.com/image/fetch/$s_!_abY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c31f054-4bcb-4b05-a929-57b0cb4500d2_971x299.png 848w, https://substackcdn.com/image/fetch/$s_!_abY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c31f054-4bcb-4b05-a929-57b0cb4500d2_971x299.png 1272w, https://substackcdn.com/image/fetch/$s_!_abY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c31f054-4bcb-4b05-a929-57b0cb4500d2_971x299.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://auth0.com/docs/customize/actions/flows-and-triggers/login-flow">https://auth0.com/docs/customize/actions/flows-and-triggers/login-flow</a></figcaption></figure></div><p>Every time someone logs in, a request is sent to my backend which then checks to see if this email has ever been seen before. If the email hasn&#8217;t, we generate a new &#8220;account&#8221; on my database, and return a unique ID for that user. If the email has already been seen, then we fetch the previously used unique ID and send it back. Simple enough. Right? Right? You know that every time I say &#8220;simple enough&#8221;, there&#8217;s like a 50% chance that it&#8217;s not actually that simple.</p><p>Auth0 Actions send that HTTP request but unlike other webhook style workflows like from <a href="https://auth0.com/docs/customize/actions/flows-and-triggers/login-flow#add-user-roles-to-id-and-access-tokens">Stripe</a> or <a href="https://auth0.com/docs/customize/actions/flows-and-triggers/login-flow#add-user-roles-to-id-and-access-tokens">Slack</a>, the Auth0 Action requests don&#8217;t come with any kind of verifiable signature. So people could send requests to my endpoint and I&#8217;d just blindly assume that they are from Auth0. After reading through the docs, I found that the way to go about this is to <a href="https://auth0.com/docs/customize/actions/write-your-first-action#add-a-secret">add a secret to the action</a> that I&#8217;d then verify from my server. I wish I didn&#8217;t have to do this but at least it&#8217;s not that much extra work.</p><h2>Verifying Auth0 tokens</h2><p>Last thing I wanted to mention in this post is verifying the tokens. One of the values of using a JWT is that they&#8217;re verifiable completely on their own. You don&#8217;t have to make a request to a third party service in order to know that it&#8217;s a valid token, which is important for things like access tokens that you might use to grant access to private information.</p><p>However, unbeknownst to me, Auth0 may actually not grant a JWT for an access token. If you do not specify an <code>audience </code>when creating your client, you will instead receive an opaque token that doesn&#8217;t have any information on its own. That means that you have to send this request to Auth0, specifically their <a href="https://auth0.com/docs/api/authentication#get-user-info">/userinfo</a> endpoint in order to get user information from the access token. I thought about setting an audience or even just sending the id token while all I care about right now is authentication, not authorization, but instead I just got lazy and started sending requests to userinfo.</p><p>This was probably a mistake because it turns out that the userinfo endpoint has a fairly low rate limit. I don&#8217;t know exactly what it is but I kept hitting it a lot, so I just threw an in-memory cache on my server side. These tokens are valid for 24 hours by default, so having a 5 minute cache isn&#8217;t going to be a problem.</p><h2>Wrapping up</h2><p>That&#8217;s all for now. Thanks for reading and I&#8217;ll see ya&#8217;ll for the next one.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[I made text embeddings for every anime]]></title><description><![CDATA[I don't know what to do with them]]></description><link>https://www.sliceofexperiments.com/p/i-made-text-embeddings-for-every</link><guid isPermaLink="false">https://www.sliceofexperiments.com/p/i-made-text-embeddings-for-every</guid><dc:creator><![CDATA[Aaron Batilo]]></dc:creator><pubDate>Tue, 23 May 2023 20:15:54 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Cixl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcdaa3407-143b-4a95-abbd-15074ceddfd8_1259x566.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>TL;DR: <a href="https://huggingface.co/datasets/abatilo/myanimelist-embeddings">https://huggingface.co/datasets/abatilo/myanimelist-embeddings</a></strong></p><div><hr></div><p>While writing this, I&#8217;m still in between jobs. I wanted to still take a rest while on the break but also I wanted to do <em>something</em> to keep myself from getting too restless. So, I embedded some anime text!</p><p>So, what is a text embedding? Simply put, machine learning doesn&#8217;t do that well with just raw text itself. Machine learning and AI techniques usually involve multiplying matrices and what not. Math stuff. Text embeddings are representations of text in large arrays, so that we can do math stuff to them. At the end of my last post, I did &#8220;announce&#8221; (kek, to my ~100 readers), that I was going to be starting a job at <a href="https://cohere.com">Cohere AI</a>. To be very explicit again, <strong>when I did this work, I had not started the job yet</strong>. This was my own work.</p><p>Anyways, Cohere has an <a href="https://docs.cohere.com/reference/embed">embeddings endpoint</a>. And a few weeks ago, Cohere released a big data set of <a href="https://txt.cohere.com/embedding-archives-wikipedia/">embeddings of all of Wikipedia</a>. While on my break, I&#8217;ve been watching a lot of anime to pass the time, and I wondered what kind of experiments I could do revolving around that. And so I scraped the synopsis details of every anime I could get my hands on, and then pushed those through the embeddings endpoint on the Cohere API.</p><h2>Scraping MAL</h2><p>I use <a href="https://myanimelist.net">MyAnimeList.net</a> for tracking the anime that I watch, but also they have synopses written up for all the anime they keep track of. I was stoked to even find out that they have an <a href="https://myanimelist.net/apiconfig/references/api/v2">official API</a>. I thought this was going to be a breeze, until I realized that they don&#8217;t have a batch or bulk list endpoint. You have to query each ID for an anime one at a time. Not only that, they don&#8217;t expose a way for you to know how many anime there are. Even more, they ask to wait between 500ms - 1000ms per request and they don&#8217;t have any formal way to actually rate limit you, so it&#8217;s more of an honor system.</p><p>The API itself is also very similar to a GraphQL style API, but it&#8217;s not. If you make a request to a single anime, you&#8217;ll get a few fields by default. You have to request additional fields with a query parameter. At first, I tried requesting every single field and had a for loop to go from 1 to 100,000 with a delay of 500ms between requests. I hit run and then a minute or two later, I get a message from a friend that knew I was doing this project, and she points out to me that MAL has stopped loading, and I check my script and am seeing dozens of timeouts. Sorry MyAnimeList! I don&#8217;t know this for sure but I&#8217;m going to guess that requesting every field must have some kind of multiplicative database query effect. So instead I reduced the fields that I was requesting just to the synopsis and alternative names and I slowed the requests to doing 1 per second.</p><pre><code>for i := 0; i &lt; 100000; i++ {
&#9;time.Sleep(1 * time.Second)

&#9;anime, resp, err := c.Anime.Details(ctx, i,
&#9;&#9;mal.Fields{
&#9;&#9;&#9;"alternative_titles",
&#9;&#9;&#9;"synopsis",
&#9;&#9;},
&#9;)
        // ...the rest of it...
}</code></pre><p>The very last ID that I got back was 55,254 but there were only 20,051 actual valid entries. Many of the IDs ended up returning a 404.</p><h2>Making the embeddings</h2><p>The Cohere API mentions that for best performance that you should use text snippets that are 512 tokens. I measured and there were something like 20 synopsis that had more than 512 tokens, which you can choose to truncate at the API level but I decided to just go for it. The other nice thing about the API is that you can actually send requests to embed in batches. So that&#8217;s what I did. I sent 20,051 synopses but did so 96 at a time. A few minutes and $20 later, I had the embeddings for all of the anime synopses!</p><p>What do I do with the embeddings? Originally, I had the idea of trying some kind of recommendation or natural language search engine over the data set, but I &#8220;ran out of time&#8221; in the sense that I ran out of attention span. Maybe I&#8217;ll come back to it. However, I did yoink the <a href="https://txt.cohere.com/embedding-archives-wikipedia/#:~:text=%23%20Get%20the%20query,%3D3)">simplified nearest neighbor code from the inspiration Cohere blog post</a> and tried a few examples.</p><pre><code>What do you want to see?: a pokemon trainer wants to be the very best
Pokemon
Pok&#233;mon are peculiar creatures with a vast array of different abilities and appearances; many people, known as Pok&#233;mon trainers, capture and train them, often with the intent of battling others. Young Satoshi has not only dreamed of becoming a Pok&#233;mon trainer but also a "Pok&#233;mon Master," and on the arrival of his 10th birthday, he finally has a chance to make that dream a reality. Unfortunately for him, all three Pok&#233;mon available to beginning trainers have already been claimed and only Pikachu, a rebellious Electric-type Pok&#233;mon, remains. However, this chance encounter would mark the start of a lifelong friendship and an epic adventure!

Setting off on a journey to become the very best, Satoshi and Pikachu travel across beautiful, sprawling regions with their friends Kasumi, a Water-type trainer, and Takeshi, a Rock-type trainer. But danger lurks around every corner. The infamous Team Rocket is always nearby, seeking to steal powerful Pok&#233;mon through nefarious schemes. It'll be up to Satoshi and his friends to thwart their efforts as he also strives to earn the eight Pok&#233;mon Gym Badges he'll need to challenge the Pok&#233;mon League, and eventually claim the title of Pok&#233;mon Master.

[Written by MAL Rewrite]

Pokemon Best Wishes!
As with both the Advanced Generation and Diamond &amp; Pearl series before it, the Best Wishes! series begins with only Satoshi, headed off to the Isshu region, located far away from Kanto, Johto, Houen, and Sinnoh, with his Pikachu. After he meets up with the new trainer and rival Shooty and the region's Professor Araragi, he gains traveling companions in Iris, a girl from a town known for its Dragon Pok&#233;mon, and Dent, Pok&#233;mon Connoisseur and the Grass Pok&#233;mon specialist of the three Sanyou City Gym Leaders.

Pokemon Sun &amp; Moon
After his mother wins a free trip to the islands, Pok&#233;mon trainer Satoshi and his partner Pikachu head for Melemele Island of the beautiful Alola region, which is filled with lots of new Pok&#233;mon and even variations of familiar faces. Eager to explore the island, Satoshi and Pikachu run wild with excitement, quickly losing their way while chasing after a Pok&#233;mon. The pair eventually stumbles upon the Pok&#233;mon School, an institution where students come to learn more about these fascinating creatures.

At the school, when he and one of the students&#8212;the no-nonsense Kaki&#8212;have a run-in with the nefarious thugs of Team Skull, Satoshi discovers the overwhelming might of the Z-Moves, powerful attacks originating from the Alola region that require the trainer and Pok&#233;mon to be in sync. Later that night, he and Pikachu have an encounter with the guardian deity Pok&#233;mon of Melemele Island, the mysterious Kapu Kokeko. The Pok&#233;mon of legend bestows upon them a Z-Ring, a necessary tool in using the Z-Moves. Dazzled by their earlier battle and now in possession of a Z-Ring, Satoshi and Pikachu decide to stay behind in the Alola Region to learn and master the strength of these powerful new attacks.

Enrolling in the Pok&#233;mon School, Satoshi is joined by classmates such as Lillie, who loves Pok&#233;mon but cannot bring herself to touch them, Kaki, and many others. Between attending classes, fending off the pesky Team Rocket&#8212;who themselves have arrived in Alola to pave the way for their organization's future plans&#8212;and taking on the Island Challenge that is necessary to master the Z-Moves, Satoshi and Pikachu are in for an exciting new adventure.

[Written by MAL Rewrite]</code></pre><p>I did also think about trying the new Cohere <a href="https://txt.cohere.com/rerank/">rerank</a> API, but my lizard brain said this project was nearing an end. I had but one more thing to do.</p><h2>Uploading to HuggingFace</h2><p>I&#8217;ve downloaded things through the HuggingFace SDKs before but I&#8217;ve never uploaded anything. It was easy though. You need to setup git with <a href="https://git-lfs.com/">large file storage</a> and then you basically just treat everything like a git repo. ezclap.</p><p>I uploaded just a simple jsonlines file, instead of doing anything more complicated or compressed and HuggingFace was able to parse it and show a preview on the README.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Cixl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcdaa3407-143b-4a95-abbd-15074ceddfd8_1259x566.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Cixl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcdaa3407-143b-4a95-abbd-15074ceddfd8_1259x566.png 424w, https://substackcdn.com/image/fetch/$s_!Cixl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcdaa3407-143b-4a95-abbd-15074ceddfd8_1259x566.png 848w, https://substackcdn.com/image/fetch/$s_!Cixl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcdaa3407-143b-4a95-abbd-15074ceddfd8_1259x566.png 1272w, https://substackcdn.com/image/fetch/$s_!Cixl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcdaa3407-143b-4a95-abbd-15074ceddfd8_1259x566.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Cixl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcdaa3407-143b-4a95-abbd-15074ceddfd8_1259x566.png" width="1259" height="566" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cdaa3407-143b-4a95-abbd-15074ceddfd8_1259x566.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:566,&quot;width&quot;:1259,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:163307,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Cixl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcdaa3407-143b-4a95-abbd-15074ceddfd8_1259x566.png 424w, https://substackcdn.com/image/fetch/$s_!Cixl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcdaa3407-143b-4a95-abbd-15074ceddfd8_1259x566.png 848w, https://substackcdn.com/image/fetch/$s_!Cixl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcdaa3407-143b-4a95-abbd-15074ceddfd8_1259x566.png 1272w, https://substackcdn.com/image/fetch/$s_!Cixl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcdaa3407-143b-4a95-abbd-15074ceddfd8_1259x566.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Wrapping up</h2><p>Cool. Thanks for reading. Check out the dataset and code to get started: <a href="https://huggingface.co/datasets/abatilo/myanimelist-embeddings">https://huggingface.co/datasets/abatilo/myanimelist-embeddings</a></p><p>See ya&#8217;ll for the next one.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Treating my EC2 instances like I'm Leonardo DiCaprio]]></title><description><![CDATA[Once they're too old, they gotta get out of here]]></description><link>https://www.sliceofexperiments.com/p/treating-my-ec2-instances-like-im</link><guid isPermaLink="false">https://www.sliceofexperiments.com/p/treating-my-ec2-instances-like-im</guid><dc:creator><![CDATA[Aaron Batilo]]></dc:creator><pubDate>Tue, 09 May 2023 20:15:29 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!o6dZ!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7aaed77e-5db5-4e6b-982e-1802998c069a_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I&#8217;ve talked plenty about <a href="https://www.sliceofexperiments.com/p/scale-from-0-saving-nickels-to-feed">how I use EKS in AWS</a> for running all of my side projects. Using Kubernetes for managing all of my actual EC2 instances is great because the management of the instances is pretty much invisible to me. I&#8217;m a big fan of purposefully keeping very ephemeral instances for a bunch of reasons. There&#8217;s the security benefits, and there are the resiliency/chaos engineering benefits. I don&#8217;t think anyone&#8217;s actually trying to hack anything that I make, and I break my apps more than any kind of chaos engineering ever would, but just let me pretend that I know what I&#8217;m doing, okay?</p><p>Now, normally, I use <a href="https://karpenter.sh/">Karpenter.sh</a> as my cluster&#8217;s node management tool. I have a provisioner configured with a <a href="https://karpenter.sh/v0.27.3/concepts/deprovisioning/#:~:text=Expiration%3A%20Karpenter%20will,a%20specific%20version).">ttlSecondsUntilExpired</a> that will delete instances if they&#8217;ve been around for more than 24 hours. The instances are already spot instances so they&#8217;re not as likely to even stick around for that long, but for low utilization instance types like the t3.mediums that I use, I&#8217;ve had instances stick around for months if I don&#8217;t do something about it.</p><p>Okay, so, Aaron, you&#8217;ve already got something that deletes instances that are old, so why are you making me read any of this? Well, it was decided that Karpenter should <a href="https://github.com/aws/karpenter/pull/1145/files#diff-c83c30f2bb82c1f849c77daeb2b44e9191459921e10d553d542a49efd3427669R75">not be schedulable on any nodes that it provisions</a>, by default. I think that kind of makes sense though. If for some reason there&#8217;s a problem with your provisioner, you don&#8217;t want Karpenter itself to be unable to run because there are no nodes. That means that while Karpenter can handle all additional worker nodes that need to serve your application requirements, you need to have some workers join your EKS cluster via other means.</p><p>The terraform-aws-eks module that I use for provisioning my cluster comes with a handy dandy built in variable for configuring the official <a href="https://github.com/terraform-aws-modules/terraform-aws-eks/tree/0f9d9fac93caf239386f47b0f117706ea78c2bec/examples/eks_managed_node_group">EKS managed node groups</a>. EKS managed node groups are a regular old autoscaling group with some extra bells and whistles. Here&#8217;s a description of the differences written by one of my newest favorite tools, <a href="https://www.perplexity.ai/search/7cf50521-9e69-4fdc-947d-d20b8c9e1f8c?s=c">Perplexity.ai</a>:</p><blockquote><p>An AWS Autoscaling Group and an AWS EKS managed node group are both used for automatic scaling and management of EC2 instances in an EKS cluster. However, there are some differences between them. An Autoscaling Group is a collection of EC2 instances that are treated as a logical grouping for the purposes of automatic scaling and management<strong><a href="https://docs.aws.amazon.com/autoscaling/ec2/userguide/auto-scaling-groups.html"><sup>1</sup></a></strong>. It lets you use Amazon EC2 Auto Scaling features such as health check replacements and scaling policies. On the other hand, an EKS managed node group automates the provisioning and lifecycle management of nodes for your Kubernetes clusters<strong><a href="https://aws.amazon.com/blogs/containers/catching-up-with-managed-node-groups-in-amazon-eks/"><sup>2</sup></a></strong>. It provides an abstraction to Amazon EC2 instances and Auto Scaling groups, enabling a set of simple one-step operations in EKS to provision and manage groups of cluster nodes. It is compatible with the Cluster Autoscaler and is backed by EC2 instances in your account which are managed by an Auto Scaling group</p></blockquote><p>But firstly, what&#8217;s wrong with the current setup? Why can&#8217;t some of these instances be a little bit more stable? WHAT PROBLEM ARE YOU TRYING TO SOLVE, AARON? Well you see, what&#8217;s been happening is that I kept this base node group at a desired size of 2 instances. One for each replica of Karpenter itself. Then any additional nodes that existed for my actual applications were provisioned by Karpenter. My cluster almost always runs with 3 nodes right now with how much compute I give each website, things like ArgoCD, etc. However, every 24 hours, the 1 node that&#8217;s managed by Karpenter would get terminated and replaced, and all of the pods that were scheduled to that node would end up moving to the existing nodes.</p><p>This would happen repeatedly and then after a few weeks of this happening every day, I&#8217;d get unlucky and end up with too many pods on a single node, and trigger problems with the container network interface trying to assign more IP addresses to a given node than is allowed. So I&#8217;d have to manually terminate those nodes to get new ones to rebalance the cluster. And this last time that I did so, I had a cascading failure, where my <a href="https://www.sliceofexperiments.com/p/scale-from-0-saving-nickels-to-feed">scale from 0 autoscaler</a> had a bunch of requests queue&#8217;d up, which triggered a bunch of pods to be created, which made everything worse and zero Karpenter pods could get scheduled and so no new nodes came up. This is basically the worst case combination of things happening.</p><p>Let&#8217;s make these EC2 instances disappear after 24 hours also.</p><p>AWS Autoscaling Groups themselves have a configuration for <code>MaxInstanceLifetime</code> which is exactly what I want for these extra nodes. There&#8217;s just one problem, the abstraction for the <a href="https://github.com/aws/containers-roadmap/issues/1190">EKS managed node groups doesn&#8217;t expose this property at all</a>. So I could go back to using raw autoscaling groups for my nodes, but then I lose all the bells and whistles of the EKS managed node groups&#8230; so what do I do?</p><p>Like all good infra people know, <em>you glue together a solution with some bash</em>.</p><pre><code>#!/bin/bash
asgs="$(aws autoscaling describe-auto-scaling-groups --filters Name=tag:kubernetes.io/cluster/red,Values=owned --query 'AutoScalingGroups[*].AutoScalingGroupName' | jq -r '.[]')"

# Loop through each asg in asgs and call aws autoscaling start-instance-refresh
for asg in $asgs; do
  aws autoscaling start-instance-refresh --auto-scaling-group-name $asg --preferences '{"InstanceWarmup":90,"MinHealthyPercentage":66}'
done</code></pre><p>Viola! Let&#8217;s just grab all the autoscaling groups that are owned by my cluster (named &#8220;red&#8221;), and then trigger an instance refresh. Put this script into a container, then run it as a CronJob from within the EKS cluster itself and bing bang boom.</p><p>As an homage to Leonardo himself, I was hoping to have the instance refresher run once every 25 hours, but alas, it doesn&#8217;t appear that cron expression evaluators can actually work that way, even if it&#8217;s syntactically valid to say */25.</p><div><hr></div><p>As an aside, by the time you&#8217;re reading this newsletter post, I&#8217;ve already publicly announced that I&#8217;ve left my position as an infra and tools engineer with <a href="https://color.com">Color</a>. I&#8217;m ecstatic to share that I&#8217;ll be joining <a href="https://cohere.com/">Cohere</a> to do some infra work there. I&#8217;m very excited to be working full time in the large language models space, and you can expect that working in such a space will inspire some pretty interesting experiments and learnings for me to write about in the future!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[An actually runnable (March 2023) tutorial for getting started with gymnasium and reinforcement learning]]></title><description><![CDATA[Complaints from an SRE trying to learn RL. PIN YOUR DEPENDENCIES]]></description><link>https://www.sliceofexperiments.com/p/an-actually-runnable-march-2023-tutorial</link><guid isPermaLink="false">https://www.sliceofexperiments.com/p/an-actually-runnable-march-2023-tutorial</guid><dc:creator><![CDATA[Aaron Batilo]]></dc:creator><pubDate>Tue, 25 Apr 2023 20:15:13 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Pvjs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb6b8b04-3e05-4a5c-ae8b-cc489eebef7b_600x400.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every time I&#8217;m told to pip install something, but the tutorial is from 4 years ago and there&#8217;s no version information, I know it&#8217;s going to be a bad time. If you read this entire post, you&#8217;ll learn all the things I&#8217;ve learned so far with trying to get into reinforcement learning as an ML fan boy but a career infra and operations engineer. Also, buckle up, this is a <strong>thick</strong> post. This first post should probably be multiple posts but nah. I&#8217;m going to write what I know now.</p><h2>Motivation</h2><p>The motivation for all this work comes from a few places. First of all, I&#8217;ve always been super interested in machine learning. People who know me know that I&#8217;ve been experimenting with machine learning for <a href="https://joshdotai.medium.com/i-made-a-computer-pretend-to-be-a-computer-that-s-pretending-to-be-a-human-here-s-what-happened-128173751967">many many years</a>. But I have another love. And that&#8217;s Super Smash Bros Melee. <strong><a href="https://www.youtube.com/shorts/DtEHRCeRxEk">Mang0 is the GOAT</a></strong>. More on this in a moment. A few weeks ago, I watched the <a href="https://www.youtube.com/watch?v=WXuK6gekU1Y">AlphaGo documentary</a>, where DeepMind did what was thought to be impossible, and they trained an AI that beat the long time world Go champion. DeepMind has done a lot of amazing projects, and a lot of their success has been from applications of reinforcement learning.</p><p>I&#8217;ve also been a casual chess fan. I have a 400 rating, am terrible at the game, but something about watching <a href="https://www.youtube.com/@GothamChess">Levy &#8220;GothamChess&#8221; Rothman</a> is very entertaining. A sentiment that chess players used to have, and that Go players in the documentary started to have, is to wonder if AI&#8217;s beating humans will mean that people will stop wanting to play the game. In fact, it&#8217;s been the opposite. AI has been found to be immensely useful in chess players being able to study the game. Just because a computer can calculate more moves ahead, humans are still fascinated in finding out how capable they can be given our limits.</p><p>Back to Melee. It&#8217;s the greatest game to have ever been created. It&#8217;s expressive, it&#8217;s fast, it&#8217;s beautiful. I love watching this game. I&#8217;m a bronze 2 shitter, but I still love watching this game. Let&#8217;s put this all together. I wondered to myself, is there a way that I can build a melee AI that will ultimately help push the meta forward? With the fearless exploratory capabilities that a reinforcement learning algorithm has, can we find new tech? If I trained a model for low tier characters, can we make them more relevant? I have no idea, but I&#8217;m interested in finding out.</p><p>Thus, I get started. This will not be the only thing I talk about in the coming weeks on this newsletter, but it might be a lot of the next few posts. Everything from <a href="https://arxiv.org/abs/1702.06230">previous RL work on Melee</a> to scaling out model training, etc. I expect this project to be a running theme for a while but I&#8217;m too scatter brained to keep to a single long term project.</p><h2>How do I get started with RL?</h2><p>A few months ago, I read <a href="https://thenewstack.io/how-ray-a-distributed-ai-framework-helps-power-chatgpt/">this article that says that OpenAI uses Ray.io</a>. When I checked out the Ray documentation, I noticed that it has reinforcement learning capabilities. At the time, I took that information and stored it back in the brain archive room. Fast forward to when I&#8217;m interested in trying some RL work and I decided that Ray is where I want to get started. Ya&#8217;ll know that I&#8217;m an operations person. Ya&#8217;ll know that I run my <a href="https://www.sliceofexperiments.com/p/scale-from-0-saving-nickels-to-feed">own Kubernetes cluster</a>. My assumption is that by the time I&#8217;m ready and capable of doing simulations in melee, that I&#8217;ll want to be able to distribute that work, and since Ray seems to be the hot shit here, I might as well start there.</p><p>So I start searching for tutorials and trying to follow them. And I notice something. <strong>None of these tutorials work</strong>. Either the tutorials aren&#8217;t for Ray, or I follow the steps and the provided code doesn&#8217;t run, or they don&#8217;t specify that you need a certain version of Python, etc. It&#8217;s maddening. I spent probably a full day and a half strictly trying just to get something running. I&#8217;m not an ML engineer, and the learning curve for getting started here feels way too hard, so I wanted to write about everything I learned.</p><h2>Laying out the tools</h2><p>Part of what made it really hard for me to get started was that there&#8217;s a set of standard terms and tools that all of these tutorials kind of just expect you to be familiar with. I was not, and I&#8217;m still not entirely sure I understand many of the specifics, but I wish someone had laid some of these things out for me when I was getting started.</p><p>I think of reinforcement learning as having 4 key components: <strong>Environment</strong>, <strong>Agent</strong>, <strong>Policy</strong>, <strong>Reward</strong>.</p><p><strong>Environment</strong> - Often a simulation when training, but otherwise, the environment the world that is being observed and acting in. This can be a game, the real world, a description of a problem, etc.</p><p><strong>Agent</strong> - This is the representation of the program. This might be the self driving car, this might be your character in a video game. An agent takes actions on an environment. In a fighting game, an agent has a move set to follow, and that&#8217;s all they can do on their own.</p><p><strong>Policy</strong> - The policy is the algorithm or strategy for what an agent will do given a certain state of the environment. When you&#8217;re training a reinforcement learning model, you&#8217;re most likely to be referring to learning a new policy. When you play a fighting game, you are actually the policy.</p><p><strong>Reward</strong> - There needs to be some kind of evaluation or feedback. The reward and cost model is going to dictate how the policy behaves.</p><p>When you put all of these 4 together, you have a policy that looks at the current environment, tells the agent to take an action, and the environment will change in some way which can give you a reward or punish you.</p><p>You&#8217;ll also have <strong>episodes</strong> / <strong>rollouts</strong>. These terms are <em>mostly</em> interchangeable from my understanding. Basically, this is one full simulation through an environment. So this could be a single chess match as one example.</p><div class="pullquote"><p>&#8220;I see the Fox up-b&#8217;ing towards stage, I&#8217;m a Falco and I&#8217;m going to down air at the ledge to knock them into the blast zone. If it works, my reward is that they lose a stock&#8221;.</p></div><p>One of the first tools that you&#8217;ll see <strong>everywhere</strong> when you try to get started with reinforcement learning is <a href="https://github.com/openai/gym">OpenAI&#8217;s gym</a>. Gym is a platform and set of abstractions for having a RL <strong>environment</strong>. It&#8217;s definitely become an RL staple and the standard way for everyone to integrate everything else. There are interfaces between gym and things like the Unity game engine, game console emulators, and more. Here&#8217;s the catch, OpenAI gym has actually ceased development. Gym has been locked in place and now all development is done under the <a href="https://github.com/Farama-Foundation/Gymnasium">Farama Foundation&#8217;s Gymnasium</a> repository. Gymnasium does its best to maintain backwards compatibility with the gym API, but if you&#8217;ve ever worked on a software project long enough, you know that dependencies get really complicated. Ray is a modern ML framework and later versions integrate with gymnasium well, but tutorials were written expecting gym.</p><p>Okay, so that&#8217;s a quick overview of gym and gymnasium. What else are we using? Ray! So what&#8217;s up with Ray? Ray is a combination distributed workflow system with overlap with projects like Apache Spark, or Dask, but with some additional features and focus on machine learning workloads. The Ray ecosystem itself integrates deeply with many other libraries, but something that makes their documentation a little confusing to a total newbie, is that Ray loves its own ecosystem. What does that mean? Ray documentation and examples take every opportunity to use other parts of the Ray ecosystem. Examples might show you how to do something with Ray&#8217;s RLLib, but then will also give you examples using RayTune and RayServe, etc. Overall, I think this is awesome, but very confusing when you&#8217;re still learning all of the boundaries and what they do.</p><p>RayTune is a library for doing parameter grid searches and optimizations. In other words, it helps you find optimal hyper parameters for your model. Conceptually, think about it as a library to help you run more experiments. RayServe will help you turn your models into a REST API so that you can send requests to your model from other services.</p><p>Lastly, I&#8217;ll make a quick call out to other ML frameworks. Ray integrates with both tensorflow1, tensorflow2, and torch. Again, if you&#8217;re an ML engineer, you&#8217;re probably comfortable with these, but when you&#8217;re just an admirer/fanboy, it&#8217;s hard to keep up with all of these data science/ml libraries. Simply put, these libraries make it easier to architect neural networks, and since Ray supports both, you&#8217;ll find examples that use both but that can cause even more confusion with dependency management. Numerical computing libraries are incredibly finicky with their own dependencies, and if you try to install both tensorflow and torch like the Ray tutorial tells you to, you&#8217;ll likely end up with a conflict of versions of shared dependencies that both libraries use.</p><p>It&#8217;s awesome that Ray wants to demonstrate compatibility with both, but man did that make things confusing.</p><h2>Making a code sample reproducible</h2><p>If you type <code>pip install ray</code> which is so common in documentation or tutorials, you&#8217;ll possibly install the latest version, or maybe you followed a tutorial 6 months ago and pip will just report that it&#8217;s already installed with whatever version you had. You could force a new version with the -U flag but that doesn&#8217;t really solve the problem. You have multiple dependencies that are going to be installed and just installing &#8220;ray&#8221; isn&#8217;t going to do anything deterministic or predictable for you.</p><p>Let&#8217;s scope in what model we want to train to something very specific. I&#8217;m going to help you run this code which we want to save as <code>main.py</code>:</p><pre><code>import gymnasium as gym
from ray.rllib.algorithms.dqn import DQNConfig

algo = DQNConfig().environment("LunarLander-v2").build()

for i in range(10):
    result = algo.train()
    print("Iteration:", i)
    print("Episode reward max:", result["episode_reward_max"])
    print("Episode reward min:", result["episode_reward_min"])
    print("Episode reward mean:", result["episode_reward_mean"])
    print()

env = gym.make("LunarLander-v2", render_mode="human")
terminated = truncated = False
observations, info = env.reset()

while True:
    env.render()
    action = algo.compute_single_action(observations)
    observations, reward, terminated, truncated, info = env.step(action)

    if terminated or truncated:
        observations, info = env.reset()</code></pre><p>That&#8217;s it. I&#8217;m going to write this big-ass post to help you run this. What does this do? There&#8217;s a classic reinforcement learning &#8220;hello world&#8221; type problem called the LunarLander.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Pvjs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb6b8b04-3e05-4a5c-ae8b-cc489eebef7b_600x400.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Pvjs!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb6b8b04-3e05-4a5c-ae8b-cc489eebef7b_600x400.gif 424w, https://substackcdn.com/image/fetch/$s_!Pvjs!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb6b8b04-3e05-4a5c-ae8b-cc489eebef7b_600x400.gif 848w, https://substackcdn.com/image/fetch/$s_!Pvjs!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb6b8b04-3e05-4a5c-ae8b-cc489eebef7b_600x400.gif 1272w, https://substackcdn.com/image/fetch/$s_!Pvjs!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb6b8b04-3e05-4a5c-ae8b-cc489eebef7b_600x400.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Pvjs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb6b8b04-3e05-4a5c-ae8b-cc489eebef7b_600x400.gif" width="600" height="400" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cb6b8b04-3e05-4a5c-ae8b-cc489eebef7b_600x400.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:400,&quot;width&quot;:600,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:711266,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Pvjs!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb6b8b04-3e05-4a5c-ae8b-cc489eebef7b_600x400.gif 424w, https://substackcdn.com/image/fetch/$s_!Pvjs!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb6b8b04-3e05-4a5c-ae8b-cc489eebef7b_600x400.gif 848w, https://substackcdn.com/image/fetch/$s_!Pvjs!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb6b8b04-3e05-4a5c-ae8b-cc489eebef7b_600x400.gif 1272w, https://substackcdn.com/image/fetch/$s_!Pvjs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb6b8b04-3e05-4a5c-ae8b-cc489eebef7b_600x400.gif 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">If you were around for the MiniClip days, this is basically level 1 of the game <a href="https://www.youtube.com/watch?v=_DBGxWYvxjU">MonkeyLander</a>.</figcaption></figure></div><p>We run 10 training iterations where each training iteration runs the default number of episodes/rollouts. Then we have an infinite loop where we render and display the trained policy running on the LunarLander game. We&#8217;ll be digging into each section of code more deeply in the coming sections.</p><h2>I just want to run it, Aaron!</h2><p>Okay, okay, so what do you need to install it? I&#8217;ve generated the FULL dependency tree to run this example consistently using <a href="https://github.com/jazzband/pip-tools">pip-tools</a>. I am running everything with <strong>Python 3.8</strong>. I specifically use <a href="https://github.com/asdf-community/asdf-python">asdf-python</a> for managing multiple versions of Python, but feel free to use <a href="https://github.com/pyenv/pyenv">pyenv</a>. Once you&#8217;re running <strong>Python 3.8</strong>, save the following content to a file named <code>requirements.txt:</code></p><pre><code>#
# This file is autogenerated by pip-compile with Python 3.8
# by the following command:
#
#    pip-compile --output-file=requirements.txt requirements.in
#
absl-py==1.4.0
    # via
    #   tensorboard
    #   tensorflow
aiosignal==1.3.1
    # via ray
astunparse==1.6.3
    # via tensorflow
attrs==22.2.0
    # via
    #   jsonschema
    #   ray
box2d-py==2.3.5
    # via gymnasium
cachetools==5.3.0
    # via google-auth
certifi==2022.12.7
    # via requests
charset-normalizer==3.1.0
    # via requests
click==8.1.3
    # via
    #   ray
    #   typer
cloudpickle==2.2.1
    # via
    #   gym
    #   gymnasium
contourpy==1.0.7
    # via matplotlib
cycler==0.11.0
    # via matplotlib
distlib==0.3.6
    # via virtualenv
dm-tree==0.1.8
    # via ray
farama-notifications==0.0.4
    # via gymnasium
filelock==3.10.6
    # via
    #   ray
    #   virtualenv
flatbuffers==23.3.3
    # via tensorflow
fonttools==4.39.2
    # via matplotlib
frozenlist==1.3.3
    # via
    #   aiosignal
    #   ray
gast==0.4.0
    # via tensorflow
google-auth==2.16.3
    # via
    #   google-auth-oauthlib
    #   tensorboard
google-auth-oauthlib==0.4.6
    # via tensorboard
google-pasta==0.2.0
    # via tensorflow
grpcio==1.51.3
    # via
    #   ray
    #   tensorboard
    #   tensorflow
gym==0.23.1
    # via ray
gym-notices==0.0.8
    # via gym
gymnasium[box2d]==0.28.1
    # via -r requirements.in
h5py==3.8.0
    # via tensorflow
idna==3.4
    # via requests
imageio==2.26.1
    # via scikit-image
importlib-metadata==6.1.0
    # via
    #   gym
    #   gymnasium
    #   markdown
importlib-resources==5.12.0
    # via
    #   jsonschema
    #   matplotlib
jax==0.4.6
    # via tensorflow
jax-jumpy==1.0.0
    # via gymnasium
jsonschema==4.17.3
    # via ray
keras==2.12.0
    # via tensorflow
kiwisolver==1.4.4
    # via matplotlib
lazy-loader==0.2
    # via scikit-image
libclang==16.0.0
    # via tensorflow
lz4==4.3.2
    # via ray
markdown==3.4.3
    # via tensorboard
markdown-it-py==2.2.0
    # via rich
markupsafe==2.1.2
    # via werkzeug
matplotlib==3.7.1
    # via ray
mdurl==0.1.2
    # via markdown-it-py
msgpack==1.0.5
    # via ray
networkx==3.0
    # via scikit-image
numpy==1.23.5
    # via
    #   contourpy
    #   gym
    #   gymnasium
    #   h5py
    #   imageio
    #   jax
    #   jax-jumpy
    #   matplotlib
    #   opt-einsum
    #   pandas
    #   pywavelets
    #   ray
    #   scikit-image
    #   scipy
    #   tensorboard
    #   tensorboardx
    #   tensorflow
    #   tifffile
oauthlib==3.2.2
    # via requests-oauthlib
opt-einsum==3.3.0
    # via
    #   jax
    #   tensorflow
packaging==23.0
    # via
    #   matplotlib
    #   scikit-image
    #   tensorboardx
    #   tensorflow
pandas==1.5.3
    # via ray
pillow==9.4.0
    # via
    #   imageio
    #   matplotlib
    #   scikit-image
pkgutil-resolve-name==1.3.10
    # via jsonschema
platformdirs==3.2.0
    # via virtualenv
protobuf==3.20.3
    # via
    #   ray
    #   tensorboard
    #   tensorboardx
    #   tensorflow
pyasn1==0.4.8
    # via
    #   pyasn1-modules
    #   rsa
pyasn1-modules==0.2.8
    # via google-auth
pygame==2.1.3
    # via gymnasium
pygments==2.14.0
    # via rich
pyparsing==3.0.9
    # via matplotlib
pyrsistent==0.19.3
    # via jsonschema
python-dateutil==2.8.2
    # via
    #   matplotlib
    #   pandas
pytz==2023.2
    # via pandas
pywavelets==1.4.1
    # via scikit-image
pyyaml==6.0
    # via ray
ray[rllib]==2.2.0
    # via -r requirements.in
requests==2.28.2
    # via
    #   ray
    #   requests-oauthlib
    #   tensorboard
requests-oauthlib==1.3.1
    # via google-auth-oauthlib
rich==13.3.2
    # via ray
rsa==4.9
    # via google-auth
scikit-image==0.20.0
    # via ray
scipy==1.9.1
    # via
    #   jax
    #   ray
    #   scikit-image
six==1.16.0
    # via
    #   astunparse
    #   google-auth
    #   google-pasta
    #   python-dateutil
    #   tensorflow
swig==4.1.1
    # via gymnasium
tabulate==0.9.0
    # via ray
tensorboard==2.12.0
    # via tensorflow
tensorboard-data-server==0.7.0
    # via tensorboard
tensorboard-plugin-wit==1.8.1
    # via tensorboard
tensorboardx==2.6
    # via ray
tensorflow==2.12.0
    # via -r requirements.in
tensorflow-estimator==2.12.0
    # via tensorflow
tensorflow-io-gcs-filesystem==0.31.0
    # via tensorflow
termcolor==2.2.0
    # via tensorflow
tifffile==2023.3.21
    # via scikit-image
typer==0.7.0
    # via ray
typing-extensions==4.5.0
    # via
    #   gymnasium
    #   rich
    #   tensorflow
urllib3==1.26.15
    # via requests
virtualenv==20.21.0
    # via ray
werkzeug==2.2.3
    # via tensorboard
wheel==0.40.0
    # via
    #   astunparse
    #   tensorboard
wrapt==1.14.1
    # via tensorflow
zipp==3.15.0
    # via
    #   importlib-metadata
    #   importlib-resources

# The following packages are considered to be unsafe in a requirements file:
# setuptools</code></pre><p>Then to make sure that you&#8217;re not going to be having any other dependencies conflict with these, make sure to run in a new virtual environment. I&#8217;m not going to go into the details of virtual environments, there&#8217;s plenty of documentation about them. But basically after you save the above contents, just run.</p><pre><code>python -m virtualenv venv
. venv/bin/activate
pip install -r requirements.txt
python main.py</code></pre><p>This will start the training loop. What were the high level dependencies that we installed? I generated that pinned version tree after installing the following dependencies:</p><pre><code>ray[rllib]
gymnasium[box2d]
tensorflow</code></pre><p>We install ray and the rllib extras, we install gymnasium for our environment and the box2d extras, and then tensorflow. So we install the general ML framework in tensorflow, we install the reinforcement learning framework in ray with rllib, and then we install the game environment with gymnasium and box2d. Box2d is going to be really valuable here because it installs pygame as a downstream dependency which will give us the actual game rendered on screen to visualize our trained algorithm which I think is at least half the fun. When you run the main.py, we&#8217;re currently only running 10 train loops which isn&#8217;t going to be enough to train a consistently functioning model, but you can change that number as you experiment.</p><h2>Breaking down our code</h2><p>The<a href="https://gymnasium.farama.org/environments/box2d/lunar_lander/"> documentation for LunarLander on the Gymnasium website</a> is actually <strong>amazing</strong>. I recommend just reading through that to understand all of what the environment actually entails. But in the context of our training loop, I want to emphasize one bit of text from the documentation.</p><div class="pullquote"><p>An episode is considered a solution if it scores at least 200 points.</p></div><h3>Q learning and Deep Q learning</h3><p>So in our code, we start with:</p><pre><code>algo = DQNConfig().environment("LunarLander-v2").build()</code></pre><p>So what is this doing? This creating an empty policy for a <a href="https://en.wikipedia.org/wiki/Q-learning#Deep_Q-learning">Deep Q Network</a>, targeted towards the LunarLander environment. We just talked about the LunarLander, but we need to talk about Deep Q Networks and what this line of code is actually doing in the ray rllib library.</p><p>Q learning is one of the many many many reinforcement learning algorithms. I&#8217;m fucking AWFUL at math, so I&#8217;m not even going to attempt to derive that or explain it. But Q learning is simply the idea that you have some function named Q, and it takes two parameters. It takes the current state of the observable environment, an action to take, and it will give you an estimation for what the reward will be. Pretty simple. So in a plain Q learning setup, you can imagine that at every step of the game, you check the return value of your Q function for every action, and you just pick the action with the best prediction. If you have a very small environment space to work through, you could imagine literally hard coding the inputs and outputs of your Q function. Imagine writing a function that adds two numbers. If you know that your inputs will only ever be 0 or 1, you could write a function like so:</p><pre><code>def add(a, b):
  if a == 0 and b == 0:
    return 0
  if a == 0 and b == 1:
    return 1
  if a == 1 and b == 0:
    return 1
  if a == 1 and b == 1:
    return 2</code></pre><p>A Deep Q Network applies a neural network for trying to learn more generalized reward calculation given more complicated states. Any practical usage of reinforcement learning is going to have way more nuances to how you calculate your output, but the basics of Q learning come from a very easy to understand concept. It makes for a great starting algorithm for a noob like me.</p><p>That being said, <a href="https://www.deepmind.com/publications/playing-atari-with-deep-reinforcement-learning">DeepMind was able to train a single model that could play most if not every Atari games using a Deep Q Network.</a> So it&#8217;s also an incredibly powerful algorithm when used by people who know what they&#8217;re doing.</p><h3>Training your model and counting your rewards</h3><p>The line of code that I have is as simple as it can get. You can actually pass in all kinds of additional configuration here for Ray to understand how to distribute the work of this algorithm. The DQNConfig is where you&#8217;d configure things like GPU allocations, number of episodes in a single training iteration, etc. But I&#8217;m going to skip that for now because this post is already dummy thiqq.</p><pre><code>for i in range(10):
    result = algo.train()
    print("Iteration:", i)
    print("Episode reward max:", result["episode_reward_max"])
    print("Episode reward min:", result["episode_reward_min"])
    print("Episode reward mean:", result["episode_reward_mean"])
    print()</code></pre><p>The next chunk of our code here is to do our training. A single call to train() will actually trigger thousands of simulations by default, then will report some statistics. Notably, we&#8217;re going to print out the episode reward. We noted earlier that a success for LunarLander takes a reward of 200. If you run this for 10 iterations, you&#8217;ll probably only see negative reward values here. In my experience, if you run with 30-50 iterations, you&#8217;ll at least see a reward max of 150-200, but that reward mean is going to stay negative. After a few hundred train loops, you&#8217;ll see a mean of over 200 but you might still have the occasional rollout with a large negative number.</p><h3>Watching your algorithm go to work</h3><pre><code>env = gym.make("LunarLander-v2", render_mode="human")
terminated = truncated = False
observations, info = env.reset()

while True:
    env.render()
    action = algo.compute_single_action(observations)
    observations, reward, terminated, truncated, info = env.step(action)

    if terminated or truncated:
        observations, info = env.reset()</code></pre><p>The last section of code is here to let us visualize how our algorithm is doing. The first line here is instantiating an entirely new gym environment for the LunarLander game, and setting the render_mode to &#8220;human&#8221;. Render modes were another thing that were really confusing to me because there&#8217;s convention about it but you&#8217;ll finds all kinds of documentation for custom render modes. Basically, the render_mode controls how the environment will&#8230; well, be rendered. The convention is that setting the render_mode to &#8220;human&#8221; will do whatever the environment needs for being viewable by human eye balls. In the case of LunarLander, that&#8217;s to render a pygame window to draw the game. By default, the environments won&#8217;t actually render anything because that takes more time and more compute. If you&#8217;re rendering a non-trivial video game environment, the game is almost guaranteed to run at 60 frames per second, which would mean 60 Q function steps per second. A game might take minutes to complete a full rollout, so this would slow down the training process by a lot. If the game can be run &#8220;headless&#8221; or purely computationally for simulation purposes, then using that headless render mode would make more sense for being able to do thousands or millions of simulations. Render modes might also attach to real physical devices when people are trying to do some experimenting, which is probably why the argument is just any arbitrary string. I would find so many tutorials that would show me a video of the results of the tutorial, but the tutorial itself NEVER explained render mode which made me think that I copy pasted something wrong or that my system was setup with the wrong dependencies, because I&#8217;d never actually see the game.</p><p>We create the environment, then we setup some defaults for variables that we&#8217;re going to use. Then comes the best part, running the game in a loop. First we call env.render() which is what updates the pygame window. Some RL environments might even just print to your terminal when you call render, but that render_mode being set to &#8220;human&#8221; has our back here. Next, we pass in the observable states to our trained policy, and it&#8217;ll return one of the defined actions according to the environment. Next, apply the action from the policy to the environment which will impact our space craft agent by having it trigger one of its thrusters. Lastly, we check to see if we&#8217;ve terminated or truncated our environment. To terminate or truncate the environment usually means something like we crashed, died on the level, or have been running for too long with no progress and got stuck. It&#8217;s up to the environment to define these states. Termination can actually also mean that we&#8217;ve beaten or won the level. If we hit either case, reset the environment back to the beginning of the level and just keep it going. If you&#8217;ve ran enough iterations of the training loop, hopefully that means you get an endless loop of the space craft landing safely between the flags. If not, you might be watching the space craft fly off the side of the screen or plummet to its death over and over again. Remember that these visualizations are not doing anything to update the model. This is purely to visualize the results of the training that has already happened.</p><h2>Conclusion</h2><p>Yeah, this post was a chunky boi. But I&#8217;m learning a lot with this project and writing this up is exactly the point of this newsletter. Checking my own understanding and trying to make this understandable is fun. It&#8217;s flexing a muscle that I should flex more often. Anyways, moving forward from this, I&#8217;m going to attempt to understand some more reinforcement learning algorithms. Q learning and DQN is great for starting, but I&#8217;m not sure if it&#8217;ll be enough to learn melee. Maybe it will be, who knows. At some point, I also need to learn more about what it&#8217;ll take to get melee available as a Ray compatible (aka gymnasium compatible) environment so that I can start doing simulations. There&#8217;s some <a href="https://arxiv.org/abs/1702.06230">previous work</a> about using reinforcement learning with melee but thanks to the melee community, the tech around running melee has never been more advanced. Some of the previous work might apply, some might now. We&#8217;ll find out and when I do, you&#8217;ll hear about it.</p><p>See ya&#8217;ll in the next one.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[This time I made GPT-4 answer 50,000 trivia questions. It's so much better]]></title><description><![CDATA[This new model is surprisingly improved]]></description><link>https://www.sliceofexperiments.com/p/this-time-i-made-gpt-4-answer-50000</link><guid isPermaLink="false">https://www.sliceofexperiments.com/p/this-time-i-made-gpt-4-answer-50000</guid><dc:creator><![CDATA[Aaron Batilo]]></dc:creator><pubDate>Tue, 11 Apr 2023 20:15:11 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!o6dZ!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7aaed77e-5db5-4e6b-982e-1802998c069a_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>TL;DR: Across 49,717 multiple choice questions, GPT-4 was correct 84.4% of the time. Keep reading to see the breakdown by category</strong></p><div><hr></div><p>In January, I<a href="https://www.sliceofexperiments.com/p/chatgpt-vs-50000-trivia-questions"> asked GPT-3.5 49,717 trivia questions</a> from the <a href="https://github.com/uberspot/OpenTriviaQA">uberspot/OpenTriviaQA</a> data set. I learned a few things and there have been a few new models since then too.</p><p>Let&#8217;s get a few corrections in place. In that last post, I claimed that ChatGPT is actually the same as the GPT-3.5 model which is what I had API access to. It turns out that this isn&#8217;t entirely true. <a href="https://openai.com/blog/introducing-chatgpt-and-whisper-apis">ChatGPT was running on a model that is now known as GPT-3.5-Turbo</a>. Not only was that model fine-tuned for chat bot type interactions, but the model itself was optimized and presumably quantized. The API costs for the turbo model were about 1/10th the costs of GPT-3.5.</p><p>And yes, I was in the middle of running this same test on that model. When I made my original post, someone was kind enough to point out to me that the results of asking the models questions actually has improved results when you ask the model to generate an explanation BEFORE giving an answer. This has been called <a href="https://arxiv.org/abs/2201.11903">chain-of-thought</a>. So I was inspired to try both GPT-3.5 and GPT-3.5-Turbo with this technique to see how the results would change. I didn&#8217;t actually finish doing any more tests with GPT-3.5 because the results after the first 10-15 thousand questions were almost identical, even with the chain of thought improved prompt. My GPT-3.5-Turbo, aka the confirmed ChatGPT model, did in fact see some improvement, but not much. I won&#8217;t share the breakdown this time around but the overall percentage of correct questions went from 66.7% up to 70.1%. Considering the model was 1/10th the cost to operate, that was awesome to have any improvements in correctness at all.</p><p>When the <a href="https://openai.com/research/gpt-4">GPT-4 release announcement was made</a>, I was quick to request access to the API. I was granted access just a few days later and I immediately went ahead and started sending all of  the same trivia questions. This time I did a little bit more work on getting a prompt that showed promise. And this time I actually included a single example of how I wanted the model to explain its thinking.</p><p>Both GPT-3.5-Turbo and GPT-4 leverage the chat API format that was released with GPT-3.5-Turbo. This format doesn&#8217;t just have you send a prompt anymore, but actually lets you format your messages to look more like a chat history with back and forth between two entities. My actual prompt that I used for this GPT-4 test looks as follows:</p><pre><code>Messages: []openai.ChatCompletionMessage{
&#9;{
&#9;&#9;Role:    "system",
&#9;&#9;Content: `You are a knowledge assistant. I will ask you a multiple choice question and you will answer it.`,
&#9;},
&#9;{
&#9;&#9;Role: "user",
&#9;&#9;Content: `You will be given a question and then you will be presented with possible
answers to choose from. If you're not sure of the answer, make your
best guess and pick one of the answers.

Follow these instructions:
- Think out loud, step by step
- Insert two blank lines to separate your answer from your explanation
- Write one of your answers and write it exactly character for character as it appears in the list of possible answers
`,
&#9;},
&#9;{
&#9;&#9;Role: "user",
&#9;&#9;Content: `Question:
What language can Harry Potter speak?

Possible answers:
Goblin
English
Mermish
Parseltounge
`,
&#9;},
&#9;{
&#9;&#9;Role: "assistant",
&#9;&#9;Content: `Thinking out loud:
Harry never has a need to speak goblin, so I can eliminate that answer. English
is the known language that he speaks which leaves mermish and parseltounge.
Mermish would be the language that merpeople speak, so I can eliminate that
answer. That leaves parseltounge as the correct answer.

Answer:


Parseltounge`,
&#9;},
&#9;{
&#9;&#9;Role:    "user",
&#9;&#9;Content: questionTemplate,
&#9;},
},
</code></pre><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments! New posts every other Tuesday</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h2>How were the results?</h2><pre><code>Category&#9;Correct&#9;Total&#9;GPT-3.5 &#9;GPT-4&#9;&#9;Delta
brain-teasers&#9;154&#9;207&#9;0.497585&#9;0.743961&#9;49.51%
video-games&#9;415&#9;599&#9;0.517529&#9;0.692821&#9;33.87%
television&#9;4033&#9;5230&#9;0.556597&#9;0.771128&#9;38.54%
entertainment&#9;224&#9;280&#9;0.582143&#9;0.8&#9;&#9;37.42%
animals&#9;&#9;1083&#9;1366&#9;0.596632&#9;0.792826&#9;32.88%
celebrities&#9;2577&#9;3196&#9;0.597309&#9;0.80632&#9;&#9;34.99%
sports&#9;&#9;2331&#9;2840&#9;0.608451&#9;0.820775&#9;34.90%
movies&#9;&#9;3484&#9;4314&#9;0.613584&#9;0.807603&#9;31.62%
for-kids&#9;648&#9;759&#9;0.638999&#9;0.853755&#9;33.61%
music&#9;&#9;4677&#9;5579&#9;0.671447&#9;0.838322&#9;24.85%
literature&#9;1126&#9;1288&#9;0.689441&#9;0.874224&#9;26.80%
hobbies&#9;&#9;1059&#9;1242&#9;0.698068&#9;0.852657&#9;22.15%
general&#9;&#9;2854&#9;3290&#9;0.700912&#9;0.867477&#9;23.76%
newest&#9;&#9;2590&#9;3016&#9;0.701923&#9;0.858753&#9;22.34%
people&#9;&#9;2457&#9;2743&#9;0.71965&#9;&#9;0.895735&#9;24.47%
technology&#9;2160&#9;2486&#9;0.7321&#9;&#9;0.868866&#9;18.68%
world&#9;&#9;4322&#9;4875&#9;0.732513&#9;0.886564&#9;21.03%
religion-faith&#9;579&#9;638&#9;0.73511&#9;&#9;0.907524&#9;23.45%
history&#9;&#9;1481&#9;1645&#9;0.746505&#9;0.900304&#9;20.60%
rated&#9;&#9;1987&#9;2185&#9;0.750572&#9;0.909382&#9;21.16%
humanities&#9;965&#9;1097&#9;0.75752&#9;&#9;0.879672&#9;16.13%
geography&#9;771&#9;842&#9;0.774347&#9;0.915677&#9;18.25%
overall&#9;&#9;41977&#9;49717&#9;0.667377&#9;0.844319&#9;26.51%</code></pre><p>Improvements in every category, and pretty big ones at that. In the above chart, we&#8217;re still ordering the categories by how successful GPT-3.5 was, but you can see some pretty amazing numbers here. The brain-teasers category improved by almost 50%. The geography category went all the way up to 91.6%. GPT-4 is by every measure in this test, &#8220;smarter&#8221;. How did the explanations do?</p><div><hr></div><p>Let&#8217;s look at the same questions as last time, but now with the explanations coming BEFORE an answer is picked.</p><pre><code>Question:
The name of what is coded in the following: 86,smopuim?

Possible answers:
it does not mean anything
the name of a software product
a NASA aircraft
a secret CIA project


Thinking out loud:
First, I will try to find any pattern or code within the given sequence "86,smopuim". If the number 86 is used as a key for Caesar cipher, I can try to decrypt the part "smopuim". However, even after attempting that, I do not find any meaningful name or phrase.

Answer:

it does not mean anything</code></pre><p>The last time we asked this question, GPT-3.5 got the answer correct, but the explanation that we got last time was basically a guess that the numbers 86 would only be used in a software product.</p><p>The explanation here actually feels more consistent given that the model wasn&#8217;t capable of reading text upside down. Reading text upside down would probably need to happen more in the training data before it would even be a consideration by the model.</p><pre><code>Question:
What number multiplied by 10 equals the square of the same number, times 5?

Possible answers:
4
5
2
10


Thinking out loud: 
Let x be the number. Then we're looking for x such that 10x = x^2 * 5. Simplifying the equation, we get 2x = x^2. Now let's check the given numbers.

- If x = 4, we have 2*4 = 8 and 4^2 = 16. This doesn't match.
- If x = 5, we have 2*5 = 10 and 5^2 = 25. This doesn't match either.
- If x = 2, we have 2*2 = 4 and 2^2 = 4. This is a match.
- If x = 10, we have 2*10 = 20 and 10^2 = 100. This doesn't match.

Answer:

2</code></pre><p>Last time, this question was answered <strong>incorrectly</strong>, but this time, GPT-4 actually appears to have been doing the math to solve the equation. I think that this example is actually insane. The model took the word description of the equation to solve and converted it, SIMPLIFIED IT, and then the completed text went through and plugged in the equation.</p><pre><code>Question:
A man is carrying three croquet balls across a bridge. The bridge has a sign that says bridge can only hold 200 pounds. The man weighs 195 pounds and the croquet balls weigh 2 pounds each. The man ends up crossing the bridge in one trip and no one else helps him. This is possible.

Possible answers:
False
True


Thinking out loud:
The man weighs 195 pounds, and each croquet ball weighs 2 pounds. He is carrying three croquet balls, which would be a total of 6 pounds added.
We calculate the total weight like this : 195 pounds (man) + 6 pounds (croquet balls) = 201 pounds. Since the bridge can only handle 200 pounds, and the total weight is 201 pounds, it should not be possible.

Answer:

False</code></pre><p>In the last test, I noted that this is weird because this riddle has been disproven by people with wrinkly brains that know how physics works. The data set says that the answer here is <strong>True</strong>. Googling this riddle has the riddle solution listed as something like: &#8220;just throw the balls across the bridge&#8221; or &#8220;just juggle the balls and then you won&#8217;t have all 6 pounds of force on the bridge&#8221;. GPT-3.5 guessed <strong>True</strong> but made the assertion that 201 pounds <em>is less</em> than 200 pounds, which is obviously incorrect. GPT-4 correctly asserted all numbers, and responded that the answer is False, which is real world correct, but data set incorrect. I&#8217;m going to leave the data set as is, but I&#8217;m glad to see that GPT-4 knows that 201 pounds is heavier than 200 pounds.</p><pre><code>Question:
If the mother of a boy spanks him and then his father comes and does the same, who hurts the most?

Possible answers:
The boys brother
The mother
The boy
The father


Thinking out loud:
In this situation, the boy is receiving the spanking from both his mother and father. The boy's brother does not have any involvement in the situation, so he wouldn't be hurt. The mother and father are administering the punishment, so they are not the ones receiving the pain. Therefore, the most reasonable answer would be the one who is actually experiencing the physical pain.

Answer:

The boy</code></pre><p>GPT-3.5 and GPT-4 answer this question with a very similar explanation. Understanding the context of the actions being taken in the question is still amazing to me.</p><pre><code>Question:
In what book of the Harry Potter series do Harry and Ginny kiss for the first time?

Possible answers:
Harry Potter and the Order of the Phoenix
Harry Potter and the Prisoner of Azkaban
Harry Potter and the Deathly Hallows
Harry Potter and the Half Blood Prince


Thinking out loud:
Harry and Ginny don't share their first kiss in the first few books, so I can eliminate the Harry Potter and the Prisoner of Azkaban. Harry Potter and the Deathly Hallows has too many other big events happening, which makes it less likely. It's either Harry Potter and the Order of the Phoenix or Harry Potter and the Half Blood Prince since their relationship develops more in those books. Harry starts to show his feelings for Ginny in the Order of the Phoenix, but I remember that their first kiss happens in the Half Blood Prince when they're celebrating a Quidditch victory.

Answer:

Harry Potter and the Half Blood Prince</code></pre><p>Another one that GPT-3.5 and GPT-4 nailed. Not much else to talk about for this one.</p><pre><code>Question:
Harry Potter marries Hermione in the last book of the series.

Possible answers:
True
False


Thinking out loud:
Harry Potter and Hermione Granger are close friends in the series, but they do not have a romantic relationship. Harry ends up marrying Ginny Weasley, and Hermione marries Ron Weasley. So, claiming that Harry marries Hermione in the last book is incorrect.

Answer:

False</code></pre><p>Last time, GPT-3.5 got this incorrect and I posited that maybe fan fiction or general internet sentiment is what influenced the model to think that Harry Potter and Hermione were married. GPT-4 knew differently this time.</p><h2>Wrapping up</h2><p>The patterns that we saw from the last test are the same. Categories with long and well known information and facts still perform the best, but GPT-4&#8217;s ability to &#8220;reason&#8221; through a problem appears to have greatly improved according to the results of the brain teasers category. Rumor has it that GPT-4 is actually a trillion parameter model, as opposed to GPT-3/3.5 which has been confirmed publicly to be a 175 million parameter model. The turbo variant hasn&#8217;t had its number of parameters confirmed but it&#8217;s safe to assume that its less than GPT-3.5 given how much cheaper the API pricing is.</p><p>So what&#8217;s changed? Well, I&#8217;m purely speculating here to be very clear, but if GPT-4 is truly a 1 trillion parameter model, then it&#8217;s possible that maybe the larger model can just &#8220;remember&#8221; facts more accurately. Maybe GPT-4 isn&#8217;t actually a trillion parameter model and instead is a similar size but the training parameters have resulted in a better ability to produce &#8220;correct&#8221; results.</p><p>Honestly, I have no idea what&#8217;s happening, but it&#8217;s really interesting to see that this model, which is practically just a more sophisticated autocomplete, can actually &#8220;remember&#8221; such a wide variety of content. Remember, this isn&#8217;t a search engine that&#8217;s trying to find the answer. This is a model that&#8217;s generating words a few characters at a time that happens to generate tokens that turn out to look like very practical and accurate human reasoning.</p><p>We&#8217;ll all just have to stay tuned to see what else comes out of this.</p><div><hr></div><p>Just like last time, I paid for these API requests out of pocket. I sent tens of thousands of requests to the GPT-3.5, GPT-3.5-Turbo and GPT-4 models, this experiment cost me about $759 for all of those requests. I&#8217;d be forever grateful if you&#8217;d consider <a href="https://www.buymeacoffee.com/mentallyanim8d">buying me a coffee</a>.</p>]]></content:encoded></item><item><title><![CDATA[Getting a client IP address through... CloudFront -> AWS NLB -> traefik -> Kubernetes]]></title><description><![CDATA[Gee Batman, that's a lot of layers!]]></description><link>https://www.sliceofexperiments.com/p/getting-a-client-ip-address-through</link><guid isPermaLink="false">https://www.sliceofexperiments.com/p/getting-a-client-ip-address-through</guid><dc:creator><![CDATA[Aaron Batilo]]></dc:creator><pubDate>Tue, 28 Mar 2023 20:15:10 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!o6dZ!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7aaed77e-5db5-4e6b-982e-1802998c069a_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>All I want is to get the client IP address to my application code, but when there are so many different layers in my networking stack, was that going to be harder than I was hoping? Turns out you can only go so far.</p><p>So let&#8217;s start at the top of the request path. If you go to <a href="https://mentallyanimated.com/">https://mentallyanimated.com/</a>, which is my website and is hosted on my playground EKS cluster, you go through a bunch of layers. I have a CloudFront distribution which has an AWS NLB as its origin, which points to traefik as my ingress controller, which then routes to an nginx container that has the static assets of the website. At each step in that request path, there&#8217;s an opportunity to lose the calling client&#8217;s IP address, which is useful for things like rate limiting or allow listing access. Even though the requests start with CloudFront, let&#8217;s start with the NLB.</p><h2>AWS Network Load Balancer</h2><p>I like using a Network Load Balancer in my stack. It&#8217;s highly performant, and a specifically, I like that it doesn&#8217;t have any smarts. I can centralize any and all additional middleware features in my traefik configuration, or swap traefik out for any ingress controller and keep any advanced features like <a href="https://doc.traefik.io/traefik/middlewares/http/ratelimit/">rate limits</a>, or <a href="https://doc.traefik.io/traefik/middlewares/http/replacepath/">path rewriting</a> within the cluster. One catch with it is that it&#8217;s layer 4 load balancer. In other words, it doesn&#8217;t really understand any concept of things like HTTP headers. It&#8217;s just a packet forwarder. So how do we maintain the relevant HTTP headers such that the client IP gets preserved?</p><p>As it turns out, NLB&#8217;s actually support a configuration setting named <a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-target-groups.html#client-ip-preservation">Client IP Preservation</a>, which sounds great, right? When creating my <code>kind: Service</code> <code>type: LoadBalancer</code> for traefik, all I&#8217;m supposed to need to do is add the right annotation to control the <a href="https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.4/guide/service/annotations/#resource-attributes">NLB target group attributes</a>.</p><pre><code><code>service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: preserve_client_ip.enabled=true</code></code></pre><p>This alone actually doesn&#8217;t work. But why not? It&#8217;s because of the way that Kubernetes networking itself works. I&#8217;m not going to try to explain it all, but I will link you to <a href="https://www.asykim.com/blog/deep-dive-into-kubernetes-external-traffic-policies">this blog post </a>which I read to understand the problem.</p><p>To make the <code>preserve_client_ip</code> to work at the NLB level, you need to set the <code>externalTrafficPolicy</code> of your <code>kind: Service</code> to <code>Local</code>, instead of the default <code>Cluster</code>. There&#8217;s just one problem with this. If you set that policy to <code>Local</code>, traefik will only ever try to route to pods on the same node as your traffic. If the targeted application pod is scheduled on a different node, traefik will just timeout trying to forward a request. That&#8217;s no good. One thing you could do with this is set an affinity rule so that your applications always get scheduled wherever there is an existing traefik pod. Similarly, if you made traefik a <code>kind: DaemonSet</code> and your application also a <code>kind: DaemonSet</code>, then you know that there will always be pods next to each other. These solutions are hacks and there must be something better!</p><p>If you look at the <a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-target-groups.html#client-ip-preservation">documentation for enabling client IP preservation</a>, there&#8217;s a fair number of considerations that you have to account for also. One of which seems particularly gnarly.</p><blockquote><ul><li><p>When client IP preservation is enabled, you might encounter TCP/IP connection limitations related to observed socket reuse on the targets. These connection limitations can occur when a client, or a NAT device in front of the client, uses the same source IP address and source port when connecting to multiple load balancer nodes simultaneously. If the load balancer routes these connections to the same target, the connections appear to the target as if they come from the same source socket, which results in connection errors. If this happens, the clients can retry (if the connection fails) or reconnect (if the connection is interrupted). You can reduce this type of connection error by increasing the number of source ephemeral ports or by increasing the number of targets for the load balancer. You can prevent this type of connection error, by disabling client IP preservation or disabling cross-zone load balancing.</p></li></ul></blockquote><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments! New posts every other Tuesday</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>traefik</h2><p>Traefik is a very full featured ingress controller. It supports a lot of different kinds of workloads. You can even start using it for HTTP/3, it supports gRPC, etc. One of the things that I learned about along this journey was about something called the <a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-target-groups.html#proxy-protocol">proxy protocol</a>. Simply put, the proxy protocol gives the NLB a way to encode things like HTTP header information so that it can be kept and interpreted downstream. Perfect. AWS NLBs also support the proxy protocol, which is still just a simple annotation to add to our <code>kind: Service</code> for traefik.</p><pre><code>service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*"</code></pre><p>And then in my terraform <code>helm_release</code> for traefik, all I had to add to enable proxy protocol functionality was set the following template values.</p><pre><code>  set {
    name  = "ports.websecure.proxyProtocol.trustedIPs[0]"
    value = "10.9.0.0/24"
  }

  set {
    name  = "ports.websecure.proxyProtocol.trustedIPs[1]"
    value = "10.9.1.0/24"
  }</code></pre><p>These are the private IP ranges for the subnet that my NLB can use, which means we just have to tell traefik that if a request comes from this IP range and is trying to use the proxy protocol, to go ahead and trust those requests.</p><p>And that&#8217;s literally it. Setting the annotation and setting the trusted IPs made it so that my applications could see the real client IP address. Unfortunately, there&#8217;s one more layer to this story.</p><h2>CloudFront</h2><p>At the top is CloudFront, which made me think that I was very lucky because <a href="https://aws.amazon.com/about-aws/whats-new/2021/10/amazon-cloudfront-client-ip-address-connection-port-header/">only recently</a> did they start supporting forwarding the client IP address. Otherwise, they actually set the <code>X-Forwarded-For</code> header to the IP address of the edge location that the client has connected with.</p><blockquote><p>Previously, IP address and client connection port information were available only in CloudFront access logs, making it harder to resolve issues or perform real-time decision-making based on these data.</p></blockquote><p>There&#8217;s a problem with this though. The industry standard is to only check with <code>X-Forwarded-For</code> header when dealing with proxy IP addresses. CloudFront&#8217;s solution here is to actually set an entirely new header. You can see these extra headers by using the <code>Managed-AllViewerAndCloudFrontHeaders-2022-06</code> request policy. They put the client&#8217;s actual IP address and the requested port under <code>CloudFront-Viewer-Address.</code> This is both a different header AND a different format since it includes the port information. Traefik and other proxies don&#8217;t know what to do with that. You might think to yourself, but Aaron, what if we just modify the headers? I had the same idea and I thought I was being super clever. Separate from a CloudFront Lambda@Edge, I learned that they have even faster and lighter weight CloudFront Functions which run a custom JavaScript runtime, but it&#8217;s perfect for modifying headers.</p><pre><code>resource "aws_cloudfront_function" "fix_x_forwarded_for" {
  name    = "fix-x-forwarded-for"
  runtime = "cloudfront-js-1.0"
  comment = "Sets the X-Forwarded-For header to the client's IP address."
  code    = &lt;&lt;EOF
function handler(event) {
    var request = event.request;
    var clientIP = event.viewer.ip;

    request.headers['x-forwarded-for'] = {value: clientIP};

    return request;
}
EOF
}</code></pre><p>I tried overriding the header before it was sent off to the NLB. Unfortunately, but actually probably fortunately, CloudFront will not forward the request at all when this header is tampered. So that means that I could set yet another header with the correct client IP value, but traefik itself doesn&#8217;t let you rewrite headers natively either. I&#8217;d have to write a custom plugin or use one of the existing plugins. Maybe Lambda@Edge will also work? That's for another time to try.</p><h2>Conclusion</h2><p>So what does this mean? It means that I can&#8217;t actually get the client IP address for requests that go through CloudFront. So that&#8217;s kind of another L for me this week.</p><p>Either way, thanks for reading and see ya&#8217;ll in the next one.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2></h2><p></p>]]></content:encoded></item><item><title><![CDATA[Trying to make my scale from 0 faster]]></title><description><![CDATA[Getting a taste of OpenKruise]]></description><link>https://www.sliceofexperiments.com/p/trying-to-make-my-scale-from-0-faster</link><guid isPermaLink="false">https://www.sliceofexperiments.com/p/trying-to-make-my-scale-from-0-faster</guid><dc:creator><![CDATA[Aaron Batilo]]></dc:creator><pubDate>Tue, 14 Mar 2023 20:15:59 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!o6dZ!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7aaed77e-5db5-4e6b-982e-1802998c069a_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A few weeks ago, I went through how I started setting up some of my services to <a href="https://www.sliceofexperiments.com/p/scale-from-0-saving-nickels-to-feed">scale from 0 pods</a>. And it actually works super well overall, and I&#8217;m pretty happy about it except for one thing. It took about 3 to 3.5 seconds in order to get that working and I was really wondering if I could do better. If I had to guess, a page loading in 3 seconds for most people would seem totally normal. Especially since once the page is loaded, everything else should operate very quickly. But to me, 3 seconds was a little too long.</p><p>That being said, my experiments here didn&#8217;t actually pan out very well, but I still wanted to share what I learned. Not every newsletter post has to be a success, right? Especially not when I try to remind myself that this newsletter is more like a public journal.</p><p>So what&#8217;s the plan? How do I make sure that I scale from 0 more quickly?</p><h1>Step 1: Make sure that there&#8217;s some compute already available</h1><p>Most of my applications are only configured to use 100 millicores of CPU. That means that we&#8217;re really only using 1/10th of a single CPU. So keeping a little headroom around just helps guarantee that the existing pod allocations are leaving enough room on a single node to quickly burst up with some CPU. I use the <a href="https://github.com/kubernetes-sigs/descheduler">kubernetes/descheduler</a> to help consolidate and bin pack pods, and since I try to keep nodes as close to 90% utilization at all times, making sure that we don&#8217;t overly scale in is big for minimizing scale from 0 time. If I didn&#8217;t have any room at all, then the new pod would have to wait for a new node which would take a few minutes.</p><p>So what do I do? I schedule some headroom. The concept of headroom is extremely easy. Create a new PriorityClass in your Kubernetes cluster, with a low value of priority, and then schedule an empty pod with the amount of compute that you want to be reserved. If someone makes a request to my scale from 0 service, that pod gets immediately scheduled and the headroom pod with it&#8217;s lower priority will get rescheduled at the cluster&#8217;s earliest convenience. It&#8217;s basically just preallocating the CPU and doing it in a way that makes sure it&#8217;s being accounted for in any bin packing logic. Here&#8217;s the exact headroom configuration that I use:</p><pre><code>apiVersion: scheduling.k8s.io/v1
description: Priority class used by headroom.
globalDefault: false
kind: PriorityClass
metadata:
  name: headroom
value: -1
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: headroom
spec:
  replicas: 1
  selector:
    matchLabels:
      run: headroom
  template:
    metadata:
      labels:
        run: headroom
    spec:
      containers:
      - image: k8s.gcr.io/pause
        name: reserve-resources
        resources:
          requests:
            cpu: 1
            memory: 2Gi
      priorityClassName: headroom</code></pre><p>This is extremely simple. The used pause container is an application that does nothing and just doesn&#8217;t exit. Defining a priority class is extremely straight forward also.</p><h1>Step 2: Pre-pull your images</h1><p>One of the steps for scheduling the pod from 0 is the need to pull the new image to the node that&#8217;s going to run it. Depending on your image size, this can take a while. So one thing that I tried experimenting with was pulling the images BEFORE needing to actually schedule any pods, and then making sure that my <code>imagePullPolicy</code> on the containers was set to <code>IfNotPresent</code>. So I did some internet searching to find ways to pull images. I had the idea of maybe just running a docker in docker daemonset that would pull images. I&#8217;m pretty sure that this would work but it felt like a maintenance burden that I just didn&#8217;t want to deal with. Eventually, I stumbled upon a Reddit post that talked about <a href="https://openkruise.io/">OpenKruise</a> and its <a href="https://openkruise.io/docs/user-manuals/imagepulljob/">ImagePullJob</a> functionality.</p><p>You might be wondering: &#8220;But Aaron, didn&#8217;t you want LESS maintenance burden?&#8221;. I did, but seeing something shiny and new made up for that maintenance burden. OpenKruise has a bunch of other functionality for what feels like native ways to extend and improve a Kubernetes cluster, but I was installing it purely to test out the ImagePullJob.</p><p>There was a catch with getting this setup though. There&#8217;s <a href="https://github.com/openkruise/kruise/issues/866">no ability for OpenKruise to pull images using the AWS STS token</a> that&#8217;s provided by the EKS control plane. This was pretty annoying because it meant that I needed to manually create a docker login token to be used, and I needed to find a way to keep it updated. That&#8217;s when I came across <a href="https://skryvets.com/blog/2021/03/15/kubernetes-pull-image-from-private-ecr-registry/">this post</a> about running a CronJob to update and save the token. I yoinked the entire implementation and set it up to work in my cluster, along with installing OpenKruise. The entire configuration ended up being about 160 lines in terraform which sounds like a lot but it&#8217;s really quite straight forward:</p><div class="github-gist" data-attrs="{&quot;innerHTML&quot;:&quot;<div id=\&quot;gist121385094\&quot; class=\&quot;gist\&quot;>\n    <div class=\&quot;gist-file\&quot; translate=\&quot;no\&quot;>\n      <div class=\&quot;gist-data\&quot;>\n        <div class=\&quot;js-gist-file-update-container js-task-list-container file-box\&quot;>\n  <div id=\&quot;file-imagepulljob-tf\&quot; class=\&quot;file my-2\&quot;>\n    \n    <div itemprop=\&quot;text\&quot; class=\&quot;Box-body p-0 blob-wrapper data type-hcl  \&quot;>\n\n        \n<div class=\&quot;js-check-bidi js-blob-code-container blob-code-content\&quot;>\n\n  <template class=\&quot;js-file-alert-template\&quot;>\n  <div data-view-component=\&quot;true\&quot; class=\&quot;flash flash-warn flash-full d-flex flex-items-center\&quot;>\n  <svg aria-hidden=\&quot;true\&quot; height=\&quot;16\&quot; viewBox=\&quot;0 0 16 16\&quot; version=\&quot;1.1\&quot; width=\&quot;16\&quot; data-view-component=\&quot;true\&quot; class=\&quot;octicon octicon-alert\&quot;>\n    <path d=\&quot;M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\&quot;></path>\n</svg>\n    <span>\n      This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.\n      <a href=\&quot;https://github.co/hiddenchars\&quot; target=\&quot;_blank\&quot;>Learn more about bidirectional Unicode characters</a>\n    </span>\n\n\n  <div data-view-component=\&quot;true\&quot; class=\&quot;flash-action\&quot;>        <a href=\&quot;{{ revealButtonHref }}\&quot; data-view-component=\&quot;true\&quot; class=\&quot;btn-sm btn\&quot;>    Show hidden characters\n</a>\n</div>\n</div></template>\n<template class=\&quot;js-line-alert-template\&quot;>\n  <span aria-label=\&quot;This line has hidden Unicode characters\&quot; data-view-component=\&quot;true\&quot; class=\&quot;line-alert tooltipped tooltipped-e\&quot;>\n    <svg aria-hidden=\&quot;true\&quot; height=\&quot;16\&quot; viewBox=\&quot;0 0 16 16\&quot; version=\&quot;1.1\&quot; width=\&quot;16\&quot; data-view-component=\&quot;true\&quot; class=\&quot;octicon octicon-alert\&quot;>\n    <path d=\&quot;M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\&quot;></path>\n</svg>\n</span></template>\n\n  <table data-hpc class=\&quot;highlight tab-size js-file-line-container js-code-nav-container js-tagsearch-file\&quot; data-tab-size=\&quot;8\&quot; data-paste-markdown-skip data-tagsearch-lang=\&quot;HCL\&quot; data-tagsearch-path=\&quot;imagepulljob.tf\&quot;>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L1\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;1\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC1\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-k\&quot;>locals</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L2\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;2\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC2\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  kruise_ecr_token_updater_service_account <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kruise-ecr-token-updater<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L3\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;3\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC3\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  kruise_ecr_token_secret_name             <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kruise-ecr-token<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L4\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;4\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC4\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L5\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;5\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC5\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  kruise_ecr_token_updater_script <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-k\&quot;>&amp;lt;&amp;lt;</span><span class=\&quot;pl-k\&quot;>EOF</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L6\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;6\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC6\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-s\&quot;>ECR_TOKEN=`aws ecr get-login-password --region $<span class=\&quot;pl-k\&quot;>${</span>AWS_REGION<span class=\&quot;pl-k\&quot;>}</span>`</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L7\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;7\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC7\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-s\&quot;>NAMESPACE_NAME=<span class=\&quot;pl-k\&quot;>${</span>kubernetes_namespace<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>kruise_system</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>metadata</span>[<span class=\&quot;pl-c1\&quot;>0</span>]<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>name</span><span class=\&quot;pl-k\&quot;>}</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L8\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;8\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC8\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-s\&quot;></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L9\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;9\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC9\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-s\&quot;>kubectl delete secret --ignore-not-found $DOCKER_SECRET_NAME -n $NAMESPACE_NAME</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L10\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;10\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC10\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-s\&quot;>kubectl create secret docker-registry $DOCKER_SECRET_NAME \\</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L11\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;11\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC11\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-s\&quot;>--docker-server=https://$<span class=\&quot;pl-k\&quot;>${</span>AWS_ACCOUNT<span class=\&quot;pl-k\&quot;>}</span>.dkr.ecr.$<span class=\&quot;pl-k\&quot;>${</span>AWS_REGION<span class=\&quot;pl-k\&quot;>}</span>.amazonaws.com \\</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L12\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;12\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC12\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-s\&quot;>--docker-username=AWS \\</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L13\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;13\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC13\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-s\&quot;>--docker-password=&amp;quot;$<span class=\&quot;pl-k\&quot;>${</span>ECR_TOKEN<span class=\&quot;pl-k\&quot;>}</span>&amp;quot; \\</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L14\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;14\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC14\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-s\&quot;>--namespace=$NAMESPACE_NAME</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L15\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;15\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC15\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-s\&quot;>echo &amp;quot;Secret was successfully updated at $(date)&amp;quot;</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L16\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;16\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC16\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-s\&quot;><span class=\&quot;pl-k\&quot;>EOF</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L17\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;17\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC17\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>}</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L18\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;18\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC18\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L19\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;19\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC19\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-k\&quot;>resource</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kubernetes_namespace<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kruise_system<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L20\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;20\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC20\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  <span class=\&quot;pl-en\&quot;>metadata</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L21\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;21\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC21\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    name <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kruise-system<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L22\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;22\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC22\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L23\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;23\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC23\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>}</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L24\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;24\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC24\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L25\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;25\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC25\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-k\&quot;>data</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>aws_iam_policy<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>ecr_read_only<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L26\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;26\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC26\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  arn <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L27\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;27\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC27\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>}</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L28\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;28\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC28\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L29\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;29\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC29\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-k\&quot;>module</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kruise_ecr_token_updater_irsa<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L30\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;30\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC30\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  source  <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L31\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;31\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC31\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  version <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>~&amp;gt; 5.0<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L32\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;32\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC32\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L33\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;33\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC33\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  create_role <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-c1\&quot;>true</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L34\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;34\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC34\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  role_name   <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kruise-ecr-token-updater-<span class=\&quot;pl-k\&quot;>${</span><span class=\&quot;pl-c1\&quot;>local</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>cluster_name</span><span class=\&quot;pl-k\&quot;>}</span><span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L35\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;35\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC35\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L36\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;36\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC36\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  role_policy_arns <span class=\&quot;pl-k\&quot;>=</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L37\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;37\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC37\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    ecr_read_only <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-c1\&quot;>data</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>aws_iam_policy</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>ecr_read_only</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>arn</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L38\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;38\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC38\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L39\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;39\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC39\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L40\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;40\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC40\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  oidc_providers <span class=\&quot;pl-k\&quot;>=</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L41\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;41\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC41\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    irsa <span class=\&quot;pl-k\&quot;>=</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L42\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;42\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC42\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>      provider_arn               <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-c1\&quot;>module</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>eks-red</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>oidc_provider_arn</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L43\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;43\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC43\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>      namespace_service_accounts <span class=\&quot;pl-k\&quot;>=</span> [<span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span><span class=\&quot;pl-k\&quot;>${</span>kubernetes_namespace<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>kruise_system</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>metadata</span>[<span class=\&quot;pl-c1\&quot;>0</span>]<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>name</span><span class=\&quot;pl-k\&quot;>}</span>:<span class=\&quot;pl-k\&quot;>${</span><span class=\&quot;pl-c1\&quot;>local</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>kruise_ecr_token_updater_service_account</span><span class=\&quot;pl-k\&quot;>}</span><span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span>]</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L44\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;44\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC44\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L45\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;45\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC45\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L46\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;46\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC46\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>}</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L47\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;47\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC47\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L48\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;48\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC48\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-k\&quot;>resource</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kubernetes_service_account<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kruise_ecr_token_updater<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L49\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;49\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC49\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  <span class=\&quot;pl-en\&quot;>metadata</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L50\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;50\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC50\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    name      <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-c1\&quot;>local</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>kruise_ecr_token_updater_service_account</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L51\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;51\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC51\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    namespace <span class=\&quot;pl-k\&quot;>=</span> kubernetes_namespace<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>kruise_system</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>metadata</span>[<span class=\&quot;pl-c1\&quot;>0</span>]<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>name</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L52\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;52\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC52\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L53\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;53\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC53\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    annotations <span class=\&quot;pl-k\&quot;>=</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L54\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;54\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC54\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>      <span class=\&quot;pl-pds\&quot;>&amp;quot;</span>eks.amazonaws.com/role-arn<span class=\&quot;pl-pds\&quot;>&amp;quot;</span> <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-c1\&quot;>module</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>kruise_ecr_token_updater_irsa</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>iam_role_arn</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L55\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;55\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC55\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L56\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;56\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC56\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L57\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;57\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC57\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>}</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L58\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;58\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC58\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L59\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;59\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC59\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-k\&quot;>resource</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kubernetes_role<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kruise_ecr_token_updater<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L60\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;60\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC60\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  <span class=\&quot;pl-en\&quot;>metadata</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L61\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;61\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC61\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    name      <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kruise-ecr-token-updater<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L62\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;62\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC62\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    namespace <span class=\&quot;pl-k\&quot;>=</span> kubernetes_namespace<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>kruise_system</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>metadata</span>[<span class=\&quot;pl-c1\&quot;>0</span>]<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>name</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L63\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;63\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC63\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L64\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;64\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC64\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L65\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;65\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC65\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  <span class=\&quot;pl-en\&quot;>rule</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L66\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;66\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC66\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    api_groups     <span class=\&quot;pl-k\&quot;>=</span> [<span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span><span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span>]</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L67\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;67\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC67\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    resources      <span class=\&quot;pl-k\&quot;>=</span> [<span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>secrets<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span>]</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L68\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;68\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC68\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    resource_names <span class=\&quot;pl-k\&quot;>=</span> [<span class=\&quot;pl-c1\&quot;>local</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>kruise_ecr_token_secret_name</span>]</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L69\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;69\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC69\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    verbs          <span class=\&quot;pl-k\&quot;>=</span> [<span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>delete<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span>]</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L70\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;70\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC70\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L71\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;71\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC71\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L72\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;72\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC72\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  <span class=\&quot;pl-en\&quot;>rule</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L73\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;73\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC73\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    api_groups <span class=\&quot;pl-k\&quot;>=</span> [<span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span><span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span>]</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L74\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;74\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC74\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    resources  <span class=\&quot;pl-k\&quot;>=</span> [<span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>secrets<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span>]</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L75\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;75\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC75\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    verbs      <span class=\&quot;pl-k\&quot;>=</span> [<span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>create<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span>]</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L76\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;76\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC76\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L77\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;77\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC77\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>}</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L78\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;78\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC78\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L79\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;79\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC79\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-k\&quot;>resource</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kubernetes_role_binding<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kruise_ecr_token_updater<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L80\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;80\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC80\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  <span class=\&quot;pl-en\&quot;>metadata</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L81\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;81\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC81\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    name      <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kruise-ecr-token-updater<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L82\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;82\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC82\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    namespace <span class=\&quot;pl-k\&quot;>=</span> kubernetes_namespace<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>kruise_system</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>metadata</span>[<span class=\&quot;pl-c1\&quot;>0</span>]<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>name</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L83\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;83\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC83\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L84\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;84\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC84\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L85\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;85\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC85\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  <span class=\&quot;pl-en\&quot;>role_ref</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L86\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;86\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC86\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    api_group <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>rbac.authorization.k8s.io<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L87\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;87\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC87\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    kind      <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>Role<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L88\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;88\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC88\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    name      <span class=\&quot;pl-k\&quot;>=</span> kubernetes_role<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>kruise_ecr_token_updater</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>metadata</span>[<span class=\&quot;pl-c1\&quot;>0</span>]<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>name</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L89\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;89\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC89\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L90\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;90\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC90\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L91\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;91\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC91\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  <span class=\&quot;pl-en\&quot;>subject</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L92\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;92\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC92\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    kind      <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>ServiceAccount<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L93\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;93\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC93\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    name      <span class=\&quot;pl-k\&quot;>=</span> kubernetes_service_account<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>kruise_ecr_token_updater</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>metadata</span>[<span class=\&quot;pl-c1\&quot;>0</span>]<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>name</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L94\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;94\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC94\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    namespace <span class=\&quot;pl-k\&quot;>=</span> kubernetes_service_account<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>kruise_ecr_token_updater</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>metadata</span>[<span class=\&quot;pl-c1\&quot;>0</span>]<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>namespace</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L95\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;95\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC95\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L96\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;96\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC96\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>}</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L97\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;97\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC97\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L98\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;98\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC98\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-k\&quot;>resource</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kubernetes_config_map<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kruise_ecr_token_updater<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L99\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;99\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC99\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  <span class=\&quot;pl-en\&quot;>metadata</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L100\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;100\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC100\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    name      <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kruise-ecr-token-updater<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L101\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;101\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC101\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    namespace <span class=\&quot;pl-k\&quot;>=</span> kubernetes_namespace<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>kruise_system</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>metadata</span>[<span class=\&quot;pl-c1\&quot;>0</span>]<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>name</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L102\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;102\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC102\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L103\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;103\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC103\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L104\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;104\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC104\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  data <span class=\&quot;pl-k\&quot;>=</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L105\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;105\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC105\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    AWS_ACCOUNT        <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-c1\&quot;>data</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>aws_caller_identity</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>current</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>account_id</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L106\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;106\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC106\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    AWS_REGION         <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-c1\&quot;>data</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>aws_region</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>current</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>name</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L107\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;107\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC107\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    DOCKER_SECRET_NAME <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-c1\&quot;>local</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>kruise_ecr_token_secret_name</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L108\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;108\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC108\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L109\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;109\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC109\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>}</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L110\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;110\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC110\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L111\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;111\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC111\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-k\&quot;>resource</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kubernetes_cron_job_v1<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kruise_ecr_token_updater<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L112\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;112\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC112\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  <span class=\&quot;pl-en\&quot;>metadata</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L113\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;113\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC113\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    name      <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kruise-ecr-token-updater<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L114\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;114\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC114\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    namespace <span class=\&quot;pl-k\&quot;>=</span> kubernetes_namespace<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>kruise_system</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>metadata</span>[<span class=\&quot;pl-c1\&quot;>0</span>]<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>name</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L115\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;115\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC115\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L116\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;116\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC116\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  <span class=\&quot;pl-en\&quot;>spec</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L117\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;117\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC117\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    schedule <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>0 */10 * * *<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L118\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;118\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC118\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    <span class=\&quot;pl-en\&quot;>job_template</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L119\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;119\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC119\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>      <span class=\&quot;pl-en\&quot;>metadata</span> {}</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L120\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;120\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC120\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>      <span class=\&quot;pl-en\&quot;>spec</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L121\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;121\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC121\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>        <span class=\&quot;pl-en\&quot;>template</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L122\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;122\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC122\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>          <span class=\&quot;pl-en\&quot;>metadata</span> {}</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L123\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;123\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC123\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>          <span class=\&quot;pl-en\&quot;>spec</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L124\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;124\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC124\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>            service_account_name <span class=\&quot;pl-k\&quot;>=</span> kubernetes_service_account<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>kruise_ecr_token_updater</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>metadata</span>[<span class=\&quot;pl-c1\&quot;>0</span>]<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>name</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L125\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;125\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC125\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>            <span class=\&quot;pl-en\&quot;>container</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L126\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;126\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC126\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>              name  <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kruise-ecr-token-updater<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L127\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;127\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC127\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>              image <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>odaniait/aws-kubectl:latest<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L128\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;128\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC128\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L129\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;129\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC129\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>              command <span class=\&quot;pl-k\&quot;>=</span> [</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L130\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;130\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC130\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>                <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>/bin/sh<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span>,</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L131\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;131\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC131\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>                <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>-c<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span>,</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L132\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;132\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC132\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>                <span class=\&quot;pl-c1\&quot;>local</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>kruise_ecr_token_updater_script</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L133\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;133\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC133\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>              ]</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L134\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;134\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC134\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L135\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;135\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC135\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>              <span class=\&quot;pl-en\&quot;>env_from</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L136\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;136\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC136\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>                <span class=\&quot;pl-en\&quot;>config_map_ref</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L137\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;137\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC137\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>                  name <span class=\&quot;pl-k\&quot;>=</span> kubernetes_config_map<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>kruise_ecr_token_updater</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>metadata</span>[<span class=\&quot;pl-c1\&quot;>0</span>]<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>name</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L138\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;138\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC138\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>                }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L139\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;139\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC139\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>              }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L140\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;140\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC140\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>            }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L141\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;141\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC141\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>          }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L142\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;142\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC142\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>        }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L143\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;143\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC143\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>      }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L144\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;144\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC144\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L145\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;145\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC145\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L146\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;146\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC146\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>}</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L147\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;147\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC147\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L148\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;148\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC148\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-k\&quot;>resource</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>helm_release<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>openkruise<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L149\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;149\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC149\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  name         <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kruise<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L150\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;150\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC150\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  namespace    <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kube-system<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L151\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;151\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC151\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  repository   <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>https://openkruise.github.io/charts/<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L152\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;152\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC152\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  chart        <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>kruise<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L153\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;153\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC153\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  version      <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>1.3.0<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L154\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;154\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC154\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  reset_values <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-c1\&quot;>true</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L155\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;155\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC155\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L156\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;156\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC156\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  <span class=\&quot;pl-en\&quot;>set</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L157\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;157\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC157\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    name  <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>installation.namespace<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L158\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;158\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC158\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    value <span class=\&quot;pl-k\&quot;>=</span> kubernetes_namespace<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>kruise_system</span><span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>metadata</span>[<span class=\&quot;pl-c1\&quot;>0</span>]<span class=\&quot;pl-k\&quot;>.</span><span class=\&quot;pl-smi\&quot;>name</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L159\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;159\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC159\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L160\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;160\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC160\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L161\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;161\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC161\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  <span class=\&quot;pl-en\&quot;>set</span> {</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L162\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;162\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC162\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    name  <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;quot;</span>installation.createNamespace<span class=\&quot;pl-pds\&quot;>&amp;quot;</span></span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L163\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;163\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC163\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    value <span class=\&quot;pl-k\&quot;>=</span> <span class=\&quot;pl-c1\&quot;>false</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L164\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;164\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC164\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  }</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-tf-L165\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;165\&quot;></td>\n          <td id=\&quot;file-imagepulljob-tf-LC165\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>}</td>\n        </tr>\n  </table>\n</div>\n\n\n    </div>\n\n  </div>\n</div>\n\n      </div>\n      <div class=\&quot;gist-meta\&quot;>\n        <a href=\&quot;https://gist.github.com/abatilo/6b287265d541d06da567893c1522999f/raw/8ac64666da613694edae7de35bee586fa4deb752/imagepulljob.tf\&quot; style=\&quot;float:right\&quot;>view raw</a>\n        <a href=\&quot;https://gist.github.com/abatilo/6b287265d541d06da567893c1522999f#file-imagepulljob-tf\&quot;>\n          imagepulljob.tf\n        </a>\n        hosted with &amp;#10084; by <a href=\&quot;https://github.com\&quot;>GitHub</a>\n      </div>\n    </div>\n</div>\n&quot;,&quot;stylesheet&quot;:&quot;https://github.githubassets.com/assets/gist-embed-aa244c59879c.css&quot;}" data-component-name="GitgistToDOM"><link rel="stylesheet" href="https://github.githubassets.com/assets/gist-embed-aa244c59879c.css"><div id="gist121385094" class="gist">
    <div class="gist-file">
      <div class="gist-data">
        <div class="js-gist-file-update-container js-task-list-container file-box">
  <div id="file-imagepulljob-tf" class="file my-2">
    
    <div itemprop="text" class="Box-body p-0 blob-wrapper data type-hcl  ">

        
<div class="js-check-bidi js-blob-code-container blob-code-content">

  
  <div data-view-component="true" class="flash flash-warn flash-full d-flex flex-items-center">
  
    

    <span>
      This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      <a href="https://github.co/hiddenchars" target="_blank">Learn more about bidirectional Unicode characters</a>
    </span>


  <div data-view-component="true" class="flash-action">        <a href="{{ revealButtonHref }}" data-view-component="true" class="btn-sm btn">    Show hidden characters
</a>
</div>
</div>

  <span data-view-component="true" class="line-alert tooltipped tooltipped-e">
    
    

</span>

  <table data-hpc="" class="highlight tab-size js-file-line-container js-code-nav-container js-tagsearch-file" data-tab-size="8" data-paste-markdown-skip="" data-tagsearch-lang="HCL" data-tagsearch-path="imagepulljob.tf">
        <tbody><tr>
          <td id="file-imagepulljob-tf-L1" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="1"></td>
          <td id="file-imagepulljob-tf-LC1" class="blob-code blob-code-inner js-file-line"><span class="pl-k">locals</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L2" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="2"></td>
          <td id="file-imagepulljob-tf-LC2" class="blob-code blob-code-inner js-file-line">  kruise_ecr_token_updater_service_account <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>kruise-ecr-token-updater<span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L3" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="3"></td>
          <td id="file-imagepulljob-tf-LC3" class="blob-code blob-code-inner js-file-line">  kruise_ecr_token_secret_name             <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>kruise-ecr-token<span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L4" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="4"></td>
          <td id="file-imagepulljob-tf-LC4" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L5" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="5"></td>
          <td id="file-imagepulljob-tf-LC5" class="blob-code blob-code-inner js-file-line">  kruise_ecr_token_updater_script <span class="pl-k">=</span> <span class="pl-s"><span class="pl-k">&lt;&lt;</span><span class="pl-k">EOF</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L6" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="6"></td>
          <td id="file-imagepulljob-tf-LC6" class="blob-code blob-code-inner js-file-line"><span class="pl-s">ECR_TOKEN=`aws ecr get-login-password --region $<span class="pl-k">${</span>AWS_REGION<span class="pl-k">}</span>`</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L7" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="7"></td>
          <td id="file-imagepulljob-tf-LC7" class="blob-code blob-code-inner js-file-line"><span class="pl-s">NAMESPACE_NAME=<span class="pl-k">${</span>kubernetes_namespace<span class="pl-k">.</span><span class="pl-smi">kruise_system</span><span class="pl-k">.</span><span class="pl-smi">metadata</span>[<span class="pl-c1">0</span>]<span class="pl-k">.</span><span class="pl-smi">name</span><span class="pl-k">}</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L8" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="8"></td>
          <td id="file-imagepulljob-tf-LC8" class="blob-code blob-code-inner js-file-line"><span class="pl-s"></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L9" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="9"></td>
          <td id="file-imagepulljob-tf-LC9" class="blob-code blob-code-inner js-file-line"><span class="pl-s">kubectl delete secret --ignore-not-found $DOCKER_SECRET_NAME -n $NAMESPACE_NAME</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L10" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="10"></td>
          <td id="file-imagepulljob-tf-LC10" class="blob-code blob-code-inner js-file-line"><span class="pl-s">kubectl create secret docker-registry $DOCKER_SECRET_NAME \</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L11" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="11"></td>
          <td id="file-imagepulljob-tf-LC11" class="blob-code blob-code-inner js-file-line"><span class="pl-s">--docker-server=https://$<span class="pl-k">${</span>AWS_ACCOUNT<span class="pl-k">}</span>.dkr.ecr.$<span class="pl-k">${</span>AWS_REGION<span class="pl-k">}</span>.amazonaws.com \</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L12" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="12"></td>
          <td id="file-imagepulljob-tf-LC12" class="blob-code blob-code-inner js-file-line"><span class="pl-s">--docker-username=AWS \</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L13" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="13"></td>
          <td id="file-imagepulljob-tf-LC13" class="blob-code blob-code-inner js-file-line"><span class="pl-s">--docker-password="$<span class="pl-k">${</span>ECR_TOKEN<span class="pl-k">}</span>" \</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L14" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="14"></td>
          <td id="file-imagepulljob-tf-LC14" class="blob-code blob-code-inner js-file-line"><span class="pl-s">--namespace=$NAMESPACE_NAME</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L15" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="15"></td>
          <td id="file-imagepulljob-tf-LC15" class="blob-code blob-code-inner js-file-line"><span class="pl-s">echo "Secret was successfully updated at $(date)"</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L16" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="16"></td>
          <td id="file-imagepulljob-tf-LC16" class="blob-code blob-code-inner js-file-line"><span class="pl-s"><span class="pl-k">EOF</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L17" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="17"></td>
          <td id="file-imagepulljob-tf-LC17" class="blob-code blob-code-inner js-file-line">}</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L18" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="18"></td>
          <td id="file-imagepulljob-tf-LC18" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L19" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="19"></td>
          <td id="file-imagepulljob-tf-LC19" class="blob-code blob-code-inner js-file-line"><span class="pl-k">resource</span> <span class="pl-s"><span class="pl-pds">"</span>kubernetes_namespace<span class="pl-pds">"</span></span> <span class="pl-s"><span class="pl-pds">"</span>kruise_system<span class="pl-pds">"</span></span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L20" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="20"></td>
          <td id="file-imagepulljob-tf-LC20" class="blob-code blob-code-inner js-file-line">  <span class="pl-en">metadata</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L21" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="21"></td>
          <td id="file-imagepulljob-tf-LC21" class="blob-code blob-code-inner js-file-line">    name <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>kruise-system<span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L22" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="22"></td>
          <td id="file-imagepulljob-tf-LC22" class="blob-code blob-code-inner js-file-line">  }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L23" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="23"></td>
          <td id="file-imagepulljob-tf-LC23" class="blob-code blob-code-inner js-file-line">}</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L24" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="24"></td>
          <td id="file-imagepulljob-tf-LC24" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L25" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="25"></td>
          <td id="file-imagepulljob-tf-LC25" class="blob-code blob-code-inner js-file-line"><span class="pl-k">data</span> <span class="pl-s"><span class="pl-pds">"</span>aws_iam_policy<span class="pl-pds">"</span></span> <span class="pl-s"><span class="pl-pds">"</span>ecr_read_only<span class="pl-pds">"</span></span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L26" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="26"></td>
          <td id="file-imagepulljob-tf-LC26" class="blob-code blob-code-inner js-file-line">  arn <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly<span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L27" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="27"></td>
          <td id="file-imagepulljob-tf-LC27" class="blob-code blob-code-inner js-file-line">}</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L28" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="28"></td>
          <td id="file-imagepulljob-tf-LC28" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L29" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="29"></td>
          <td id="file-imagepulljob-tf-LC29" class="blob-code blob-code-inner js-file-line"><span class="pl-k">module</span> <span class="pl-s"><span class="pl-pds">"</span>kruise_ecr_token_updater_irsa<span class="pl-pds">"</span></span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L30" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="30"></td>
          <td id="file-imagepulljob-tf-LC30" class="blob-code blob-code-inner js-file-line">  source  <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks<span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L31" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="31"></td>
          <td id="file-imagepulljob-tf-LC31" class="blob-code blob-code-inner js-file-line">  version <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>~&gt; 5.0<span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L32" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="32"></td>
          <td id="file-imagepulljob-tf-LC32" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L33" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="33"></td>
          <td id="file-imagepulljob-tf-LC33" class="blob-code blob-code-inner js-file-line">  create_role <span class="pl-k">=</span> <span class="pl-c1">true</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L34" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="34"></td>
          <td id="file-imagepulljob-tf-LC34" class="blob-code blob-code-inner js-file-line">  role_name   <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>kruise-ecr-token-updater-<span class="pl-k">${</span><span class="pl-c1">local</span><span class="pl-k">.</span><span class="pl-smi">cluster_name</span><span class="pl-k">}</span><span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L35" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="35"></td>
          <td id="file-imagepulljob-tf-LC35" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L36" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="36"></td>
          <td id="file-imagepulljob-tf-LC36" class="blob-code blob-code-inner js-file-line">  role_policy_arns <span class="pl-k">=</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L37" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="37"></td>
          <td id="file-imagepulljob-tf-LC37" class="blob-code blob-code-inner js-file-line">    ecr_read_only <span class="pl-k">=</span> <span class="pl-c1">data</span><span class="pl-k">.</span><span class="pl-smi">aws_iam_policy</span><span class="pl-k">.</span><span class="pl-smi">ecr_read_only</span><span class="pl-k">.</span><span class="pl-smi">arn</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L38" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="38"></td>
          <td id="file-imagepulljob-tf-LC38" class="blob-code blob-code-inner js-file-line">  }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L39" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="39"></td>
          <td id="file-imagepulljob-tf-LC39" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L40" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="40"></td>
          <td id="file-imagepulljob-tf-LC40" class="blob-code blob-code-inner js-file-line">  oidc_providers <span class="pl-k">=</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L41" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="41"></td>
          <td id="file-imagepulljob-tf-LC41" class="blob-code blob-code-inner js-file-line">    irsa <span class="pl-k">=</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L42" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="42"></td>
          <td id="file-imagepulljob-tf-LC42" class="blob-code blob-code-inner js-file-line">      provider_arn               <span class="pl-k">=</span> <span class="pl-c1">module</span><span class="pl-k">.</span><span class="pl-smi">eks-red</span><span class="pl-k">.</span><span class="pl-smi">oidc_provider_arn</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L43" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="43"></td>
          <td id="file-imagepulljob-tf-LC43" class="blob-code blob-code-inner js-file-line">      namespace_service_accounts <span class="pl-k">=</span> [<span class="pl-s"><span class="pl-pds">"</span><span class="pl-k">${</span>kubernetes_namespace<span class="pl-k">.</span><span class="pl-smi">kruise_system</span><span class="pl-k">.</span><span class="pl-smi">metadata</span>[<span class="pl-c1">0</span>]<span class="pl-k">.</span><span class="pl-smi">name</span><span class="pl-k">}</span>:<span class="pl-k">${</span><span class="pl-c1">local</span><span class="pl-k">.</span><span class="pl-smi">kruise_ecr_token_updater_service_account</span><span class="pl-k">}</span><span class="pl-pds">"</span></span>]</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L44" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="44"></td>
          <td id="file-imagepulljob-tf-LC44" class="blob-code blob-code-inner js-file-line">    }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L45" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="45"></td>
          <td id="file-imagepulljob-tf-LC45" class="blob-code blob-code-inner js-file-line">  }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L46" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="46"></td>
          <td id="file-imagepulljob-tf-LC46" class="blob-code blob-code-inner js-file-line">}</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L47" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="47"></td>
          <td id="file-imagepulljob-tf-LC47" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L48" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="48"></td>
          <td id="file-imagepulljob-tf-LC48" class="blob-code blob-code-inner js-file-line"><span class="pl-k">resource</span> <span class="pl-s"><span class="pl-pds">"</span>kubernetes_service_account<span class="pl-pds">"</span></span> <span class="pl-s"><span class="pl-pds">"</span>kruise_ecr_token_updater<span class="pl-pds">"</span></span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L49" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="49"></td>
          <td id="file-imagepulljob-tf-LC49" class="blob-code blob-code-inner js-file-line">  <span class="pl-en">metadata</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L50" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="50"></td>
          <td id="file-imagepulljob-tf-LC50" class="blob-code blob-code-inner js-file-line">    name      <span class="pl-k">=</span> <span class="pl-c1">local</span><span class="pl-k">.</span><span class="pl-smi">kruise_ecr_token_updater_service_account</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L51" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="51"></td>
          <td id="file-imagepulljob-tf-LC51" class="blob-code blob-code-inner js-file-line">    namespace <span class="pl-k">=</span> kubernetes_namespace<span class="pl-k">.</span><span class="pl-smi">kruise_system</span><span class="pl-k">.</span><span class="pl-smi">metadata</span>[<span class="pl-c1">0</span>]<span class="pl-k">.</span><span class="pl-smi">name</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L52" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="52"></td>
          <td id="file-imagepulljob-tf-LC52" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L53" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="53"></td>
          <td id="file-imagepulljob-tf-LC53" class="blob-code blob-code-inner js-file-line">    annotations <span class="pl-k">=</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L54" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="54"></td>
          <td id="file-imagepulljob-tf-LC54" class="blob-code blob-code-inner js-file-line">      <span class="pl-pds">"</span>eks.amazonaws.com/role-arn<span class="pl-pds">"</span> <span class="pl-k">=</span> <span class="pl-c1">module</span><span class="pl-k">.</span><span class="pl-smi">kruise_ecr_token_updater_irsa</span><span class="pl-k">.</span><span class="pl-smi">iam_role_arn</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L55" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="55"></td>
          <td id="file-imagepulljob-tf-LC55" class="blob-code blob-code-inner js-file-line">    }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L56" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="56"></td>
          <td id="file-imagepulljob-tf-LC56" class="blob-code blob-code-inner js-file-line">  }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L57" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="57"></td>
          <td id="file-imagepulljob-tf-LC57" class="blob-code blob-code-inner js-file-line">}</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L58" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="58"></td>
          <td id="file-imagepulljob-tf-LC58" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L59" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="59"></td>
          <td id="file-imagepulljob-tf-LC59" class="blob-code blob-code-inner js-file-line"><span class="pl-k">resource</span> <span class="pl-s"><span class="pl-pds">"</span>kubernetes_role<span class="pl-pds">"</span></span> <span class="pl-s"><span class="pl-pds">"</span>kruise_ecr_token_updater<span class="pl-pds">"</span></span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L60" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="60"></td>
          <td id="file-imagepulljob-tf-LC60" class="blob-code blob-code-inner js-file-line">  <span class="pl-en">metadata</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L61" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="61"></td>
          <td id="file-imagepulljob-tf-LC61" class="blob-code blob-code-inner js-file-line">    name      <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>kruise-ecr-token-updater<span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L62" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="62"></td>
          <td id="file-imagepulljob-tf-LC62" class="blob-code blob-code-inner js-file-line">    namespace <span class="pl-k">=</span> kubernetes_namespace<span class="pl-k">.</span><span class="pl-smi">kruise_system</span><span class="pl-k">.</span><span class="pl-smi">metadata</span>[<span class="pl-c1">0</span>]<span class="pl-k">.</span><span class="pl-smi">name</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L63" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="63"></td>
          <td id="file-imagepulljob-tf-LC63" class="blob-code blob-code-inner js-file-line">  }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L64" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="64"></td>
          <td id="file-imagepulljob-tf-LC64" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L65" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="65"></td>
          <td id="file-imagepulljob-tf-LC65" class="blob-code blob-code-inner js-file-line">  <span class="pl-en">rule</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L66" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="66"></td>
          <td id="file-imagepulljob-tf-LC66" class="blob-code blob-code-inner js-file-line">    api_groups     <span class="pl-k">=</span> [<span class="pl-s"><span class="pl-pds">"</span><span class="pl-pds">"</span></span>]</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L67" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="67"></td>
          <td id="file-imagepulljob-tf-LC67" class="blob-code blob-code-inner js-file-line">    resources      <span class="pl-k">=</span> [<span class="pl-s"><span class="pl-pds">"</span>secrets<span class="pl-pds">"</span></span>]</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L68" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="68"></td>
          <td id="file-imagepulljob-tf-LC68" class="blob-code blob-code-inner js-file-line">    resource_names <span class="pl-k">=</span> [<span class="pl-c1">local</span><span class="pl-k">.</span><span class="pl-smi">kruise_ecr_token_secret_name</span>]</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L69" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="69"></td>
          <td id="file-imagepulljob-tf-LC69" class="blob-code blob-code-inner js-file-line">    verbs          <span class="pl-k">=</span> [<span class="pl-s"><span class="pl-pds">"</span>delete<span class="pl-pds">"</span></span>]</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L70" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="70"></td>
          <td id="file-imagepulljob-tf-LC70" class="blob-code blob-code-inner js-file-line">  }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L71" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="71"></td>
          <td id="file-imagepulljob-tf-LC71" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L72" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="72"></td>
          <td id="file-imagepulljob-tf-LC72" class="blob-code blob-code-inner js-file-line">  <span class="pl-en">rule</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L73" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="73"></td>
          <td id="file-imagepulljob-tf-LC73" class="blob-code blob-code-inner js-file-line">    api_groups <span class="pl-k">=</span> [<span class="pl-s"><span class="pl-pds">"</span><span class="pl-pds">"</span></span>]</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L74" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="74"></td>
          <td id="file-imagepulljob-tf-LC74" class="blob-code blob-code-inner js-file-line">    resources  <span class="pl-k">=</span> [<span class="pl-s"><span class="pl-pds">"</span>secrets<span class="pl-pds">"</span></span>]</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L75" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="75"></td>
          <td id="file-imagepulljob-tf-LC75" class="blob-code blob-code-inner js-file-line">    verbs      <span class="pl-k">=</span> [<span class="pl-s"><span class="pl-pds">"</span>create<span class="pl-pds">"</span></span>]</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L76" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="76"></td>
          <td id="file-imagepulljob-tf-LC76" class="blob-code blob-code-inner js-file-line">  }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L77" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="77"></td>
          <td id="file-imagepulljob-tf-LC77" class="blob-code blob-code-inner js-file-line">}</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L78" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="78"></td>
          <td id="file-imagepulljob-tf-LC78" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L79" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="79"></td>
          <td id="file-imagepulljob-tf-LC79" class="blob-code blob-code-inner js-file-line"><span class="pl-k">resource</span> <span class="pl-s"><span class="pl-pds">"</span>kubernetes_role_binding<span class="pl-pds">"</span></span> <span class="pl-s"><span class="pl-pds">"</span>kruise_ecr_token_updater<span class="pl-pds">"</span></span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L80" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="80"></td>
          <td id="file-imagepulljob-tf-LC80" class="blob-code blob-code-inner js-file-line">  <span class="pl-en">metadata</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L81" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="81"></td>
          <td id="file-imagepulljob-tf-LC81" class="blob-code blob-code-inner js-file-line">    name      <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>kruise-ecr-token-updater<span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L82" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="82"></td>
          <td id="file-imagepulljob-tf-LC82" class="blob-code blob-code-inner js-file-line">    namespace <span class="pl-k">=</span> kubernetes_namespace<span class="pl-k">.</span><span class="pl-smi">kruise_system</span><span class="pl-k">.</span><span class="pl-smi">metadata</span>[<span class="pl-c1">0</span>]<span class="pl-k">.</span><span class="pl-smi">name</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L83" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="83"></td>
          <td id="file-imagepulljob-tf-LC83" class="blob-code blob-code-inner js-file-line">  }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L84" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="84"></td>
          <td id="file-imagepulljob-tf-LC84" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L85" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="85"></td>
          <td id="file-imagepulljob-tf-LC85" class="blob-code blob-code-inner js-file-line">  <span class="pl-en">role_ref</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L86" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="86"></td>
          <td id="file-imagepulljob-tf-LC86" class="blob-code blob-code-inner js-file-line">    api_group <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>rbac.authorization.k8s.io<span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L87" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="87"></td>
          <td id="file-imagepulljob-tf-LC87" class="blob-code blob-code-inner js-file-line">    kind      <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>Role<span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L88" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="88"></td>
          <td id="file-imagepulljob-tf-LC88" class="blob-code blob-code-inner js-file-line">    name      <span class="pl-k">=</span> kubernetes_role<span class="pl-k">.</span><span class="pl-smi">kruise_ecr_token_updater</span><span class="pl-k">.</span><span class="pl-smi">metadata</span>[<span class="pl-c1">0</span>]<span class="pl-k">.</span><span class="pl-smi">name</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L89" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="89"></td>
          <td id="file-imagepulljob-tf-LC89" class="blob-code blob-code-inner js-file-line">  }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L90" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="90"></td>
          <td id="file-imagepulljob-tf-LC90" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L91" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="91"></td>
          <td id="file-imagepulljob-tf-LC91" class="blob-code blob-code-inner js-file-line">  <span class="pl-en">subject</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L92" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="92"></td>
          <td id="file-imagepulljob-tf-LC92" class="blob-code blob-code-inner js-file-line">    kind      <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>ServiceAccount<span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L93" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="93"></td>
          <td id="file-imagepulljob-tf-LC93" class="blob-code blob-code-inner js-file-line">    name      <span class="pl-k">=</span> kubernetes_service_account<span class="pl-k">.</span><span class="pl-smi">kruise_ecr_token_updater</span><span class="pl-k">.</span><span class="pl-smi">metadata</span>[<span class="pl-c1">0</span>]<span class="pl-k">.</span><span class="pl-smi">name</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L94" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="94"></td>
          <td id="file-imagepulljob-tf-LC94" class="blob-code blob-code-inner js-file-line">    namespace <span class="pl-k">=</span> kubernetes_service_account<span class="pl-k">.</span><span class="pl-smi">kruise_ecr_token_updater</span><span class="pl-k">.</span><span class="pl-smi">metadata</span>[<span class="pl-c1">0</span>]<span class="pl-k">.</span><span class="pl-smi">namespace</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L95" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="95"></td>
          <td id="file-imagepulljob-tf-LC95" class="blob-code blob-code-inner js-file-line">  }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L96" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="96"></td>
          <td id="file-imagepulljob-tf-LC96" class="blob-code blob-code-inner js-file-line">}</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L97" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="97"></td>
          <td id="file-imagepulljob-tf-LC97" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L98" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="98"></td>
          <td id="file-imagepulljob-tf-LC98" class="blob-code blob-code-inner js-file-line"><span class="pl-k">resource</span> <span class="pl-s"><span class="pl-pds">"</span>kubernetes_config_map<span class="pl-pds">"</span></span> <span class="pl-s"><span class="pl-pds">"</span>kruise_ecr_token_updater<span class="pl-pds">"</span></span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L99" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="99"></td>
          <td id="file-imagepulljob-tf-LC99" class="blob-code blob-code-inner js-file-line">  <span class="pl-en">metadata</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L100" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="100"></td>
          <td id="file-imagepulljob-tf-LC100" class="blob-code blob-code-inner js-file-line">    name      <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>kruise-ecr-token-updater<span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L101" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="101"></td>
          <td id="file-imagepulljob-tf-LC101" class="blob-code blob-code-inner js-file-line">    namespace <span class="pl-k">=</span> kubernetes_namespace<span class="pl-k">.</span><span class="pl-smi">kruise_system</span><span class="pl-k">.</span><span class="pl-smi">metadata</span>[<span class="pl-c1">0</span>]<span class="pl-k">.</span><span class="pl-smi">name</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L102" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="102"></td>
          <td id="file-imagepulljob-tf-LC102" class="blob-code blob-code-inner js-file-line">  }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L103" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="103"></td>
          <td id="file-imagepulljob-tf-LC103" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L104" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="104"></td>
          <td id="file-imagepulljob-tf-LC104" class="blob-code blob-code-inner js-file-line">  data <span class="pl-k">=</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L105" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="105"></td>
          <td id="file-imagepulljob-tf-LC105" class="blob-code blob-code-inner js-file-line">    AWS_ACCOUNT        <span class="pl-k">=</span> <span class="pl-c1">data</span><span class="pl-k">.</span><span class="pl-smi">aws_caller_identity</span><span class="pl-k">.</span><span class="pl-smi">current</span><span class="pl-k">.</span><span class="pl-smi">account_id</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L106" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="106"></td>
          <td id="file-imagepulljob-tf-LC106" class="blob-code blob-code-inner js-file-line">    AWS_REGION         <span class="pl-k">=</span> <span class="pl-c1">data</span><span class="pl-k">.</span><span class="pl-smi">aws_region</span><span class="pl-k">.</span><span class="pl-smi">current</span><span class="pl-k">.</span><span class="pl-smi">name</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L107" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="107"></td>
          <td id="file-imagepulljob-tf-LC107" class="blob-code blob-code-inner js-file-line">    DOCKER_SECRET_NAME <span class="pl-k">=</span> <span class="pl-c1">local</span><span class="pl-k">.</span><span class="pl-smi">kruise_ecr_token_secret_name</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L108" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="108"></td>
          <td id="file-imagepulljob-tf-LC108" class="blob-code blob-code-inner js-file-line">  }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L109" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="109"></td>
          <td id="file-imagepulljob-tf-LC109" class="blob-code blob-code-inner js-file-line">}</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L110" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="110"></td>
          <td id="file-imagepulljob-tf-LC110" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L111" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="111"></td>
          <td id="file-imagepulljob-tf-LC111" class="blob-code blob-code-inner js-file-line"><span class="pl-k">resource</span> <span class="pl-s"><span class="pl-pds">"</span>kubernetes_cron_job_v1<span class="pl-pds">"</span></span> <span class="pl-s"><span class="pl-pds">"</span>kruise_ecr_token_updater<span class="pl-pds">"</span></span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L112" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="112"></td>
          <td id="file-imagepulljob-tf-LC112" class="blob-code blob-code-inner js-file-line">  <span class="pl-en">metadata</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L113" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="113"></td>
          <td id="file-imagepulljob-tf-LC113" class="blob-code blob-code-inner js-file-line">    name      <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>kruise-ecr-token-updater<span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L114" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="114"></td>
          <td id="file-imagepulljob-tf-LC114" class="blob-code blob-code-inner js-file-line">    namespace <span class="pl-k">=</span> kubernetes_namespace<span class="pl-k">.</span><span class="pl-smi">kruise_system</span><span class="pl-k">.</span><span class="pl-smi">metadata</span>[<span class="pl-c1">0</span>]<span class="pl-k">.</span><span class="pl-smi">name</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L115" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="115"></td>
          <td id="file-imagepulljob-tf-LC115" class="blob-code blob-code-inner js-file-line">  }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L116" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="116"></td>
          <td id="file-imagepulljob-tf-LC116" class="blob-code blob-code-inner js-file-line">  <span class="pl-en">spec</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L117" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="117"></td>
          <td id="file-imagepulljob-tf-LC117" class="blob-code blob-code-inner js-file-line">    schedule <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>0 */10 * * *<span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L118" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="118"></td>
          <td id="file-imagepulljob-tf-LC118" class="blob-code blob-code-inner js-file-line">    <span class="pl-en">job_template</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L119" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="119"></td>
          <td id="file-imagepulljob-tf-LC119" class="blob-code blob-code-inner js-file-line">      <span class="pl-en">metadata</span> {}</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L120" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="120"></td>
          <td id="file-imagepulljob-tf-LC120" class="blob-code blob-code-inner js-file-line">      <span class="pl-en">spec</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L121" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="121"></td>
          <td id="file-imagepulljob-tf-LC121" class="blob-code blob-code-inner js-file-line">        <span class="pl-en">template</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L122" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="122"></td>
          <td id="file-imagepulljob-tf-LC122" class="blob-code blob-code-inner js-file-line">          <span class="pl-en">metadata</span> {}</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L123" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="123"></td>
          <td id="file-imagepulljob-tf-LC123" class="blob-code blob-code-inner js-file-line">          <span class="pl-en">spec</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L124" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="124"></td>
          <td id="file-imagepulljob-tf-LC124" class="blob-code blob-code-inner js-file-line">            service_account_name <span class="pl-k">=</span> kubernetes_service_account<span class="pl-k">.</span><span class="pl-smi">kruise_ecr_token_updater</span><span class="pl-k">.</span><span class="pl-smi">metadata</span>[<span class="pl-c1">0</span>]<span class="pl-k">.</span><span class="pl-smi">name</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L125" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="125"></td>
          <td id="file-imagepulljob-tf-LC125" class="blob-code blob-code-inner js-file-line">            <span class="pl-en">container</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L126" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="126"></td>
          <td id="file-imagepulljob-tf-LC126" class="blob-code blob-code-inner js-file-line">              name  <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>kruise-ecr-token-updater<span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L127" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="127"></td>
          <td id="file-imagepulljob-tf-LC127" class="blob-code blob-code-inner js-file-line">              image <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>odaniait/aws-kubectl:latest<span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L128" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="128"></td>
          <td id="file-imagepulljob-tf-LC128" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L129" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="129"></td>
          <td id="file-imagepulljob-tf-LC129" class="blob-code blob-code-inner js-file-line">              command <span class="pl-k">=</span> [</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L130" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="130"></td>
          <td id="file-imagepulljob-tf-LC130" class="blob-code blob-code-inner js-file-line">                <span class="pl-s"><span class="pl-pds">"</span>/bin/sh<span class="pl-pds">"</span></span>,</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L131" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="131"></td>
          <td id="file-imagepulljob-tf-LC131" class="blob-code blob-code-inner js-file-line">                <span class="pl-s"><span class="pl-pds">"</span>-c<span class="pl-pds">"</span></span>,</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L132" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="132"></td>
          <td id="file-imagepulljob-tf-LC132" class="blob-code blob-code-inner js-file-line">                <span class="pl-c1">local</span><span class="pl-k">.</span><span class="pl-smi">kruise_ecr_token_updater_script</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L133" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="133"></td>
          <td id="file-imagepulljob-tf-LC133" class="blob-code blob-code-inner js-file-line">              ]</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L134" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="134"></td>
          <td id="file-imagepulljob-tf-LC134" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L135" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="135"></td>
          <td id="file-imagepulljob-tf-LC135" class="blob-code blob-code-inner js-file-line">              <span class="pl-en">env_from</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L136" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="136"></td>
          <td id="file-imagepulljob-tf-LC136" class="blob-code blob-code-inner js-file-line">                <span class="pl-en">config_map_ref</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L137" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="137"></td>
          <td id="file-imagepulljob-tf-LC137" class="blob-code blob-code-inner js-file-line">                  name <span class="pl-k">=</span> kubernetes_config_map<span class="pl-k">.</span><span class="pl-smi">kruise_ecr_token_updater</span><span class="pl-k">.</span><span class="pl-smi">metadata</span>[<span class="pl-c1">0</span>]<span class="pl-k">.</span><span class="pl-smi">name</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L138" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="138"></td>
          <td id="file-imagepulljob-tf-LC138" class="blob-code blob-code-inner js-file-line">                }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L139" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="139"></td>
          <td id="file-imagepulljob-tf-LC139" class="blob-code blob-code-inner js-file-line">              }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L140" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="140"></td>
          <td id="file-imagepulljob-tf-LC140" class="blob-code blob-code-inner js-file-line">            }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L141" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="141"></td>
          <td id="file-imagepulljob-tf-LC141" class="blob-code blob-code-inner js-file-line">          }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L142" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="142"></td>
          <td id="file-imagepulljob-tf-LC142" class="blob-code blob-code-inner js-file-line">        }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L143" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="143"></td>
          <td id="file-imagepulljob-tf-LC143" class="blob-code blob-code-inner js-file-line">      }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L144" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="144"></td>
          <td id="file-imagepulljob-tf-LC144" class="blob-code blob-code-inner js-file-line">    }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L145" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="145"></td>
          <td id="file-imagepulljob-tf-LC145" class="blob-code blob-code-inner js-file-line">  }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L146" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="146"></td>
          <td id="file-imagepulljob-tf-LC146" class="blob-code blob-code-inner js-file-line">}</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L147" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="147"></td>
          <td id="file-imagepulljob-tf-LC147" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L148" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="148"></td>
          <td id="file-imagepulljob-tf-LC148" class="blob-code blob-code-inner js-file-line"><span class="pl-k">resource</span> <span class="pl-s"><span class="pl-pds">"</span>helm_release<span class="pl-pds">"</span></span> <span class="pl-s"><span class="pl-pds">"</span>openkruise<span class="pl-pds">"</span></span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L149" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="149"></td>
          <td id="file-imagepulljob-tf-LC149" class="blob-code blob-code-inner js-file-line">  name         <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>kruise<span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L150" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="150"></td>
          <td id="file-imagepulljob-tf-LC150" class="blob-code blob-code-inner js-file-line">  namespace    <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>kube-system<span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L151" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="151"></td>
          <td id="file-imagepulljob-tf-LC151" class="blob-code blob-code-inner js-file-line">  repository   <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>https://openkruise.github.io/charts/<span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L152" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="152"></td>
          <td id="file-imagepulljob-tf-LC152" class="blob-code blob-code-inner js-file-line">  chart        <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>kruise<span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L153" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="153"></td>
          <td id="file-imagepulljob-tf-LC153" class="blob-code blob-code-inner js-file-line">  version      <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>1.3.0<span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L154" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="154"></td>
          <td id="file-imagepulljob-tf-LC154" class="blob-code blob-code-inner js-file-line">  reset_values <span class="pl-k">=</span> <span class="pl-c1">true</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L155" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="155"></td>
          <td id="file-imagepulljob-tf-LC155" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L156" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="156"></td>
          <td id="file-imagepulljob-tf-LC156" class="blob-code blob-code-inner js-file-line">  <span class="pl-en">set</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L157" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="157"></td>
          <td id="file-imagepulljob-tf-LC157" class="blob-code blob-code-inner js-file-line">    name  <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>installation.namespace<span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L158" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="158"></td>
          <td id="file-imagepulljob-tf-LC158" class="blob-code blob-code-inner js-file-line">    value <span class="pl-k">=</span> kubernetes_namespace<span class="pl-k">.</span><span class="pl-smi">kruise_system</span><span class="pl-k">.</span><span class="pl-smi">metadata</span>[<span class="pl-c1">0</span>]<span class="pl-k">.</span><span class="pl-smi">name</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L159" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="159"></td>
          <td id="file-imagepulljob-tf-LC159" class="blob-code blob-code-inner js-file-line">  }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L160" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="160"></td>
          <td id="file-imagepulljob-tf-LC160" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L161" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="161"></td>
          <td id="file-imagepulljob-tf-LC161" class="blob-code blob-code-inner js-file-line">  <span class="pl-en">set</span> {</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L162" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="162"></td>
          <td id="file-imagepulljob-tf-LC162" class="blob-code blob-code-inner js-file-line">    name  <span class="pl-k">=</span> <span class="pl-s"><span class="pl-pds">"</span>installation.createNamespace<span class="pl-pds">"</span></span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L163" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="163"></td>
          <td id="file-imagepulljob-tf-LC163" class="blob-code blob-code-inner js-file-line">    value <span class="pl-k">=</span> <span class="pl-c1">false</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L164" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="164"></td>
          <td id="file-imagepulljob-tf-LC164" class="blob-code blob-code-inner js-file-line">  }</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-tf-L165" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="165"></td>
          <td id="file-imagepulljob-tf-LC165" class="blob-code blob-code-inner js-file-line">}</td>
        </tr>
  </tbody></table>
</div>


    </div>

  </div>
</div>

      </div>
      <div class="gist-meta">
        <a href="https://gist.github.com/abatilo/6b287265d541d06da567893c1522999f/raw/8ac64666da613694edae7de35bee586fa4deb752/imagepulljob.tf" style="float:right">view raw</a>
        <a href="https://gist.github.com/abatilo/6b287265d541d06da567893c1522999f#file-imagepulljob-tf">
          imagepulljob.tf
        </a>
        hosted with &#10084; by <a href="https://github.com">GitHub</a>
      </div>
    </div>
</div>
</div><p>So we create a ServiceAccount that can write secrets to the EKS control plane, and then once every 10 hours, we fetch a new secret from ECR and then store it. That token will be available for OpenKruise to use, so then we install OpenKruise itself.</p><p>OpenKruise will run partially as a DaemonSet which is what I was already thinking of doing.</p><p>Now, in order to use the ImagePullJob, I simply added the following manifest to the kustomize manifests for each application:</p><div class="github-gist" data-attrs="{&quot;innerHTML&quot;:&quot;<div id=\&quot;gist121385174\&quot; class=\&quot;gist\&quot;>\n    <div class=\&quot;gist-file\&quot; translate=\&quot;no\&quot;>\n      <div class=\&quot;gist-data\&quot;>\n        <div class=\&quot;js-gist-file-update-container js-task-list-container file-box\&quot;>\n  <div id=\&quot;file-imagepulljob-yaml\&quot; class=\&quot;file my-2\&quot;>\n    \n    <div itemprop=\&quot;text\&quot; class=\&quot;Box-body p-0 blob-wrapper data type-yaml  \&quot;>\n\n        \n<div class=\&quot;js-check-bidi js-blob-code-container blob-code-content\&quot;>\n\n  <template class=\&quot;js-file-alert-template\&quot;>\n  <div data-view-component=\&quot;true\&quot; class=\&quot;flash flash-warn flash-full d-flex flex-items-center\&quot;>\n  <svg aria-hidden=\&quot;true\&quot; height=\&quot;16\&quot; viewBox=\&quot;0 0 16 16\&quot; version=\&quot;1.1\&quot; width=\&quot;16\&quot; data-view-component=\&quot;true\&quot; class=\&quot;octicon octicon-alert\&quot;>\n    <path d=\&quot;M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\&quot;></path>\n</svg>\n    <span>\n      This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.\n      <a href=\&quot;https://github.co/hiddenchars\&quot; target=\&quot;_blank\&quot;>Learn more about bidirectional Unicode characters</a>\n    </span>\n\n\n  <div data-view-component=\&quot;true\&quot; class=\&quot;flash-action\&quot;>        <a href=\&quot;{{ revealButtonHref }}\&quot; data-view-component=\&quot;true\&quot; class=\&quot;btn-sm btn\&quot;>    Show hidden characters\n</a>\n</div>\n</div></template>\n<template class=\&quot;js-line-alert-template\&quot;>\n  <span aria-label=\&quot;This line has hidden Unicode characters\&quot; data-view-component=\&quot;true\&quot; class=\&quot;line-alert tooltipped tooltipped-e\&quot;>\n    <svg aria-hidden=\&quot;true\&quot; height=\&quot;16\&quot; viewBox=\&quot;0 0 16 16\&quot; version=\&quot;1.1\&quot; width=\&quot;16\&quot; data-view-component=\&quot;true\&quot; class=\&quot;octicon octicon-alert\&quot;>\n    <path d=\&quot;M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\&quot;></path>\n</svg>\n</span></template>\n\n  <table data-hpc class=\&quot;highlight tab-size js-file-line-container js-code-nav-container js-tagsearch-file\&quot; data-tab-size=\&quot;8\&quot; data-paste-markdown-skip data-tagsearch-lang=\&quot;YAML\&quot; data-tagsearch-path=\&quot;imagepulljob.yaml\&quot;>\n        <tr>\n          <td id=\&quot;file-imagepulljob-yaml-L1\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;1\&quot;></td>\n          <td id=\&quot;file-imagepulljob-yaml-LC1\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-ent\&quot;>apiVersion</span>: <span class=\&quot;pl-s\&quot;>apps.kruise.io/v1alpha1</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-yaml-L2\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;2\&quot;></td>\n          <td id=\&quot;file-imagepulljob-yaml-LC2\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-ent\&quot;>kind</span>: <span class=\&quot;pl-s\&quot;>ImagePullJob</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-yaml-L3\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;3\&quot;></td>\n          <td id=\&quot;file-imagepulljob-yaml-LC3\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-ent\&quot;>metadata</span>:</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-yaml-L4\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;4\&quot;></td>\n          <td id=\&quot;file-imagepulljob-yaml-LC4\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  <span class=\&quot;pl-ent\&quot;>name</span>: <span class=\&quot;pl-s\&quot;>sudokurace</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-yaml-L5\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;5\&quot;></td>\n          <td id=\&quot;file-imagepulljob-yaml-LC5\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  <span class=\&quot;pl-ent\&quot;>namespace</span>: <span class=\&quot;pl-s\&quot;>kruise-system</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-yaml-L6\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;6\&quot;></td>\n          <td id=\&quot;file-imagepulljob-yaml-LC6\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-ent\&quot;>spec</span>:</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-yaml-L7\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;7\&quot;></td>\n          <td id=\&quot;file-imagepulljob-yaml-LC7\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  <span class=\&quot;pl-ent\&quot;>image</span>: <span class=\&quot;pl-s\&quot;><span class=\&quot;pl-pds\&quot;>&amp;#39;</span>911907402684.dkr.ecr.us-west-2.amazonaws.com/sudokurace:<span class=\&quot;pl-pds\&quot;>&amp;#39;</span></span>  <span class=\&quot;pl-c\&quot;><span class=\&quot;pl-c\&quot;>#</span> Tag set by kustomization.yaml</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-yaml-L8\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;8\&quot;></td>\n          <td id=\&quot;file-imagepulljob-yaml-LC8\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  <span class=\&quot;pl-ent\&quot;>pullSecrets</span>:</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-yaml-L9\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;9\&quot;></td>\n          <td id=\&quot;file-imagepulljob-yaml-LC9\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  <span class=\&quot;pl-c\&quot;><span class=\&quot;pl-c\&quot;>#</span> Must match https://gist.github.com/abatilo/6b287265d541d06da567893c1522999f#file-imagepulljob-tf-L3</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-imagepulljob-yaml-L10\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;10\&quot;></td>\n          <td id=\&quot;file-imagepulljob-yaml-LC10\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  - <span class=\&quot;pl-s\&quot;>kruise-ecr-token</span></td>\n        </tr>\n  </table>\n</div>\n\n\n    </div>\n\n  </div>\n</div>\n\n      </div>\n      <div class=\&quot;gist-meta\&quot;>\n        <a href=\&quot;https://gist.github.com/abatilo/c0d4b9251bfc543f8b1dd572998a2d46/raw/33186a7662c64517db9e61910352e7b5129cf991/imagepulljob.yaml\&quot; style=\&quot;float:right\&quot;>view raw</a>\n        <a href=\&quot;https://gist.github.com/abatilo/c0d4b9251bfc543f8b1dd572998a2d46#file-imagepulljob-yaml\&quot;>\n          imagepulljob.yaml\n        </a>\n        hosted with &amp;#10084; by <a href=\&quot;https://github.com\&quot;>GitHub</a>\n      </div>\n    </div>\n</div>\n&quot;,&quot;stylesheet&quot;:&quot;https://github.githubassets.com/assets/gist-embed-aa244c59879c.css&quot;}" data-component-name="GitgistToDOM"><link rel="stylesheet" href="https://github.githubassets.com/assets/gist-embed-aa244c59879c.css"><div id="gist121385174" class="gist">
    <div class="gist-file">
      <div class="gist-data">
        <div class="js-gist-file-update-container js-task-list-container file-box">
  <div id="file-imagepulljob-yaml" class="file my-2">
    
    <div itemprop="text" class="Box-body p-0 blob-wrapper data type-yaml  ">

        
<div class="js-check-bidi js-blob-code-container blob-code-content">

  
  <div data-view-component="true" class="flash flash-warn flash-full d-flex flex-items-center">
  
    

    <span>
      This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      <a href="https://github.co/hiddenchars" target="_blank">Learn more about bidirectional Unicode characters</a>
    </span>


  <div data-view-component="true" class="flash-action">        <a href="{{ revealButtonHref }}" data-view-component="true" class="btn-sm btn">    Show hidden characters
</a>
</div>
</div>

  <span data-view-component="true" class="line-alert tooltipped tooltipped-e">
    
    

</span>

  <table data-hpc="" class="highlight tab-size js-file-line-container js-code-nav-container js-tagsearch-file" data-tab-size="8" data-paste-markdown-skip="" data-tagsearch-lang="YAML" data-tagsearch-path="imagepulljob.yaml">
        <tbody><tr>
          <td id="file-imagepulljob-yaml-L1" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="1"></td>
          <td id="file-imagepulljob-yaml-LC1" class="blob-code blob-code-inner js-file-line"><span class="pl-ent">apiVersion</span>: <span class="pl-s">apps.kruise.io/v1alpha1</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-yaml-L2" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="2"></td>
          <td id="file-imagepulljob-yaml-LC2" class="blob-code blob-code-inner js-file-line"><span class="pl-ent">kind</span>: <span class="pl-s">ImagePullJob</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-yaml-L3" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="3"></td>
          <td id="file-imagepulljob-yaml-LC3" class="blob-code blob-code-inner js-file-line"><span class="pl-ent">metadata</span>:</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-yaml-L4" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="4"></td>
          <td id="file-imagepulljob-yaml-LC4" class="blob-code blob-code-inner js-file-line">  <span class="pl-ent">name</span>: <span class="pl-s">sudokurace</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-yaml-L5" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="5"></td>
          <td id="file-imagepulljob-yaml-LC5" class="blob-code blob-code-inner js-file-line">  <span class="pl-ent">namespace</span>: <span class="pl-s">kruise-system</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-yaml-L6" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="6"></td>
          <td id="file-imagepulljob-yaml-LC6" class="blob-code blob-code-inner js-file-line"><span class="pl-ent">spec</span>:</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-yaml-L7" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="7"></td>
          <td id="file-imagepulljob-yaml-LC7" class="blob-code blob-code-inner js-file-line">  <span class="pl-ent">image</span>: <span class="pl-s"><span class="pl-pds">'</span>911907402684.dkr.ecr.us-west-2.amazonaws.com/sudokurace:<span class="pl-pds">'</span></span>  <span class="pl-c"><span class="pl-c">#</span> Tag set by kustomization.yaml</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-yaml-L8" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="8"></td>
          <td id="file-imagepulljob-yaml-LC8" class="blob-code blob-code-inner js-file-line">  <span class="pl-ent">pullSecrets</span>:</td>
        </tr>
        <tr>
          <td id="file-imagepulljob-yaml-L9" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="9"></td>
          <td id="file-imagepulljob-yaml-LC9" class="blob-code blob-code-inner js-file-line">  <span class="pl-c"><span class="pl-c">#</span> Must match https://gist.github.com/abatilo/6b287265d541d06da567893c1522999f#file-imagepulljob-tf-L3</span></td>
        </tr>
        <tr>
          <td id="file-imagepulljob-yaml-L10" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="10"></td>
          <td id="file-imagepulljob-yaml-LC10" class="blob-code blob-code-inner js-file-line">  - <span class="pl-s">kruise-ecr-token</span></td>
        </tr>
  </tbody></table>
</div>


    </div>

  </div>
</div>

      </div>
      <div class="gist-meta">
        <a href="https://gist.github.com/abatilo/c0d4b9251bfc543f8b1dd572998a2d46/raw/33186a7662c64517db9e61910352e7b5129cf991/imagepulljob.yaml" style="float:right">view raw</a>
        <a href="https://gist.github.com/abatilo/c0d4b9251bfc543f8b1dd572998a2d46#file-imagepulljob-yaml">
          imagepulljob.yaml
        </a>
        hosted with &#10084; by <a href="https://github.com">GitHub</a>
      </div>
    </div>
</div>
</div><p>Every time the image tag changed, which it would for every deployment, a new ImagePullJob would get scheduled and OpenKruise would download the specified image in for each node in the cluster. Now I wouldn&#8217;t have to wait for the images to be downloaded on that node whenever a page was requested.</p><h1>Step 3: Reduce the polling interval for pod readiness</h1><p>As I outline in the scale to 0 post, I settled on using the <a href="https://github.com/kedacore/http-add-on/blob/148a5292f157ee30c7cb8bf6bdbb3eecb181e6f2/interceptor/config/serving.go#L24-L28">keda/http-add-on</a> for scaling from 0. And this step is super boring because all I did for this one was set the <a href="https://github.com/kedacore/http-add-on/blob/148a5292f157ee30c7cb8bf6bdbb3eecb181e6f2/interceptor/config/serving.go#L24-L28">DeploymentCachePollIntervalMS</a> to a lower number. By default, it checks to see if the deployment has new pods once every 250 milliseconds. I dropped that to 100 milliseconds so that we&#8217;d notice more quickly once there was a pod ready to take traffic.</p><h1>Wrapping up</h1><p>I was really excited to get OpenKruise to work and start doing the ImagePullJobs. Unfortunately, or maybe fortunately, my images are all extremely tiny. They&#8217;re statically linked Go binaries and even though I include the entirety of each set of static assets for the website, the containers only end up being about 30-40 MB even while uncompressed. These take milliseconds for my EKS nodes to pull from ECR. They&#8217;re so fast that even after having the images pre-pulled, the containers basically didn&#8217;t come up any faster at all. Maybe in the 10s of milliseconds. The ImagePullJobs are also not retroactive, in the sense that they execute one time, at time of image tag update, but if new nodes come up, the image isn&#8217;t pulled there. Since I run everything on EC2 spot nodes, my nodes churn fairly often, which nullified the whole point of being able to pre-pull.</p><p>What really helped with scale from 0 time ended up being just reducing that polling interval. The containers now get scheduled and respond in about 2.5 seconds, so we&#8217;re shaving upwards of a full second off from the page load. Does it really make a difference? No, probably not, but it was fun to try regardless. I think if I had larger images like a Python or Node application, that the ImagePullJob would still be pretty worth it. Alas, that&#8217;s not the situation I&#8217;m in.</p><p>See you for the next one. Thanks for reading</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Building a personalized Ask Me Anything chat bot using HTMX and gpt-index]]></title><description><![CDATA[It actually kind of works?]]></description><link>https://www.sliceofexperiments.com/p/building-a-personalized-ask-me-anything</link><guid isPermaLink="false">https://www.sliceofexperiments.com/p/building-a-personalized-ask-me-anything</guid><dc:creator><![CDATA[Aaron Batilo]]></dc:creator><pubDate>Tue, 14 Feb 2023 21:15:06 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!SRC8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38f23606-1544-426c-a5ec-f3b626c6918d_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>TL;DR:</strong> Check out &lt;I&#8217;ve killed the deployment!&gt;</p><p>Quick preamble. There&#8217;s several levels of experiments here. Maybe most notably, the main contents of this post were actually recorded with my Google Pixel 7 Pro recording app, and then I asked ChatGPT to help correct and fix any punctuation and grammatical errors. I thought it would be interesting to see how well ChatGPT would do with the flaky speech-to-text, and I took my dog on a walk at the same time. I only did some very light manual editing on the content to make up for any random things I said while on the walk!</p><div><hr></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!SRC8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38f23606-1544-426c-a5ec-f3b626c6918d_1024x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!SRC8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38f23606-1544-426c-a5ec-f3b626c6918d_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!SRC8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38f23606-1544-426c-a5ec-f3b626c6918d_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!SRC8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38f23606-1544-426c-a5ec-f3b626c6918d_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!SRC8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38f23606-1544-426c-a5ec-f3b626c6918d_1024x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!SRC8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38f23606-1544-426c-a5ec-f3b626c6918d_1024x1024.png" width="1024" height="1024" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/38f23606-1544-426c-a5ec-f3b626c6918d_1024x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1024,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1603140,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!SRC8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38f23606-1544-426c-a5ec-f3b626c6918d_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!SRC8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38f23606-1544-426c-a5ec-f3b626c6918d_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!SRC8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38f23606-1544-426c-a5ec-f3b626c6918d_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!SRC8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F38f23606-1544-426c-a5ec-f3b626c6918d_1024x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">an oil painting of a steam punk robot that's holding a piece of paper with a question mark on it</figcaption></figure></div><p>It's Sunday, February 12th and I'm taking Chopper for a walk. Today, I was hoping to talk about my next newsletter post, as there's a lot to discuss. Recently, I came across a post in the Lenny newsletter about <a href="https://www.lennysnewsletter.com/p/i-built-a-lenny-chatbot-using-gpt">training a chatbot to answer questions based on their content</a>. I found it interesting and wondered how easy it would be to do the same. The Lenny newsletter chatbot used a library called <a href="https://gpt-index.readthedocs.io/en/latest/index.html">GPT Index</a>. Initially, I looked into fine-tuning my own GPT-3 model, but I didn't have enough content to train it. While some articles suggested that only a couple of dozen examples were enough, I didn't want to go that route. So, I thought of making my chatbot using GPT Index, but I needed content to feed into the model. I thought of writing about myself and my work history.</p><p>The first major idea that I thought of was to have a chatbot that was intended for the kinds of conversations that you have with a technical recruiter. With all of the layoffs that have been happening recently, it made me wonder if people would be interested in some tool that would help them skip that first step and try to save everyone some time.</p><p>Questions like 'Are you authorized to work in the United States?' could be answered by the chatbot. While this might seem like a frequently asked questions page, most of my projects are experimental and don't necessarily solve a problem, but I don't feel bad about it. So, I started writing a bunch of text about myself, my life, my history, and my work history.</p><p>Then, I downloaded the GPT index and ran almost the exact code that was used in the Lenny newsletter.</p><pre><code>from gpt_index import (GPTSimpleVectorIndex, LLMPredictor, PromptHelper,
                       SimpleDirectoryReader)
from langchain import OpenAI


def construct_index(directory_path):
    max_input_size = 4096
    num_outputs = 1024
    max_chunk_overlap = 60
    chunk_size_limit = 800

    # define LLM
    llm_predictor = LLMPredictor(
        llm=OpenAI(
            temperature=0.2, model_name="text-davinci-003", max_tokens=num_outputs
        )
    )
    prompt_helper = PromptHelper(
        max_input_size,
        num_outputs,
        max_chunk_overlap,
        chunk_size_limit=chunk_size_limit,
    )

    documents = SimpleDirectoryReader(directory_path).load_data()

    index = GPTSimpleVectorIndex(
        documents,
        llm_predictor=llm_predictor,
        prompt_helper=prompt_helper,
    )

    index.save_to_disk("index.json")


if __name__ == "__main__":
    construct_index("data")
</code></pre><p>It worked well without any additional information or configuration. That was really interesting, and I decided to understand more about what GPT index does.</p><p>GPT index is actually more like a search engine than anything else. It provides several ways to easily source data from your file system or Google Docs, for example, and then convert and run them through the <a href="https://platform.openai.com/docs/guides/embeddings">GPT language model embeddings</a>. So what are these embeddings? Simply put, they are numerical representations of words, and since they are numerical, we can perform mathematical operations on them. For every document you have, GPT index will call the embeddings at a point, pass in that document, and then store that in a location of your choosing.</p><p>I learned that GPT Index can store the vectors that represent your documents in various databases like <a href="https://www.pinecone.io/">Pinecone</a> or <a href="https://weaviate.io/">Weaviate</a>. But since I did not have a lot of documents, doing everything in memory is completely reasonable. Once we have all of these embeddings then I started to dig deeper.</p><p>The vector models use a similarity metric to search for the most similar content from your list of documents. GPT Index has <a href="https://github.com/jerryjliu/gpt_index/blob/a6c89159bf8e7086bea2f4305cff3f0a4102e370/gpt_index/prompts/default_prompts.py">several prompts</a> available that can be used to combine or rewrite the results found from the similarity search. For example:</p><pre><code>DEFAULT_REFINE_PROMPT_TMPL = (
    "The original question is as follows: {query_str}\n"
    "We have provided an existing answer: {existing_answer}\n"
    "We have the opportunity to refine the existing answer"
    "(only if needed) with some more context below.\n"
    "------------\n"
    "{context_msg}\n"
    "------------\n"
    "Given the new context, refine the original answer to better "
    "answer the question. "
    "If the context isn't useful, return the original answer."
)</code></pre><p>The similarity search is why specialized databases are used, as they are very good parallelizing the computations needed for doing large scale comparisons. Every document in your entire library needs to be compared with the prompts or questions that were passed to you.</p><p>Now, you have a working chatbot that can answer questions. The next thing to do is to expose it with a web app so that I could share the link with people.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments! New posts every other Tuesday</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>This brings us to experiment part two. For this post, we used a library called <a href="https://htmx.org/">htmx</a> to build the interactivity of the web page. Normally, I build websites in React and TypeScript, but a co-worker recently told me about htmx and I was curious. And since all I needed was a text box and a button, it seemed like a good opportunity to try htmx. Before we get too far into it, I have to say I think I am actually a fan. So what is htmx? htmx is a way to add interactivity to your website where you don't have to write any JavaScript to do the various functionalities of a modern web app. You can add a set of defined attributes to your HTML elements.</p><p>You can do things like send HTTP requests and your back end needs to respond with HTML fragments. I have a lot to learn about using htmx, but I was pretty easily able to write my search/chatbot interface to send the question to my back end. In as simple as a few HTML elements, I was ready to send a request to my backend.</p><pre><code>&lt;form hx-post="/query" hx-target="#results"&gt;
  &lt;label&gt;
    &lt;span&gt;Ask me your own question&lt;/span&gt;
    &lt;textarea"&gt;&lt;/textarea&gt;
    &lt;button type="submit" hx-indicator="#indicator"&gt;
      Search
    &lt;/button&gt;
  &lt;/label&gt;
&lt;/form&gt;

&lt;div id="results"&gt;&lt;/div&gt;
</code></pre><p>Now, one thing to get into is how did I deploy all of this. I have a set of templates and patterns that I use in a monorepo, <a href="https://www.sliceofexperiments.com/p/one-dockerfile-is-all-it-takes-falling">which I have laid out in a previous newsletter post</a>. But the whole template pretty much relies on using Go.</p><p>However, GPT index is a Python library. One thing that I thought of doing was to take the implementation of the GPT index, or at least the bare minimums for what I was using, and rewrite that in Go. Which, maybe I will do one day.</p><div class="pullquote"><p>Edit: As of February 18, 2023, I did rewrite just the part of gptindex that I was using, but in Go. Requests are on the order of 70% faster, and I&#8217;ve reduced the CPU and memory footprint by about 90%. Start up time is 2-3 seconds down from 40-50 seconds which means I can <a href="https://www.sliceofexperiments.com/p/scale-from-0-saving-nickels-to-feed">scale this service to 0, which I talked about in the past</a> </p></div><p>But for now, at the time of writing, I just wrapped the single library call that I used in the GPT index into a FastAPI application and put all of that into a Docker container. Then I could go back to using a Go-based backend to serve my HTML file. And I would send requests from the Go application to the Python application.</p><pre><code>from fastapi import FastAPI
from gpt_index import GPTSimpleVectorIndex
from pydantic import BaseModel

app = FastAPI()
index = GPTSimpleVectorIndex.load_from_disk("index.json")


@app.get("/")
async def root(query: str = ""):
    response = index.query(query)
    if response.response is None:
        return {"error": "no response"}
    return {"response": response.response.strip()}


@app.get("/healthz")
async def healthz():
    return "ok"</code></pre><p>Eventually, I got that all wired up and deployed onto my Kubernetes cluster that I use for hosting all of my applications. You can visit &lt;I&#8217;ve killed the deployment!&gt; and ask me questions, or rather, ask a question about me.</p><p>At this point, I have a functioning MVP where users can ask questions and get answers. The next thing I decided to focus on was experimenting with ways to format my data and documents to best work with the way that GPT index fetches the answers.</p><p>Currently, I am using the <a href="https://github.com/jerryjliu/gpt_index/blob/a6c89159bf8e7086bea2f4305cff3f0a4102e370/gpt_index/readers/file/base.py#L34">SimpleDirectoryReader</a>, where each file in a folder corresponds to a single document. The default for similarity is to take the contents of the most similar document in its entirety and rewrite that for the response. For simple questions, that means breaking up my documents into several files that are three to five sentences long. But there is a trade-off because the more documents I have, the more similarity comparisons need to be made to find the most similar document.</p><p>On top of that, when GPT index is synthesizing its response, sometimes having the additional context that surrounds the specific sentence or paragraph that answers the question can be useful in crafting a more thorough answer. So, I have to take that into consideration.</p><p>Then there is the top N for similarity, aka, how many documents do I take out of the index for crafting the response.</p><p>There is another trade-off to consider when splitting information across multiple documents for GPT index. It's important to take into account the combined index and context, which may not be possible unless you increase the number of documents to retrieve. Sharding the data for context like work history can also be a challenge, especially when answering questions about experience with specific technologies. For instance, if a recruiter asks about projects done with Python and the default similarity top N is set to one, the bot would only retrieve a project description from a single job, even though Python was used in multiple projects across multiple jobs.</p><p>Now, I need to invert the way that I split my data across documents to get a thorough response. Another possible idea is to duplicate the information into multiple documents and hope that the similarity search picks the document where I've repeated information. For example, I can have documents for each job and also have documents that are specific to describing experience with a single technology, such as a text file with descriptions of projects I've done in Python. I don&#8217;t think I found the exact right balance in my time frame, but I think there&#8217;s a lot of potential.</p><p>I'm still very curious about what kinds of results I could get if I fine-tuned the general GPT-3 model. One benefit of this search style approach is that it minimizes the chance of hallucinations in the model, aka answering questions with information that is not true or is not in the index. This chatbot only surfaces preexisting information. So I shouldn't be too worried about it making ridiculous responses.</p><p>I shared this URL with a couple of friends of mine, and it's no surprise every single one of them, including my wife, came up with questions to ask it that I did not prewrite. That isn't all that unexpected. But it was still a fun project nonetheless.</p><p>What do you think, dear reader? Do you think personalized FAQ style chatbots are worth it? If you&#8217;re searching for a job, do you think you would ever send a recruiter a message linking to your own resume chatbot? And if you are a recruiter, how do you feel about being given a link like this to ask all your questions too? I would love to know what you think.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Scale from 0: Saving nickels to feed my AWS addiction]]></title><description><![CDATA[Keeping my bad ideas from bankrupting me]]></description><link>https://www.sliceofexperiments.com/p/scale-from-0-saving-nickels-to-feed</link><guid isPermaLink="false">https://www.sliceofexperiments.com/p/scale-from-0-saving-nickels-to-feed</guid><dc:creator><![CDATA[Aaron Batilo]]></dc:creator><pubDate>Tue, 31 Jan 2023 21:16:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!5A9A!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3a382a1-1303-4cc0-8da1-6f042692bc42_880x455.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>So, I&#8217;m going to keep it real honest with ya&#8217;ll this week. This post won&#8217;t have quite the same <em>refinement</em> that I usually try to stick to, but I kind of wanted to try writing in a more natural tone for me. Trying to sound all proper and professional is sucking some of the fun out of my experiments. Let me know what you think. I&#8217;m not sure if this tone fits for a newsletter. Maybe it&#8217;s better for a blog, but you know what, does it matter? I&#8217;m writing for me and I just get a kick out of things when people find the content useful or interesting.</p><div><hr></div><p>Anyways&#8230;</p><p>I&#8217;ve come to learn that most of my ideas are really bad. In my mind, I keep hoping that some project of mine is going to go kind of viral and like, maybe I become famous for it or something. In reality, I know that banking on any one particular site to do well isn&#8217;t great. On top of that, my projects are always pretty small. An old co-worker of mine said that I have &#8220;one weekend syndrome&#8221;. I&#8217;m only interested in a project for <a href="https://www.sliceofexperiments.com/p/how-i-built-a-scalable-real-time-multiplayer">about a weekend before I move on</a>, which is&#8230; it&#8217;s pretty true. It&#8217;s hard to build an actual product that people would pay for in a single weekend.</p><p>Here&#8217;s the other catch, I also think of these projects like going out on a fishing boat. I&#8217;ve gone fishing twice in my life. One of those times was with my father-in-law in Mexico where we went out on a boat that had like 12 fishing rods sticking out of it, even though there was only 4 humans on the boat. How does that relate to my side projects? I know most of my ideas are bad, so instead of worrying about how bad they are, I ignore it completely and think that if I put out as many as I can, then maybe one of them will actually stick. <a href="https://en.wikipedia.org/wiki/Rovio_Entertainment#:~:text=Prior%20to%20creating%20Angry%20Birds,contracts%20and%20independently%20released%20titles.">Angry Birds was the 51st game made by Rovio</a>. That&#8217;s a lot of games. And now they have a whole ass movie and stuff. BUUUUUUUT, I don&#8217;t have any funding. I don&#8217;t have angel investors or VCs funding anything. So I need to pay for everything and that can get pretty expensive.</p><p>A lot of hosting websites will give you a free tier of some kind and that&#8217;s great, but the free tier tends to only be useful for one project worth of compute unless you do something that&#8217;s a little cheeky.</p><p>Heroku&#8217;s basic tier costs about <a href="https://www.heroku.com/pricing">$5 a month</a>. <a href="https://fly.io/">Fly.io</a> gives you about <a href="https://fly.io/docs/about/pricing/#free-allowances">$6 a month</a> worth of compute on the free tier. The smallest DigitalOcean VM is still going to run you <a href="https://www.digitalocean.com/pricing">$4 a month</a>. Vercel is <a href="https://vercel.com/pricing">free</a> but only for personal use and not commercial use. AWS has a pretty cool sounding free tier. For the first 12 months you get things like 750 hours a month worth of compute for free if you run the smallest possible instances. But it&#8217;s pretty well understood these days that the AWS free tier is <a href="https://www.lastweekinaws.com/blog/is-the-aws-free-tier-really-free/">delightfully deceptive</a>. And you only get one of those EC2 instances for free. If you run two for the month, you&#8217;re going to pay $7.60 a month for that second t3.micro. If you&#8217;re comfortable with AWS, you can run it as a spot instance and that&#8217;s still $2.27 a month.</p><p>If I had a bunch of side projects, this all starts to add up really quickly. But let&#8217;s be real. My AWS bill is something like $210 a month. $75 a month for my EKS control plane. About $30 a month for 3 t3.medium EC2 instances. About $55 a month for my Aurora Postgres cluster, and about $40 a month on CloudWatch logs. The rest is in small storage costs like ECR or S3. Yes, yes. I know about things like serverless Aurora. I know that I could reduce how much logging I&#8217;m doing.</p><p>So scaling to 0 isn&#8217;t exactly going to save me that much money.  That&#8217;s not the point though, I like messing with the infra that I have and hey, it&#8217;s my experiments. I get to do what I want. I consider it an investment in myself and my growth because I have an amazing time just messing around with all of the things in my cluster. I like the Kubernetes ecosystem. Tons of software is available these days as a docker container and sometimes even a helm chart, which means it takes literally minutes for me to stand up a new project to play with in some cases. Fortunately, that amount of money is probably going to stay pretty flat for as long as I keep making projects. So if I come up with like 40 ideas, I might actually break even. Scaling to 0 helps with that so we&#8217;re going to talk about it.</p><div class="pullquote"><p>&#8220;What do you even mean when you say scale to 0, Aaron?&#8221;</p></div><p>Yeah, good point. So when I&#8217;m talking about scaling to 0, I&#8217;m talking about making sure that if a project or website that I&#8217;m running isn&#8217;t actually being visited by anyone, that the code isn&#8217;t actually running anywhere at all. Scaling on demand, serverless, functions as a service. These are some of the other names that I&#8217;ve heard used for this. It&#8217;s a really interesting concept. But SOMETHING has to run, right? Yep, so what am I running? How am I accepting requests to my EKS cluster and how is it knowing to schedule pods?</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments! New posts every other week</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>OpenFaaS</h2><p><a href="https://www.openfaas.com/">OpenFaaS</a> is a functions as a service framework that you can deploy to a Kubernetes cluster. Also maybe worth trying one day but it basically reinvents how you do things like deployments. You use their CLI for building and deploying functions and I wanted to keep things more Kubernetes native, which actually leads me to&#8230;</p><h2>KNative</h2><p><a href="https://knative.dev/docs/">KNative</a> is probably the default that most people end up coming across. I haven&#8217;t actually tried it but thought I should mention it. I&#8217;ve purposefully decided that I wanted to try to avoid KNative entirely if I could, because in order to use it, you have to install a <a href="https://knative.dev/docs/install/yaml-install/serving/install-serving-with-yaml/#install-a-networking-layer">separate networking layer</a>. That&#8217;s a little more complexity than I care to actually try right now, but maybe one day.</p><h2>Sablier</h2><p><a href="https://github.com/acouvreur/sablier">Sablier</a> is actually the first project that I tried. And conceptually, I actually REALLY like how it works. When I set it up, I configured it as a plugin for <a href="https://traefik.io/">Traefik</a> which is my ingress controller of choice these days. I got it set up in about 25 minutes and started to play with it. The way it works is that you set it up as Traefik middleware, which means when Traefik receives a request, before it makes it to your service, it goes through sablier first. The way sablier itself works is that when the middleware is triggered, it sends a blocking request to the sablier service that you install into your cluster. When the middleware sends a request to sablier, sablier checks to see if it needs to <a href="https://github.com/acouvreur/sablier/blob/b7a5f30cb177c471584ac1eda576cf580c49411a/app/providers/kubernetes.go#L115-L116">change the number of replicas available</a>.</p><p>Sablier has a few different modes. It has a &#8220;dynamic&#8221; mode which will immediately serve a temporary web page that you can customize like so:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!5A9A!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3a382a1-1303-4cc0-8da1-6f042692bc42_880x455.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5A9A!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3a382a1-1303-4cc0-8da1-6f042692bc42_880x455.png 424w, https://substackcdn.com/image/fetch/$s_!5A9A!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3a382a1-1303-4cc0-8da1-6f042692bc42_880x455.png 848w, https://substackcdn.com/image/fetch/$s_!5A9A!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3a382a1-1303-4cc0-8da1-6f042692bc42_880x455.png 1272w, https://substackcdn.com/image/fetch/$s_!5A9A!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3a382a1-1303-4cc0-8da1-6f042692bc42_880x455.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5A9A!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3a382a1-1303-4cc0-8da1-6f042692bc42_880x455.png" width="880" height="455" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f3a382a1-1303-4cc0-8da1-6f042692bc42_880x455.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:455,&quot;width&quot;:880,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:84613,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!5A9A!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3a382a1-1303-4cc0-8da1-6f042692bc42_880x455.png 424w, https://substackcdn.com/image/fetch/$s_!5A9A!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3a382a1-1303-4cc0-8da1-6f042692bc42_880x455.png 848w, https://substackcdn.com/image/fetch/$s_!5A9A!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3a382a1-1303-4cc0-8da1-6f042692bc42_880x455.png 1272w, https://substackcdn.com/image/fetch/$s_!5A9A!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3a382a1-1303-4cc0-8da1-6f042692bc42_880x455.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The temporary page is setup to auto-refresh itself until the actual application is up and running. I really like this feature. It&#8217;s great for any website that has static content. The other mode is called &#8220;blocking&#8221; and unfortunately, this is where I ran into a big problem. Blocking mode waits before forwarding the request. The first request for a &#8220;blocking&#8221; mode request will actually fail, which <a href="https://github.com/acouvreur/sablier/issues/131">I&#8217;ve reported</a> but I can&#8217;t seem to find a good fix for. There are some other interesting oddities about sablier. Namely, you configure it by having a <a href="https://github.com/acouvreur/sablier/blob/main/app/providers/kubernetes.go#L39-L57">specially formatted name</a>. Like&#8230; there&#8217;s already other configuration in this project, why am I configuring things like the Kubernetes namespace of the scale target by joining a string with underscores?</p><h2>keda-http-addon</h2><p>The project I ended up sticking with is the <a href="https://github.com/kedacore/http-add-on">keda-http-addon</a>. It&#8217;s also an experimental project but I&#8217;ve been wanting to play with its core <a href="https://keda.sh/">KEDA</a> component for a while. KEDA itself is a set of abstractions for managing event based autoscaling and it&#8217;s a lot more flexible than the build in horizontal pod autoscalers. You can use things like <a href="https://keda.sh/docs/2.9/scalers/aws-sqs/">SQS queue</a> attributes as your scale metric, or use <a href="https://keda.sh/docs/2.9/scalers/postgresql/">database queries </a> to trigger your scale out events. The HTTP addon works as a proxy that queues up HTTP requests. So you configure your ingress controller to send all requests to the &#8220;interceptor&#8221; and the interceptor holds the requests while your applications scale out. This has a different problem from sablier though which I still need to figure out. The first request to the HTTP addon takes like 25 seconds to respond, even though the application has been running after 3-4 seconds.</p><div class="pullquote"><p>Edit: On February 7th, my PR for why the first request was taking so long was merged, but at time of writing this edit, a new version of the add-on hasn&#8217;t been released yet. You can check out the fix <a href="https://github.com/kedacore/http-add-on/pull/598">here</a>.</p></div><p>BUT, the request doesn&#8217;t fail completely which is a big deal. For applications like <a href="https://catstories.ai">CatStories.ai</a>, it receives webhooks from Twilio when you send text messages. Having the first request fail like with sablier&#8217;s &#8220;blocking&#8221; mode is not going to be great for that use case.</p><div><hr></div><h2>Closing</h2><p>Another totally random thing to consider. EC2 instances have a <a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-eni.html#AvailableIpPerENI">max number of IP addresses</a> that it can have. That&#8217;s actually why I don&#8217;t use smaller EC2 instances for my cluster. So if I have too many applications running, even if they don&#8217;t use much actual compute, I may be forced to spin up another EC2 instances due to IP address exhaustion. There are ways around this too by using separate CIDRs and what not but I don&#8217;t have those configured&#8230; yet.</p><p>That&#8217;s all I&#8217;ve got for this week. Maybe a less exciting experiment than some of the other things I&#8217;ve written about but that&#8217;s what I played with. Now some of my really low traffic projects like <a href="https://terragen.sh">https://terragen.sh</a> and <a href="https://catstories.ai">https://catstories.ai</a> might take 25 seconds to load but the sites are up when people want to check them out but they&#8217;re not eating up resources when no one is checking them out. And I can keep the links alive on my various internet bios and stuff.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[I made ChatGPT answer 50,000 trivia questions. Are we doomed?]]></title><description><![CDATA[So, how smart is it?]]></description><link>https://www.sliceofexperiments.com/p/chatgpt-vs-50000-trivia-questions</link><guid isPermaLink="false">https://www.sliceofexperiments.com/p/chatgpt-vs-50000-trivia-questions</guid><dc:creator><![CDATA[Aaron Batilo]]></dc:creator><pubDate>Tue, 17 Jan 2023 21:16:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!SAfd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F11117f97-0e8c-4202-b543-da1bdcb1c416_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>TL;DR: Across 49,717 multiple choice questions, ChatGPT was correct 66.7% of the time. Keep reading to see the breakdown by category</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!SAfd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F11117f97-0e8c-4202-b543-da1bdcb1c416_1024x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!SAfd!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F11117f97-0e8c-4202-b543-da1bdcb1c416_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!SAfd!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F11117f97-0e8c-4202-b543-da1bdcb1c416_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!SAfd!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F11117f97-0e8c-4202-b543-da1bdcb1c416_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!SAfd!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F11117f97-0e8c-4202-b543-da1bdcb1c416_1024x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!SAfd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F11117f97-0e8c-4202-b543-da1bdcb1c416_1024x1024.png" width="1024" height="1024" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/11117f97-0e8c-4202-b543-da1bdcb1c416_1024x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1024,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1508067,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!SAfd!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F11117f97-0e8c-4202-b543-da1bdcb1c416_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!SAfd!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F11117f97-0e8c-4202-b543-da1bdcb1c416_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!SAfd!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F11117f97-0e8c-4202-b543-da1bdcb1c416_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!SAfd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F11117f97-0e8c-4202-b543-da1bdcb1c416_1024x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">&#8220;robot at jeopardy podium&#8221; from DALLE-2</figcaption></figure></div><div><hr></div><p><a href="https://github.com/abatilo/multiple-choice-ai">About 6 years ago</a>, I had the idea to see how well you could answer trivia questions with AI. I used word2vec and picked answers based on which multiple choice answer had the highest cosine similarity to the average of the vector of the sentence. Simply put, this&#8230; didn&#8217;t work at all. The percentage of questions that this method got correct was not notably better than selecting answers at random.</p><p>When the word2vec paper first came out, it was revolutionary that we could do analogies for the semantic meanings of words. &#8220;<a href="https://kawine.github.io/blog/nlp/2019/06/21/word-analogies.html">King - Man + Woman = Queen</a>&#8221;. I was floored. The <a href="https://arxiv.org/abs/1301.3781">original paper</a> came out in 2013, and here we are in 2023. A whopping 10 years later and countless generations of advancement in machine learning, comprehension, understanding, etc. In June of 2017, we were blessed with the &#8220;<a href="https://arxiv.org/abs/1706.03762">Attention is all you need</a>&#8221; paper which introduced the transformer architecture that was the step function that has brought us this revolution in natural language processing. So, what&#8217;s different now?</p><p>In late 2022, OpenAI released <a href="https://openai.com/blog/chatgpt/">ChatGPT</a>. Which, at the heart of it all, is a brilliant UX that someone came up with for interacting with GPT-3.5. The original GPT-3 was made available in the summer of 2020 in private beta, and while there&#8217;s a <a href="https://beta.openai.com/playground">playground UI</a>, usage of GPT-3 was still fairly intimidating for the every day person. The playground featured all the knobs and parameters for tuning GPT-3&#8217;s response and that turned off many people from using it. ChatGPT on the other hand was&#8230; a chat bot. It looked no different than talking to someone in Slack or Microsoft Teams. Anyone could sign up, there was no wait list, and no scary knobs. <a href="https://twitter.com/sama/status/1599668808285028353">ChatGPT gained 1 million users in only 5 days</a>. For the rest of this article, I will refer to the model as ChatGPT, even though that&#8217;s not perfectly accurate since ChatGPT is a fine-tuned version of GPT-3.5.</p><p>So, how smart is it? 6 years later from my original experiment, I wanted to find out what the capabilities are of some of the most impressive machine learning we&#8217;ve seen to date. We&#8217;ve seen the creative works of ChatGPT. I even made a fun website for my wife and I to use: https://catstories.ai. ChatGPT is amazing at creative tasks, but for tasks that involve more knowledge, the opinions are significantly more split. Some people are convinced that ChatGPT, with it&#8217;s intuitive interface, is going to replace traditional search engine&#8217;s like Google. <a href="https://www.theverge.com/2023/1/4/23538552/microsoft-bing-chatgpt-search-google-competition">Microsoft is integrating ChatGPT directly into its search engine</a>, along with other products.</p><p>ChatGPT is taught everything it knows by reading vast amounts of the internet, and it uses that information when it generates text. For the non-technical, you can think of it like your phone&#8217;s autocomplete, but way smarter. If you&#8217;re clever about how you prompt ChatGPT, you can get it to do revolutionary things and test its knowledge. For example, ChatGPT was able to pass the <a href="https://twitter.com/hadip/status/1609722809055600641">Bar exam</a>. You can have it write marketing copy, or write emails for you. But if we&#8217;re going to use it as a source of truth to replace things like search engines, how does it do with general knowledge?</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments! New posts every other Tuesday</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h1>How did I test it?</h1><p>The <a href="https://github.com/uberspot/OpenTriviaQA">uberspot/OpenTriviaQA</a> dataset is a creative commons data set of multiple choice questions. The questions are split into 22 categories such as: animals, entertainment, history, movies, and sports. Most of the questions have 4 possible multiple choice answers, but some of the questions are true or false questions.</p><p>Questions come in the following format:</p><pre><code>#Q What is the name of Rocky Balboas restaurant?
^ Adrians
A Rockys
B Mickeys
C Adrians
D Apollos</code></pre><p>First, you have the question itself. Then you have the correct answer. Lastly, you have your options for answer, one on each line.</p><p>My script would download each of the categories and ask each and every question to ChatGPT. For the above question, I would format my prompt to look like so:</p><pre><code>I am a highly intelligent multiple choice trivia bot. You are given a multiple choice question. You must choose the correct answer from one of answers. Only include the answer and nothing else.
Question:
What is the name of Rocky Balboas restaurant?

Possible answers:
Rockys
Mickeys
Adrians
Apollos

Your answer:</code></pre><p>I would then send this to ChatGPT and compare its answer with the correct answer.</p><h1>Soooooo, how&#8217;d it do?</h1><p>Here&#8217;s the raw data, sorted from least to most correct:</p><pre><code><strong>Category        Correct Total   Percentage
</strong>brain-teasers&#9;103&#9;207&#9;0.497585
video-games&#9;310&#9;599&#9;0.517529
television&#9;2911&#9;5230&#9;0.556597
entertainment&#9;163&#9;280&#9;0.582143
animals&#9;&#9;815&#9;1366&#9;0.596632
celebrities&#9;1909&#9;3196&#9;0.597309
sports&#9;&#9;1728&#9;2840&#9;0.608451
movies&#9;&#9;2647&#9;4314&#9;0.613584
for-kids&#9;485&#9;759&#9;0.638999
music&#9;&#9;3746&#9;5579&#9;0.671447
literature&#9;888&#9;1288&#9;0.689441
hobbies&#9;&#9;867&#9;1242&#9;0.698068
general&#9;&#9;2306&#9;3290&#9;0.700912
newest&#9;&#9;2117&#9;3016&#9;0.701923
people&#9;&#9;1974&#9;2743&#9;0.71965
technology&#9;1820&#9;2486&#9;0.7321
world&#9;&#9;3571&#9;4875&#9;0.732513
religion-faith&#9;469&#9;638&#9;0.73511
history&#9;&#9;1228&#9;1645&#9;0.746505
rated&#9;&#9;1640&#9;2185&#9;0.750572
humanities&#9;831&#9;1097&#9;0.75752
geography&#9;652&#9;842&#9;0.774347<strong>
overall &#9;33180   49717   0.667377</strong></code></pre><p>It&#8217;s really interesting to see how these categories breakdown. Notably, ChatGPT did not do well with the brain-teasers category. Let&#8217;s look at a few examples of these questions.</p><pre><code>#Q The name of what is coded in the following: 86,smopuim?
A it does not mean anything
B the name of a software product
C a NASA aircraft
D a secret CIA project</code></pre><p>This one took me a while to figure out but ChatGPT got it correct. The answer is B. Those characters say windows,98 upside down. When I was talking to a friend of mine over at <a href="https://fearless.tools">Fearless AWS</a>, he suggested that I try to get ChatGPT to explain itself. So I did that.</p><pre><code>I am a highly intelligent multiple choice trivia bot. You are given a multiple choice question. You must choose the correct answer from one of answers. Only include the answer on the first line. On the next line, explain your answer.
Question:
The name of what is coded in the following: 86,smopuim?

Possible answers:
it does not mean anything
the name of a software product
a NASA aircraft
a secret CIA project

Your answer:
the name of a software product
The numbers 86 and smopuim are likely a code for a software product, as they are not a recognizable acronym or phrase.</code></pre><p>At first, I was impressed that ChatGPT was able to figure this out but it actually seems like ChatGPT just guessed.</p><p>Let&#8217;s look at an example where ChatGPT was incorrect.</p><pre><code>I am a highly intelligent multiple choice trivia bot. You are given a multiple choice question. You must choose the correct answer from one of answers. Only include the answer on the first line. On the next line, explain your answer.
Question:
What number multiplied by 10 equals the square of the same number, times 5?

Possible answers:
4
5
2
10

Your answer:
5
Explanation: 5 multiplied by 10 is equal to 50, which is the square of 5, times 5.</code></pre><p>Here&#8217;s a great demonstration of how ChatGPT can be pretty dumb. It got arithmetic completely wrong. Because it just generates text, that doesn&#8217;t mean that it has correctly encoded arithmetic. The correct answer here is 2. 2 squared is 4, times 5 is 20. 2 times 10 is also 20.</p><p>Let&#8217;s look at another example where ChatGPT was correct according to the data set, but the explanation doesn&#8217;t quite add up:</p><pre><code>I am a highly intelligent multiple choice trivia bot. You are given a multiple choice question. You must choose the correct answer from one of answers. Only include the answer on the first line. On the next line, explain your answer.
Question:
A man is carrying three croquet balls across a bridge. The bridge has a sign that says bridge can only hold 200 pounds. The man weighs 195 pounds and the croquet balls weigh 2 pounds each. The man ends up crossing the bridge in one trip and no one else helps him. This is possible.


Possible answers:
False
True

Your answer:
True
The man is carrying three croquet balls that weigh 2 pounds each, for a total of 6 pounds. This means that the man and the croquet balls combined weigh 201 pounds, which is less than the maximum weight of 200 pounds that the bridge can hold. Therefore, the man is able to cross the bridge in one trip.</code></pre><p>So, this is a really interesting one. The data set doesn&#8217;t have an explanation for any of these brain teasers but it lists the correct answer as &#8220;true&#8221;. If you do some googling, you&#8217;ll find that the answer of the riddle is supposed to be &#8220;true&#8221; because the man can juggle the balls. Ignoring the <a href="https://physics.stackexchange.com/questions/36167/does-juggling-balls-reduce-the-total-weight-of-the-juggler-and-balls">actual physics</a> of such a situation, the ChatGPT answer, once again, gets the arithmetic completely incorrect in saying that 201 pounds is less than the maximum weight of 200 pounds.</p><p>Let&#8217;s look at one final example that addresses something in the NLP field called the <a href="https://en.wikipedia.org/wiki/Winograd_schema_challenge">Winograd schema</a>. Which, to over simplify, is basically being able to understand how pronouns resolve to ambiguous actors in the same sentence.</p><pre><code>I am a highly intelligent multiple choice trivia bot. You are given a multiple choice question. You must choose the correct answer from one of answers. Only include the answer on the first line. On the next line, explain your answer.
Question:
If the mother of a boy spanks him and then his father comes and does the same, who hurts the most?

Possible answers:
The boys brother
The mother
The boy
The father

Your answer:
The boy

Explanation: The boy is the one being spanked, so he would experience the most pain.</code></pre><p>In this answer, ChatGPT explains that the boy is the one being spanked and so even though there might be some ambiguity between the usages of &#8220;him&#8221; and &#8220;his&#8221; referring to either the boy or the father, ChatGPT understands the context of the boy being spanked.</p><p>These brain teasers require logical conclusions that it seems like ChatGPT doesn&#8217;t really reason through. It makes sense that this is the category that ChatGPT has failed most on, but there are also some impressive answers. That being said, even asking ChatGPT to explain itself is a generated response. There&#8217;s nothing to say that this is legitimately how ChatGPT is thinking about the problems.</p><p>Let&#8217;s look at another category, literature, which presumably ChatGPT should be good at because there should be plenty of text and references to learn about the literature.These brain teasers require logical conclusions that it seems like ChatGPT doesn&#8217;t really reason through. It makes sense that this is the category that ChatGPT has failed most on.</p><pre><code>I am a highly intelligent multiple choice trivia bot. You are given a multiple choice question. You must choose the correct answer from one of answers. Only include the answer on the first line. On the next line, explain your answer.
Question:
In what book of the Harry Potter series do Harry and Ginny kiss for the first time?

Possible answers:
Harry Potter and the Order of the Phoenix
Harry Potter and the Prisoner of Azkaban
Harry Potter and the Deathly Hallows
Harry Potter and the Half Blood Prince

Your answer:
Harry Potter and the Half Blood Prince

Harry and Ginny kiss for the first time in Harry Potter and the Half Blood Prince, when Harry visits Ginny in the Gryffindor common room.</code></pre><p>In this case, ChatGPT is correct. Harry Potter is popular enough with a large fandom that it makes sense that this is probably talked about alot.</p><pre><code>I am a highly intelligent multiple choice trivia bot. You are given a multiple choice question. You must choose the correct answer from one of answers. Only include the answer on the first line. On the next line, explain your answer.
Question:
Harry Potter marries Hermione in the last book of the series.

Possible answers:
True
False

Your answer:
True

Explanation: In the epilogue of the final book, Harry and Hermione are married and have two children.</code></pre><p>Another really interesting case here. Harry marries Ginny and has two kids, not Hermione. Trying to think through how this might have happened, I wonder if internet comments and maybe fan fiction that was part of the training set had enough content that preferred Harry and Hermione to be together. The implication there for the greater knowledge base of ChatGPT is that you could influence what it deems to be true if there&#8217;s enough of an *opinion* in its training data.</p><h1>Wrapping up</h1><p>Like I said, there are almost 50,000 questions. Digging into each and every single one of them is not practical for a single post, but it is really interesting to try to intuit about why ChatGPT is better or worse at various categories. In categories like brain-teasers, there&#8217;s much more logic than needs to be applied and combined to answer some of the questions. In other low scoring categories like television and video games, my guess is that because the content itself is not in an indexable / consumable format, ChatGPT doesn&#8217;t have as much information about it. ChatGPT hasn&#8217;t played the games themselves or watched the television shows. There might be 3rd party content ABOUT what happened in the shows but the contents of the show would be mentioned in passing instead.</p><p>In high scoring categories, the categories all tend to be for contents that have been documented in text long before we had audio, video, etc. The geography category and the history category have content that has existed long before we had the technology and storage to regularly keep things like television content.</p><p>All that being said, a 66% correctness is still pretty impressive given such a vast selection of topics. But as we learned, sometimes the answers are reportedly correct but the explanations are not always correct. I don&#8217;t think ChatGPT or any large language model that we have right now is the strong AI or general AI that some articles want you to believe it is. I don&#8217;t think any large language models that are generative in nature should replace traditional search engines that <strong>surface</strong> high quality information. There are search engines like <a href="https://www.cnet.com/tech/services-and-software/search-engine-you-com-launches-chatgpt-style-chatbot/">you.com</a> that are exploring the intersection but it&#8217;s still early days for this technology!</p><div><hr></div><p>Between development and doing the final runs, this experiment cost me about $100 to hit the GPT-3 API for all the questions. I&#8217;d be forever grateful if you&#8217;d consider <a href="https://www.buymeacoffee.com/mentallyanim8d">buying me a coffee</a>. If a company would be willing to sponsor the work, it&#8217;d be interesting to try different prompts, randomizing the orders of the answers, try running the tests multiple times, etc. I&#8217;d also love to run the same test over the <a href="https://github.com/jwolle1/jeopardy_clue_dataset">full Jeopardy question set</a>, but given how expensive this test was, running the test over a question set almost 10x as much would be too much for me to do right now.</p><p>As always, the code that I wrote for this is all available. Check out this post&#8217;s code, <strong><a href="https://github.com/abatilo/sliceofexperiments/blob/main/how-smart-is-gpt3/results.txt">including the answers of ChatGPT</a>,</strong> by <a href="https://github.com/abatilo/sliceofexperiments/tree/main/how-smart-is-gpt3">clicking here</a>.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[A comprehensive guide for the fastest possible Docker builds in human existence]]></title><description><![CDATA[Gotta go fast]]></description><link>https://www.sliceofexperiments.com/p/a-comprehensive-guide-for-the-fastest</link><guid isPermaLink="false">https://www.sliceofexperiments.com/p/a-comprehensive-guide-for-the-fastest</guid><dc:creator><![CDATA[Aaron Batilo]]></dc:creator><pubDate>Tue, 03 Jan 2023 21:15:56 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!U1aq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5a3d64c6-66e6-40da-a3a4-c70cea565e1a_1280x1280.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>For the first post of the year, I&#8217;ve got quite the large post! I hope everyone had a great winter holiday season.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!U1aq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5a3d64c6-66e6-40da-a3a4-c70cea565e1a_1280x1280.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!U1aq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5a3d64c6-66e6-40da-a3a4-c70cea565e1a_1280x1280.png 424w, https://substackcdn.com/image/fetch/$s_!U1aq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5a3d64c6-66e6-40da-a3a4-c70cea565e1a_1280x1280.png 848w, https://substackcdn.com/image/fetch/$s_!U1aq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5a3d64c6-66e6-40da-a3a4-c70cea565e1a_1280x1280.png 1272w, https://substackcdn.com/image/fetch/$s_!U1aq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5a3d64c6-66e6-40da-a3a4-c70cea565e1a_1280x1280.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!U1aq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5a3d64c6-66e6-40da-a3a4-c70cea565e1a_1280x1280.png" width="1280" height="1280" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/5a3d64c6-66e6-40da-a3a4-c70cea565e1a_1280x1280.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1280,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:216299,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!U1aq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5a3d64c6-66e6-40da-a3a4-c70cea565e1a_1280x1280.png 424w, https://substackcdn.com/image/fetch/$s_!U1aq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5a3d64c6-66e6-40da-a3a4-c70cea565e1a_1280x1280.png 848w, https://substackcdn.com/image/fetch/$s_!U1aq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5a3d64c6-66e6-40da-a3a4-c70cea565e1a_1280x1280.png 1272w, https://substackcdn.com/image/fetch/$s_!U1aq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5a3d64c6-66e6-40da-a3a4-c70cea565e1a_1280x1280.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A few months ago, I migrated my newsletter to substack and the first post I published here was about using S3 as your remote layer cache. When that post was published, a <a href="https://twitter.com/iamvlaaaaaaad/status/1584240466320850945">kind user on Twitter</a> told me about <a href="https://depot.dev">https://depot.dev</a>. <a href="https://twitter.com/kylegalbraith/status/1584284385854906368?s=20">One of the founders of Depot responded to the thread</a> and it got me really curious about trying the platform.</p><div class="embedded-post-wrap" data-attrs="{&quot;id&quot;:80180484,&quot;url&quot;:&quot;https://sliceofexperiments.substack.com/p/how-to-use-the-s3-for-your-docker&quot;,&quot;publication_id&quot;:1135332,&quot;publication_name&quot;:&quot;A slice of experiments&quot;,&quot;publication_logo_url&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/7aaed77e-5db5-4e6b-982e-1802998c069a_1024x1024.png&quot;,&quot;title&quot;:&quot;How to use S3 for your docker layer cache&quot;,&quot;truncated_body_text&quot;:&quot;As of May 2022, the buildkit backend for building containers has experimentally supported using S3 as a backend cache. For over 2 years, there&#8217;s been no other way to store max mode container manifests in AWS, as ECR still doesn&#8217;t support cache manifests. On the bright side, in October 2022, we finally started getting some communications from AWS about s&#8230;&quot;,&quot;date&quot;:&quot;2022-10-23T17:21:55.892Z&quot;,&quot;like_count&quot;:1,&quot;comment_count&quot;:0,&quot;bylines&quot;:[{&quot;id&quot;:31493138,&quot;name&quot;:&quot;Aaron Batilo&quot;,&quot;previous_name&quot;:null,&quot;photo_url&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/70ad4baa-dcc7-4e3c-a6f0-d8438cebe984_2159x2160.jpeg&quot;,&quot;bio&quot;:&quot;I'm a software engineer building products, platforms, and infrastructure. I like building to learn and want to write about what I've built, and what I learned building it.&quot;,&quot;profile_set_up_at&quot;:&quot;2022-10-10T01:33:14.953Z&quot;,&quot;publicationUsers&quot;:[{&quot;id&quot;:1086624,&quot;user_id&quot;:31493138,&quot;publication_id&quot;:1135332,&quot;role&quot;:&quot;admin&quot;,&quot;public&quot;:true,&quot;is_primary&quot;:true,&quot;publication&quot;:{&quot;id&quot;:1135332,&quot;name&quot;:&quot;A slice of experiments&quot;,&quot;subdomain&quot;:&quot;sliceofexperiments&quot;,&quot;custom_domain&quot;:null,&quot;custom_domain_optional&quot;:false,&quot;hero_text&quot;:&quot;Every other week, learn everything that I've learned from my tiny code and infra experiments&quot;,&quot;logo_url&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/7aaed77e-5db5-4e6b-982e-1802998c069a_1024x1024.png&quot;,&quot;author_id&quot;:31493138,&quot;theme_var_background_pop&quot;:&quot;#A33ACB&quot;,&quot;created_at&quot;:&quot;2022-10-09T20:25:35.895Z&quot;,&quot;rss_website_url&quot;:null,&quot;email_from_name&quot;:null,&quot;copyright&quot;:&quot;Aaron Batilo&quot;,&quot;founding_plan_name&quot;:null,&quot;community_enabled&quot;:true,&quot;invite_only&quot;:false,&quot;payments_state&quot;:&quot;disabled&quot;}},{&quot;id&quot;:1129492,&quot;user_id&quot;:31493138,&quot;publication_id&quot;:1176235,&quot;role&quot;:&quot;admin&quot;,&quot;public&quot;:true,&quot;is_primary&quot;:false,&quot;publication&quot;:{&quot;id&quot;:1176235,&quot;name&quot;:&quot;Devouring Denver&quot;,&quot;subdomain&quot;:&quot;devouringdenver&quot;,&quot;custom_domain&quot;:null,&quot;custom_domain_optional&quot;:false,&quot;hero_text&quot;:&quot;Read Denver food reviews from an amateur critic with poor budgeting skills&quot;,&quot;logo_url&quot;:null,&quot;author_id&quot;:31493138,&quot;theme_var_background_pop&quot;:&quot;#2EE240&quot;,&quot;created_at&quot;:&quot;2022-11-05T16:02:07.796Z&quot;,&quot;rss_website_url&quot;:null,&quot;email_from_name&quot;:null,&quot;copyright&quot;:&quot;Aaron Batilo&quot;,&quot;founding_plan_name&quot;:null,&quot;community_enabled&quot;:true,&quot;invite_only&quot;:false,&quot;payments_state&quot;:&quot;disabled&quot;}}],&quot;twitter_screen_name&quot;:&quot;aaronbatilo&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null,&quot;inviteAccepted&quot;:true}],&quot;utm_campaign&quot;:null,&quot;belowTheFold&quot;:false,&quot;type&quot;:&quot;newsletter&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="EmbeddedPostToDOM"><a class="embedded-post" native="true" href="https://sliceofexperiments.substack.com/p/how-to-use-the-s3-for-your-docker?utm_source=substack&amp;utm_campaign=post_embed&amp;utm_medium=web"><div class="embedded-post-header"><img class="embedded-post-publication-logo" src="https://substackcdn.com/image/fetch/$s_!o6dZ!,w_56,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7aaed77e-5db5-4e6b-982e-1802998c069a_1024x1024.png"><span class="embedded-post-publication-name">A slice of experiments</span></div><div class="embedded-post-title-wrapper"><div class="embedded-post-title">How to use S3 for your docker layer cache</div></div><div class="embedded-post-body">As of May 2022, the buildkit backend for building containers has experimentally supported using S3 as a backend cache. For over 2 years, there&#8217;s been no other way to store max mode container manifests in AWS, as ECR still doesn&#8217;t support cache manifests. On the bright side, in October 2022, we finally started getting some communications from AWS about s&#8230;</div><div class="embedded-post-cta-wrapper"><span class="embedded-post-cta">Read more</span></div><div class="embedded-post-meta">4 years ago &#183; 1 like &#183; Aaron Batilo</div></a></div><p>I got signed up and started building my containers and I was blown away. Since they  <a href="https://depot.dev/docs/integrations/github-actions">support GitHub OIDC for authentication</a> and since they&#8217;re a drop in replacement for <code>docker build</code>, getting setup genuinely took a few minutes. I had to switch off using my current bake file approach since Depot doesn&#8217;t currently support bake (but they&#8217;re interested in supporting it!) but even then, running all of my docker builds in parallel via a GitHub Actions matrix, my builds went from about 3.5 minutes down to about 50 seconds in the cold case and about 15 seconds when a given container in my monorepo hadn&#8217;t changed.</p><p>I was flabbergasted. I became obsessed with understanding how the improvements were so ridiculous. The Depot founders were not shy about sharing some of the secret sauce. I spoke with them directly but they clearly <a href="https://depot.dev/docs">explain the improvements right in their documentation</a>.</p><blockquote><p>Depot is a remote container build service that makes image builds 3-14x faster than building Docker images inside generic CI providers. Docker image builds get sent to a fast builder instance with a persistent cache. The resulting image can then be downloaded locally or pushed to a registry.</p></blockquote><p>The persistent cache! The SSD that they attach to their builder instances holds a cache that makes everything significantly faster. Having the cache right on disk means that you don&#8217;t have to spend any time transferring any cache artifacts from a remote location. Depot themselves have a <a href="https://depot.dev/docs/self-hosted/architecture">well documented architecture</a> for how they&#8217;ve implemented their builders. It&#8217;s isolated, secure, and managed. They support a flurry of other features, but I wanted to figure out how to simulate their setup for my own learning and now I&#8217;m here to share it with you, and to share what additional features are available when you know that you have a persistent cache.</p><p><strong>To be clear, I&#8217;m not officially affiliated to Depot in any way whatsoever. This is not a paid post. I&#8217;m just genuinely impressed and fascinated by their product.</strong></p><div><hr></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments! New posts every other Tuesday</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h2>Who is this for?</h2><p>This post is written with the assumption that you already have some experience with Kubernetes and with AWS. The concepts and examples should translate fairly directly to other clouds or other managed Kubernetes offerings</p><h2>Running Docker buildkit in Kubernetes</h2><p>As it turns out, Docker buildkit already supports being able to run in Kubernetes via its &#8220;<a href="https://docs.docker.com/build/drivers/kubernetes/">Kubernetes driver</a>&#8221;.</p><pre><code><code>&#8658;  docker buildx create \
  --bootstrap \
  --name=kube \
  --driver=kubernetes \
  --driver-opt=[key=value,...]</code></code></pre><p>This command will quickly use your current Kubernetes context and create a remote buildx agent that you can use to build your containers. This creates all of the relevant Kubernetes resources for you automatically. This method creates a <code>kind: Deployment</code> in your cluster that you can scale out. The <code>&#8212;driver-opt</code> that are documented in the link above will let you control things like number of replicas, and CPU/memory allocations.</p><p>Of course, if you want even more control, you can refer to <a href="https://github.com/moby/buildkit/tree/master/examples/kubernetes">buildkit&#8217;s list of examples in their GitHub repo</a>. They have examples for authenticating with TLS, examples of using other deployment types in your Kubernetes cluster, etc. For this article, we&#8217;re going to break it down step by step and we&#8217;re going to rely on the Kubernetes cluster authentication for accessing the remote agents. From inspecting the resource that using the docker command gives us, we can come up with the following minimal Kubernetes deployment.</p><pre><code>---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: remote-buildkit-agent
  labels:
    app: remote-buildkit-agent
spec:
  replicas: 1
  selector:
    matchLabels:
      app: remote-buildkit-agent
  template:
    metadata:
      labels:
        app: remote-buildkit-agent
    spec:
      containers:
        - name: buildkitd
          image: moby/buildkit:buildx-stable-1
          readinessProbe:
            exec:
              command:
                - "buildctl"
                - "debug"
                - "workers"
          securityContext:
            privileged: true</code></pre><p>That&#8217;s it. That&#8217;s the bare minimum you need to schedule a remote agent. Now we need to figure out how to connect to it so that we can start to send it work to do.</p><p>As a quick aside, you&#8217;ll get a massive speed up for pushing your docker images, if you run this remote buildkit agent in the same cloud region that you&#8217;re storing your images. I run my EKS cluster in us-west-2 and all of my ECR repositories are also configured for us-west-2. If you&#8217;d like to read more about this speed up, I talk about it in a previous newsletter post where I got almost a 30% improvement.</p><div class="embedded-post-wrap" data-attrs="{&quot;id&quot;:77445749,&quot;url&quot;:&quot;https://sliceofexperiments.substack.com/p/pushing-big-docker-images-more-quickly-using&quot;,&quot;publication_id&quot;:1135332,&quot;publication_name&quot;:&quot;A slice of experiments&quot;,&quot;publication_logo_url&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/7aaed77e-5db5-4e6b-982e-1802998c069a_1024x1024.png&quot;,&quot;title&quot;:&quot;Pushing big docker images more quickly using docker buildx and your EKS cluster&quot;,&quot;truncated_body_text&quot;:&quot;Building containers is easy when your applications are small. I&#8217;m usually doing projects in Golang, but at work we&#8217;re a monolithic Django application and after we install all our system dependencies and application dependencies, we end up with a 4.5GB container. It takes a while to build and even longer to push the container. Because of that, I started &#8230;&quot;,&quot;date&quot;:&quot;2021-08-21T23:01:26.000Z&quot;,&quot;like_count&quot;:1,&quot;comment_count&quot;:0,&quot;bylines&quot;:[{&quot;id&quot;:31493138,&quot;name&quot;:&quot;Aaron Batilo&quot;,&quot;previous_name&quot;:null,&quot;photo_url&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/70ad4baa-dcc7-4e3c-a6f0-d8438cebe984_2159x2160.jpeg&quot;,&quot;bio&quot;:&quot;I'm a software engineer building products, platforms, and infrastructure. I like building to learn and want to write about what I've built, and what I learned building it.&quot;,&quot;profile_set_up_at&quot;:&quot;2022-10-10T01:33:14.953Z&quot;,&quot;publicationUsers&quot;:[{&quot;id&quot;:1086624,&quot;user_id&quot;:31493138,&quot;publication_id&quot;:1135332,&quot;role&quot;:&quot;admin&quot;,&quot;public&quot;:true,&quot;is_primary&quot;:true,&quot;publication&quot;:{&quot;id&quot;:1135332,&quot;name&quot;:&quot;A slice of experiments&quot;,&quot;subdomain&quot;:&quot;sliceofexperiments&quot;,&quot;custom_domain&quot;:null,&quot;custom_domain_optional&quot;:false,&quot;hero_text&quot;:&quot;Every other week, learn everything that I've learned from my tiny code and infra experiments&quot;,&quot;logo_url&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/7aaed77e-5db5-4e6b-982e-1802998c069a_1024x1024.png&quot;,&quot;author_id&quot;:31493138,&quot;theme_var_background_pop&quot;:&quot;#A33ACB&quot;,&quot;created_at&quot;:&quot;2022-10-09T20:25:35.895Z&quot;,&quot;rss_website_url&quot;:null,&quot;email_from_name&quot;:null,&quot;copyright&quot;:&quot;Aaron Batilo&quot;,&quot;founding_plan_name&quot;:null,&quot;community_enabled&quot;:true,&quot;invite_only&quot;:false,&quot;payments_state&quot;:&quot;disabled&quot;}},{&quot;id&quot;:1129492,&quot;user_id&quot;:31493138,&quot;publication_id&quot;:1176235,&quot;role&quot;:&quot;admin&quot;,&quot;public&quot;:true,&quot;is_primary&quot;:false,&quot;publication&quot;:{&quot;id&quot;:1176235,&quot;name&quot;:&quot;Devouring Denver&quot;,&quot;subdomain&quot;:&quot;devouringdenver&quot;,&quot;custom_domain&quot;:null,&quot;custom_domain_optional&quot;:false,&quot;hero_text&quot;:&quot;Read Denver food reviews from an amateur critic with poor budgeting skills&quot;,&quot;logo_url&quot;:null,&quot;author_id&quot;:31493138,&quot;theme_var_background_pop&quot;:&quot;#2EE240&quot;,&quot;created_at&quot;:&quot;2022-11-05T16:02:07.796Z&quot;,&quot;rss_website_url&quot;:null,&quot;email_from_name&quot;:null,&quot;copyright&quot;:&quot;Aaron Batilo&quot;,&quot;founding_plan_name&quot;:null,&quot;community_enabled&quot;:true,&quot;invite_only&quot;:false,&quot;payments_state&quot;:&quot;disabled&quot;}}],&quot;twitter_screen_name&quot;:&quot;aaronbatilo&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null,&quot;inviteAccepted&quot;:true}],&quot;utm_campaign&quot;:null,&quot;belowTheFold&quot;:true,&quot;type&quot;:&quot;newsletter&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="EmbeddedPostToDOM"><a class="embedded-post" native="true" href="https://sliceofexperiments.substack.com/p/pushing-big-docker-images-more-quickly-using?utm_source=substack&amp;utm_campaign=post_embed&amp;utm_medium=web"><div class="embedded-post-header"><img class="embedded-post-publication-logo" src="https://substackcdn.com/image/fetch/$s_!o6dZ!,w_56,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7aaed77e-5db5-4e6b-982e-1802998c069a_1024x1024.png" loading="lazy"><span class="embedded-post-publication-name">A slice of experiments</span></div><div class="embedded-post-title-wrapper"><div class="embedded-post-title">Pushing big docker images more quickly using docker buildx and your EKS cluster</div></div><div class="embedded-post-body">Building containers is easy when your applications are small. I&#8217;m usually doing projects in Golang, but at work we&#8217;re a monolithic Django application and after we install all our system dependencies and application dependencies, we end up with a 4.5GB container. It takes a while to build and even longer to push the container. Because of that, I started &#8230;</div><div class="embedded-post-cta-wrapper"><span class="embedded-post-cta">Read more</span></div><div class="embedded-post-meta">5 years ago &#183; 1 like &#183; Aaron Batilo</div></a></div><h2>Authenticating with remote buildkit using a kubeconfig</h2><p>When you run the <code>docker buildx create</code> command, a file gets created on your computer with the buildx authentication configuration. If you use the following command:</p><pre><code>&#8658;  docker buildx create --name remote-buildkit-agent --bootstrap --use --driver kubernetes</code></pre><p>Then your configuration will be created like so:</p><pre><code>&#8658;  cat ~/.docker/buildx/instances/remote-buildkit-agent | jq
{
  "Name": "remote-buildkit-agent",
  "Driver": "kubernetes",
  "Nodes": [
    {
      "Name": "remote-buildkit-agent0",
      "Endpoint": "kubernetes:///remote-buildkit-agent?deployment=&amp;kubeconfig=",
      "Platforms": null,
      "Flags": null,
      "DriverOpts": null,
      "Files": null
    }
  ],
  "Dynamic": false
}</code></pre><p>This file will be named after the agent that you created. The file you see above is what gets created for you automatically, but we can remove most of the fields and everything will still work. All you actually need is:</p><pre><code>&#8658;  cat remote-buildkit-agent | jq
{
  "Name": "remote-buildkit-agent",
  "Driver": "kubernetes",
  "Nodes": [
    {
      "Name": "remote-buildkit-agent",
      "Endpoint": "kubernetes:///remote-buildkit-agent?deployment=&amp;kubeconfig=",
      "DriverOpts": {
        "namespace": "default"
      }
    }
  ]
}</code></pre><p>Do take note that with this configuration, the <code>kubeconfig</code> field is blank which means that the docker client will look for your <code>kubeconfig</code> at the default location. By applying the minimal deployment file and creating this configuration file manually, you&#8217;ll be able to start using the remote agent already.</p><pre><code>docker buildx use remote-buildkit-agent</code></pre><p>Now every time you execute <code>docker buildx build</code>, you&#8217;ll execute the build remotely. Your local CPU and memory won&#8217;t be used at all. This also means that if you specify a larger CPU or memory allocation in the deployment, that you&#8217;ll be able to leverage significantly more powerful build machines. That alone could help speed up your docker builds actually, and that&#8217;s even before we get to any of the caching optimizations.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments! New posts every other Tuesday</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>Configuring a more generous garbage collection policy for buildkit</h2><p>If you build some image a few times with this existing remote agent, you&#8217;ll probably notice that a lot of your steps get nicely cached, but then quickly stop being cached when you build another image. This is because buildkit keeps only a limited amount of space for its cache. Fortunately for us though, we can modify that limit and have garbage collection happen less often, which makes the cache much more useful for us.</p><p>If you look at the buildkit GitHub repo, we can find some <a href="https://github.com/moby/buildkit/blob/master/docs/buildkitd.toml.md">documentation and examples for the default buildkit configuration</a>. This documentation does a great job on its own for all of the other config options, but the sections that we really care about here are the <code>*.*.gcpolicy</code> rules. These sections are tiered and allow for multiple levels of eviction policy. I will make one quick shout out for the <code>max-parallelism</code> option which you might want to leverage when you have a large number of CPUs allocated to your buildkit agent.</p><p>Let&#8217;s take a look at one of the sections in the documented example:</p><pre><code>  [[worker.oci.gcpolicy]]
    keepBytes = 512000000
    keepDuration = 172800
    filters = [ "type==source.local", "type==exec.cachemount", "type==source.git.checkout"]

  [[worker.oci.gcpolicy]]
    all = true
    keepBytes = 1024000000</code></pre><p>First we see the sections <code>keepBytes</code> which is the number of bytes you want to allocate to the matching cache. <code>512000000 bytes</code>, aka 512MB is being allocated for specific types of artifacts. There&#8217;s the local cache for source code that&#8217;s a part of the docker context, there are cache mounts (which we&#8217;ll talk more about in the next session), and artifacts for a remote git checkout (like when you run <code>docker buildx build</code> on a public GitHub URL). At the same time, you can also set a time based eviction. That&#8217;s what <code>keepDuration</code> is for. The value <code>172800</code> is in seconds which translates to 48 hours. In this case, evictions will happen with whichever case happens first.</p><p>The second <code>gcpolicy</code> has the <code>all</code> key which will be the fallback. <code>1024000000</code> bytes is 1GB of space for all other caches and cache types. If you have a large number of dependencies in your build, you can easily spend more than 1GB on space.</p><p>This is the big win that will drastically speed up our builds. Let&#8217;s get into how we integrate that in with our existing Kubernetes deployment. Ultimately, this consists of making two changes. The first being that we need to allocate an EBS volume to store our actual cache, and then we need to update the config that our remote agent uses and mount that to the buildkit container.</p><pre><code>---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: remote-buildkit-agent
  labels:
    app: remote-buildkit-agent
spec:
  replicas: 1
  selector:
    matchLabels:
      app: remote-buildkit-agent
  template:
    metadata:
      labels:
        app: remote-buildkit-agent
    spec:
      containers:
        - name: buildkitd
          image: moby/buildkit:buildx-stable-1
          volumeMounts:
            - name: config
              mountPath: /etc/buildkit
            - name: var-lib-buildkit
              mountPath: /var/lib/buildkit
          readinessProbe:
            exec:
              command:
                - "buildctl"
                - "debug"
                - "workers"
          securityContext:
            privileged: true
      volumes:
        - name: config
          configMap:
            name: remote-buildkit-agent
        - name: var-lib-buildkit
          persistentVolumeClaim:
            claimName: remote-buildkit-agent
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: remote-buildkit-agent
data:
  buildkitd.toml: |
    root = "/var/lib/buildkit"

    [worker]

    [worker.containerd]
      enabled = false

    [worker.oci]
      enabled = true
      gc = true
      gckeepstorage = 30000000000
      snapshotter = "overlayfs"

      [[worker.oci.gcpolicy]]
        filters = ["type==source.local", "type==exec.cachemount", "type==source.git.checkout"]
        keepBytes = 10240000000
        keepDuration = 604800

      [[worker.oci.gcpolicy]]
        keepBytes = 30000000000

      [[worker.oci.gcpolicy]]
        all = true
        keepBytes = 30000000000
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: remote-buildkit-agent
spec:
  resources:
    requests:
      storage: "50Gi"
  accessModes:
    - "ReadWriteOnce"</code></pre><p>Take all the contents of the above yaml and assuming you have a CSI provisioner configured on your cluster, this will allocate a 50Gi persistent volume, and then we attach that to the remote-buildkit-agent container, and we also create a ConfigMap with a minimally configured buildkit file that will increase the garbage collection thresholds. In this example, the buildkit config file is configured for a 30Gb persistent cache. That means we can hold significantly more layers in the local cache and layers that haven&#8217;t been touched will still get evicted as we fill up the cache.</p><h2>Leveraging mount caches with persistent disk buildkit</h2><p>Newer versions of the buildkit backend support an entirely different kind of cache called the <a href="https://docs.docker.com/build/cache/#use-the-dedicated-run-cache">run cache mount</a>. Anywhere that you specify a <code>RUN </code>command in your Dockerfile, you can add a cache mount that lets you specify directories that you&#8217;d like to cache as the <code>type==exec.cachemount</code> artifact in your cache. For example:</p><pre><code>RUN \
    --mount=type=cache,target=/var/cache/apt \
    apt-get update &amp;&amp; apt-get install -y git</code></pre><p>This stores the <code>/var/cache/apt</code> directory into a different type of cache that&#8217;s explicit, instead of having the generic layer cache store the results of the <code>apt</code> commands.</p><p>As opposed to the buildkit inline cache, or the existing remote caches for remote registries, you won&#8217;t see as much documentation about this time of cache yet both because it&#8217;s a new feature, but also because there isn&#8217;t a build provider in the world that can leverage this type of cache except for <a href="https://depot.dev">Depot</a>. This is because providers like GitHub Actions don&#8217;t give you dedicated disk space that you can use. GitHub Actions and CircleCI, etc, all let you cache directories on the host within some limit, and there&#8217;s an <a href="https://github.com/moby/buildkit/issues/1512">open issue</a> for buildkit to be able to specify the cache directory that gets used. However, at the moment, either you run your own remote agents or you use <a href="https://depot.dev">Depot</a> to get persistent disk. Having a run cache mount is a phenomenal speed increase if your applications can use incremental compilation of some kind.</p><p>For example, the Go toolchain introduced a package <a href="https://go.dev/doc/go1.10">build cache in 1.10</a>. That means that you can have a Dockerfile line like so:</p><pre><code>RUN \
  --mount=type=cache,target=/root/.cache \
  --mount=type=cache,target=/root/go/pkg/mod \
  go test -v ./... &amp;&amp; \
  CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go install -ldflags="-w -s" ./cmd/...</code></pre><p>If you run your tests at the same time as your build like I outlined in my <a href="https://sliceofexperiments.substack.com/p/the-4-ways-to-run-your-unit-tests">previous newsletter post</a>, then the built test artifacts and results will also be leveraged with this run type. This makes running your tests and building your actual artifacts as fasts as possible because you&#8217;ve minimized re-work.</p><p>For even more examples of using a run cache mount, I&#8217;d recommend that you check out <a href="https://vsupalov.com/buildkit-cache-mount-dockerfile/">this article by Vladislav Supalov</a>. </p><h2>Wrapping it all into a GitHub Action Workflow</h2><p>Combining some of the steps from earlier in this post, you can authenticate a GitHub Action Workflow just by writing a kubeconfig and the listed docker config. Normally, you would use the <code>docker/setup-buildx-action</code> action with the right driver to get a buildkit agent in your cluster. Unfortunately, if you have already created the agents like you would with the custom config, this action doesn&#8217;t let you connect to existing agents. So you&#8217;ll need your own version of the same command. With EKS, I leverage the <code>aws eks update-kubeconfig</code> command and then write the json file myself. The next big key here is the <code>docker buildx use</code> command. </p><pre><code>      - name: Connect to remote buildkit agent
        env:
          REMOTE_AGENT_NAME: remote-buildkit-agent
        run: |
          aws eks update-kubeconfig --name your-cluster-name
          mkdir -p ~/.docker/buildx/instances/
          cat &lt;&lt; EOF &gt; ~/.docker/buildx/instances/"$REMOTE_AGENT_NAME"
          {
            "Name": "$REMOTE_AGENT_NAME",
            "Driver": "kubernetes",
            "Nodes": [
              {
                "Name": "$REMOTE_AGENT_NAME",
                "Endpoint": "kubernetes:///$REMOTE_AGENT_NAME?deployment=&amp;kubeconfig=",
                "DriverOpts": {
                  "namespace": "default"
                }
              }
            ]
          }
          EOF
          docker buildx use "$REMOTE_AGENT_NAME"</code></pre><p>If you set the <code>docker buildx context</code> correctly, then if you use the available <a href="https://github.com/docker/build-push-action">docker/buildx-push-action</a>, you&#8217;ll automatically use your remote buildkit agent with your persistent cache. You could even use <a href="https://github.com/docker/bake-action">docker/bake-action</a> with this setup since it&#8217;s native buildkit. This is actually one of the advantages of using the self-hosted option over <a href="https://depot.dev">Depot&#8217;s</a> offering.</p><h2>What&#8217;s next?</h2><p>The EBS persistent volume that we used in this post can only be attached to once pod replica at a time. That means that we can&#8217;t scale out our build agents. Even if you increase the number of replicas for the deployment, the PVC will only attach to one of the replicas. One way that you can improve this is by using a Kubernetes <code>StatefulSet</code>. But now you&#8217;ll have multiple copies of the caches which are not guaranteed to be the same. The Kubernetes buildkit driver does have a parameter called <a href="https://docs.docker.com/build/drivers/kubernetes/#synopsis">loadbalance</a> which you can leverage for <em>some</em> amount of predicability but not much. Something that I do want to try experimenting with in the future is using an EFS mount in EKS so that I can attach the same filesystem to multiple replicas.</p><p>Another problem with our configuration is that we currently run the buildkit agent as a privileged container. From a security perspective, it&#8217;s not ideal that there are additional privileges. There are ways to run the buildkit agent with rootless mode being set to false but that&#8217;s for another time.</p><p>The last big piece of functionality that is missing here is any configuration for doing cross platform docker builds. For Go applications, cross compilation is very easy, but for some application stacks, it&#8217;s significantly easier to build natively on other platforms.</p><p>Just for the final piece of thoroughness, many of the problems that this custom, self-hosted configuration has, are solved by the folks doing <a href="https://depot.dev">Depot</a>. Again, I&#8217;m not officially affiliated with them at all. Just a genuinely big fan of their product.</p><div><hr></div><p>That&#8217;s all for now, folks. This ended up being quite a long post! As usual, sample files are <a href="https://github.com/abatilo/sliceofexperiments/tree/main/fastest-docker-builds">also available on GitHub</a>. Let me know what you think and I&#8217;ll see you next time. Please consider subscribing to get future posts, and consider sharing with your friends!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[The 4 ways to run your unit tests in CI with Dockerfiles]]></title><description><![CDATA[Performance, convenience, and portability. Which will you choose?]]></description><link>https://www.sliceofexperiments.com/p/the-4-ways-to-run-your-unit-tests</link><guid isPermaLink="false">https://www.sliceofexperiments.com/p/the-4-ways-to-run-your-unit-tests</guid><dc:creator><![CDATA[Aaron Batilo]]></dc:creator><pubDate>Tue, 20 Dec 2022 21:15:58 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!NVQG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F60b284f7-4661-4bc6-b445-0eb6adc6dbc1_1282x1050.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Running your unit tests as early and as often as you can in your development cycle can help for rapidly building software, and doing it fearlessly and confidently. But with the abundance and wide adoption of containerization, how does this change the way that you do your continuous integration? One of the largest benefits about Dockerfiles is that they can be completely self contained. Your CI vendor of choice starts to matter less and less because the Dockerfiles themselves are portable and predictable. Almost every CI vendor these days can build a Dockerfile if you just point to one. Do you still need to run your tests on your CI provider directly? In this post, we&#8217;re going to go over the 4 different ways that you can run your unit tests when you have a Dockerfile. We&#8217;ll go over some of the pros and cons of each method.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments! New posts every other Tuesday</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h1>Method #1: Don&#8217;t. Run your tests directly in CI hosts</h1><p>This might be everyone&#8217;s default approach. Organizations that are still new to using containers in general might only be interested in doing so for your final deployment. In this pattern, you do things like run your tests and build your release artifacts in a separate environment, and then copy them directly into your Dockerfile.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NVQG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F60b284f7-4661-4bc6-b445-0eb6adc6dbc1_1282x1050.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NVQG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F60b284f7-4661-4bc6-b445-0eb6adc6dbc1_1282x1050.png 424w, https://substackcdn.com/image/fetch/$s_!NVQG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F60b284f7-4661-4bc6-b445-0eb6adc6dbc1_1282x1050.png 848w, https://substackcdn.com/image/fetch/$s_!NVQG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F60b284f7-4661-4bc6-b445-0eb6adc6dbc1_1282x1050.png 1272w, https://substackcdn.com/image/fetch/$s_!NVQG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F60b284f7-4661-4bc6-b445-0eb6adc6dbc1_1282x1050.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NVQG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F60b284f7-4661-4bc6-b445-0eb6adc6dbc1_1282x1050.png" width="1282" height="1050" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/60b284f7-4661-4bc6-b445-0eb6adc6dbc1_1282x1050.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1050,&quot;width&quot;:1282,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:179261,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!NVQG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F60b284f7-4661-4bc6-b445-0eb6adc6dbc1_1282x1050.png 424w, https://substackcdn.com/image/fetch/$s_!NVQG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F60b284f7-4661-4bc6-b445-0eb6adc6dbc1_1282x1050.png 848w, https://substackcdn.com/image/fetch/$s_!NVQG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F60b284f7-4661-4bc6-b445-0eb6adc6dbc1_1282x1050.png 1272w, https://substackcdn.com/image/fetch/$s_!NVQG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F60b284f7-4661-4bc6-b445-0eb6adc6dbc1_1282x1050.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nrLQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb5ff64d-78c4-4617-8810-1e8f254ee848_810x602.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nrLQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb5ff64d-78c4-4617-8810-1e8f254ee848_810x602.png 424w, https://substackcdn.com/image/fetch/$s_!nrLQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb5ff64d-78c4-4617-8810-1e8f254ee848_810x602.png 848w, https://substackcdn.com/image/fetch/$s_!nrLQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb5ff64d-78c4-4617-8810-1e8f254ee848_810x602.png 1272w, https://substackcdn.com/image/fetch/$s_!nrLQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb5ff64d-78c4-4617-8810-1e8f254ee848_810x602.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nrLQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb5ff64d-78c4-4617-8810-1e8f254ee848_810x602.png" width="810" height="602" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/cb5ff64d-78c4-4617-8810-1e8f254ee848_810x602.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:602,&quot;width&quot;:810,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:98907,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!nrLQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb5ff64d-78c4-4617-8810-1e8f254ee848_810x602.png 424w, https://substackcdn.com/image/fetch/$s_!nrLQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb5ff64d-78c4-4617-8810-1e8f254ee848_810x602.png 848w, https://substackcdn.com/image/fetch/$s_!nrLQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb5ff64d-78c4-4617-8810-1e8f254ee848_810x602.png 1272w, https://substackcdn.com/image/fetch/$s_!nrLQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb5ff64d-78c4-4617-8810-1e8f254ee848_810x602.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Pros:</p><ul><li><p>Arguably the most straightforward</p></li><li><p>Plenty of examples and documentation for how to do this since it&#8217;s the default for a lot of people</p></li><li><p>Easiest to adopt in transition to Dockerfiles</p></li></ul><p>Cons:</p><ul><li><p>Not portable. Coupled directly to CI provider</p></li><li><p>Need to maintain your CI environment on top of other environments</p></li></ul><p>There is plenty of documentation for how to do this. Installing things like Python in your CI environment is going to be heavily supported and widely adopted. This method is easy to get started with. However, some of the cons are incredibly inconvenient. Relying on your CI vendor means that you&#8217;ll probably end up integrating with a lot of their proprietary functionality which means vendor lock in. Solving that problem is what systems like <a href="https://dagger.io">https://dagger.io</a> and <a href="https://earthly.dev">https://earthly.dev</a> are trying to do. Additionally, you&#8217;ll notice that this workflow requires you to install python in your CI environment. You also have to have python installed in your deployed environments. This means you have a bifurcation in how you manage and administer your CI environment and how you manage and administer your live environments.</p><p>In this example, you&#8217;re only installing Python, but in real environments with useful applications, you need to install more complicated dependencies. In the Python community, it&#8217;s very normal for libraries to rely on or expect operating system level dependencies to be installed. In languages like Go, you can have all dependencies be statically linked, but in Python, if you want to communicate with a Postgres database, almost all of the popular libraries expect you to have an operating system dependency like <a href="https://packages.debian.org/sid/libpq-dev">libpq-dev</a> to be installed. Now you need to install libpq in your CI environment and your live environment. And this problem balloons. If you want to be truly confident, you need to coordinate that when you update your CI environment, that you also update your live environment at the same time. If you don&#8217;t, you risk that you&#8217;re actually running your tests with different versions of dependencies than your live environment, which means you might get different behaviors in each environment.</p><h1>Method #2: Run your tests on a pre-built CI image</h1><p>Many CI services will let you specify the container to actually execute your CI steps in. In GitHub Actions, this is the <a href="https://docs.github.com/en/actions/using-jobs/running-jobs-in-a-container">container key</a>. In my example, I&#8217;m just using the official python image, but you could separately build a CI image that has all of your prerequisites.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cWlw!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F98521a0b-2ea5-4049-84f5-068aca1480ba_1282x864.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cWlw!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F98521a0b-2ea5-4049-84f5-068aca1480ba_1282x864.png 424w, https://substackcdn.com/image/fetch/$s_!cWlw!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F98521a0b-2ea5-4049-84f5-068aca1480ba_1282x864.png 848w, https://substackcdn.com/image/fetch/$s_!cWlw!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F98521a0b-2ea5-4049-84f5-068aca1480ba_1282x864.png 1272w, https://substackcdn.com/image/fetch/$s_!cWlw!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F98521a0b-2ea5-4049-84f5-068aca1480ba_1282x864.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cWlw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F98521a0b-2ea5-4049-84f5-068aca1480ba_1282x864.png" width="1282" height="864" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/98521a0b-2ea5-4049-84f5-068aca1480ba_1282x864.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:864,&quot;width&quot;:1282,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:159092,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!cWlw!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F98521a0b-2ea5-4049-84f5-068aca1480ba_1282x864.png 424w, https://substackcdn.com/image/fetch/$s_!cWlw!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F98521a0b-2ea5-4049-84f5-068aca1480ba_1282x864.png 848w, https://substackcdn.com/image/fetch/$s_!cWlw!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F98521a0b-2ea5-4049-84f5-068aca1480ba_1282x864.png 1272w, https://substackcdn.com/image/fetch/$s_!cWlw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F98521a0b-2ea5-4049-84f5-068aca1480ba_1282x864.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Pros:</p><ul><li><p>Decreasingly less coupled to your CI provider, relative to method 1</p></li></ul><p>Cons:</p><ul><li><p>Requires a separate CI process for the base image that you want to use</p></li></ul><p>In method 1, we had to install system dependencies to the host runners for the CI provider. In method 2, we extract just that step into its own dedicated Dockerfile and then we rely on the CI provider to be able to let us override the runtime environment that executes your steps. In the GitHub Actions ecosystem, it&#8217;s normal to have actions that install certain tools, but most tools are unlikely to document how to install the tool itself into a GitHub Actions pipeline. But if you install the tools in your Dockerfile, you can likely just have a RUN command with their suggested install command.</p><p>On the downsides, this might be a little complicated still. Most of these CI providers will not let you define the override container to be an image that was built and loaded during the pipeline. That means that the image needs to be already built, and the CI environment needs to have the credentials to pull the image. This is a fairly straight forward one time setup but it can still be rather annoying to do the first time.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments! New posts every Tuesday</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h1>Method #3: Run your tests in your docker build</h1><p>What about a more portable way to build your images? If you run the test command directly in your Dockerfile, then all you need to do to test your code is to build your Dockerfile, and the CI provider that you use doesn&#8217;t really matter.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-S0o!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb95283b-62dd-4236-99b0-e5efbd008ca3_1282x826.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-S0o!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb95283b-62dd-4236-99b0-e5efbd008ca3_1282x826.png 424w, https://substackcdn.com/image/fetch/$s_!-S0o!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb95283b-62dd-4236-99b0-e5efbd008ca3_1282x826.png 848w, https://substackcdn.com/image/fetch/$s_!-S0o!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb95283b-62dd-4236-99b0-e5efbd008ca3_1282x826.png 1272w, https://substackcdn.com/image/fetch/$s_!-S0o!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb95283b-62dd-4236-99b0-e5efbd008ca3_1282x826.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-S0o!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb95283b-62dd-4236-99b0-e5efbd008ca3_1282x826.png" width="1282" height="826" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/bb95283b-62dd-4236-99b0-e5efbd008ca3_1282x826.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:826,&quot;width&quot;:1282,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:148995,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!-S0o!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb95283b-62dd-4236-99b0-e5efbd008ca3_1282x826.png 424w, https://substackcdn.com/image/fetch/$s_!-S0o!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb95283b-62dd-4236-99b0-e5efbd008ca3_1282x826.png 848w, https://substackcdn.com/image/fetch/$s_!-S0o!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb95283b-62dd-4236-99b0-e5efbd008ca3_1282x826.png 1272w, https://substackcdn.com/image/fetch/$s_!-S0o!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb95283b-62dd-4236-99b0-e5efbd008ca3_1282x826.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yOO6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F0c8cb6e2-ab09-47af-8f61-ca93c003d764_978x676.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yOO6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F0c8cb6e2-ab09-47af-8f61-ca93c003d764_978x676.png 424w, https://substackcdn.com/image/fetch/$s_!yOO6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F0c8cb6e2-ab09-47af-8f61-ca93c003d764_978x676.png 848w, https://substackcdn.com/image/fetch/$s_!yOO6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F0c8cb6e2-ab09-47af-8f61-ca93c003d764_978x676.png 1272w, https://substackcdn.com/image/fetch/$s_!yOO6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F0c8cb6e2-ab09-47af-8f61-ca93c003d764_978x676.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yOO6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F0c8cb6e2-ab09-47af-8f61-ca93c003d764_978x676.png" width="978" height="676" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/0c8cb6e2-ab09-47af-8f61-ca93c003d764_978x676.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:676,&quot;width&quot;:978,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:111515,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!yOO6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F0c8cb6e2-ab09-47af-8f61-ca93c003d764_978x676.png 424w, https://substackcdn.com/image/fetch/$s_!yOO6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F0c8cb6e2-ab09-47af-8f61-ca93c003d764_978x676.png 848w, https://substackcdn.com/image/fetch/$s_!yOO6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F0c8cb6e2-ab09-47af-8f61-ca93c003d764_978x676.png 1272w, https://substackcdn.com/image/fetch/$s_!yOO6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F0c8cb6e2-ab09-47af-8f61-ca93c003d764_978x676.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Pros:</p><ul><li><p>Extremely portable. Every CI provider has the ability to build a Docker image these days</p></li><li><p>Running your tests can be completely self contained</p></li><li><p>By definition, a successfully built Docker container is a container that passed all of its tests</p></li></ul><p>Cons:</p><ul><li><p>Building your Docker image can take a lot longer</p></li><li><p>Your final Docker image will have all of your test dependencies installed even if you don&#8217;t need them to run your application. Bigger images means longer pushes and pulls</p></li><li><p>Accessing secrets can be hard</p></li></ul><p>I really enjoy this method for how self contained it is, and the guarantees it creates. If you run your tests in the container image itself, then your ability to move between CI vendors gets easier and easier if you need to, but more importantly, everything is self contained. Your Dockerfile can have all of your operating system level dependencies needed to run your tests, which means that you don&#8217;t have to install anything at all on the CI&#8217;s host machine. One of my absolute favorite parts about this is the unison of your build environment and your run environment. You know that anything you needed for the tests to run will be available for your application execution. This is really handy for interpreted programming languages. Lastly, every single image that gets built and pushed this way is required to pass your tests. If a test fails, the image build won&#8217;t succeed and there&#8217;s nothing to push. It&#8217;s nice knowing that every single image you have has passed your unit test suite.</p><p>On the downside, it can be really inconvenient to build your tests every single time. Remote and containerized development environments are becoming increasingly popular due to how complex our service architectures are becoming. You might be working on multiple services or need your development environments to have massive clusters of compute if you&#8217;re a data scientist, so you need a remote enviornment. There are several tools and companies that are trying to enable this workflow like <a href="https://skaffold.dev">https://skaffold.dev</a>, <a href="https://tilt.dev/">https://tilt.dev</a>, or paid services like <a href="https://www.okteto.com/">https://okteto.com</a>. These tools will sync your code to remote environments and re-build, re-push, and trigger a re-pull of your containers in a watch mode and reload them in your remote environment. If your organization is advanced enough to have a setup like this, firstly, congratulations. Second, running your tests every time might be a huge pain if your language ecosystem doesn&#8217;t have a good way of doing incremental or changed files based test executions. Since you&#8217;ve installed all of your test dependencies, the surface area and size of your application runtime container is going to have a lot of extra cruft. For example, in the Python ecosystem, it&#8217;s very normal to have tools like <a href="https://github.com/psf/black">psf/black</a> or <a href="https://github.com/PyCQA/pylint">PyCQA/pylint</a> to be part of your CI steps. But once those are installed, you don&#8217;t need them to run your code, but since the exist in the Docker image, they&#8217;ll be part of each push and pull of the image. Even worse, sometimes these tools need their own operating system level dependencies that are just going to be laying around and be unused. Lastly, sometimes your CI environments need to access other systems. Passing in secrets or other configuration in general can be really annoying to have to explicitly do every time there&#8217;s a new secret or new config file.</p><h1>Method #4: Run your tests in a multi-stage docker build</h1><p>Use a Docker multi-stage build for getting rid of the cruft from method #3. Keep separate stages for just your CI, independent of your final image, but keep them in a single Dockerfile for the usability.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!MtEq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4b9676e8-21b8-43db-975a-779e08406452_1500x974.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!MtEq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4b9676e8-21b8-43db-975a-779e08406452_1500x974.png 424w, https://substackcdn.com/image/fetch/$s_!MtEq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4b9676e8-21b8-43db-975a-779e08406452_1500x974.png 848w, https://substackcdn.com/image/fetch/$s_!MtEq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4b9676e8-21b8-43db-975a-779e08406452_1500x974.png 1272w, https://substackcdn.com/image/fetch/$s_!MtEq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4b9676e8-21b8-43db-975a-779e08406452_1500x974.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!MtEq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4b9676e8-21b8-43db-975a-779e08406452_1500x974.png" width="1456" height="945" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/4b9676e8-21b8-43db-975a-779e08406452_1500x974.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:945,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:174386,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!MtEq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4b9676e8-21b8-43db-975a-779e08406452_1500x974.png 424w, https://substackcdn.com/image/fetch/$s_!MtEq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4b9676e8-21b8-43db-975a-779e08406452_1500x974.png 848w, https://substackcdn.com/image/fetch/$s_!MtEq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4b9676e8-21b8-43db-975a-779e08406452_1500x974.png 1272w, https://substackcdn.com/image/fetch/$s_!MtEq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F4b9676e8-21b8-43db-975a-779e08406452_1500x974.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UfP0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F86932862-566e-4456-a494-09b40a57712d_978x714.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UfP0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F86932862-566e-4456-a494-09b40a57712d_978x714.png 424w, https://substackcdn.com/image/fetch/$s_!UfP0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F86932862-566e-4456-a494-09b40a57712d_978x714.png 848w, https://substackcdn.com/image/fetch/$s_!UfP0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F86932862-566e-4456-a494-09b40a57712d_978x714.png 1272w, https://substackcdn.com/image/fetch/$s_!UfP0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F86932862-566e-4456-a494-09b40a57712d_978x714.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UfP0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F86932862-566e-4456-a494-09b40a57712d_978x714.png" width="978" height="714" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/86932862-566e-4456-a494-09b40a57712d_978x714.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:714,&quot;width&quot;:978,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:126168,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!UfP0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F86932862-566e-4456-a494-09b40a57712d_978x714.png 424w, https://substackcdn.com/image/fetch/$s_!UfP0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F86932862-566e-4456-a494-09b40a57712d_978x714.png 848w, https://substackcdn.com/image/fetch/$s_!UfP0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F86932862-566e-4456-a494-09b40a57712d_978x714.png 1272w, https://substackcdn.com/image/fetch/$s_!UfP0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F86932862-566e-4456-a494-09b40a57712d_978x714.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Pros:</p><ul><li><p>Still portable overall</p></li><li><p>Smaller image sizes at the end of the day, keep only what you need</p></li><li><p>Stages can be built in parallel</p></li></ul><p>Cons:</p><ul><li><p>More config back on the CI provider</p></li><li><p>More complex. Large multi-stage Dockerfiles can be hard to understand</p></li><li><p>More redundancy in the Dockerfile itself</p></li><li><p>CI commands themselves are not codified in the Dockerfile</p></li><li><p>Need to keep better track of which dependencies are dev only vs runtime</p></li></ul><p>I&#8217;ve worked on a number of Python and Nodejs based applications that have benefited from an approach like this. You get all of the portability of having your build be self contained, but with a twist. Docker builds have a <code>&#8212;target</code> flag which lets you specify to only build up and through a certain stage. In our case, what we do is we isolate all of the dependencies needed for CI in one stage, and then in CI we build to that stage, then run all of our test commands, and then we can build the rest of the image with our normal <code>&#8212;target.</code></p><p>Approaches like this try to take a middle ground on the pros and cons we&#8217;ve seen up to this point. In a remote development environment, you can do your remote development with an earlier target and without running any of the test commands.</p><p>Unfortunately, depending on the programming language you&#8217;re using, there might be a lot of redundancy. A lot of package managers will let you specify if you want to install just dev dependencies or prod dependencies. For running your tests, you usually have to install them all, and then in a different stage you have to run the same command but without the dev dependencies. It&#8217;s not super common that package managers have a command for uninstalling just the dev dependencies. Another con to this approach is that for things like operating system dependencies, it can be really hard to know what you need for CI steps and what you need for runtime. In package managers like a package.json, there are separate sections for dev dependencies, but this doesn&#8217;t always translate easily to the fact that tools like <a href="https://github.com/nodejs/node-gyp">nodejs/node-gyp</a> need Python to be installed to execute, but once artifacts are built, you no longer need to have Python or node-gyp installed at all.</p><h1>Conclusion</h1><p>In this post we went over a few different ideas for how you can leverage Docker for doing your CI, on top of having your applications running in a container. In an upcoming newsletter post, I&#8217;m going to go over how I made method #3 outrageously fast on my Golang docker builds.</p><p>All code samples can also be seen on GitHub by viewing <a href="https://github.com/abatilo/sliceofexperiments/blob/main/.github/workflows/unit-tests-in-dockerfiles.yaml">this repo</a>.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[One Dockerfile is all it takes, falling in love with bake]]></title><description><![CDATA[Using docker bake for outrageously simple monorepo docker builds]]></description><link>https://www.sliceofexperiments.com/p/one-dockerfile-is-all-it-takes-falling</link><guid isPermaLink="false">https://www.sliceofexperiments.com/p/one-dockerfile-is-all-it-takes-falling</guid><dc:creator><![CDATA[Aaron Batilo]]></dc:creator><pubDate>Tue, 06 Dec 2022 21:15:02 GMT</pubDate><enclosure url="https://substackcdn.com/image/youtube/w_728,c_limit/b5hjqSnliKk" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div id="youtube2-b5hjqSnliKk" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;b5hjqSnliKk&quot;,&quot;startTime&quot;:&quot;20&quot;,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/b5hjqSnliKk?start=20&amp;rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><p>I love <a href="https://docs.docker.com/engine/reference/commandline/buildx_bake/">docker buildx bake</a>. Also, I was really proud of the newsletter post title and the reference to the Dua Lipa song.</p><p>I like experimenting with a lot of different ideas, and for the last several years, I've been trying to figure out how to maximize my ability to try new ideas with as little work as possible. In this post, I want to go over what I've settled on these days, which is to use a monolithic git repository, and to leverage Docker's bake functionality. We'll talk about how I structure my repository, how I build the containers, etc. If you'd just like to look through the repo, check it out at <a href="https://github.com/abatilo/newsletter-bake-monorepo">abatilo/newsletter-bake-monorepo</a>.</p><p>For the backend services, I write web services with Go. I define a global go module at the root of my repository. Go workspaces are a thing as of Go 1.18 but their value is largely when you want to make it easy to work on module changes across repos. For frontend applications, I use <code>create-react-app</code> react applications. I'm not much of a frontend developer, and even though I know there are benefits to some of the newer frameworks, this one is comfortable to me which lets me focus on building. With the react applications, I do leverage npm workspaces. I serve the static assets directly from my Go application, which means that there's only one application container that I need to worry about. That also means that I can serve the frontend and backend on the same domain, which means I get to avoid any kind of CORS configuration. I serve the applications behind CloudFront as my CDN, and so there's still static asset caching, and backend applications get API acceleration, DDoS protection, and more thanks to the CDN. I organize building the containers using a docker-bake file. Bake is a high level build command available to people using docker buildx. In it's simplest definition, you can write a file in the Hashicorp terraform format, and running bake will build all of the targets, and will share common image stages when it can, and will traverse the dependency tree in parallel as much as possible. You can easily define several docker containers, and with a single command, build all of them, very comparable to having a Makefile.</p><p>With this high level overview, let's break it down.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments! New posts every other week.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>One Dockerfile is all it takes, falling in love with bake</h2><p><a href="https://github.com/abatilo/newsletter-bake-monorepo/blob/main/Dockerfile">Here&#8217;s a link to the whole Dockerfile.</a> How&#8217;s this work? It&#8217;s a <a href="https://docs.docker.com/build/building/multi-stage/">multi-stage build</a> where we build the static assets in one stage, build the go binaries in another stage, and put everything together in the last stage.</p><p>The frontend stages:</p><pre><code>FROM node:18.10.0-alpine as node-modules

WORKDIR /app/web
COPY ./web/package.json ./web/package-lock.json ./
COPY ./web/app1/package.json ./app1/
COPY ./web/app2/package.json ./app2/
RUN npm ci --no-audit

FROM node-modules as node-builder
COPY ./web ./

ARG app
RUN npm run build --workspace ${app}</code></pre><p>So we start out with a stage that&#8217;s exclusively for installing dependencies. With <code>npm</code>, if you want deterministic builds, you want to use <a href="https://docs.npmjs.com/cli/v9/commands/npm-ci">npm ci</a> for your build environments. And with <code>npm workspaces</code>, you have to copy in the <code>package.json</code> for each workspace so that the <code>npm ci </code>can make sure to install everything needed. There are ways to install the dependencies for a single workspace, but since we&#8217;ll end up configuring our docker cache for the entirety of this stage, downloading the cache all at once actually ends up being significantly faster than downloading the dependencies for each workspace in parallel. Especially since there&#8217;s a lot of shared dependencies between each website. We keep the <code>node-modules</code> itself as an independent stage so that when we cache it, it doesn&#8217;t need to worry about the <code>ARG</code> for the app that we want to actually build. That&#8217;ll make more sense when we get to looking at the contents of the <code>docker-bake.hcl</code></p><p>The backend stages:</p><pre><code>FROM golang:1.19.0 as go-modules

# Install dependencies
WORKDIR /app
COPY ./go.mod ./go.sum ./
RUN go mod download

FROM go-modules as go-builder

COPY ./internal ./internal
COPY ./cmd ./cmd
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go install -ldflags="-w -s" ./cmd/...</code></pre><p>Same as the frontend stages, we have a stage that&#8217;s for go dependencies, and we download them all at once and we&#8217;ll cache the entire stage. And as opposed to the frontend build, we actually just compile the entirety of the repo. The Go compiler is extremely fast, and in a future newsletter post that I have planned, we&#8217;ll talk about how to leverage the Go compiler build cache to make this even faster. Consider subscribing to find out how I do that in a Dockerfile!</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.sliceofexperiments.com/subscribe?"><span>Subscribe now</span></a></p><p>The final stage:</p><pre><code>FROM gcr.io/distroless/static-debian11:nonroot
ENV STATIC_ASSETS_PATH /static

ARG app
COPY --from=node-builder /app/web/${app}/build /static
COPY --from=go-builder /go/bin/${app} /usr/local/bin/entrypoint
ENTRYPOINT ["entrypoint"]</code></pre><p>Just like I mentioned, we take the static assets for a specific app and the statically linked binary and put those into the same final container.</p><h2>Writing your docker-bake.hcl</h2><p>The common config:</p><pre><code>variable "GITHUB_SHA" {
  default = "latest"
}

variable "REGISTRY" {
  default = "ghcr.io/abatilo/newsletter-bake-monorepo"
}

group "default" {
  targets = [
    "app1",
    "app2",
  ]
} </code></pre><p>I use the GitHub SHA for my container tags but you can use whatever you want. Writing a <code>variable</code> block in the bake file will automatically be populated by any environment variables that are available when the <code>docker buildx bake</code> command is invoked. In GitHub Actions, <code>GITHUB_SHA</code> is always populated. Setting a default means that I get to build the containers locally without having to remember to set <code>GITHUB_SHA</code>.</p><p>The next <code>variable</code> block is for the container registry prefix to push to, without the image name. This one I don&#8217;t actually override anywhere but there isn&#8217;t a way to set local variables like in Terraform with <code>locals</code> blocks. So I define the registry in one place and we&#8217;ll use it for each application&#8217;s container target.</p><p><code>group &#8220;default&#8221;</code> is the default target to build if all you do is run <code>docker buildx bake</code> without any additional arguments. I define all of the images in my project here.</p><p>Defining the cache targets:</p><pre><code>target "node-modules" {
  target = "node-modules"
  cache-from = ["type=gha,scope=node-modules"]
  cache-to = ["type=gha,mode=max,scope=node-modules"]
}

target "go-modules" {
  target = "go-modules"
  cache-from = ["type=gha,scope=go-modules"]
  cache-to = ["type=gha,mode=max,scope=go-modules"]
}</code></pre><p>Bake will build a dependency graph for any shared docker stages that get used by later images, and will walk the dependency graph to build the different targets. This is awesome for the monorepo and how I structured the Dockerfile. It means that I can define the dependency stages as their own images with the cache config, and when I run <code>docker buildx bake</code>, these two targets will be built first and since bake knows that the different apps depend on these targets, we build the dependencies first before them getting used in parallel for each application build. Buildkit has a backend for specifically using the GitHub Actions cache API which is incredibly easy to use. You can read more about <a href="https://docs.docker.com/build/building/cache/backends/gha/">this cache backend here</a>.</p><pre><code>target "app1" {
  contexts = {
    node-modules = "target:node-modules",
    go-modules = "target:go-modules"
  }
  args = {
    app = "app1"
  }
  tags = [
    "${REGISTRY}/app1:${GITHUB_SHA}",
  ]
  cache-from = ["type=gha,scope=app1"]
  cache-to = ["type=gha,mode=max,scope=app1"]
}

target "app2" {
  contexts = {
    node-modules = "target:node-modules",
    go-modules = "target:go-modules"
  }
  args = {
    app = "app2"
  }
  tags = [
    "${REGISTRY}/app2:${GITHUB_SHA}",
  ]
  cache-from = ["type=gha,scope=app2"]
  cache-to = ["type=gha,mode=max,scope=app2"]
}</code></pre><p>Now, we have examples of the actual definitions for a single application&#8217;s docker container. Notice that we set the <code>contexts</code> key here which references the dependency targets we just defined. You can basically think of this like the <code>depends_on</code> block if you&#8217;ve used Terraform before. The <code>args</code> key let&#8217;s us populate the <code>ARG</code> variables in the <code>Dockerfile</code>. This is what is ultimately different between each of the containers. <a href="https://github.com/docker/buildx/issues/934">In the future</a>, the buildx team might support using <code>for_each</code> loops like in Terraform but for now each block will have a bit of duplication.</p><h2>What does CI look like?</h2><p>There&#8217;s an amazing <a href="https://github.com/docker/bake-action">docker/bake-action</a> which makes it insanely easy to build all of your containers in the most optimal way. Since we&#8217;ve set the <code>group &#8220;default&#8221;</code> block in the <code>docker-bake.hcl</code>, config is very minimal. One step in your GitHub Action workflow file will build all of your images and will push all of your cache layers, tag all of your containers, and push all your final images. You&#8217;ll still have to do things like checkout the code and don&#8217;t forget that you&#8217;ll want to use the <a href="https://github.com/docker/setup-buildx-action">docker/setup-buildx-action</a> since bake is a buildx feature. There&#8217;s one quick gotcha for the actual <code>docker/bake-action</code>. We don&#8217;t want to push PR builds and we don&#8217;t want to pollute the cache with PR builds.</p><pre><code>      - if: github.event_name == 'push'
        uses: docker/bake-action@v2.3.0
        with:
          push: true

      - if: github.event_name == 'pull_request'
        uses: docker/bake-action@v2.3.0
        with:
          push: false
          # Explicitly remove pushing to cache to not pollute the cache with PR
          # specific layers.
          set: |
            *.cache-to=</code></pre><p>The way we set <code>cache-to</code> to an empty string is inspired by <a href="https://github.com/int128/docker-build-cache-config-action">int128/docker-build-cache-config-action</a>. The wildcard sets the <code>cache-to</code> property for all targets.</p><h2>Wrapping up</h2><p>Reminder that a full blown example repo is available at <a href="https://github.com/abatilo/newsletter-bake-monorepo">abatilo/newsletter-bake-monorepo</a>. This project structure makes it so insanely easy to add new projects to CI and makes it easy for me to continue to try new ideas. Maintenance cost is very low since it&#8217;s a single Dockerfile and the Dockerfile itself is very minimal. These docker builds are optimized for small docker images, parallel builds, and let&#8217;s me try new ideas without much effort.</p><p>I hope you give docker bake a try. I think it&#8217;s a great way to maintain many images in the same repo and keep builds optimal and simple.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[Don't be a fool, version your tool]]></title><description><![CDATA[The easiest way to get semantic versioning]]></description><link>https://www.sliceofexperiments.com/p/the-easiest-way-automate-releases</link><guid isPermaLink="false">https://www.sliceofexperiments.com/p/the-easiest-way-automate-releases</guid><dc:creator><![CDATA[Aaron Batilo]]></dc:creator><pubDate>Tue, 22 Nov 2022 21:16:04 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!4QZx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6716e5e-5084-4e04-b2bb-ae9e9cd561f6_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!4QZx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6716e5e-5084-4e04-b2bb-ae9e9cd561f6_1024x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4QZx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6716e5e-5084-4e04-b2bb-ae9e9cd561f6_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!4QZx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6716e5e-5084-4e04-b2bb-ae9e9cd561f6_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!4QZx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6716e5e-5084-4e04-b2bb-ae9e9cd561f6_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!4QZx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6716e5e-5084-4e04-b2bb-ae9e9cd561f6_1024x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4QZx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6716e5e-5084-4e04-b2bb-ae9e9cd561f6_1024x1024.png" width="1024" height="1024" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/c6716e5e-5084-4e04-b2bb-ae9e9cd561f6_1024x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1024,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1210248,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!4QZx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6716e5e-5084-4e04-b2bb-ae9e9cd561f6_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!4QZx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6716e5e-5084-4e04-b2bb-ae9e9cd561f6_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!4QZx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6716e5e-5084-4e04-b2bb-ae9e9cd561f6_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!4QZx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6716e5e-5084-4e04-b2bb-ae9e9cd561f6_1024x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">2d cartoon robot sending mail from DALL&#183;E 2</figcaption></figure></div><p>I'm a huge fan of open sourcing small utilities, but I never want to release something without automating new versions. As best as I can tell, projects always look more serious and stable if there&#8217;s multiple versions that have been published. In this post, I'm going to go over everything that I did to prepare and automate versioning and releasing an open source project. I've used the same base layout for several projects like <a href="https://github.com/abatilo/actions-poetry">actions-poetry (261 stars)</a>, <a href="https://github.com/abatilo/typed-json-dataclass">typed-json-dataclass (79 stars)</a>, <a href="https://github.com/abatilo/release-info-action">release-info-action (6 stars)</a>, and have even released company internal tools the same way, but today we'll be focused on my most recent utility, <a href="https://github.com/abatilo/github-ratelimit-metrics">github-ratelimit-metrics</a> and the combination of settings that I use for making releases as easy as possible for myself.</p><p>There&#8217;s three parts to making this work well:</p><ol><li><p><a href="https://docs.github.com/en/actions">GitHub Actions</a></p></li><li><p><a href="https://github.com/amannn/action-semantic-pull-request">amannn/action-semantic-pull-request</a></p></li><li><p><a href="https://github.com/semantic-release/semantic-release">semantic-release/semantic-release</a></p></li></ol><p>We&#8217;ll be releasing these projects with <a href="https://semver.org/">semantic versioning (SemVer)</a>, which I appreciate as a consumer, because it let&#8217;s me easily understand whether or not I should expect an update to impact my usage.</p><p>So the first thing that we want to do is make sure that the repositories we want to configure this all on, have squash merging with the following default:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!DBOt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfac571f-104d-4bc0-888e-0d6987749c54_1303x837.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!DBOt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfac571f-104d-4bc0-888e-0d6987749c54_1303x837.png 424w, https://substackcdn.com/image/fetch/$s_!DBOt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfac571f-104d-4bc0-888e-0d6987749c54_1303x837.png 848w, https://substackcdn.com/image/fetch/$s_!DBOt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfac571f-104d-4bc0-888e-0d6987749c54_1303x837.png 1272w, https://substackcdn.com/image/fetch/$s_!DBOt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfac571f-104d-4bc0-888e-0d6987749c54_1303x837.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!DBOt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfac571f-104d-4bc0-888e-0d6987749c54_1303x837.png" width="1303" height="837" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/bfac571f-104d-4bc0-888e-0d6987749c54_1303x837.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:837,&quot;width&quot;:1303,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:164196,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!DBOt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfac571f-104d-4bc0-888e-0d6987749c54_1303x837.png 424w, https://substackcdn.com/image/fetch/$s_!DBOt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfac571f-104d-4bc0-888e-0d6987749c54_1303x837.png 848w, https://substackcdn.com/image/fetch/$s_!DBOt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfac571f-104d-4bc0-888e-0d6987749c54_1303x837.png 1272w, https://substackcdn.com/image/fetch/$s_!DBOt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfac571f-104d-4bc0-888e-0d6987749c54_1303x837.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Allow squash merging with Default to pull request title and description</figcaption></figure></div><p>The default is relevant because you might accidentally forget to include a conventional commit header with a commit which will prevent the later automation from knowing how to update the semantic version.</p><p>Then configure your repository with a GitHub Action that uses <a href="https://github.com/amannn/action-semantic-pull-request">amannn/action-semantic-pull-request</a>. The README has the workflow that I pretty much copy paste into my projects.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!wlE6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F87bce20e-069e-454e-a7fa-db406a0b9608_1378x1108.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!wlE6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F87bce20e-069e-454e-a7fa-db406a0b9608_1378x1108.png 424w, https://substackcdn.com/image/fetch/$s_!wlE6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F87bce20e-069e-454e-a7fa-db406a0b9608_1378x1108.png 848w, https://substackcdn.com/image/fetch/$s_!wlE6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F87bce20e-069e-454e-a7fa-db406a0b9608_1378x1108.png 1272w, https://substackcdn.com/image/fetch/$s_!wlE6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F87bce20e-069e-454e-a7fa-db406a0b9608_1378x1108.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!wlE6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F87bce20e-069e-454e-a7fa-db406a0b9608_1378x1108.png" width="1378" height="1108" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/87bce20e-069e-454e-a7fa-db406a0b9608_1378x1108.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1108,&quot;width&quot;:1378,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:177247,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!wlE6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F87bce20e-069e-454e-a7fa-db406a0b9608_1378x1108.png 424w, https://substackcdn.com/image/fetch/$s_!wlE6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F87bce20e-069e-454e-a7fa-db406a0b9608_1378x1108.png 848w, https://substackcdn.com/image/fetch/$s_!wlE6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F87bce20e-069e-454e-a7fa-db406a0b9608_1378x1108.png 1272w, https://substackcdn.com/image/fetch/$s_!wlE6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F87bce20e-069e-454e-a7fa-db406a0b9608_1378x1108.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">https://github.com/amannn/action-semantic-pull-request#installation</figcaption></figure></div><p>This should help ensure that you always keep a <a href="https://www.conventionalcommits.org/en/v1.0.0/">conventional commit</a> header in your PRs. <a href="https://www.conventionalcommits.org/en/v1.0.0/">Conventional commits</a> are a specification that maps directly to knowing how you should change the numbers that are part of your semantic version. You can prefix a commit with something like <code>feat:</code> which would correspond to a minor version bump, or <code>fix:</code> which would correspond to a patch version bump.</p><p>Next, you want to configure <a href="https://github.com/semantic-release/semantic-release">semantic-release</a> which is primarily a tool for automatically building and publishing packages to NPM, but with a little bit of configuration, it ends up being an amazing tool for general release management.</p><p>semantic-release has a <a href="https://semantic-release.gitbook.io/semantic-release/extending/plugins-list">large list of available plugins</a> which will let you control what platform you publish to and what documentation to create.</p><p>I place the following content into a file called <code>.releaserc</code> in the root of my repositories:</p><pre><code>{
  "branches": ["main"],
  "plugins": [
    "@semantic-release/commit-analyzer",
    "@semantic-release/release-notes-generator",
    "@semantic-release/github"
  ]
}</code></pre><p>The default branch for semantic-release is still master, so I switch it to using main. And then I use the commit-analyzer, release-notes-generator, and github plugins.</p><p>The commit-analyzer is what will read our conventional commit headers, the release-notes-generator will create a changelog and the github plugin will make releases on GitHub, using the generated changelog for the release description and will comment on related issues and related pull requests. These plugins together can be used for any kind of project. GitHub Actions, docker containers, etc.</p><p>One of the reasons that I really appreciate using semantic-release is for the commenting features. When semantic-release is executed, a comment is posted back to the pull request when a PR is included in a release, and if the pull request mentions fixing an issue, the tool will also comment on the issue letting watchers know that the fix has been released.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!7Ot8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F656e352b-7df8-45cf-a830-2ded924da2ec_1307x385.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7Ot8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F656e352b-7df8-45cf-a830-2ded924da2ec_1307x385.png 424w, https://substackcdn.com/image/fetch/$s_!7Ot8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F656e352b-7df8-45cf-a830-2ded924da2ec_1307x385.png 848w, https://substackcdn.com/image/fetch/$s_!7Ot8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F656e352b-7df8-45cf-a830-2ded924da2ec_1307x385.png 1272w, https://substackcdn.com/image/fetch/$s_!7Ot8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F656e352b-7df8-45cf-a830-2ded924da2ec_1307x385.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!7Ot8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F656e352b-7df8-45cf-a830-2ded924da2ec_1307x385.png" width="1307" height="385" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/656e352b-7df8-45cf-a830-2ded924da2ec_1307x385.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:385,&quot;width&quot;:1307,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:63308,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!7Ot8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F656e352b-7df8-45cf-a830-2ded924da2ec_1307x385.png 424w, https://substackcdn.com/image/fetch/$s_!7Ot8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F656e352b-7df8-45cf-a830-2ded924da2ec_1307x385.png 848w, https://substackcdn.com/image/fetch/$s_!7Ot8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F656e352b-7df8-45cf-a830-2ded924da2ec_1307x385.png 1272w, https://substackcdn.com/image/fetch/$s_!7Ot8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F656e352b-7df8-45cf-a830-2ded924da2ec_1307x385.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Example of a comment from semantic-release</figcaption></figure></div><p>I love this feature for letting contributors know when their new features are available, or when bugs have been fixed. The contributors can instantly know which version they need to upgrade to to get the changes.</p><p>So how does this all get linked together? Let&#8217;s look at the release flow for <a href="https://github.com/abatilo/github-ratelimit-metrics">github-ratelimit-metrics</a>.</p><p>You can look at the <a href="https://github.com/abatilo/github-ratelimit-metrics/blob/5d4d6735cdb47877ee27e7626ea81166ff1af1af/.github/workflows/main.yaml">full workflow here</a> but since it&#8217;s large, we&#8217;ll break it down in this post.</p><p>Every workflow release job starts the same</p><pre><code>      - uses: actions/checkout@v3

      - name: Generate release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: npx semantic-release</code></pre><p>First we check out the repo so that we can get the <code>.releaserc</code>, and then immediately run <code>npx semantic-release</code>. GitHub Actions runners always have an LTS version of nodejs already installed which includes npm and npx. npx is a tool which will download and execute the latest version of an npm package. So executing <code>npx semantic-release </code>will immediately execute semantic-release which will automatically pick up the .releaserc. And we pass in the GITHUB_TOKEN from the workflow which has all of the permissions that it needs by default.</p><p>If there&#8217;s a new version warranted by the commits since the last time this workflow was ran, then semantic-release will publish that to the GitHub releases. In the case of a Docker based project, now we can fetch that version so that we can tag the container.</p><pre><code>      - name: Get Latest Release
        id: latest_version
        uses: abatilo/release-info-action@v1.3.1
        with:
          owner: ${{ github.repository_owner }}
          repo: ${{ github.event.repository.name }}

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Prep docker tag
        uses: docker/metadata-action@v3
        id: metadata
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=raw,value=${{ steps.latest_version.outputs.latest_tag }}</code></pre><p>So let&#8217;s fetch the latest release version for this repo, and then we&#8217;ll pass the output of the latest tag into the <a href="https://github.com/docker/metadata-action">docker/metadata-action</a> action.</p><pre><code>      - name: Set docker cache tags
        uses: int128/docker-build-cache-config-action@v1
        id: cache
        with:
          image: ghcr.io/${{ github.repository }}/cache

      - uses: docker/build-push-action@v2
        id: build
        with:
          push: ${{ github.event_name == 'push' &amp;&amp; github.ref == 'refs/heads/main' }}
          tags: ${{ steps.metadata.outputs.tags }}
          labels: ${{ steps.metadata.outputs.labels }}
          platforms: linux/amd64,linux/arm64
          cache-from: ${{ steps.cache.outputs.cache-from }}
          cache-to: ${{ steps.cache.outputs.cache-to }}</code></pre><p>Then we can build our container using the tags and the container will get pushed.</p><p>And that&#8217;s all it takes! If you&#8217;re releasing a GitHub Action, you can skip all the parts about building a docker container. If you&#8217;re releasing a cross compiled go binary, you can run something like the <a href="https://github.com/goreleaser/goreleaser-action">goreleaser-action</a> directly after the semantic-release step, and goreleaser will actually edit the release created by semantic-release with all of the compiled binaries. This way, you get all the comment automation and changelog automation from semantic-release and still get all of the binary compilation from goreleaser all on the same release.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[How we count GitHub API requests]]></title><description><![CDATA[If you talk too much, GitHub will shut you up]]></description><link>https://www.sliceofexperiments.com/p/monitor-your-github-app-rate-limits</link><guid isPermaLink="false">https://www.sliceofexperiments.com/p/monitor-your-github-app-rate-limits</guid><dc:creator><![CDATA[Aaron Batilo]]></dc:creator><pubDate>Tue, 08 Nov 2022 21:15:45 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!6l5-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2e26649-0ecc-4a63-bd2f-92eaa98036c0_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6l5-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2e26649-0ecc-4a63-bd2f-92eaa98036c0_1024x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6l5-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2e26649-0ecc-4a63-bd2f-92eaa98036c0_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!6l5-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2e26649-0ecc-4a63-bd2f-92eaa98036c0_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!6l5-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2e26649-0ecc-4a63-bd2f-92eaa98036c0_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!6l5-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2e26649-0ecc-4a63-bd2f-92eaa98036c0_1024x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6l5-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2e26649-0ecc-4a63-bd2f-92eaa98036c0_1024x1024.png" width="1024" height="1024" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/a2e26649-0ecc-4a63-bd2f-92eaa98036c0_1024x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1024,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:782254,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!6l5-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2e26649-0ecc-4a63-bd2f-92eaa98036c0_1024x1024.png 424w, https://substackcdn.com/image/fetch/$s_!6l5-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2e26649-0ecc-4a63-bd2f-92eaa98036c0_1024x1024.png 848w, https://substackcdn.com/image/fetch/$s_!6l5-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2e26649-0ecc-4a63-bd2f-92eaa98036c0_1024x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!6l5-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2e26649-0ecc-4a63-bd2f-92eaa98036c0_1024x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Octocat holding too many books, generated by DALLE-2</figcaption></figure></div><p>At work we&#8217;ve been building a lot of GitHub automation for our monorepo. My team is in charge of a lot of infrastructure and tooling, and we&#8217;re always trying to find ways to leverage automation to making our product engineers more productive. That being said, we&#8217;ve recently run into the problem where the single <a href="https://docs.github.com/en/developers/apps/getting-started-with-apps/about-apps">GitHub App</a> that we&#8217;ve been doing most of our automation under, has been hitting its <a href="https://docs.github.com/en/developers/apps/building-github-apps/rate-limits-for-github-apps#server-to-server-rate-limits-for-github-enterprise-cloud">15,000 requests per hour rate limit</a>. For both security reasons and these rate limit reasons, we&#8217;ve started creating multiple GitHub Apps with different responsibilities.</p><p>The first time we hit a rate limit, it was extremely disruptive to our eng org. A lot of the automation that run are CI checks on pull requests, and many of them are required to pass before someone can merge in a pull request. Hitting the rate limits left dozens of our engineers unable to merge and ship code, and we didn&#8217;t have any proactive monitoring for this use case. I did some looking around and was surprised to find out that there actually wasn&#8217;t really much available for this as a ready to use solution.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>Another option that I came across would be to create a GitHub Action workflow where you print out your current rate limit status. You could have a workflow like below but it would be really hard to alert on that.</p><div class="github-gist" data-attrs="{&quot;innerHTML&quot;:&quot;<div id=\&quot;gist118769537\&quot; class=\&quot;gist\&quot;>\n    <div class=\&quot;gist-file\&quot; translate=\&quot;no\&quot;>\n      <div class=\&quot;gist-data\&quot;>\n        <div class=\&quot;js-gist-file-update-container js-task-list-container file-box\&quot;>\n  <div id=\&quot;file-ratelimit-yml\&quot; class=\&quot;file my-2\&quot;>\n    \n    <div itemprop=\&quot;text\&quot; class=\&quot;Box-body p-0 blob-wrapper data type-yaml  \&quot;>\n\n        \n<div class=\&quot;js-check-bidi js-blob-code-container blob-code-content\&quot;>\n\n  <template class=\&quot;js-file-alert-template\&quot;>\n  <div data-view-component=\&quot;true\&quot; class=\&quot;flash flash-warn flash-full d-flex flex-items-center\&quot;>\n  <svg aria-hidden=\&quot;true\&quot; height=\&quot;16\&quot; viewBox=\&quot;0 0 16 16\&quot; version=\&quot;1.1\&quot; width=\&quot;16\&quot; data-view-component=\&quot;true\&quot; class=\&quot;octicon octicon-alert\&quot;>\n    <path fill-rule=\&quot;evenodd\&quot; d=\&quot;M8.22 1.754a.25.25 0 00-.44 0L1.698 13.132a.25.25 0 00.22.368h12.164a.25.25 0 00.22-.368L8.22 1.754zm-1.763-.707c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0114.082 15H1.918a1.75 1.75 0 01-1.543-2.575L6.457 1.047zM9 11a1 1 0 11-2 0 1 1 0 012 0zm-.25-5.25a.75.75 0 00-1.5 0v2.5a.75.75 0 001.5 0v-2.5z\&quot;></path>\n</svg>\n  \n    <span>\n      This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.\n      <a href=\&quot;https://github.co/hiddenchars\&quot; target=\&quot;_blank\&quot;>Learn more about bidirectional Unicode characters</a>\n    </span>\n\n\n  <div data-view-component=\&quot;true\&quot; class=\&quot;flash-action\&quot;>        <a href=\&quot;{{ revealButtonHref }}\&quot; data-view-component=\&quot;true\&quot; class=\&quot;btn-sm btn\&quot;>    Show hidden characters\n</a>\n</div>\n</div></template>\n<template class=\&quot;js-line-alert-template\&quot;>\n  <span aria-label=\&quot;This line has hidden Unicode characters\&quot; data-view-component=\&quot;true\&quot; class=\&quot;line-alert tooltipped tooltipped-e\&quot;>\n    <svg aria-hidden=\&quot;true\&quot; height=\&quot;16\&quot; viewBox=\&quot;0 0 16 16\&quot; version=\&quot;1.1\&quot; width=\&quot;16\&quot; data-view-component=\&quot;true\&quot; class=\&quot;octicon octicon-alert\&quot;>\n    <path fill-rule=\&quot;evenodd\&quot; d=\&quot;M8.22 1.754a.25.25 0 00-.44 0L1.698 13.132a.25.25 0 00.22.368h12.164a.25.25 0 00.22-.368L8.22 1.754zm-1.763-.707c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0114.082 15H1.918a1.75 1.75 0 01-1.543-2.575L6.457 1.047zM9 11a1 1 0 11-2 0 1 1 0 012 0zm-.25-5.25a.75.75 0 00-1.5 0v2.5a.75.75 0 001.5 0v-2.5z\&quot;></path>\n</svg>\n</span></template>\n\n  <table data-hpc class=\&quot;highlight tab-size js-file-line-container js-code-nav-container js-tagsearch-file\&quot; data-tab-size=\&quot;8\&quot; data-paste-markdown-skip data-tagsearch-lang=\&quot;YAML\&quot; data-tagsearch-path=\&quot;ratelimit.yml\&quot;>\n        <tr>\n          <td id=\&quot;file-ratelimit-yml-L1\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;1\&quot;></td>\n          <td id=\&quot;file-ratelimit-yml-LC1\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-ent\&quot;>name</span>: <span class=\&quot;pl-s\&quot;>Print rate limits</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-ratelimit-yml-L2\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;2\&quot;></td>\n          <td id=\&quot;file-ratelimit-yml-LC2\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-ent\&quot;>on</span>:</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-ratelimit-yml-L3\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;3\&quot;></td>\n          <td id=\&quot;file-ratelimit-yml-LC3\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  <span class=\&quot;pl-ent\&quot;>push</span>:</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-ratelimit-yml-L4\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;4\&quot;></td>\n          <td id=\&quot;file-ratelimit-yml-LC4\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-ratelimit-yml-L5\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;5\&quot;></td>\n          <td id=\&quot;file-ratelimit-yml-LC5\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-ent\&quot;>jobs</span>:</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-ratelimit-yml-L6\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;6\&quot;></td>\n          <td id=\&quot;file-ratelimit-yml-LC6\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>  <span class=\&quot;pl-ent\&quot;>rate-limit</span>:</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-ratelimit-yml-L7\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;7\&quot;></td>\n          <td id=\&quot;file-ratelimit-yml-LC7\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    <span class=\&quot;pl-ent\&quot;>runs-on</span>: <span class=\&quot;pl-s\&quot;>ubuntu-latest</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-ratelimit-yml-L8\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;8\&quot;></td>\n          <td id=\&quot;file-ratelimit-yml-LC8\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>    <span class=\&quot;pl-ent\&quot;>steps</span>:</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-ratelimit-yml-L9\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;9\&quot;></td>\n          <td id=\&quot;file-ratelimit-yml-LC9\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>      - <span class=\&quot;pl-ent\&quot;>name</span>: <span class=\&quot;pl-s\&quot;>Generate token</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-ratelimit-yml-L10\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;10\&quot;></td>\n          <td id=\&quot;file-ratelimit-yml-LC10\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>        <span class=\&quot;pl-ent\&quot;>id</span>: <span class=\&quot;pl-s\&quot;>generate_token</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-ratelimit-yml-L11\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;11\&quot;></td>\n          <td id=\&quot;file-ratelimit-yml-LC11\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>        <span class=\&quot;pl-ent\&quot;>uses</span>: <span class=\&quot;pl-s\&quot;>tibdex/github-app-token@v1</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-ratelimit-yml-L12\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;12\&quot;></td>\n          <td id=\&quot;file-ratelimit-yml-LC12\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>        <span class=\&quot;pl-ent\&quot;>with</span>:</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-ratelimit-yml-L13\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;13\&quot;></td>\n          <td id=\&quot;file-ratelimit-yml-LC13\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>          <span class=\&quot;pl-ent\&quot;>app_id</span>: <span class=\&quot;pl-s\&quot;>${{ secrets.APP_ID }}</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-ratelimit-yml-L14\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;14\&quot;></td>\n          <td id=\&quot;file-ratelimit-yml-LC14\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>          <span class=\&quot;pl-ent\&quot;>private_key</span>: <span class=\&quot;pl-s\&quot;>${{ secrets.PRIVATE_KEY }}</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-ratelimit-yml-L15\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;15\&quot;></td>\n          <td id=\&quot;file-ratelimit-yml-LC15\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>\n</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-ratelimit-yml-L16\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;16\&quot;></td>\n          <td id=\&quot;file-ratelimit-yml-LC16\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>      - <span class=\&quot;pl-ent\&quot;>env</span>:</td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-ratelimit-yml-L17\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;17\&quot;></td>\n          <td id=\&quot;file-ratelimit-yml-LC17\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>          <span class=\&quot;pl-ent\&quot;>GITHUB_TOKEN</span>: <span class=\&quot;pl-s\&quot;>${{ steps.generate_token.outputs.token }}</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-ratelimit-yml-L18\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;18\&quot;></td>\n          <td id=\&quot;file-ratelimit-yml-LC18\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;>        <span class=\&quot;pl-ent\&quot;>run</span>: <span class=\&quot;pl-s\&quot;>|</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-ratelimit-yml-L19\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;19\&quot;></td>\n          <td id=\&quot;file-ratelimit-yml-LC19\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-s\&quot;>          curl \\</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-ratelimit-yml-L20\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;20\&quot;></td>\n          <td id=\&quot;file-ratelimit-yml-LC20\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-s\&quot;>            -H &amp;quot;Accept: application/vnd.github+json&amp;quot; \\</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-ratelimit-yml-L21\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;21\&quot;></td>\n          <td id=\&quot;file-ratelimit-yml-LC21\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-s\&quot;>            -H &amp;quot;Authorization: Bearer $GITHUB_TOKEN&amp;quot; \\</span></td>\n        </tr>\n        <tr>\n          <td id=\&quot;file-ratelimit-yml-L22\&quot; class=\&quot;blob-num js-line-number js-code-nav-line-number js-blob-rnum\&quot; data-line-number=\&quot;22\&quot;></td>\n          <td id=\&quot;file-ratelimit-yml-LC22\&quot; class=\&quot;blob-code blob-code-inner js-file-line\&quot;><span class=\&quot;pl-s\&quot;>            https://api.github.com/rate_limit | jq &amp;#39;.&amp;#39;</span></td>\n        </tr>\n  </table>\n</div>\n\n\n    </div>\n\n  </div>\n</div>\n\n      </div>\n      <div class=\&quot;gist-meta\&quot;>\n        <a href=\&quot;https://gist.github.com/abatilo/e9ba9fe74e90533411d343013490ac92/raw/461af5825a482a085a054524fe4fdbd6d9627fe0/ratelimit.yml\&quot; style=\&quot;float:right\&quot;>view raw</a>\n        <a href=\&quot;https://gist.github.com/abatilo/e9ba9fe74e90533411d343013490ac92#file-ratelimit-yml\&quot;>\n          ratelimit.yml\n        </a>\n        hosted with &amp;#10084; by <a href=\&quot;https://github.com\&quot;>GitHub</a>\n      </div>\n    </div>\n</div>\n&quot;,&quot;stylesheet&quot;:&quot;https://github.githubassets.com/assets/gist-embed-ee182801b10b.css&quot;}" data-component-name="GitgistToDOM"><link rel="stylesheet" href="https://github.githubassets.com/assets/gist-embed-ee182801b10b.css"><div id="gist118769537" class="gist">
    <div class="gist-file">
      <div class="gist-data">
        <div class="js-gist-file-update-container js-task-list-container file-box">
  <div id="file-ratelimit-yml" class="file my-2">
    
    <div itemprop="text" class="Box-body p-0 blob-wrapper data type-yaml  ">

        
<div class="js-check-bidi js-blob-code-container blob-code-content">

  
  <div data-view-component="true" class="flash flash-warn flash-full d-flex flex-items-center">
  
    

  
    <span>
      This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      <a href="https://github.co/hiddenchars" target="_blank">Learn more about bidirectional Unicode characters</a>
    </span>


  <div data-view-component="true" class="flash-action">        <a href="{{ revealButtonHref }}" data-view-component="true" class="btn-sm btn">    Show hidden characters
</a>
</div>
</div>

  <span data-view-component="true" class="line-alert tooltipped tooltipped-e">
    
    

</span>

  <table data-hpc="" class="highlight tab-size js-file-line-container js-code-nav-container js-tagsearch-file" data-tab-size="8" data-paste-markdown-skip="" data-tagsearch-lang="YAML" data-tagsearch-path="ratelimit.yml">
        <tbody><tr>
          <td id="file-ratelimit-yml-L1" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="1"></td>
          <td id="file-ratelimit-yml-LC1" class="blob-code blob-code-inner js-file-line"><span class="pl-ent">name</span>: <span class="pl-s">Print rate limits</span></td>
        </tr>
        <tr>
          <td id="file-ratelimit-yml-L2" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="2"></td>
          <td id="file-ratelimit-yml-LC2" class="blob-code blob-code-inner js-file-line"><span class="pl-ent">on</span>:</td>
        </tr>
        <tr>
          <td id="file-ratelimit-yml-L3" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="3"></td>
          <td id="file-ratelimit-yml-LC3" class="blob-code blob-code-inner js-file-line">  <span class="pl-ent">push</span>:</td>
        </tr>
        <tr>
          <td id="file-ratelimit-yml-L4" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="4"></td>
          <td id="file-ratelimit-yml-LC4" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-ratelimit-yml-L5" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="5"></td>
          <td id="file-ratelimit-yml-LC5" class="blob-code blob-code-inner js-file-line"><span class="pl-ent">jobs</span>:</td>
        </tr>
        <tr>
          <td id="file-ratelimit-yml-L6" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="6"></td>
          <td id="file-ratelimit-yml-LC6" class="blob-code blob-code-inner js-file-line">  <span class="pl-ent">rate-limit</span>:</td>
        </tr>
        <tr>
          <td id="file-ratelimit-yml-L7" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="7"></td>
          <td id="file-ratelimit-yml-LC7" class="blob-code blob-code-inner js-file-line">    <span class="pl-ent">runs-on</span>: <span class="pl-s">ubuntu-latest</span></td>
        </tr>
        <tr>
          <td id="file-ratelimit-yml-L8" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="8"></td>
          <td id="file-ratelimit-yml-LC8" class="blob-code blob-code-inner js-file-line">    <span class="pl-ent">steps</span>:</td>
        </tr>
        <tr>
          <td id="file-ratelimit-yml-L9" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="9"></td>
          <td id="file-ratelimit-yml-LC9" class="blob-code blob-code-inner js-file-line">      - <span class="pl-ent">name</span>: <span class="pl-s">Generate token</span></td>
        </tr>
        <tr>
          <td id="file-ratelimit-yml-L10" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="10"></td>
          <td id="file-ratelimit-yml-LC10" class="blob-code blob-code-inner js-file-line">        <span class="pl-ent">id</span>: <span class="pl-s">generate_token</span></td>
        </tr>
        <tr>
          <td id="file-ratelimit-yml-L11" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="11"></td>
          <td id="file-ratelimit-yml-LC11" class="blob-code blob-code-inner js-file-line">        <span class="pl-ent">uses</span>: <span class="pl-s">tibdex/github-app-token@v1</span></td>
        </tr>
        <tr>
          <td id="file-ratelimit-yml-L12" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="12"></td>
          <td id="file-ratelimit-yml-LC12" class="blob-code blob-code-inner js-file-line">        <span class="pl-ent">with</span>:</td>
        </tr>
        <tr>
          <td id="file-ratelimit-yml-L13" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="13"></td>
          <td id="file-ratelimit-yml-LC13" class="blob-code blob-code-inner js-file-line">          <span class="pl-ent">app_id</span>: <span class="pl-s">${{ secrets.APP_ID }}</span></td>
        </tr>
        <tr>
          <td id="file-ratelimit-yml-L14" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="14"></td>
          <td id="file-ratelimit-yml-LC14" class="blob-code blob-code-inner js-file-line">          <span class="pl-ent">private_key</span>: <span class="pl-s">${{ secrets.PRIVATE_KEY }}</span></td>
        </tr>
        <tr>
          <td id="file-ratelimit-yml-L15" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="15"></td>
          <td id="file-ratelimit-yml-LC15" class="blob-code blob-code-inner js-file-line">
</td>
        </tr>
        <tr>
          <td id="file-ratelimit-yml-L16" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="16"></td>
          <td id="file-ratelimit-yml-LC16" class="blob-code blob-code-inner js-file-line">      - <span class="pl-ent">env</span>:</td>
        </tr>
        <tr>
          <td id="file-ratelimit-yml-L17" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="17"></td>
          <td id="file-ratelimit-yml-LC17" class="blob-code blob-code-inner js-file-line">          <span class="pl-ent">GITHUB_TOKEN</span>: <span class="pl-s">${{ steps.generate_token.outputs.token }}</span></td>
        </tr>
        <tr>
          <td id="file-ratelimit-yml-L18" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="18"></td>
          <td id="file-ratelimit-yml-LC18" class="blob-code blob-code-inner js-file-line">        <span class="pl-ent">run</span>: <span class="pl-s">|</span></td>
        </tr>
        <tr>
          <td id="file-ratelimit-yml-L19" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="19"></td>
          <td id="file-ratelimit-yml-LC19" class="blob-code blob-code-inner js-file-line"><span class="pl-s">          curl \</span></td>
        </tr>
        <tr>
          <td id="file-ratelimit-yml-L20" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="20"></td>
          <td id="file-ratelimit-yml-LC20" class="blob-code blob-code-inner js-file-line"><span class="pl-s">            -H "Accept: application/vnd.github+json" \</span></td>
        </tr>
        <tr>
          <td id="file-ratelimit-yml-L21" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="21"></td>
          <td id="file-ratelimit-yml-LC21" class="blob-code blob-code-inner js-file-line"><span class="pl-s">            -H "Authorization: Bearer $GITHUB_TOKEN" \</span></td>
        </tr>
        <tr>
          <td id="file-ratelimit-yml-L22" class="blob-num js-line-number js-code-nav-line-number js-blob-rnum" data-line-number="22"></td>
          <td id="file-ratelimit-yml-LC22" class="blob-code blob-code-inner js-file-line"><span class="pl-s">            https://api.github.com/rate_limit | jq '.'</span></td>
        </tr>
  </tbody></table>
</div>


    </div>

  </div>
</div>

      </div>
      <div class="gist-meta">
        <a href="https://gist.github.com/abatilo/e9ba9fe74e90533411d343013490ac92/raw/461af5825a482a085a054524fe4fdbd6d9627fe0/ratelimit.yml" style="float:right">view raw</a>
        <a href="https://gist.github.com/abatilo/e9ba9fe74e90533411d343013490ac92#file-ratelimit-yml">
          ratelimit.yml
        </a>
        hosted with &#10084; by <a href="https://github.com">GitHub</a>
      </div>
    </div>
</div>
</div><p>The <a href="https://sbulav.github.io/monitoring/monitoring-github-rate-limits/">next post</a> that I came across outlines a python script that you can run to expose the metrics as prometheus metrics, but it only gives you snippets. There&#8217;s no fully working project that you can use. But this project is pretty close to something we could have used.</p><p>Moreover, there&#8217;s no way to get the rate limits on behalf of another entity. So if you want to get the remaining number of requests for a GitHub App, you need to authenticate as that app and fetch an installation token that&#8217;s specific to that app&#8217;s installation to your repo.</p><p>Fortunately, you can get remaining requests either from parsing HTTP headers but there&#8217;s also a <a href="https://docs.github.com/en/rest/rate-limit">dedicated rate limit endpoint</a> which is what we used in the example workflow. Having a dedicated endpoint is nice since that means we don&#8217;t have to have every single use case of an app&#8217;s credentials independently report on its remaining headers.</p><p>In comes my latest project <a href="https://github.com/abatilo/github-ratelimit-metrics">github-ratelimit-metrics</a>. A web server that exports remaining limits as a prometheus metric, very similar to the python scripts mentioned in the blog post I linked above. With a configurable interval, it&#8217;ll check rate limits and update the prometheus gauges with the latest values. Along with the project itself, I&#8217;ve put together a <a href="https://github.com/abatilo/charts/tree/main/charts/github-ratelimit-metrics">helm chart for the project</a> to make it easy to deploy in Kubernetes environments, but since <a href="https://github.com/abatilo/github-ratelimit-metrics">github-ratelimit-metrics</a> is actually a go static binary, it&#8217;s both very easy to run just as a binary, and also is already built and published for both linux/amd64 and linux/arm64 architecture compatible docker containers.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.sliceofexperiments.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading A slice of experiments!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>