AWS CloudFormationでStorybookのCI/CD環境構築


## 概要

Storybookはコンポーネントのカタログやドキュメント化に重宝するツールですが、チーム内で共有するには適切なホスティング環境が必要です。この記事では、AWS CloudFormationを使って、GitHubからの自動デプロイ、Basic認証付きのStorybookホスティング環境を構築する方法を紹介します。

## 完成イメージ

以下のような環境を自動構築します：

```mermaid
flowchart TD
    Developer([開発者]) -->|コードをプッシュ| GitHub[(GitHub リポジトリ)]
    GitHub -->|変更を検知| CodePipeline[AWS CodePipeline]
    CodePipeline -->|ビルドトリガー| CodeBuild[AWS CodeBuild]
    CodeBuild -->|Storybookビルド| BuildArtifact[ビルド成果物]
    BuildArtifact -->|デプロイ| S3[(Amazon S3 バケット)]
    CodeBuild -->|キャッシュ無効化| CloudFront[Amazon CloudFront]
    CloudFront -->|コンテンツ配信| S3
    User([ユーザー]) -->|アクセス| CloudFront
    CloudFrontFunction[CloudFront Function] -->|Basic認証| CloudFront
```

## メリット

- GitHubへのプッシュで自動デプロイ
- CloudFront Functionによる簡易認証
- CloudFrontによる高速コンテンツ配信
- CloudFormationでインフラをコード化

## 構築手順

### 1. CloudFormationテンプレートの作成

まず、`storybook-cicd.yml`というファイル名でCloudFormationテンプレートを作成します：

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "Storybook CI/CD Pipeline with Basic Authentication"

Parameters:
  GitHubBranch:
    Type: String
    Default: main
    Description: デプロイするブランチ

  BasicAuthToken:
    Type: String
    Description: "Base64でエンコードされたユーザー名:パスワード"
    NoEcho: true
  
  RepositoryName:
    Type: String
    Description: "GitHubリポジトリ名（organization/repository形式）"
    
  GithubConnectionArn:
    Type: String
    Description: "AWS CodeStar GitHub接続のARN"

