<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Leonti Bielski</title>
    <description>The latest articles on DEV Community by Leonti Bielski (@leonti).</description>
    <link>https://dev.to/leonti</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F221323%2Fad402a99-57bc-45fa-a041-b2a0ce164ce3.jpeg</url>
      <title>DEV Community: Leonti Bielski</title>
      <link>https://dev.to/leonti</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/leonti"/>
    <language>en</language>
    <item>
      <title>AWS Budget Killswitch: Disable AWS Services When Budget Is Exceeded</title>
      <dc:creator>Leonti Bielski</dc:creator>
      <pubDate>Mon, 06 Jul 2020 03:47:39 +0000</pubDate>
      <link>https://dev.to/leonti/aws-budget-killswitch-disable-aws-services-when-budget-is-exceeded-36oc</link>
      <guid>https://dev.to/leonti/aws-budget-killswitch-disable-aws-services-when-budget-is-exceeded-36oc</guid>
      <description>&lt;p&gt;I like creating small projects and hosting them on AWS. AWS is what I use at work and using it for my personal projects makes sense because it's convenient and it's what I know. &lt;br&gt;
However, my personal projects don't make me any money so a huge increase in traffic doesn't mean an increase in revenue to cover the cost of an AWS bill. &lt;br&gt;
Sometimes I just want to put something out there and see if people like it. Call me paranoid, but I'm afraid of a situation when I wake up one day and discover that I have a &lt;a href="https://mobile.twitter.com/ChrisShort/status/1279406322837082114"&gt;$2700 bill&lt;/a&gt; in my AWS account for something that doesn't make any money. &lt;/p&gt;

&lt;p&gt;I've always wanted to have a hard limit on AWS spending on my personal account. Something like: "Stop everything when my bill reaches $100 a month". There is no such functionality on AWS and I appreciate how hard would it be to implement it. There would not be a solution which works for everyone. Some people would only want to stop services which are the main culprits, some would like to stop everything. Would you want to have your s3 buckets emptied if you have a sudden spike in your bill? Also, there is a matter of production services. Very few companies would be ok with terminating their infrastructure when the bill goes above a certain budget. &lt;br&gt;
For people who just like to tinker and are ok with their services being down in an emergency such solution might be useful. This is where "AWS Budget Killswitch" comes in.&lt;/p&gt;
&lt;h2&gt;
  
  
  AWS Budget Killswitch
&lt;/h2&gt;

&lt;p&gt;The solution is very simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create an emergency budget, a certain value which would constitute an unacceptable spending&lt;/li&gt;
&lt;li&gt;When budget is reached send an email and disable public-facing AWS services, currently only CloudFront and ApiGateway are supported&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The app is implemented as a &lt;a href="https://aws.amazon.com/serverless/sam/"&gt;SAM&lt;/a&gt; template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;AWSTemplateFormatVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2010-09-09'&lt;/span&gt;
&lt;span class="na"&gt;Transform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless-2016-10-31&lt;/span&gt;
&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;

&lt;span class="na"&gt;Parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;NotificationEmail&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;String&lt;/span&gt;
  &lt;span class="na"&gt;BudgetLimit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Number&lt;/span&gt;

