{"id":"l-aws-4-1","courseId":"aws-cloud-foundations","lang":"en","kind":"lesson","summary":"Ship Lantern’s front end from a private S3 bucket through Amazon CloudFront with origin access control, HTTPS on a custom domain, sensible cache headers and invalidations.","html":"<p>Remember the street-food festival that took Lantern down? Most of those requests were for the same few files: the home page, a stylesheet, JavaScript and one poster. Lantern’s front end is a Vite app that builds to static files, so it doesn’t need a server at all. It needs a place to keep files, <strong>Amazon S3</strong>, and a global cache in front of it, <strong>Amazon CloudFront</strong>.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">The shape of it</h2>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 230\" role=\"img\" aria-labelledby=\"l-aws-4-1-t1\"><defs><marker id=\"arrow-l-aws-4-1-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-aws-4-1-t1\">A browser requests lantern.example over HTTPS from a CloudFront edge location. On a cache hit the edge answers directly. On a miss, CloudFront signs a request with origin access control to the private S3 bucket, which allows only that distribution.</title>\n  <rect class=\"d-box\" x=\"20\" y=\"90\" width=\"120\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"80\" y=\"123\" text-anchor=\"middle\">Browser</text>\n  <rect class=\"d-box-accent\" x=\"240\" y=\"80\" width=\"170\" height=\"76\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"325\" y=\"110\" text-anchor=\"middle\">CloudFront edge</text>\n  <text class=\"d-label-muted\" x=\"325\" y=\"132\" text-anchor=\"middle\">cache</text>\n  <rect class=\"d-box-primary\" x=\"510\" y=\"80\" width=\"150\" height=\"76\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"585\" y=\"110\" text-anchor=\"middle\">S3 bucket</text>\n  <text class=\"d-label-muted\" x=\"585\" y=\"132\" text-anchor=\"middle\">private, BPA on</text>\n  <path class=\"d-arrow\" d=\"M140 108 L236 108\" marker-end=\"url(#arrow-l-aws-4-1-1)\"/>\n  <text class=\"d-label\" x=\"188\" y=\"98\" text-anchor=\"middle\">HTTPS</text>\n  <path class=\"d-arrow d-dashed\" d=\"M238 132 L144 132\" marker-end=\"url(#arrow-l-aws-4-1-1)\"/>\n  <text class=\"d-label-muted\" x=\"190\" y=\"152\" text-anchor=\"middle\">cache hit</text>\n  <path class=\"d-arrow\" d=\"M410 108 L506 108\" marker-end=\"url(#arrow-l-aws-4-1-1)\"/>\n  <text class=\"d-label\" x=\"458\" y=\"98\" text-anchor=\"middle\">miss: signed</text>\n  <text class=\"d-label-muted\" x=\"325\" y=\"200\" text-anchor=\"middle\">Bucket policy: allow s3:GetObject only for this distribution</text>\n</svg><figcaption>Viewers hit a nearby edge location; only cache misses reach the private bucket</figcaption></figure>\n<p>Most requests are <strong>cache hits</strong>, answered by an edge location near the visitor without touching S3. On a <strong>miss</strong>, CloudFront fetches the file from the bucket, signing the request with <strong>origin access control (OAC)</strong>. The bucket stays private, with Block Public Access on, and its policy allows reads from that one distribution only.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Step by step</h2>\n<h3 id=\"sec-3\" tabindex=\"-1\">1. Build and create a private bucket</h3>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">npm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> run</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> build</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">                      # Vite writes dist/</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">aws</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> s3</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> mb</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> s3://lantern-site-prod</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --region</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> eu-west-1</span></span></code></pre></div>\n<p>The new bucket is private by default. Do <strong>not</strong> turn on static website hosting: OAC needs the bucket’s regular endpoint, and the website endpoint only speaks HTTP and requires public access.</p>\n<h3 id=\"sec-4\" tabindex=\"-1\">2. Upload with the right cache headers</h3>\n<p>Vite puts a content hash in asset file names (<code dir=\"ltr\">assets/index-4f8a1c.js</code>), so a changed file always gets a new name. Those can be cached for a year. <code dir=\"ltr\">index.html</code> keeps its name, so browsers and CloudFront should always check for a newer one:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">aws</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> s3</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> sync</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> dist/</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> s3://lantern-site-prod/</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --delete</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --exclude</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"index.html\"</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --cache-control</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"public,max-age=31536000,immutable\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">aws</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> s3</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> cp</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> dist/index.html</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> s3://lantern-site-prod/index.html</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --cache-control</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"no-cache\"</span></span></code></pre></div>\n<p><code dir=\"ltr\">--delete</code> removes files from the bucket that no longer exist in <code dir=\"ltr\">dist/</code>, so old bundles don’t pile up.</p>\n<h3 id=\"sec-5\" tabindex=\"-1\">3. Get a certificate in us-east-1</h3>\n<p>For HTTPS on <code dir=\"ltr\">lantern.example</code>, request a public certificate from <strong>AWS Certificate Manager</strong> in <strong><code dir=\"ltr\">us-east-1</code></strong>. CloudFront only uses certificates from that Region, whatever Region your bucket is in. Validate it with the DNS record ACM gives you.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">aws</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> acm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> request-certificate</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --region</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> us-east-1</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --domain-name</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> lantern.example</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --validation-method</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> DNS</span></span></code></pre></div>\n<h3 id=\"sec-6\" tabindex=\"-1\">4. Create the distribution</h3>\n<p>In the CloudFront console, create a distribution with:</p>\n<ul>\n<li><strong>Origin</strong>: the bucket’s regular endpoint, with <strong>origin access control</strong> (sign requests).</li>\n<li><strong>Default root object</strong>: <code dir=\"ltr\">index.html</code>.</li>\n<li><strong>Viewer protocol policy</strong>: redirect HTTP to HTTPS.</li>\n<li><strong>Alternate domain name</strong> <code dir=\"ltr\">lantern.example</code> and the us-east-1 certificate.</li>\n<li><strong>Cache policy</strong> <code dir=\"ltr\">CachingOptimized</code>, and the managed <strong>SecurityHeadersPolicy</strong> response headers policy, which adds HSTS and related headers.</li>\n</ul>\n<p>If the front end uses client-side routes such as <code dir=\"ltr\">/events/street-food</code>, those paths don’t exist as files, and S3 answers through OAC with 403. Add a custom error response that serves <code dir=\"ltr\">/index.html</code> with status 200 for 403 errors, or a small CloudFront Function that rewrites those paths, so that deep links shared on social media open the right page instead of an error.</p>\n<h3 id=\"sec-7\" tabindex=\"-1\">5. Let only that distribution read the bucket</h3>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"json\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">site-bucket-policy.json</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"Version\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"2012-10-17\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"Statement\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    {</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Sid\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"AllowCloudFrontServicePrincipalReadOnly\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Effect\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Allow\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Principal\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: { </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"Service\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"cloudfront.amazonaws.com\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Action\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"s3:GetObject\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Resource\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"arn:aws:s3:::lantern-site-prod/*\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      \"Condition\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: {</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        \"StringEquals\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: {</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">          \"AWS:SourceArn\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"arn:aws:cloudfront::111122223333:distribution/E2QWRUHEXAMPLE\"</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">        }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  ]</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>The condition is what makes this safe: without it, any CloudFront distribution in any account could read the bucket.</p>\n<h3 id=\"sec-8\" tabindex=\"-1\">6. Point DNS and test</h3>\n<p>Create alias <code dir=\"ltr\">A</code> and <code dir=\"ltr\">AAAA</code> records for <code dir=\"ltr\">lantern.example</code> pointing to the distribution (in Amazon Route 53, or as a CNAME at another DNS host for a subdomain). Then check that <code dir=\"ltr\">https://lantern.example</code> loads, and that the bucket’s own URL returns <strong>403 Access Denied</strong>: that 403 proves the bucket is private.</p>\n<h3 id=\"sec-9\" tabindex=\"-1\">7. On every deploy</h3>\n<p>Repeat step 2, then invalidate only the file that keeps its name:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">aws</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> cloudfront</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> create-invalidation</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --distribution-id</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> E2QWRUHEXAMPLE</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --paths</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"/index.html\"</span></span></code></pre></div>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Invalidating everything on every deploy</span></p>\n<p><code dir=\"ltr\">--paths &quot;/*&quot;</code> works, but it throws away the whole cache, so the next visitors all miss and hit S3 at once, exactly the stampede the CDN exists to prevent. With hashed file names and a no-cache <code dir=\"ltr\">index.html</code>, one path is enough.</p>\n</aside>\n<h2 id=\"sec-10\" tabindex=\"-1\">Cost and security in one glance</h2>\n<p>You pay S3 for storage and for requests, and with CloudFront in front, S3 only sees cache misses. Transfer from S3 to CloudFront is not charged; CloudFront bills for requests and data delivered to viewers on pay-as-you-go, or you can choose one of its flat-rate plans that bundle the CDN with AWS WAF, DNS and S3 credits for a fixed monthly price with no overage charges. Either way, the festival spike now lands on edge locations built for it.</p>\n<p>Security-wise: one private bucket, one bucket policy with a <code dir=\"ltr\">SourceArn</code> condition, HTTPS only, security headers on. Next you add the dynamic part: the RSVP API.</p>\n","toc":[{"id":"sec-1","text":"The shape of it","level":2},{"id":"sec-2","text":"Step by step","level":2},{"id":"sec-3","text":"1. Build and create a private bucket","level":3},{"id":"sec-4","text":"2. Upload with the right cache headers","level":3},{"id":"sec-5","text":"3. Get a certificate in us-east-1","level":3},{"id":"sec-6","text":"4. Create the distribution","level":3},{"id":"sec-7","text":"5. Let only that distribution read the bucket","level":3},{"id":"sec-8","text":"6. Point DNS and test","level":3},{"id":"sec-9","text":"7. On every deploy","level":3},{"id":"sec-10","text":"Cost and security in one glance","level":2}],"takeaways":["The bucket stays private; CloudFront reads it through origin access control, and a bucket policy allows only that one distribution.","Origin access control works with the bucket’s regular endpoint, not the S3 static website endpoint.","A certificate for a CloudFront custom domain must live in AWS Certificate Manager in us-east-1, requested or imported there.","Give hashed asset files a long cache lifetime and index.html a short one, so deploys show up without mass invalidations.","Serving from edge locations absorbs traffic spikes and reduces data transfer from the origin."],"further":[{"title":"Restrict access to an Amazon S3 origin","url":"https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html"},{"title":"Get started with a CloudFront standard distribution","url":"https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/GettingStarted.SimpleDistribution.html"},{"title":"Requirements for using SSL/TLS certificates with CloudFront","url":"https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cnames-and-https-requirements.html"},{"title":"Invalidate files to remove content","url":"https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html"}],"quiz":[{"q":"<p>Why does Lantern keep the site bucket private instead of turning on S3 static website hosting and making it public?</p>","options":[{"text":"Public buckets cannot serve HTML files.","why":"A public bucket with website hosting serves HTML fine. The reasons are security and control, not capability."},{"text":"CloudFront refuses to cache content from public buckets.","why":"CloudFront can use a public website endpoint as a custom origin. Lantern avoids it because it leaves the bucket open to direct access."},{"text":"S3 static website hosting is no longer available.","why":"Website endpoints still exist. They only serve HTTP, though, and require public read access."},{"text":"With origin access control, visitors can only reach files through CloudFront, over HTTPS, and the bucket never needs public access.","why":"Correct. One bucket policy grants read to one distribution, and Block Public Access stays on."}],"answer":3},{"q":"<p>You request a certificate for <code dir=\"ltr\">lantern.example</code> in AWS Certificate Manager in <code dir=\"ltr\">eu-west-1</code>, but CloudFront does not list it. Why?</p>","options":[{"text":"CloudFront only uses certificates issued in the US East (N. Virginia) Region, <code dir=\"ltr\">us-east-1</code>.","why":"Correct. CloudFront is a global service whose certificates must be requested or imported in us-east-1."},{"text":"The certificate needs a few hours to replicate to all Regions.","why":"ACM certificates are never replicated between Regions. A certificate exists only in the Region where you created it."},{"text":"CloudFront requires certificates from a third-party authority.","why":"ACM public certificates work with CloudFront, at no charge for the certificate itself."}],"answer":0},{"q":"<p>After a deploy, visitors still see the old version of the home page for hours, while new JavaScript files load fine. What is the most likely cause?</p>","options":[{"text":"S3 sync did not upload the new <code dir=\"ltr\">index.html</code>.","why":"Possible, but the new hashed JS files arrived, so the upload itself ran. The symptom points at caching."},{"text":"CloudFront is serving a cached <code dir=\"ltr\">index.html</code> because it was uploaded with a long cache lifetime.","why":"Correct. Give <code dir=\"ltr\">index.html</code> a short or no-cache header, and invalidate <code dir=\"ltr\">/index.html</code> after each deploy."},{"text":"Browsers never cache HTML, so this must be a DNS problem.","why":"Browsers and CDNs both cache HTML according to the Cache-Control header. DNS does not decide which version of a file is served."}],"answer":1}],"exercise":{"kind":"order","items":["build","bucket","upload","cert","distribution","policy","dns","verify"],"title":"Ship the static site","prompt":"<p>Put the steps for Lantern’s first production deploy of the front end in a working order. The bucket stays private and the site is served only through CloudFront on <code dir=\"ltr\">https://lantern.example</code>. Finish the build-and-S3 work first, then do the certificate, CloudFront and DNS work.</p>\n","hints":["You can’t upload files you haven’t built, or into a bucket that doesn’t exist.","The distribution needs the certificate when you attach the custom domain, and the bucket policy needs the distribution’s ARN.","DNS goes last before testing, so visitors never land on a half-configured distribution."],"explanation":"<p>Build first so there is something to upload, then create the private bucket and sync the files with their cache headers. The certificate must exist (and be validated) in us-east-1 before you can attach <code dir=\"ltr\">lantern.example</code> to the distribution. The distribution must exist before the bucket policy, because the policy’s <code dir=\"ltr\">AWS:SourceArn</code> condition names the distribution’s ARN. Only when CloudFront can read the bucket do you point DNS at it, and the final check proves both halves: the site loads through CloudFront and the bucket refuses direct requests with 403.</p>\n","checkLabels":{},"itemText":{"build":"Run <code dir=\"ltr\">npm run build</code> to produce the <code dir=\"ltr\">dist/</code> folder.","bucket":"Create the private <code dir=\"ltr\">lantern-site-prod</code> bucket in Lantern’s Region, with Block Public Access left on.","upload":"Upload <code dir=\"ltr\">dist/</code> with <code dir=\"ltr\">aws s3 sync</code>, long cache headers for hashed assets and <code dir=\"ltr\">no-cache</code> for <code dir=\"ltr\">index.html</code>.","cert":"Request and DNS-validate an ACM certificate for <code dir=\"ltr\">lantern.example</code> in <code dir=\"ltr\">us-east-1</code>.","distribution":"Create the CloudFront distribution with origin access control, <code dir=\"ltr\">index.html</code> as default root object and the certificate attached.","policy":"Add the bucket policy that allows <code dir=\"ltr\">s3:GetObject</code> for <code dir=\"ltr\">cloudfront.amazonaws.com</code> with an <code dir=\"ltr\">AWS:SourceArn</code> condition for this distribution.","dns":"Create alias A and AAAA records for <code dir=\"ltr\">lantern.example</code> pointing at the distribution.","verify":"Check that <code dir=\"ltr\">https://lantern.example</code> loads and that the bucket’s direct URL returns 403."}},"words":669,"readMinutes":3}