Host Storybook on AWS S3

Stefan Alfbo - Sep 5 '23 - - Dev Community

Storybook is a static website and is therefore a perfect fit to be hosted on a S3 bucket.

I will go through how to setup the hosting and configuration of the site which will use the following ingredients:

  • AWS S3 to store all the assets
  • CDK with TypeScript to define the infrastructure as code
  • IP access list to restrict access to the site
  • Route53 to enable the use of a custom domain name
  • AWS CLI, tool manage AWS services

Begin with installing the AWS CLI

curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install**
Enter fullscreen mode Exit fullscreen mode

I will presume you have an application with Storybook already added to it. Lets make a new directory in that project for the infrastructure code.

mkdir storybook-hosting && cd $_

# Install CDK
npm install -g aws-cdk

# Create a Typescript CDK application without GIT repo
npx cdk init app --language typescript --generate-only

# Install the dependencies
npm ci

# Open the IDE
code .
Enter fullscreen mode Exit fullscreen mode

Start with modifying the storybook-hosting application, bin/storybook-hosting.ts.

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { StorybookHostingStack } from '../lib/storybook-hosting-stack';

const app = new cdk.App();
new StorybookHostingStack(app, 'StorybookHostingStack', {
  description: 'The infrastructure for hosting Storybook with IP restriction.',
  env: { account: '<replace with correct account>', region: '<replace with correct region>' },
});
Enter fullscreen mode Exit fullscreen mode

Now open the lib/storybook-hosting-stack.ts file. We will start with adding a new method to the class where we will make it possible to add IP addresses that should have access to the site.

createIpAccessList(): string[] {
  // Making it as an object so we can use the properties
  // as nice descriptions of the ip addresses
  const ipAddresses = {
    myNetwork: '10.10.10.10/32',
    myFriendsNetwork: '192.168.1.1/31',
  }

  return [...Object.values(ipAddresses)]
}
Enter fullscreen mode Exit fullscreen mode

With that in place we can move on and import some libraries that we will need before configuring the S3 bucket.

import {
  aws_iam as iam,
  aws_route53 as route53,
  aws_s3 as s3,
  Stack,
  StackProps,
} from 'aws-cdk-lib'

import { Construct } from 'constructs'
Enter fullscreen mode Exit fullscreen mode

The imports will make more since when we have added our next method for setting up the S3 static website.

// The method takes two parameters, the domain name for the
// site, and the IP access list.
setupS3StaticWebsiteHosting(
    storybookDomainName: string,
    ipAccessList: string[]
  ): s3.Bucket {

    // Create the bucket which will have the same name as
    // the domain for the site
    const bucket = new s3.Bucket(this, storybookDomainName, {
      bucketName: storybookDomainName,
      websiteIndexDocument: 'index.html',
      websiteErrorDocument: 'error.html',
    })

    // Create an IAM policy for a accessing the objects
    // in the bucket
    const ipAccessListPolicy = new iam.PolicyStatement({
      actions: ['s3:GetObject'],
      effect: iam.Effect.ALLOW,
      resources: [bucket.arnForObjects('*')],
      principals: [new iam.AnyPrincipal()],
    })

    // Here we add a condition that only the ip addresses
    // in ipAccessList will be allowed to access the bucket
    ipAccessListPolicy.addCondition('IpAddress', {
      'aws:SourceIp': ipAccessList,
    })

    bucket.addToResourcePolicy(ipAccessListPolicy)

    // Add permission to the GitHub Pipeline role to deploy new assets to the bucket
    // Only necessary if you use GitHub actions to deploy the story book solution
    // to the S3 bucket
    const gitHubPipelineRole = new iam.ArnPrincipal(
      'arn:aws:iam::<use your account number>:role/github-pipeline-oidc-<pipeline role>'
    )
    bucket.policy?.document.addStatements(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        principals: [gitHubPipelineRole],
        actions: ['s3:GetObject', 's3:PutObject', 's3:DeleteObject'],
        resources: [bucket.arnForObjects('*')],
      })
    )
    bucket.policy?.document.addStatements(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        principals: [gitHubPipelineRole],
        actions: ['s3:ListBucket'],
        resources: [bucket.bucketArn],
      })
    )

    return bucket
  }
Enter fullscreen mode Exit fullscreen mode

I have tried to comment the code above as much as possible. The last two policies are only interesting if you want to setup a GitHub action workflow for a CI/CD solution. Finally we need a method for configuring the Route53 service so that we can use a domain name for the site.

configureDNS(bucket: s3.Bucket) {
    const domainName = 'example.com'
    const publicHostedZone = route53.PublicHostedZone.fromLookup(
      this,
      'example-com-hosted-zone',
      {
        domainName: domainName,
      }
    )

    const _ = new route53.CnameRecord(this, 'CName', {
      recordName: 'storybook',
      zone: publicHostedZone,
      domainName: bucket.bucketWebsiteDomainName,
    })
  }
Enter fullscreen mode Exit fullscreen mode

This code expect you to already have registered a domain name through Route53, and in this case, example.com. The cname record will use storybook as a subdomain for the site, which means that the complete address will be, storybook.example.com.

To tie all this together there has to be some changes in the constructor of the stack.

constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props)

    const storybookDomainName = 'storybook.example.com'
    const ipAccessList = this.createIpAccessList()
    const bucket = this.setupS3StaticWebsiteHosting(
      storybookDomainName,
      ipAccessList
    )

    this.configureDNS(bucket)
  }
Enter fullscreen mode Exit fullscreen mode

The CDK application is complete now and we can deploy it to AWS. That can be done from the terminal like this. However make sure that your AWS credentials are initialized correctly for the AWS CLI.

npx cdk deploy StorybookHostingStack
Enter fullscreen mode Exit fullscreen mode

When the deployment has finished the infrastructure is all done for the hosting part of the site and we have to push some content to the S3 bucket.

To do that we will need to add Storybook Deployer to our web project.

npm i @storybook/storybook-deployer --save-dev
Enter fullscreen mode Exit fullscreen mode

This simple tool will enable the possibility to deploy storybook to a S3 bucket. After the dependency has been installed we can add a new script entry in the package.json file. This script will handle the deployment for us.

"deploy:storybook": "mkdir -p ../build && NODE_OPTIONS=--openssl-legacy-provider storybook-to-aws-s3 -o ../build/storybook --aws-profile=NONE --bucket-path=storybook.example.com/",
Enter fullscreen mode Exit fullscreen mode

Use the help to see what options you have with the tool.

storybook-to-aws-s3 --help
Enter fullscreen mode Exit fullscreen mode

The last step is to actually deploy some content to the site by running the script.

npm run deploy:storybook
Enter fullscreen mode Exit fullscreen mode

Now everything should be in place and there is restricted access to the storybook site at storybook.example.com.

One thing to note here is that we don’t have any TLS certificate for the site so you will need to use http and not https when accessing the site.

This is a quite simple and fast setup for making your story book more accessible for your team (and cheap).

Happy surfing!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .