<?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"><channel><title><![CDATA[Rahul's Loop]]></title><description><![CDATA[Rahul's Loop]]></description><link>https://rahulpoudel.com.np</link><generator>RSS for Node</generator><lastBuildDate>Wed, 13 May 2026 15:25:51 GMT</lastBuildDate><atom:link href="https://rahulpoudel.com.np/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Creating a Global Image CDN with AWS S3 and CloudFront]]></title><description><![CDATA[Pre-requisites: AWS CLI v2 Installed & Configured , 1 or more images for test ✅

TL;DR (What will we be doing?)

Create and secure an S3 bucket (with versioning, encryption, and public access block).

Upload images using the AWS CLI (aws s3 sync).

S...]]></description><link>https://rahulpoudel.com.np/creating-a-global-image-cdn-with-aws-s3-and-cloudfront</link><guid isPermaLink="true">https://rahulpoudel.com.np/creating-a-global-image-cdn-with-aws-s3-and-cloudfront</guid><category><![CDATA[CDN]]></category><category><![CDATA[cloudfront]]></category><category><![CDATA[AWS]]></category><category><![CDATA[ aws cli v2]]></category><dc:creator><![CDATA[Rahul S. Poudel]]></dc:creator><pubDate>Wed, 05 Feb 2025 18:39:05 GMT</pubDate><content:encoded><![CDATA[<blockquote>
<p>Pre-requisites: <strong>AWS CLI v2 Installed &amp; Configured</strong> , 1 or more images for test ✅</p>
</blockquote>
<h2 id="heading-tldr-what-will-we-be-doing"><strong>TL;DR (What will we be doing?)</strong></h2>
<ol>
<li><p><strong>Create and secure an S3 bucket</strong> (with versioning, encryption, and public access block).</p>
</li>
<li><p><strong>Upload images</strong> using the AWS CLI (<code>aws s3 sync</code>).</p>
</li>
<li><p><strong>Set up a CloudFront distribution</strong> with an Origin Access Identity (OAI) to securely serve images and configure caching policies.</p>
</li>
<li><p><strong>Secure S3 access</strong> with a bucket policy that restricts direct access, ensuring images are only served via CloudFront.</p>
</li>
<li><p><strong>Test the setup</strong> to verify cache hits/misses and overall performance.</p>
</li>
</ol>
<hr />
<h3 id="heading-what-is-a-cdn-and-why-use-it"><strong>What is a CDN and Why Use It?</strong></h3>
<ul>
<li><p>A network of globally distributed servers that cache static content (like images, CSS, JavaScript, etc).</p>
</li>
<li><p><strong>Benefits:</strong></p>
<ul>
<li><p><strong>Reduced Latency:</strong> Users receive content from the nearest edge location.</p>
</li>
<li><p><strong>Improved Performance:</strong> Faster load times and reduced load on our origin server (S3).</p>
</li>
<li><p><strong>Enhanced Security:</strong> Can help with DDoS protection and secure content delivery.</p>
</li>
</ul>
</li>
</ul>
<hr />
<h2 id="heading-step-1-create-an-s3-bucket"><strong>Step 1: Create an S3 Bucket</strong> 📦🌍</h2>
<p>Let’s start by creating two S3 buckets in different regions. Note that when creating a bucket in <strong>us-east-1</strong> (the default region), we do not need to specify a location constraint.</p>
<h3 id="heading-i-for-us-east-1"><strong>i) For</strong> <code>us-east-1</code><strong>:</strong></h3>
<h3 id="heading-command"><strong>Command:</strong></h3>
<pre><code class="lang-bash">aws s3api create-bucket --bucket image-bucket-2025 --region us-east-1
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"Location"</span>: <span class="hljs-string">"/image-bucket-2025"</span>
}
</code></pre>
<h3 id="heading-ii-for-us-west-2-and-other-regions"><strong>ii) For</strong> <code>us-west-2</code> <strong>and other regions:</strong></h3>
<h3 id="heading-command-1"><strong>Command:</strong></h3>
<pre><code class="lang-bash">aws s3api create-bucket --bucket image-bucket-west-2025 --region us-west-2 --create-bucket-configuration LocationConstraint=us-west-2
</code></pre>
<p><strong>Output (From (ii) just above) :</strong></p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"Location"</span>: <span class="hljs-string">"http://image-bucket-west-2025.s3.amazonaws.com/"</span>
}
</code></pre>
<p><strong>Handy Notes:</strong></p>
<ul>
<li><p>When creating a bucket in <strong>us-east-1</strong>, do not include the <code>--create-bucket-configuration LocationConstraint=us-east-1</code> parameter because this region is treated as the default and including these results <code>InvalidLocationConstraint</code> error.</p>
</li>
<li><p>For other regions (e.g., us-west-2), ensure both --region and --create-bucket-configuration LocationConstraint match.</p>
</li>
</ul>
<hr />
<h2 id="heading-step-2-enable-versioning-on-the-bucket"><strong>Step 2: Enable Versioning on the Bucket</strong> ⏳🔁</h2>
<p><strong>Enabling</strong> versioning allows us to recover objects from accidental deletions or modifications.</p>
<h3 id="heading-command-2"><strong>Command:</strong></h3>
<pre><code class="lang-bash">aws s3api put-bucket-versioning --bucket image-bucket-2025 --versioning-configuration Status=Enabled
</code></pre>
<p><strong>Handy Notes:</strong></p>
<ul>
<li>If we mistype the bucket name (e.g., using <code>image-bucket-2024</code>), we might encounter an "Access Denied" error if we don’t have the proper permissions for that bucket.</li>
</ul>
<hr />
<h2 id="heading-step-3-enable-default-encryption"><strong>Step 3: Enable Default Encryption</strong> 🔒🛡️</h2>
<p>Enabling Server-Side Encryption (SSE-S3) to ensure that objects stored in S3 are encrypted using AES256.</p>
<h3 id="heading-command-3"><strong>Command:</strong></h3>
<pre><code class="lang-bash">aws s3api put-bucket-encryption --bucket image-bucket-2025 --server-side-encryption-configuration <span class="hljs-string">'{ "Rules": [{ "ApplyServerSideEncryptionByDefault": { "SSEAlgorithm": "AES256" } }] }'</span>
</code></pre>
<hr />
<h2 id="heading-step-4-block-public-access"><strong>Step 4: Block Public Access</strong> 🚫🔓</h2>
<p>For security, configure this bucket to block public access so that only CloudFront (using an OAI) can retrieve objects.</p>
<h3 id="heading-command-4"><strong>Command:</strong></h3>
<pre><code class="lang-bash">aws s3api put-public-access-block --bucket image-bucket-2025 --public-access-block-configuration <span class="hljs-string">'{ "BlockPublicAcls": true, "IgnorePublicAcls": true, "BlockPublicPolicy": true, "RestrictPublicBuckets": true }'</span>
</code></pre>
<p>Handy Notes:</p>
<ul>
<li><strong>OAI (Origin Access Identity)</strong>: A special CloudFront user identity that allows CloudFront to securely access S3 buckets without exposing them to the public. It acts as a middleman and allows CloudFront to fetch objects from S3 while blocking direct public access.</li>
</ul>
<hr />
<h2 id="heading-step-5-upload-files-to-s3"><strong>Step 5: Upload Files to S3</strong> 📤🖼️</h2>
<p>Explore how to sync a local folder containing images to our S3 bucket using the CLI.</p>
<h3 id="heading-commands"><strong>Commands:</strong></h3>
<ol>
<li><p>Create a test folder and add an image:</p>
<pre><code class="lang-bash"> <span class="hljs-built_in">cd</span> ~
 mkdir test-image