Resources:
  # S3バケット - Storybook用デプロイ先
  storybookBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub "${AWS::StackName}-storybook"
      AccessControl: Private
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      LifecycleConfiguration:
        Rules:
          - Id: expire-noncurrent-versions
            Status: Enabled
            NoncurrentVersionExpiration:
              NoncurrentDays: 30
          - Id: delete-incomplete-mpu-7days
            AbortIncompleteMultipartUpload:
              DaysAfterInitiation: 7
            Prefix: ""
            Status: Enabled
      VersioningConfiguration:
        Status: Enabled

  # S3バケット - パイプラインアーティファクト用
  pipelineBucket:
    Type: AWS::S3::Bucket
    Properties:
      AccessControl: Private
      BucketName: !Sub "${AWS::StackName}-pipeline"
      LifecycleConfiguration:
        Rules:
          - Id: expire-noncurrent-versions
            Status: Enabled
            NoncurrentVersionExpiration:
              NoncurrentDays: 30
          - Id: delete-incomplete-mpu-7days
            AbortIncompleteMultipartUpload:
              DaysAfterInitiation: 7
            Prefix: ""
            Status: Enabled
      VersioningConfiguration:
        Status: Enabled

  # CloudFront Function for Basic Auth
  basicAuthFunction:
    Type: AWS::CloudFront::Function
    Properties:
      Name: !Sub "${AWS::StackName}-basic-auth"
      AutoPublish: true
      FunctionConfig:
        Comment: "Basic Authentication for Storybook"
        Runtime: cloudfront-js-2.0
      FunctionCode: !Sub |
        function handler(event) {
          var request = event.request;
          var headers = request.headers;

          // echo -n username:password | base64
          var authString = "Basic ${BasicAuthToken}";

          if (
            typeof headers.authorization === "undefined" ||
            headers.authorization.value !== authString
          ) {
            return {
              statusCode: 401,
              statusDescription: "Unauthorized",
              headers: { "www-authenticate": { value: "Basic" } }
            };
          }

          return request;
        }

  # CloudFront OriginAccessControl
  storybookOAC:
    Type: AWS::CloudFront::OriginAccessControl
    Properties:
      OriginAccessControlConfig:
        Description: Origin Access Control for Storybook
        Name: !Sub ${AWS::StackName}-storybook-oac
        OriginAccessControlOriginType: s3
        SigningBehavior: always
        SigningProtocol: sigv4

  # CloudFront Distribution
  storybookDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Comment: !Sub "${AWS::StackName} Storybook Distribution with Basic Auth"
        DefaultCacheBehavior:
          AllowedMethods:
            - GET
            - HEAD
            - OPTIONS
          CachedMethods:
            - GET
            - HEAD
            - OPTIONS
          Compress: true
          DefaultTTL: 86400 # 1日
          ForwardedValues:
            Cookies:
              Forward: none
            QueryString: false
          MaxTTL: 31536000 # 1年
          MinTTL: 0
          TargetOriginId: S3Origin
          ViewerProtocolPolicy: redirect-to-https
          FunctionAssociations:
            - EventType: viewer-request
              FunctionARN: !GetAtt basicAuthFunction.FunctionARN
        DefaultRootObject: index.html
        Enabled: true
        HttpVersion: http2
        Origins:
          - DomainName: !GetAtt storybookBucket.RegionalDomainName
            Id: S3Origin
            OriginAccessControlId: !GetAtt storybookOAC.Id
            S3OriginConfig: {}
        PriceClass: PriceClass_100
        ViewerCertificate:
          CloudFrontDefaultCertificate: true

  # S3バケットポリシー - CloudFrontからのアクセス許可
  storybookBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref storybookBucket
      PolicyDocument:
        Statement:
          - Sid: AllowCloudFrontServicePrincipal
            Effect: Allow
            Principal:
              Service: cloudfront.amazonaws.com
            Action: s3:GetObject
            Resource: !Sub ${storybookBucket.Arn}/*
            Condition:
              StringEquals:
                AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${storybookDistribution}

  # CodeBuildプロジェクト
  codeBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      Artifacts:
        Type: CODEPIPELINE
      Environment:
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/amazonlinux2-x86_64-standard:5.0
        PrivilegedMode: true
        Type: LINUX_CONTAINER
        EnvironmentVariables:
          - Name: DEPLOYMENT_BUCKET
            Type: PLAINTEXT
            Value: !Ref storybookBucket
          - Name: DISTRIBUTION_ID
            Type: PLAINTEXT
            Value: !Ref storybookDistribution
      ServiceRole: !GetAtt codeBuildServiceRole.Arn
      Source:
        BuildSpec: |
          version: 0.2
          phases:
            install:
              runtime-versions:
                nodejs: 22
              commands:
                - npm install
            build:
              commands:
                - npm run build-storybook
            post_build:
              commands:
                - aws s3 sync storybook-static s3://$DEPLOYMENT_BUCKET/ --exact-timestamps --delete
                - aws cloudfront create-invalidation --distribution-id $DISTRIBUTION_ID --paths "/*"
          artifacts:
            files:
              - '**/*'
            base-directory: storybook-static
        Type: CODEPIPELINE

  # CodePipeline
  codePipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      ArtifactStore:
        Location: !Ref pipelineBucket
        Type: S3
      RoleArn: !GetAtt codePipelineServiceRole.Arn
      Stages:
        - Name: Source
          Actions:
            - Name: Source
              ActionTypeId:
                Category: Source
                Owner: AWS
                Provider: CodeStarSourceConnection
                Version: '1'
              Configuration:
                ConnectionArn: !Ref GithubConnectionArn
                DetectChanges: true
                FullRepositoryId: !Ref RepositoryName
                BranchName: !Ref GitHubBranch
                OutputArtifactFormat: CODEBUILD_CLONE_REF
              OutputArtifacts:
                - Name: SourceCode
              Region: !Ref AWS::Region
              RunOrder: 1
        - Name: Build
          Actions:
            - Name: BuildAndDeploy
              ActionTypeId:
                Category: Build
                Owner: AWS
                Provider: CodeBuild
                Version: '1'
              Configuration:
                ProjectName: !Ref codeBuildProject
              InputArtifacts:
                - Name: SourceCode
              OutputArtifacts:
                - Name: BuildOutput
              Region: !Ref AWS::Region
              RunOrder: 1
          OnFailure:
            Result: ROLLBACK

  # IAMロール - CodeBuild用
  codeBuildServiceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: codebuild.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonS3FullAccess
        - arn:aws:iam::aws:policy/CloudFrontFullAccess

  # IAMロール - CodePipeline用
  codePipelineServiceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: codepipeline.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSCodeBuildAdminAccess
        - arn:aws:iam::aws:policy/AmazonS3FullAccess
        - arn:aws:iam::aws:policy/AWSCodeStarFullAccess