&lt;span class="na"&gt;Resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;KillswitchBudgetTopic&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::SNS::Topic&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Subscription&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;DisableServicesFunction.Arn&lt;/span&gt;
          &lt;span class="na"&gt;Protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lambda"&lt;/span&gt;

  &lt;span class="na"&gt;KillswitchBudget&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Budgets::Budget&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
      &lt;span class="na"&gt;Budget&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;BudgetLimit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Amount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;BudgetLimit&lt;/span&gt;
          &lt;span class="na"&gt;Unit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;USD&lt;/span&gt;
        &lt;span class="na"&gt;TimeUnit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MONTHLY&lt;/span&gt;
        &lt;span class="na"&gt;BudgetType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;COST&lt;/span&gt;
      &lt;span class="na"&gt;NotificationsWithSubscribers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Notification&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;NotificationType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ACTUAL&lt;/span&gt;
            &lt;span class="na"&gt;ComparisonOperator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GREATER_THAN&lt;/span&gt;
            &lt;span class="na"&gt;Threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;99&lt;/span&gt;
          &lt;span class="na"&gt;Subscribers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;SubscriptionType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EMAIL&lt;/span&gt;
              &lt;span class="na"&gt;Address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;NotificationEmail&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;SubscriptionType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SNS&lt;/span&gt;
              &lt;span class="na"&gt;Address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;KillswitchBudgetTopic&lt;/span&gt;

  &lt;span class="na"&gt;SnsInvokeLambdaPermission&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;            
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Lambda::Permission&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
      &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lambda:InvokeFunction&lt;/span&gt;
      &lt;span class="na"&gt;FunctionName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;DisableServicesFunction&lt;/span&gt;
      &lt;span class="na"&gt;Principal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sns.amazonaws.com'&lt;/span&gt;
      &lt;span class="na"&gt;SourceArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;KillswitchBudgetTopic&lt;/span&gt;              

  &lt;span class="na"&gt;DisableServicesFunction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless::Function&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;CodeUri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;disable-services&lt;/span&gt;
      &lt;span class="na"&gt;Handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app.handler&lt;/span&gt;
      &lt;span class="na"&gt;Runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nodejs12.x&lt;/span&gt;
      &lt;span class="na"&gt;MemorySize&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;256&lt;/span&gt;  
      &lt;span class="na"&gt;Policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;AWSLambdaExecute&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2012-10-17'&lt;/span&gt;
        &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
            &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cloudfront:ListDistributions&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cloudfront:GetDistributionConfig&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cloudfront:UpdateDistribution&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;apigateway:*'&lt;/span&gt;
            &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;  
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;It creates a budget with 2 subscriptions, email, and an SNS topic.&lt;br&gt;&lt;br&gt;
SNS topic is configured to invoke a Lambda which contains the logic to disable services. AWS doesn't have a big red "Disable" button for each service, each one is unique, so disabling them is different per service.&lt;br&gt;&lt;br&gt;
You can disable a CloudFront distribution, but ApiGateway stage can only be deleted. Luckily you can set throttling limits to 0, so it can also be made unreachable.  &lt;/p&gt;

&lt;p&gt;Here is how the Lambda looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AWS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cloudfront&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;AWS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CloudFront&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apigateway&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;AWS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APIGateway&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;disableDistribution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cloudfront&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getDistributionConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;disabledConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DistributionConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cloudfront&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updateDistribution&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;DistributionConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;disabledConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;IfMatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ETag&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;disableCloudfront&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;distributions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cloudfront&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listDistributions&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;distributions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DistributionList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;disableDistribution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setStageLimits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;restApiId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stageName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;apigateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getStage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;restApiId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;stageName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;apigateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updateStage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;restApiId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;stageName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;patchOperations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
        &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;replace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/*/*/throttling/burstLimit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;replace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/*/*/throttling/rateLimit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;disableApiGateways&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;apigateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getRestApis&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apiIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;restApiId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;apigateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getStages&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;restApiId&lt;/span&gt;
    &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;setStageLimits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;restApiId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stageName&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
  &lt;span class="p"&gt;}))&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;disableCloudfront&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;disableApiGateways&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;There is nothing else to it, it's a very blunt instrument, but it will make sure that if you have a huge spike in traffic that you don't expect you'll end up with your app down instead of paying an exorbitant bill.&lt;br&gt;&lt;br&gt;
This is definitely something only for tinkerers like me, not for a production use.&lt;br&gt;&lt;br&gt;
The proper solution would be using an &lt;a href="https://aws.amazon.com/waf/"&gt;AWS WAF&lt;/a&gt; but I'm too cheap to spend $6 a month for a very unlickely event of a DDoS attack on one of my small projects.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Leonti/aws-budget-killswitch"&gt;Here&lt;/a&gt; is a Github repository ready to be deployed.&lt;/p&gt;