</code></pre>
</li>
<li><p>Manually add an image to this folder for convenience, e.g., put “word-embedding-apple.png” inside this folder. (The image is as shown below)</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738780408135/8741d12e-fce1-497f-a9ee-b96ec8d1d379.png" alt="word-embedding-apple.png" class="image--center mx-auto" /></p>
</li>
<li><p>Upload/Sync our local image folder and its content to our created S3 bucket:</p>
<pre><code class="lang-bash"> aws s3 sync ~/test-image s3://image-bucket-2025/images
</code></pre>
</li>
</ol>
<p><strong>Output (From 3 just above):</strong></p>
<pre><code class="lang-bash">upload: test-image/word-embedding-apple.png to s3://image-bucket-2025/images/word-embedding-apple.png
</code></pre>
<p>Handy Notes:</p>
<ul>
<li>Like we uploaded an entire folder/directory, can use <code>aws s3 cp</code>for a single file upload. Refer to the official docs here for more: <a target="_blank" href="https://docs.aws.amazon.com/cli/latest/reference/s3/cp.html">https://docs.aws.amazon.com/cli/latest/reference/s3/cp.html</a></li>
</ul>
<hr />
<h2 id="heading-step-6-create-a-cloudfront-distribution"><strong>Step 6: Create a CloudFront Distribution</strong></h2>
<p>CloudFront is a Content Delivery Network (CDN) that caches our static assets globally, reducing latency. In this setup, CloudFront will use an Origin Access Identity (OAI) to securely retrieve content from our S3 bucket.</p>
<h3 id="heading-61-create-an-origin-access-identity-oai"><strong>6.1: Create an Origin Access Identity (OAI)</strong> 👤🔑</h3>
<h3 id="heading-command-5"><strong>Command:</strong></h3>
<pre><code class="lang-bash">aws cloudfront create-cloud-front-origin-access-identity --cloud-front-origin-access-identity-config <span class="hljs-string">'{
  "CallerReference": "aws-cli-example-caller-2025",
  "Comment": "OAI for image-bucket-2025"
}'</span>
</code></pre>
<p><strong>Output (From command above):</strong></p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"Location"</span>: <span class="hljs-string">"https://cloudfront.amazonaws.com/2020-05-31/origin-access-identity/cloudfront/ERTF9F3HTHDQ"</span>,
    <span class="hljs-attr">"ETag"</span>: <span class="hljs-string">"E1GY74M2WSZ75"</span>,
    <span class="hljs-attr">"CloudFrontOriginAccessIdentity"</span>: {
        <span class="hljs-attr">"Id"</span>: <span class="hljs-string">"ERTF9F3HTHDQ"</span>,
        <span class="hljs-attr">"S3CanonicalUserId"</span>: <span class="hljs-string">"d63a2ec4030735c8b201ccf613941bfbf4944a7cc27cc72e1a6799d29d505e6ab5efed9b418d2eb350450e73b1a89ca8"</span>,
        <span class="hljs-attr">"CloudFrontOriginAccessIdentityConfig"</span>: {
            <span class="hljs-attr">"CallerReference"</span>: <span class="hljs-string">"aws-cli-example-caller-2025"</span>,
            <span class="hljs-attr">"Comment"</span>: <span class="hljs-string">"OAI for image-bucket-2025"</span>
        }
    }
}
</code></pre>
<p>Take note of the returned OAI’s <strong>ID</strong> (e.g., <code>ERTF9F3HTHDQ</code>). We will need this in next steps.</p>
<h3 id="heading-62-create-a-cloudfront-distribution"><strong>6.2: Create a CloudFront Distribution</strong> 🌐⚡</h3>
<h3 id="heading-command-6"><strong>Command:</strong></h3>
<pre><code class="lang-bash">aws cloudfront create-distribution --distribution-config <span class="hljs-string">'{
  "CallerReference": "aws-cli-example-caller-2025",
  "Aliases": {
    "Quantity": 0,
    "Items": []
  },
  "DefaultRootObject": "",
  "Origins": {
    "Quantity": 1,
    "Items": [{
      "Id": "S3-image-bucket-2025",
      "DomainName": "image-bucket-2025.s3.amazonaws.com",
      "OriginPath": "",
      "CustomHeaders": {
        "Quantity": 0
      },
      "S3OriginConfig": {
        "OriginAccessIdentity": "origin-access-identity/cloudfront/ERTF9F3HTHDQ"
      }
    }]
  },
  "DefaultCacheBehavior": {
    "TargetOriginId": "S3-image-bucket-2025",
    "ViewerProtocolPolicy": "redirect-to-https",
    "AllowedMethods": {
      "Quantity": 2,
      "Items": ["GET", "HEAD"],
      "CachedMethods": {
        "Quantity": 2,
        "Items": ["GET", "HEAD"]
      }
    },
    "TrustedSigners": {
      "Enabled": false,
      "Quantity": 0
    },
    "ForwardedValues": {
      "QueryString": false,
      "Cookies": {
        "Forward": "none"
      },
      "Headers": {
        "Quantity": 0
      },
      "QueryStringCacheKeys": {
        "Quantity": 0
      }
    },
    "MinTTL": 0,
    "DefaultTTL": 86400,
    "MaxTTL": 31536000,
    "Compress": true
  },
  "Comment": "CloudFront distribution for image hosting",
  "Enabled": true,
  "PriceClass": "PriceClass_100",
  "ViewerCertificate": {
    "CloudFrontDefaultCertificate": true
  },
  "Restrictions": {
    "GeoRestriction": {
      "RestrictionType": "none",
      "Quantity": 0
    }
  }
}'</span>
</code></pre>
<blockquote>
<p>Not all the information in above command are important to remember, always refer the official docs for reference, some are defaults. (Please refer to resources mentioned at the end)</p>
</blockquote>
<ul>
<li><p>This command creates a CloudFront distribution to serve images from our S3 bucket <code>image-bucket-2025</code>.</p>
</li>
<li><p>It uses an <strong>Origin Access Identity (OAI)</strong> for secure access to the bucket and enables HTTPS with the default CloudFront certificate.</p>
</li>
<li><p>The caching behavior is configured with Time to Leave (TTL). TTL settings (<code>MinTTL</code>, <code>DefaultTTL</code>, <code>MaxTTL</code>) are used to control cache expiration times.</p>
</li>
<li><p>Compression is also enabled for faster delivery.</p>
</li>
<li><p>This created distribution covers all major regions globally with the most cost-effective <strong>PriceClass_100</strong> plan.</p>
</li>
</ul>
<hr />
<h2 id="heading-step-7-secure-s3-access-with-a-bucket-policy"><strong>Step 7: Secure S3 Access with a Bucket Policy</strong> 📝✅</h2>
<p>Update the bucket policy so that only the CloudFront OAI can access our S3 bucket.</p>
<h3 id="heading-command-7"><strong>Command:</strong></h3>
<pre><code class="lang-bash">aws s3api put-bucket-policy --bucket image-bucket-2025 --policy <span class="hljs-string">'{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ERTF9F3HTHDQ"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::image-bucket-2025/*"
    }
  ]
}'</span>
</code></pre>
<hr />
<h2 id="heading-step-8-list-cloudfront-distributions"><strong>Step 8: List CloudFront Distributions</strong> 🔍📜</h2>
<p>We can use this command to list distributions and verify our CloudFront domain name.</p>
<h3 id="heading-command-8"><strong>Command:</strong></h3>
<pre><code class="lang-bash">aws cloudfront list-distributions --query <span class="hljs-string">"DistributionList.Items[].{Id:Id, DomainName:DomainName}"</span> --output table
</code></pre>
<p><strong>Output (From command above):</strong></p>
<pre><code class="lang-bash">-----------------------------------------------------
|                 ListDistributions                 |
+--------------------------------+------------------+
|           DomainName           |       Id         |
+--------------------------------+------------------+
|  d2z61qbc2yzo2i.cloudfront.net |  E2XXAEVCAOEBMR  |
+--------------------------------+------------------+
</code></pre>
<p>Handy Note:</p>
<ul>
<li>This <code>DomainName</code> from the above table will be used to compose the image URL for testing in step 9.</li>
</ul>
<hr />
<h2 id="heading-step-9-test-the-image-cdn-setup"><strong>Step 9: Test the Image CDN Setup</strong> ✅📶</h2>
<p>Test our CloudFront distribution by accessing an image via its URL. It should look something like this.</p>
<h3 id="heading-command-9"><strong>Command:</strong></h3>
<blockquote>
<p>Run this twice in your terminal and see the difference the first time and second time.</p>
</blockquote>
<pre><code class="lang-bash">curl -I https://d2z61qbc2yzo2i.cloudfront.net/images/word-embedding-apple.png
</code></pre>
<p><strong>Output (First Time) -</strong> <code>CACHE MISS</code></p>
<pre><code class="lang-bash">HTTP/2 200
content-type: image/png
content-length: 32369
date: Fri, 07 Feb 2025 07:19:32 GMT
last-modified: Fri, 07 Feb 2025 07:13:57 GMT
etag: <span class="hljs-string">"c3688188d41acb74b262270a39438e7a"</span>
x-amz-server-side-encryption: AES256
x-amz-version-id: egHDizTCdO8xADRuUwJDa_XJ2nhtLIjz
accept-ranges: bytes
server: AmazonS3
x-cache: Miss from cloudfront
via: 1.1 fb465ef388ebb25e5a872213f9ac3e9c.cloudfront.net (CloudFront)
x-amz-cf-pop: MRS52-C1
x-amz-cf-id: jO8Ti8t4dTrcDq_k2UHtw5K5QAX2x2k7anGXkh3fjtxk-jZ34SFdUw==
</code></pre>
<ul>
<li><p>If we see <code>x-cache: Miss from cloudfront</code> for the x-cache, this means our image is not yet cached, for the first fetch, OAI will fetch the image straight from the origin, which is our S3 bucket.</p>
</li>
<li><p>After fetching, CloudFront caches it for future requests. The next request for the same image will then result in a "Hit" status if it is served from the cache.</p>
</li>
</ul>
<p><strong>Output (Second Time) -</strong> <code>CACHE HIT</code></p>
<pre><code class="lang-bash">HTTP/2 200
content-type: image/png
content-length: 32369
date: Fri, 07 Feb 2025 07:19:32 GMT
last-modified: Fri, 07 Feb 2025 07:13:57 GMT
etag: <span class="hljs-string">"c3688188d41acb74b262270a39438e7a"</span>
x-amz-server-side-encryption: AES256
x-amz-version-id: egHDizTCdO8xADRuUwJDa_XJ2nhtLIjz
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 9ba4efea4d7fc27f92a66f28df5d1152.cloudfront.net (CloudFront)
x-amz-cf-pop: MRS52-C1
x-amz-cf-id: YyBR5UXQ1JLQf4Tqm4N1_V74KqoeGWv1768b8Erkuobuj6nzpzmQUQ==
age: 15
</code></pre>
<ul>
<li>If we see <code>x-cache: Hit from cloudfront</code> for the x-cache, this means our image was already cached, OAI found this image was available in cache from just 15 seconds ago, indicated by <code>age: 15</code> .</li>
</ul>
<h2 id="heading-finally-access-the-cached-hit-image-in-our-browser-using"><strong>Finally, access the cached (hit) image in our browser using:</strong></h2>
<pre><code class="lang-bash">https://d2z61qbc2yzo2i.cloudfront.net/images/word-embedding-apple.png
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738778827245/b1f3a177-f22f-49d0-9667-6539352d4763.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this way, this approach provides a minimal, secure, scalable image hosting solution using AWS S3 and CloudFront CDN using a collection of AWS CLI v2 commands. Further enhancements like automatic resizing/compression for uploaded images using Lambda@Edge, adding cache invalidation, creating frontend for this, setting up monitoring, connecting with other services and many more are possible on top of this. Similarly, it’s worth considering alternatives like <strong>Cloudflare R2</strong> which provides us with similar object storage service, which is already S3-compatible, and the best part / major advantage over AWS S3 is R2’s <strong>Zero egress fee</strong> (means you don't pay for sending data out of a cloud service). Next article in this series, I will share my learnings with a deep dive into various <strong>cache invalidation approaches</strong>.</p>
<hr />
<h3 id="heading-resources">📄 Resources</h3>
<ul>
<li><p><a target="_blank" href="https://awscli.amazonaws.com/v2/documentation/api/latest/index.html#">https://awscli.amazonaws.com/v2/documentation/api/latest/index.html</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/aws/aws-cli">https://github.com/aws/aws-cli</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html">https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html</a> (Install AWS CLI v2)</p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-using.html">https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-using.html</a></p>
</li>
</ul>
<hr />
]]></content:encoded></item></channel></rss>