Host Storybook on AWS S3
Tags: 100daystooffload, storybook, aws, cdk
Published: 2023-09-05 19:16:31

[Storybook](https://storybook.js.org/) 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](https://aws.amazon.com/s3/) to store all the assets
- [CDK](https://aws.amazon.com/cdk/) with TypeScript to define the infrastructure as code
- IP access list to restrict access to the site
- [Route53](https://aws.amazon.com/route53/) to enable the use of a custom domain name
- [AWS CLI,](https://aws.amazon.com/cli/) tool manage AWS services

Begin with installing the AWS CLI

```bash
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install**
```

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.

```bash
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 .
```

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

```tsx
#!/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>' },
});
```

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.

```tsx
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)]
}
```

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

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

import { Construct } from 'constructs'
```

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

```tsx
// 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
  }
```

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.

```tsx
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,
    })
  }
```

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.

```tsx
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)
  }
```

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.

```bash
npx cdk deploy StorybookHostingStack
```

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](https://github.com/storybook-eol/storybook-deployer) to our web project.

```bash
npm i @storybook/storybook-deployer --save-dev
```

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.

```bash
"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/",
```

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

```bash
storybook-to-aws-s3 --help
```

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

```bash
npm run deploy:storybook
```

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!