</description>
      <category>aws</category>
    </item>
    <item>
      <title>Testing: A Practical Approach</title>
      <dc:creator>Leonti Bielski</dc:creator>
      <pubDate>Wed, 24 Jun 2020 07:44:28 +0000</pubDate>
      <link>https://dev.to/leonti/testing-a-practical-approach-f67</link>
      <guid>https://dev.to/leonti/testing-a-practical-approach-f67</guid>
      <description>&lt;p&gt;Automated testing is important to build software in a sustainable way. No one wants to manually test their entire application on every change. That would be slow and a waste of time.&lt;br&gt;
On the other hand, covering an application with all possible kinds of automated tests also has a significant cost. If not done right tests can make application architecture rigid and slow to change - the exact things we aim to improve with automated testing in the first place.&lt;br&gt;
It's an important part of our job as software developers to make calls about what needs to be tested and to what degree.&lt;br&gt;
During my career, I had changed my views on testing from just writing tests because it's a "best practice" to a more practical approach about when and which type of tests to write.&lt;br&gt;
In this article, I'd like to share my thoughts on software testing with the hope it can be beneficial for someone who is on the same journey of writing better software.&lt;br&gt;
This is not meant to be a definitive testing guide, it's meant to be a starting point for a more critical view on testing practices.&lt;/p&gt;

&lt;h1&gt;
  
  
  Context
&lt;/h1&gt;

&lt;p&gt;The context in which application is built and used is very important when thinking about testing. Are you an early-stage startup or a big fintech company? Are you working on a new experimental way display content to users or are you adding a feature to an existing product which is going to be rolled out to millions of users?&lt;br&gt;
Kent Beck has an excellent talk "3x Explore, Expand, Extract"&lt;br&gt;
&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/FlJN6_4yI2A"&gt;
&lt;/iframe&gt;
&lt;br&gt;
I highly recommend it. The gist of it is this - your approach to testing and software quality, in general, should depend on at what stage of product maturity you are. If you are just starting out, exploring, and experimenting then high automated testing coverage is not needed. You might even go for a while without any automated tests at all until you know what works and what needs to be solidified.&lt;br&gt;
Once you know which parts of your product are going to stay you should improve test coverage and code quality in those areas.&lt;/p&gt;

&lt;h1&gt;
  
  
  TDD
&lt;/h1&gt;

&lt;p&gt;Test-Driven Development is a bit like a religion. There are people who believe in its core values and there are fundamentalists which preach the "one true way" of doing it.&lt;br&gt;
The most popular understanding of TDD seems to be: code should have 100% test coverage at any cost and that a unit is a class (in languages that have classes).&lt;br&gt;
This approach leads to hundreds of useless tests which mostly test mocks just for the sake of the coverage. &lt;br&gt;
It's hard to ask for the removal of such tests because no one wants to be that guy which is against automated testing ;)&lt;br&gt;
When I first read DHH's post about &lt;a href="https://dhh.dk/2014/tdd-is-dead-long-live-testing.html"&gt;TDD&lt;/a&gt; I thought the guy was crazy because TDD was meant to solve all of the problems of software quality. Now I get it.&lt;br&gt;
There is a great talk by Ian Cooper titled "TDD, Where Did It All Go Wrong" which touches on the problems with the traditional understanding of TDD and how it differs from the original idea.&lt;br&gt;
&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/EZ05e7EMOLM"&gt;
&lt;/iframe&gt;
&lt;br&gt;
Here are the key takeaways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"Unit" is not a single class, it's a module with public API.&lt;/li&gt;
&lt;li&gt;Avoid mocks.&lt;/li&gt;
&lt;li&gt;Test behaviour, not implementation.
This approach avoids so many problems. Since we are testing a public API of a module there is no need for mocks. Imagine you test a &lt;code&gt;Calculator&lt;/code&gt; module which can add and multiply numbers. It uses &lt;code&gt;Adder&lt;/code&gt; and &lt;code&gt;Multiplier&lt;/code&gt; classes underneath.
But we don't care. All we care about is whether it can successfully add and multiply numbers, so we just test that.
&lt;code&gt;Calculator&lt;/code&gt; module is free to change its implementation, remove &lt;code&gt;Adder&lt;/code&gt; and &lt;code&gt;Multiplier&lt;/code&gt; dependencies and perform calculations using a third-party &lt;code&gt;Math&lt;/code&gt; library. Not a single test would have to be changed and we would still be confident that &lt;code&gt;Calculator&lt;/code&gt; works correctly.
Compare this to a traditional approach:&lt;/li&gt;
&lt;li&gt;Write a test for &lt;code&gt;Calculator&lt;/code&gt; using &lt;code&gt;Adder&lt;/code&gt; and &lt;code&gt;Multiplier&lt;/code&gt; mocks. Make sure they are called with the right arguments.&lt;/li&gt;
&lt;li&gt;Write a test for &lt;code&gt;Adder&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Write a test for &lt;code&gt;Multiplier&lt;/code&gt;.
Now we have 3 brittle tests from which only 2 are testing the actual logic. If we need to switch to a third-party &lt;code&gt;Math&lt;/code&gt; library we'd have to rewrite the &lt;code&gt;Calculator&lt;/code&gt; test, and remove &lt;code&gt;Adder&lt;/code&gt; and &lt;code&gt;Multiplier&lt;/code&gt; ones. That would slow us down and more importantly when you have to change tests during refactoring how are you sure that you can rely on modified tests?
It has been a joy working on projects which embrace "unit is a module" and "no mocks" approaches. Some say that if you don't test classes in isolation it leads to a tight coupling between classes. In my experience, it's not a problem at all. If developers are free to painlessly refactor and move stuff around it usually leads to a better code organisation over time. How often have you reconsidered moving a dependency because of the burden of adjusting unit tests?&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  Slow and fast tests
&lt;/h1&gt;

