<?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:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[William Lee]]></title><description><![CDATA[Good Vibes - Senior Full Stack Web Developer]]></description><link>https://william-lee.com/</link><image><url>https://william-lee.com/favicon.png</url><title>William Lee</title><link>https://william-lee.com/</link></image><generator>Ghost 5.62</generator><lastBuildDate>Fri, 10 Apr 2026 11:58:34 GMT</lastBuildDate><atom:link href="https://william-lee.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[You only push once - Cross Region Replication for AWS ECR]]></title><description><![CDATA[Why setting up Cross Region Replication in AWS ECR is worth exploring, with examples of common Docker -> ECR patterns.]]></description><link>https://william-lee.com/you-only-push-once-ecr-crr/</link><guid isPermaLink="false">66a9bde481fde50137f0f1fd</guid><category><![CDATA[DevOps]]></category><category><![CDATA[DevEx]]></category><category><![CDATA[ECR]]></category><category><![CDATA[AWS]]></category><dc:creator><![CDATA[William Lee]]></dc:creator><pubDate>Wed, 31 Jul 2024 06:00:00 GMT</pubDate><content:encoded><![CDATA[<p>It&apos;s a common pattern where you need to push your docker image and make it readily available to multiple servers in multiple regions.</p>
<h2 id="patterns">Patterns</h2>
<p>Over time i&apos;ve seen the following patterns take shape:</p>
<h3 id="pattern-1-single-imagemultiple-ecr-per-region">Pattern 1: Single Image/Multiple ECR per Region</h3>
<figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://william-lee.com/content/images/2024/07/image-3.png" class="kg-image" alt loading="lazy" width="657" height="675" srcset="https://william-lee.com/content/images/size/w600/2024/07/image-3.png 600w, https://william-lee.com/content/images/2024/07/image-3.png 657w"><figcaption><span style="white-space: pre-wrap;">Illustration of docker image pushed to each region&apos;s ECR - which is read by servers</span></figcaption></figure>
<p>Pros: </p>
<ul><li>Fast instance creation times - Each server instance has <strong>fast</strong> network access to their same region ECR.</li><li>Explicit Architecture.</li></ul>
<p>Cons:</p>
<ul><li>DevOps Maintenance <ul><li>Must write code that is concerned about pushing to each region.</li><li>More moving parts/mental load.</li><li>More error prone - Need to retry and babysit the deploy if the push to one region is disrupted.</li></ul></li><li>Cost - Per region ECR billing (1 USD a month as of writing per ECR).</li></ul>
<h3 id="pattern-2-single-imagesingle-region-ecr-source">Pattern 2: Single Image/Single Region ECR Source</h3>
<figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://william-lee.com/content/images/2024/07/image-5.png" class="kg-image" alt loading="lazy" width="657" height="675" srcset="https://william-lee.com/content/images/size/w600/2024/07/image-5.png 600w, https://william-lee.com/content/images/2024/07/image-5.png 657w"><figcaption><span style="white-space: pre-wrap;">Illustration of docker image being pushed to a single Region ECR and other regions referencing the image</span></figcaption></figure>
<p>Pros:</p>
<ul><li>Simpler DevOps/Mental load: Docker image is only pushed to a single ECR.</li><li>Cheaper: Single ECR to manage.</li></ul>
<p>Cons:</p>
<ul><li>Slower instance creation times: Due to geographical constraints affecting network speeds - instances will take longer to create/spin up. This will be more evident when autoscaling latency is crucial.</li></ul>
<h3 id="conclusion">Conclusion</h3>
<p><strong>Pattern 1</strong> has the advantage of quick instance creation from a docker definition due geographical redundancy, but has too many moving parts from a DevOps experience perspective</p>
<p><strong>Pattern 2</strong> from a DevOps perspective is simple to grok. However the lack of geographical redundancy might be a deal breaker  - imagine if tasks in the UK region need to scale up quickly - but need to fetch the docker definition from the source region in Australia.</p>
<h2 id="single-ecrcrrthe-best-of-all-patterns">Single ECR/CRR - The best of all patterns</h2>
<p>This approach takes the <strong>best of</strong> the solutions we explored above - and leverages <a href="https://aws.amazon.com/blogs/containers/cross-region-replication-in-amazon-ecr-has-landed/?ref=william-lee.com">AWS Cross Region Replication</a> to fill in the cracks.</p>
<p>It&apos;s important to grok that it&apos;s a <a href="https://aws.amazon.com/blogs/containers/cross-region-replication-in-amazon-ecr-has-landed/?ref=william-lee.com">per region</a> setting (for private and public repositories) thus we need to filter ECRs via name.</p>
<figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://william-lee.com/content/images/2024/07/image-7.png" class="kg-image" alt loading="lazy" width="657" height="675" srcset="https://william-lee.com/content/images/size/w600/2024/07/image-7.png 600w, https://william-lee.com/content/images/2024/07/image-7.png 657w"><figcaption><span style="white-space: pre-wrap;">Illustration of docker image being pushed into a single source ECR - and Cross Region Replication copying it automatically to other regions</span></figcaption></figure>
<p>Pros:</p>
<ul><li>Less IaC /Easy to grok (Single Region/ECR push).</li><li>Free: Just pay for ECR storage costs - and uses the AWS backbone for network transfer/replication,</li><li>Fast Replication: I personally found It takes around 15 seconds for an image to be available on the other side of the world for a 500mb image.</li><li>Supports Cross Account replication<ul><li>Great for Disaster Recovery.</li><li>Great for multiple environments (i.e develop and production),</li></ul></li><li>No need to manually create corresponding ECR in other regions - CRR creates them for you (although as outlined below - if using IaC, it&apos;s recommended to create them first before enabling CRR).</li><li>ECS handles that slight delay in replication by re-querying ECR until the image is there. </li></ul>
<p>Cons</p>
<ul><li>CRR happens behind the scenes so finding the originating region must be found in the terraform or in the AWS console by inspecting the settings of the repo. </li></ul>
<h3 id="iac-gotchas">IaC gotchas</h3>
<ul><li>It&apos;s better to explicitly create the ECRs in each region required first and then apply CRR. <ul><li>This is because IaC may become confused and try create ECRs in the other regions for you - when you have to reference them once again (e.g referencing them for deployment code). </li></ul></li><li>Remember that CRR is a <strong>per region</strong> setting - and you should specify CRR infrastructure code in one place.<ul><li>I was caught out where we had CRR IaC code in two repositories. This creates an unfortunate situation where each repository&apos;s deployments overrode the CRR settings of the other.</li></ul></li></ul>
<h3 id="takeaway-notes">Takeaway notes</h3>
<p>It&apos;s definitely worth exploring CRR!</p>
<ul><li>We found our build times were cut by 20 minutes (billable) - In our case this includes an image build for each and every region (and finally pushing).</li><li>Improved DevOps Experience - we didn&apos;t have to worry about each individual region painstakingly - or worry about network issues breaking things requiring a retry - we only push once - and the image becomes available in every ECR/region we required.</li><li>Reduced lines of code relating to multiple regions (only need to reference the source region).</li></ul>]]></content:encoded></item><item><title><![CDATA[Improve Integration Tests With GitHub Action Service Containers]]></title><description><![CDATA[We explore how GitHub Action Services can improve integration tests, have better performance than docker layer caching, cut build times, and are better for parallel tests that run on seperate runners.]]></description><link>https://william-lee.com/improve-integration-tests-with-github-action-service-containers/</link><guid isPermaLink="false">64fd72ba7421730107ad3e93</guid><category><![CDATA[GitHub Actions]]></category><category><![CDATA[Cost Saving]]></category><category><![CDATA[DevEx]]></category><category><![CDATA[DevOps]]></category><dc:creator><![CDATA[William Lee]]></dc:creator><pubDate>Mon, 11 Sep 2023 11:45:08 GMT</pubDate><content:encoded><![CDATA[<h2 id="setting-the-scene">Setting The Scene</h2>
<p>Typically  integration test suites require a set of temporary docker services to connect with such as a relational database, or some caching service.</p>
<figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://william-lee.com/content/images/2023/09/integration-test-deps-1-.svg" class="kg-image" alt="Integration Test Suite/Services Architecture" loading="lazy" width="625" height="505"><figcaption><span style="white-space: pre-wrap;">Integration Test Suite/Services Architecture</span></figcaption></figure>
<p>We tend to set up services  via a docker-compose.yml file - so integration tests can have services exposed to them and be torn down easily. </p>
<h2 id="how-can-we-improve-on-this">How Can We Improve On This?</h2>
<p>We can explore <a href="https://docs.github.com/en/actions/using-containerized-services/about-service-containers?ref=william-lee.com">GitHub service containers</a>! </p>
<p>They allows us to specify docker images and which ports to expose to the github runner that is running your test suite. The syntax is almost identical to a docker compose yml file. The only differences I found were declaring <code>env</code> variables and other nuances such as specifying bespoke health check parameters.</p>
<h2 id="why-use-github-action-service-containers">Why Use Github Action Service Containers?</h2>
<ul><li>No need to write you own health check scripts </li></ul>
<p>GitHub Actions does this for you.</p>
<figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://william-lee.com/content/images/2023/09/health-checks.png" class="kg-image" alt loading="lazy" width="2000" height="188" srcset="https://william-lee.com/content/images/size/w600/2023/09/health-checks.png 600w, https://william-lee.com/content/images/size/w1000/2023/09/health-checks.png 1000w, https://william-lee.com/content/images/size/w1600/2023/09/health-checks.png 1600w, https://william-lee.com/content/images/size/w2400/2023/09/health-checks.png 2400w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Screenshot of included health checks</span></figcaption></figure>
<p>Some services such as elastic search offer extra health check options which you specify under <code>env</code></p>
<pre><code class="language-yaml">elasticsearch:
  image: docker.elastic.co/elasticsearch/elasticsearch:7.1.1
  ports:
    - 9200:9200
    - 9300:9300
  env:
    discovery.type: single-node
    options: &gt;-
      --health-cmd &quot;curl http://localhost:9200/_cluster/health&quot;
      --health-interval 10s
      --health-timeout 5s
      --health-retries 10</code></pre>
<ul><li> Cost/Time savings - I found they cut build times by around a minute</li></ul>
<p>Before (running <code>docker build</code>):</p>
<figure class="kg-card kg-image-card"><img src="https://william-lee.com/content/images/2023/09/before-time.jpg" class="kg-image" alt loading="lazy" width="2000" height="843" srcset="https://william-lee.com/content/images/size/w600/2023/09/before-time.jpg 600w, https://william-lee.com/content/images/size/w1000/2023/09/before-time.jpg 1000w, https://william-lee.com/content/images/size/w1600/2023/09/before-time.jpg 1600w, https://william-lee.com/content/images/size/w2400/2023/09/before-time.jpg 2400w" sizes="(min-width: 720px) 720px"></figure>
<p>After:</p>
<figure class="kg-card kg-image-card"><img src="https://william-lee.com/content/images/2023/09/CleanShot-2023-09-11-at-16.54.56@2x.png" class="kg-image" alt loading="lazy" width="2000" height="188" srcset="https://william-lee.com/content/images/size/w600/2023/09/CleanShot-2023-09-11-at-16.54.56@2x.png 600w, https://william-lee.com/content/images/size/w1000/2023/09/CleanShot-2023-09-11-at-16.54.56@2x.png 1000w, https://william-lee.com/content/images/size/w1600/2023/09/CleanShot-2023-09-11-at-16.54.56@2x.png 1600w, https://william-lee.com/content/images/size/w2400/2023/09/CleanShot-2023-09-11-at-16.54.56@2x.png 2400w" sizes="(min-width: 720px) 720px"></figure>
<ul><li>Faster than utilizing docker layer caching. </li></ul>
<p>I found using GitHub Actions Service Containers faster/more efficient than creating a job or <code>docker build</code> and then utilizing docker layer caching, such that subsequent jobs only need to fetch from the cache.</p>
<p>The reason being reading docker layers from the cache was far slower than the process GitHub Action Service Containers go through (sidenote: GitHub don&apos;t charge for ingress).</p>
<ul><li>Less set up for individual tests. </li></ul>
<p>If you&apos;re running your integration tests in a <a href="https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs?ref=william-lee.com">matrix/parallel </a>each individual test needs to set up services individually (added bonus: each test runs in their own runner so we don&apos;t have to worry about memory - we can retry &quot;just&quot; the test that failed if we have flakey tests too).</p>
<p>If this only takes a minute for each runner instead of  2 - we get compounded time saved and when running your integration tests in  parallel we&apos;re only as slow as our slowest test - so we save on total wait time.</p>
<h2 id="when-you-might-not-want-to-use-them">When You Might Not Want To Use Them:</h2>
<ul><li>Disparate testing processes between your build environment and local development environment </li></ul>
<p>Using a docker-compose.yml is same experience both locally and CI, we hard code versions and tags in our GitHub action workflow files  so they can become out of date/ might be a gotcha.</p>
<p>Could be negated if run your Github Actions locally using <a href="https://www.google.com/url?sa=t&amp;rct=j&amp;q=&amp;esrc=s&amp;source=web&amp;cd=&amp;cad=rja&amp;uact=8&amp;ved=2ahUKEwjglqfjyZ-BAxWVk1YBHaiVCNMQFnoECCUQAQ&amp;url=https%3A%2F%2Fgithub.com%2Fnektos%2Fact&amp;usg=AOvVaw0kac6X8bzUvQEaGkAVeFZQ&amp;opi=89978449">nektos/act</a> if you want local verification - but not always desirable.</p>
<ul><li>Two docker versions to keep track of - Local and GitHub Action workflow</li></ul>
<p>Again  -  Could be negated if run your Github Actions locally using <a href="https://www.google.com/url?sa=t&amp;rct=j&amp;q=&amp;esrc=s&amp;source=web&amp;cd=&amp;cad=rja&amp;uact=8&amp;ved=2ahUKEwjglqfjyZ-BAxWVk1YBHaiVCNMQFnoECCUQAQ&amp;url=https%3A%2F%2Fgithub.com%2Fnektos%2Fact&amp;usg=AOvVaw0kac6X8bzUvQEaGkAVeFZQ&amp;opi=89978449">nektos/act</a> if you want local verification - but not always desirable</p>
<ul><li>Larger/messier GitHub action workflows files.</li></ul>
<p>We can go from one single neat command to set everything up (i.e one single command which invokes all the necessary start up scripts and post health checks) to an inflated number of lines specifying the service definitions.</p>
<h2 id="summary">Summary</h2>
<p>I&apos;ve found GitHub Actions Service Containers to cut down service set up by around 50% in all of my cases. For situations where you need to run your tests in parallel and each task runner has to set  up their own services - the pros for using services containers outweight the cons. </p>
<p>The major cost in my opinion is having duplicate docker image definitions between local and in CI that must be manually synced. </p>
<p>We should be striving for faster build times and developer experience. Slow integration tests in CI are more common than you think. Running them in parallel means tests run in their own environment, ease of visibility in GitHub - as opposed to all tests running in on one runner and just seeing a sprawl of console logs and finally allow developers to easily retry just the test that failed as opposed to the entire suite  again.</p>]]></content:encoded></item></channel></rss>