Outputs:
  StorybookURL:
    Description: "Storybook URL"
    Value: !Sub "https://${storybookDistribution.DomainName}"
```

### 2. テンプレートのデプロイ

AWS CloudFormationでスタックを作成します：

```bash
# AWS CLIを使う場合
aws cloudformation create-stack \
  --stack-name storybook-cicd \
  --template-body file://storybook-cicd.yml \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameters \
    ParameterKey=GitHubBranch,ParameterValue=main \
    ParameterKey=RepositoryName,ParameterValue=your-org/your-repo \
    ParameterKey=GithubConnectionArn,ParameterValue=arn:aws:codestar-connections:region:account:connection/xxxx \
    ParameterKey=BasicAuthToken,ParameterValue=YOUR_BASE64_TOKEN
```

または、AWS Management Consoleからも作成できます。

### 3. GitHub接続の設定

CodePipelineがGitHubリポジトリからコードを取得するには、AWS CodeStar接続を設定する必要があります：

1. AWS Management Consoleで「Developer Tools」→「Settings」→「Connections」を開く
    - https://console.aws.amazon.com/codesuite/settings/connections
2. 「Create connection」をクリック
3. プロバイダーとして「GitHub」を選択し、接続名を入力
4. 「Connect to GitHub」をクリックし、認証を完了
5. 作成された接続のARNをテンプレートの`GithubConnectionArn`パラメータに設定

### 4. Basic認証の設定

Basic認証のトークンは以下のコマンドで生成できます：

```bash
echo -n "username:password" | base64
```

生成されたトークンをテンプレートの`BasicAuthToken`パラメータに設定します。

## アクセス方法

スタックが作成されると、CloudFrontのURLが出力されます。このURLにアクセスすると、Basic認証が要求されます。設定したユーザー名とパスワードを入力することでStorybookにアクセスできます。

## カスタマイズポイント

- **独自ドメインの設定**: ACM証明書を作成し、CloudFrontに関連付けることで独自ドメインが使用できます
- **認証情報の管理**: AWS Systems Manager Parameter Storeで認証情報を管理することも検討できます
- **ビルドカスタマイズ**: buildspecを修正して、Storybookのビルド前にテストを実行するなどの拡張が可能です

## まとめ

今回紹介したCloudFormationテンプレートを使えば、GitHubへのプッシュから自動デプロイまでのCI/CD環境と、Basic認証付きのStorybookホスティング環境を簡単に構築できます。CloudFront Functionsを活用することで、サーバーレスなBasic認証を実現しました。

また、CloudFormationを使用することで、環境の再現性や構成管理が容易になります。開発チームのStorybookをスムーズに共有し、コンポーネントライブラリの品質向上に役立ててください。

以上、お疲れさまでした！