&lt;p&gt;Given that everybody seems to have their own definition of what "integration test" is I prefer alternative definition where you group tests by being "fast" and "slow".&lt;br&gt;
"Fast" tests are those which don't talk to slow IO (no matter how many classes are involved) and "slow" are those which do. Database and network calls are usually slow. Writing and reading small files to an SSD drive is usually pretty fast. Not doing any IO is even faster :)&lt;br&gt;
The way to avoid performing IO calls during tests is to replace those parts of the code which do with stubs or "alternates". There are multiple ways to do it and functional programming particularly shines at separating IO from the rest of the code, but the main idea is to provide an interface for external interactions and 2 implementations - one for testing which responds with pre-canned values and the real one which talks to the external world (it needs to be tested separately).&lt;br&gt;
Again, the context of your application is very important here and I would argue there is no universal "best practice" approach.&lt;br&gt;
Imagine you have an application which takes a JSON payload and puts it into a Postgres database. What could go wrong here?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;JSON codec might be misconfigured&lt;/li&gt;
&lt;li&gt;Extracting data from the JSON payload and putting it into database might be broken.&lt;/li&gt;
&lt;li&gt;Database schema might be different from the one that the code expects.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is no logic to be tested here, so the test which would give you the most bang for your buck would be the one which accepts JSON payload from a string and writes it to the database.&lt;br&gt;
There is nothing wrong with not having a "unit" test for this if it gives you no value. You can introduce it later when you have logic in your JSON-&amp;gt;DB pipeline.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;There is no single prescribed model for software testing. No application is the same and no team is the same. It is important to step back and evaluate your testing strategy.&lt;br&gt;
If your current testing approach doesn't feel right maybe it's because what is considered a best practice is not right for your application. There is no point of striving for 100% unit test coverage in an application which is mostly a conduit between an HTTP request and a DB.&lt;br&gt;
When you do write unit tests, avoiding mocks and testing behaviour instead of implementation goes a long way to achieve a balance between software quality and development speed.&lt;/p&gt;

</description>
      <category>tdd</category>
      <category>testing</category>
    </item>
    <item>
      <title>Deploying an SPA on AWS</title>
      <dc:creator>Leonti Bielski</dc:creator>
      <pubDate>Mon, 22 Jun 2020 07:47:11 +0000</pubDate>
      <link>https://dev.to/leonti/deploying-an-spa-on-aws-34lf</link>
      <guid>https://dev.to/leonti/deploying-an-spa-on-aws-34lf</guid>
      <description>&lt;p&gt;Hosting a static website on AWS has many advantages but it has a few moving parts which need to fit together to get a fast and scalable static website.&lt;/p&gt;

