とりあえずReactをIaCを使ってCloudFrontにデプロイする

## はじめに

Reactアプリのデプロイ、CloudFormationでサクッと構築したい！という方に向けて、最小限の手順でデプロイする方法を紹介します。S3でホスティングして、CloudFrontで配信するシンプルな構成です。

## 必要なもの

- AWS CLI
- make コマンド
- 以下のファイル:
  - CloudFormationテンプレート（cloudfront.yaml）
  - Makefile

## デプロイ手順

### 1. デプロイ用ファイルを用意

```yaml:cloudfront.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'CloudFormation template for React App deployment with S3 and CloudFront'

Parameters:
  Environment:
    Type: String
    Default: dev
    AllowedValues:
      - dev
      - stg
      - prod
    Description: Environment name

Resources:
  S3Bucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    Properties:
      BucketName: !Sub react-app-${Environment}-${AWS::AccountId}-${AWS::Region}
      PublicAccessBlockConfiguration:
        BlockPublicAcls: false
        BlockPublicPolicy: true
        IgnorePublicAcls: false
        RestrictPublicBuckets: true
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      VersioningConfiguration:
        Status: Enabled
      WebsiteConfiguration:
        IndexDocument: index.html
        ErrorDocument: index.html
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerPreferred
      CorsConfiguration:
        CorsRules:
          - AllowedHeaders: ['*']
            AllowedMethods: [GET, HEAD]
            AllowedOrigins: ['*']
            MaxAge: 3600

  S3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3Bucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: cloudfront.amazonaws.com
            Action: s3:GetObject
            Resource: !Sub ${S3Bucket.Arn}/*
            Condition:
              StringEquals:
                AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}

  CloudFrontOriginAccessControl:
    Type: AWS::CloudFront::OriginAccessControl
    Properties:
      OriginAccessControlConfig:
        Name: !Sub ${AWS::StackName}-OAC
        Description: Origin Access Control for S3
        SigningBehavior: always
        SigningProtocol: sigv4
        OriginAccessControlOriginType: s3

  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
          - DomainName: !Sub ${S3Bucket.RegionalDomainName}
            Id: S3Origin
            S3OriginConfig:
              OriginAccessIdentity: ''
            OriginAccessControlId: !Ref CloudFrontOriginAccessControl
        Enabled: true
        DefaultRootObject: index.html
        DefaultCacheBehavior:
          AllowedMethods:
            - GET
            - HEAD
            - OPTIONS
          CachedMethods:
            - GET
            - HEAD
            - OPTIONS
          TargetOriginId: S3Origin
          ForwardedValues:
            QueryString: false
            Cookies:
              Forward: none
          ViewerProtocolPolicy: redirect-to-https
          Compress: true
          DefaultTTL: 86400    # 1 day
          MinTTL: 0
          MaxTTL: 31536000    # 1 year
        CustomErrorResponses:
          - ErrorCode: 403
            ResponseCode: 200
            ResponsePagePath: /index.html
          - ErrorCode: 404
            ResponseCode: 200
            ResponsePagePath: /index.html
        ViewerCertificate:
          CloudFrontDefaultCertificate: true
        PriceClass: PriceClass_100
        HttpVersion: http2
        IPV6Enabled: true

Outputs:
  BucketName:
    Description: Name of S3 bucket to hold website content
    Value: !Ref S3Bucket

  BucketDomainName:
    Description: Domain name of S3 bucket
    Value: !GetAtt S3Bucket.DomainName

  CloudFrontDistributionId:
    Description: ID of CloudFront distribution
    Value: !Ref CloudFrontDistribution

  CloudFrontDomainName:
    Description: Domain name of CloudFront distribution
    Value: !GetAtt CloudFrontDistribution.DomainName

  WebsiteURL:
    Description: URL for website hosted on S3
    Value: !Sub https://${CloudFrontDistribution.DomainName}
```

まず、最小限必要なファイルを配置します。
```
your-project/
  ├── templates/
  │   └── cloudfront.yaml  # CloudFormationテンプレート
  ├── ui/
  │   └── chat-app/
  │       └── dist/        # Reactのビルドファイル
  └── Makefile
```

### 2. デプロイコマンドを実行

コマンドを実行させるために下のmakefileを作成して下さい。