&lt;p&gt;While working on &lt;a href="https://avocv.com"&gt;AvoCV&lt;/a&gt; I decided to host it on AWS for a few reasons.&lt;br&gt;
Backend for the project is built using AWS Gateway, so hosting the frontend on AWS Gateway was a natural choice since it would reduce the burden of switching between different cloud providers.&lt;br&gt;
Hosting a static website on AWS is also dirt-cheap, which was also important.&lt;/p&gt;

&lt;p&gt;In this post I’ll describe the steps you need to take between creating your SPA frontend project and being able to access it from your own domain.&lt;/p&gt;
&lt;h1&gt;
  
  
  Domain name
&lt;/h1&gt;

&lt;p&gt;The first thing you need to do is to get a domain name from any of the domain registrars.&lt;br&gt;
After you have your domain you need to create a hosted zone in &lt;a href="https://console.aws.amazon.com/route53"&gt;Route53&lt;/a&gt;, it costs $0.50 per month.&lt;br&gt;
Route53 will create NS records automatically which you can use to “connect” your domain from your registrar to your hosted zone in Route53. Every registrar should have instructions on how to change NameServer records.&lt;/p&gt;
&lt;h1&gt;
  
  
  S3 hosting and Cloudfront
&lt;/h1&gt;

&lt;p&gt;The easy way to make static files available publicly on AWS is to put them in a public s3 bucket. The url that you’ll get ain’t pretty though. It will look something like this: &lt;code&gt;http://&amp;lt;bucket-name&amp;gt;.s3-website-us-east-1.amazonaws.com&lt;/code&gt;&lt;br&gt;
It’s definitely not user-friendly and it’s not HTTPS which is a must nowadays.&lt;br&gt;
This is why we need a Cloudfront distribution in front of the bucket. It will hide the ugly url and will make sure requests are HTTPS.&lt;/p&gt;
&lt;h1&gt;
  
  
  Public S3 bucket
&lt;/h1&gt;

&lt;p&gt;The whole setup apart from Route53 is done in CloudFormation. This is how s3 bucket setup looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;S3BucketLogs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::S3::Bucket&lt;/span&gt;
    &lt;span class="na"&gt;DeletionPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Delete&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;AccessControl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LogDeliveryWrite&lt;/span&gt;
      &lt;span class="na"&gt;BucketName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${AWS::StackName}-logs'&lt;/span&gt;

  &lt;span class="na"&gt;S3BucketRoot&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::S3::Bucket&lt;/span&gt;
    &lt;span class="na"&gt;DeletionPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Delete&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;AccessControl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PublicRead&lt;/span&gt;
      &lt;span class="na"&gt;BucketName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${AWS::StackName}-root'&lt;/span&gt;
      &lt;span class="na"&gt;LoggingConfiguration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;DestinationBucketName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;S3BucketLogs&lt;/span&gt;
        &lt;span class="na"&gt;LogFilePrefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cdn/'&lt;/span&gt;
      &lt;span class="na"&gt;WebsiteConfiguration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ErrorDocument&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;index.html'&lt;/span&gt;
        &lt;span class="na"&gt;IndexDocument&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;index.html'&lt;/span&gt;

  &lt;span class="na"&gt;S3BucketPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::S3::BucketPolicy&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;S3BucketRoot&lt;/span&gt;
      &lt;span class="na"&gt;PolicyDocument&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2012-10-17'&lt;/span&gt;
        &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Allow'&lt;/span&gt;
            &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s3:GetObject'&lt;/span&gt;
            &lt;span class="na"&gt;Principal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;
            &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${S3BucketRoot.Arn}/*'&lt;/span&gt;    
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;&lt;code&gt;${AWS::StackName}&lt;/code&gt; will resolve to the name of your stack, so if you name your stack &lt;code&gt;my-frontend&lt;/code&gt; the name of the bucket will be &lt;code&gt;my-frontend-root&lt;/code&gt;.&lt;br&gt;
We are telling AWS that the bucket should be publicly accessible with PublicRead access control and with a BucketPolicysaying that every object should be public.&lt;br&gt;
This setup even includes access logs which are being put into a special &lt;code&gt;&amp;lt;stack-name&amp;gt;-logs&lt;/code&gt; bucket.&lt;br&gt;
Why &lt;code&gt;ErrorDocument&lt;/code&gt; and &lt;code&gt;IndexDocument&lt;/code&gt; are the same?&lt;br&gt;
This is because in an SPA all requests go to index.html and then JavaScript decided which route to show.&lt;br&gt;
When you navigate to &lt;code&gt;https://avocv.com/editor&lt;/code&gt; AWS will try to find &lt;code&gt;editor&lt;/code&gt; file in the bucket, but it won’t be there. Normally it would return an error page, but we are overriding it with &lt;code&gt;ErrorDocument: index.html&lt;/code&gt; so it loads &lt;code&gt;index.html&lt;/code&gt; anyway. Once the page is loaded JavaScript looks the url in the browser, sees &lt;code&gt;/editor&lt;/code&gt; and knows that it needs to load &lt;code&gt;Editor&lt;/code&gt; page.&lt;br&gt;
If this behaviour is not desirable you can change this entry to something else, for example, &lt;code&gt;error.html&lt;/code&gt;. Just make sure &lt;code&gt;error.html&lt;/code&gt; is in the bucket.&lt;/p&gt;
&lt;h1&gt;
  
  
  SSL Certificate
&lt;/h1&gt;

&lt;p&gt;Creating SSL in CloudFormation is easy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;CertificateManagerCertificate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::CertificateManager::Certificate&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DomainName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;DomainName&lt;/span&gt;
      &lt;span class="na"&gt;SubjectAlternativeNames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s"&gt;www.${DomainName}&lt;/span&gt;
      &lt;span class="na"&gt;ValidationMethod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DNS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The only “gotcha” is that the SSL certificate will need to be verified (you’ll have to add verification entries to your domain records). You will see a pending certificate in AWS Console and since your hosted zone is in Route53 it’s as easy as pressing a button.&lt;/p&gt;

&lt;h1&gt;
  
  
  CloudFront SSL-only distribution
&lt;/h1&gt;

&lt;p&gt;Creating a CloudFront distribution is a little bit more involved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;CloudFrontDistribution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::CloudFront::Distribution&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DistributionConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Aliases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;DomainName&lt;/span&gt;
        &lt;span class="na"&gt;CustomErrorResponses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ErrorCachingMinTTL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
            &lt;span class="na"&gt;ErrorCode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;404&lt;/span&gt;
            &lt;span class="na"&gt;ResponseCode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt;
            &lt;span class="na"&gt;ResponsePagePath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/index.html'&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ErrorCachingMinTTL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
            &lt;span class="na"&gt;ErrorCode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;403&lt;/span&gt;
            &lt;span class="na"&gt;ResponseCode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt;
            &lt;span class="na"&gt;ResponsePagePath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/index.html'&lt;/span&gt;            
        &lt;span class="na"&gt;DefaultCacheBehavior&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;AllowedMethods&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GET&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;HEAD&lt;/span&gt;
          &lt;span class="na"&gt;CachedMethods&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GET&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;HEAD&lt;/span&gt;
          &lt;span class="na"&gt;Compress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;DefaultTTL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;86400&lt;/span&gt;
          &lt;span class="na"&gt;ForwardedValues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;Cookies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;Forward&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;none&lt;/span&gt;
            &lt;span class="na"&gt;QueryString&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;MaxTTL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;31536000&lt;/span&gt;
          &lt;span class="na"&gt;SmoothStreaming&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
          &lt;span class="na"&gt;TargetOriginId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;S3-${AWS::StackName}-root'&lt;/span&gt;
          &lt;span class="na"&gt;ViewerProtocolPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;redirect-to-https'&lt;/span&gt;
        &lt;span class="na"&gt;DefaultRootObject&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;index.html'&lt;/span&gt;
        &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;HttpVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http2&lt;/span&gt;
        &lt;span class="na"&gt;IPV6Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;Logging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;S3BucketLogs.DomainName&lt;/span&gt;
          &lt;span class="na"&gt;IncludeCookies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
          &lt;span class="na"&gt;Prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cdn/'&lt;/span&gt;
        &lt;span class="na"&gt;Origins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;CustomOriginConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;HTTPPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
              &lt;span class="na"&gt;HTTPSPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;443&lt;/span&gt;
              &lt;span class="na"&gt;OriginKeepaliveTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
              &lt;span class="na"&gt;OriginProtocolPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https-only'&lt;/span&gt;
              &lt;span class="na"&gt;OriginReadTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
              &lt;span class="na"&gt;OriginSSLProtocols&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TLSv1&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TLSv1.1&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TLSv1.2&lt;/span&gt;
            &lt;span class="na"&gt;DomainName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;S3BucketRoot.DomainName&lt;/span&gt;
            &lt;span class="na"&gt;Id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;S3-${AWS::StackName}-root'&lt;/span&gt;
        &lt;span class="na"&gt;PriceClass&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PriceClass_All&lt;/span&gt;
        &lt;span class="na"&gt;ViewerCertificate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;AcmCertificateArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;CertificateManagerCertificate&lt;/span&gt;
          &lt;span class="na"&gt;SslSupportMethod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sni-only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;We are using the same trick with custom error pages so they all would return &lt;code&gt;index.html&lt;/code&gt;.&lt;br&gt;
By setting an alias we are making the public s3 bucket accessible from our domain.&lt;br&gt;
SSL is configured to use the certificate created in the same stack for our custom domain.&lt;/p&gt;
&lt;h1&gt;
  
  
  Redirecting WWW users to a naked domain
&lt;/h1&gt;

&lt;p&gt;This step is optional, but personally I prefer to have a non-www domains, so I’d like to redirect users coming to &lt;a href="https://www.avocv.com"&gt;https://www.avocv.com&lt;/a&gt; to &lt;a href="https://avocv.com"&gt;https://avocv.com&lt;/a&gt;. For this we’ll need a special s3 bucket and another CloudFront distribution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;S3BucketWWW&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS::S3::Bucket"&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
      &lt;span class="na"&gt;BucketName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${AWS::StackName}-www-redirect'&lt;/span&gt;
      &lt;span class="na"&gt;AccessControl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PublicRead&lt;/span&gt;
      &lt;span class="na"&gt;WebsiteConfiguration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;RedirectAllRequestsTo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;HostName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s"&gt;${DomainName}&lt;/span&gt;
          &lt;span class="na"&gt;Protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;

  &lt;span class="na"&gt;CloudFrontDistributionRedirect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::CloudFront::Distribution&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DistributionConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Origins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;DomainName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${AWS::StackName}-www-redirect.s3-website-${AWS::Region}.amazonaws.com'&lt;/span&gt;
          &lt;span class="na"&gt;Id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;S3-${AWS::StackName}-www-redirect'&lt;/span&gt;
          &lt;span class="na"&gt;CustomOriginConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;OriginProtocolPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http-only&lt;/span&gt;
        &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;HttpVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http2&lt;/span&gt;
        &lt;span class="na"&gt;IPV6Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;Logging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;S3BucketLogs.DomainName&lt;/span&gt;
          &lt;span class="na"&gt;IncludeCookies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
          &lt;span class="na"&gt;Prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cdn-redirects/'&lt;/span&gt;
        &lt;span class="na"&gt;Aliases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;www.${DomainName}'&lt;/span&gt;
        &lt;span class="na"&gt;DefaultCacheBehavior&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;AllowedMethods&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GET&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;HEAD&lt;/span&gt;
          &lt;span class="na"&gt;CachedMethods&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GET&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;HEAD&lt;/span&gt;
          &lt;span class="na"&gt;TargetOriginId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;S3-${AWS::StackName}-www-redirect'&lt;/span&gt;
          &lt;span class="na"&gt;Compress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;True&lt;/span&gt;
          &lt;span class="na"&gt;DefaultTTL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;604800&lt;/span&gt;
          &lt;span class="na"&gt;ForwardedValues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;QueryString&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false'&lt;/span&gt;
            &lt;span class="na"&gt;Cookies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;Forward&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;none&lt;/span&gt;
          &lt;span class="na"&gt;ViewerProtocolPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redirect-to-https&lt;/span&gt;
        &lt;span class="na"&gt;PriceClass&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PriceClass_All&lt;/span&gt;
        &lt;span class="na"&gt;ViewerCertificate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;AcmCertificateArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;CertificateManagerCertificate&lt;/span&gt;
          &lt;span class="na"&gt;SslSupportMethod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sni-only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Public S3 buckets have ability to redirect user requests. In our case we create an empty public bucket which would redirect all requests to it to &lt;code&gt;https://&amp;lt;your-domain-name&amp;gt;&lt;/code&gt;, so now all we have to do is to make sure all requests from &lt;code&gt;www.&amp;lt;your-domain-name&amp;gt;&lt;/code&gt; would end up in that bucket which would then redirect them to a naked domain &lt;code&gt;&amp;lt;your-domain-name&amp;gt;&lt;/code&gt;.&lt;br&gt;
We do this with another simplified CloudFront distribution which has a &lt;code&gt;www&lt;/code&gt; alias &lt;code&gt;www.${DomainName}&lt;/code&gt;.&lt;br&gt;
What this means is that when users navigates to &lt;a href="http://www.avocv.com"&gt;www.avocv.com&lt;/a&gt; request will be handled by a redirect CloudFront distribution which will send it to a redirect bucket, which in turn will redirect it to avocv.com and it will be picked up by the proper CloudFront distribution and the request will end up in the destination s3 bucket. That’s a lot of redirects!&lt;/p&gt;
&lt;h1&gt;
  
  
  Route53 records
&lt;/h1&gt;

&lt;p&gt;The last thing we need to do is to tell Route53 that domain is “connected” to our CloudFront distributions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;Route53RecordSetGroup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Route53::RecordSetGroup&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;HostedZoneName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${DomainName}.'&lt;/span&gt;
      &lt;span class="na"&gt;RecordSets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;DomainName&lt;/span&gt;
        &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;A&lt;/span&gt;
        &lt;span class="na"&gt;AliasTarget&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DNSName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;CloudFrontDistribution.DomainName&lt;/span&gt;
          &lt;span class="na"&gt;EvaluateTargetHealth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
          &lt;span class="na"&gt;HostedZoneId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Z2FDTNDATAQYW2&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;www.${DomainName}'&lt;/span&gt;
        &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;A&lt;/span&gt;
        &lt;span class="na"&gt;AliasTarget&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DNSName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;CloudFrontDistributionRedirect.DomainName&lt;/span&gt;
          &lt;span class="na"&gt;EvaluateTargetHealth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
          &lt;span class="na"&gt;HostedZoneId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Z2FDTNDATAQYW2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Please note that &lt;code&gt;HostedZoneId&lt;/code&gt; doesn’t refer to your domain’s zone id, it a special zone id (&lt;code&gt;Z2FDTNDATAQYW2&lt;/code&gt;) for an alias record which points to a CloudFront distribution.&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-aliastarget.html"&gt;https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-aliastarget.html&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;
  
  
  Full setup
&lt;/h1&gt;

&lt;p&gt;You can find the full setup in this repository: &lt;a href="https://github.com/Leonti/aws-static-website"&gt;https://github.com/Leonti/aws-static-website&lt;/a&gt;&lt;br&gt;
It also includes an example deploy script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3 &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--delete&lt;/span&gt; &lt;span class="nt"&gt;--acl&lt;/span&gt; &lt;span class="s2"&gt;"public-read"&lt;/span&gt; build s3://&amp;lt;stack-name&amp;gt;-root                                               
aws cloudfront create-invalidation &lt;span class="nt"&gt;--distribution-id&lt;/span&gt; &amp;lt;distribution-id&amp;gt; &lt;span class="nt"&gt;--paths&lt;/span&gt; &lt;span class="s2"&gt;"/index.html"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;It will sync your files with s3 bucket and invalidate &lt;code&gt;index.html&lt;/code&gt; which is the only file you need to invalidate when building a modern SPA.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudfront</category>
      <category>cloudformation</category>
      <category>spa</category>
    </item>
  </channel>
</rss>