```makefile
# 環境変数
REGION ?= us-east-1
ENV ?= dev
STACK_NAME ?= react-app-stack
TEMPLATE_PATH ?= templates/cloudfront.yaml
DIST_PATH ?= ui/chat-app/dist

# バケット名を取得するヘルパー関数
define get_bucket_name
$(shell aws cloudformation describe-stacks \
    --stack-name $(STACK_NAME) \
    --region $(REGION) \
    --query 'Stacks[0].Outputs[?OutputKey==`BucketName`].OutputValue' \
    --output text)
endef

# UIデプロイ
.PHONY: ui-deploy
ui-deploy:
	aws cloudformation create-stack \
		--stack-name $(STACK_NAME) \
		--template-body file://$(TEMPLATE_PATH) \
		--region $(REGION) \
		--parameters ParameterKey=Environment,ParameterValue=$(ENV)

# UIスタック更新
.PHONY: ui-update
ui-update:
	aws cloudformation update-stack \
		--stack-name $(STACK_NAME) \
		--template-body file://$(TEMPLATE_PATH) \
		--region $(REGION) \
		--parameters ParameterKey=Environment,ParameterValue=$(ENV)

# UIスタック削除
.PHONY: ui-delete
ui-delete:
	aws cloudformation delete-stack \
		--stack-name $(STACK_NAME) \
		--region $(REGION)

# UIスタック状態確認
.PHONY: ui-status
ui-status:
	aws cloudformation describe-stacks \
		--stack-name $(STACK_NAME) \
		--region $(REGION)

# UIバケット名を取得
.PHONY: ui-get-bucket
ui-get-bucket:
	@echo "Retrieving bucket name..."
	@echo "Bucket name: $(call get_bucket_name)"

# UIS3同期
.PHONY: ui-sync
ui-sync:
	@echo "Syncing UI files to bucket..."
	aws s3 sync $(DIST_PATH) s3://$(call get_bucket_name)

# UIキャッシュ無効化
.PHONY: ui-invalidate
ui-invalidate:
	@echo "Invalidating CloudFront cache..."
	aws cloudfront create-invalidation \
		--distribution-id $$(aws cloudformation describe-stacks \
			--stack-name $(STACK_NAME) \
			--region $(REGION) \
			--query 'Stacks[0].Outputs[?OutputKey==`CloudFrontDistributionId`].OutputValue' \
			--output text) \
		--paths "/*"

# ヘルプ
.PHONY: help
help:
	@echo "UI関連コマンド:"
	@echo "  make ui-deploy            - UIのCloudFormationスタックをデプロイ"
	@echo "  make ui-deploy ENV=prod   - UI本番環境にデプロイ"
	@echo "  make ui-update           - UIスタックを更新"
	@echo "  make ui-delete           - UIスタックを削除"
	@echo "  make ui-status           - UIスタックの状態を確認"
	@echo "  make ui-sync             - UIビルドファイルをS3に同期"
	@echo "  make ui-invalidate       - CloudFrontのキャッシュを無効化"
	@echo ""
	@echo "環境変数:"
	@echo "  REGION         - AWSリージョン (デフォルト: us-east-1)"
	@echo "  ENV            - 環境名 (デフォルト: dev)"
	@echo "  STACK_NAME     - スタック名 (デフォルト: react-app-stack)"
	@echo "  TEMPLATE_PATH  - テンプレートパス (デフォルト: templates/cloudfront.yaml)"
	@echo "  DIST_PATH      - ビルドファイルパス (デフォルト: ui/chat-app/dist)"
```

たった3ステップでデプロイ完了します：

```bash
# 1. インフラをデプロイ
make ui-deploy

# 2. Reactのビルドファイルをアップロード
make ui-sync

# 3. CloudFrontのキャッシュを無効化
make ui-invalidate
```

以上です！CloudFormationのOutputsに表示されるURLにアクセスすれば、アプリが表示されます。

## インフラの中身

CloudFormationテンプレートで作成されるリソースは以下の通りです：

- S3バケット
  - Reactアプリのホスティング用
  - 暗号化有効
  - バージョニング有効

- CloudFront
  - HTTPSリダイレクト
  - キャッシュ設定（デフォルト1日）
  - SPA対応（404/403を200にリダイレクト）

## 便利な運用コマンド

```bash
# スタックの状態確認
make ui-status

# バケット名の確認
make ui-get-bucket

# 環境の指定（dev/stg/prod）
make ui-deploy ENV=prod

# スタックの削除
make ui-delete
```

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

- 環境変数で設定変更が可能
  ```bash
  REGION       # AWSリージョン
  ENV          # 環境名（dev/stg/prod）
  STACK_NAME   # スタック名
  ```

- CloudFormationテンプレートの主な設定箇所
  - バケット名のフォーマット
  - CloudFrontのキャッシュ設定
  - CORSの設定

## まとめ

これで最小構成のデプロイ環境の構築は完了です。あとは必要に応じて：

- Route 53でドメイン設定
- WAFでセキュリティ設定
- CodePipelineで自動デプロイ

などを追加していけば、本番環境としても使える構成になります。

CloudFormationを使うことで、環境の再現性が担保され、複数環境の管理も容易になります。まずはこのベース構成から始めて、必要な機能を追加していくアプローチがおすすめです。