CDKで実現！セキュアなSphinxホスティングIP制限とBasic認証

# はじめに

AWS CDK（Cloud Development Kit）を活用して、S3 にホスティングされた静的サイトを CloudFront で配信し、IP アドレス制限および Basic 認証を適用する方法を解説します。
さらに、GitHub リリースの作成時に GitHub Actions を利用して自動的にデプロイを行う設定も紹介します。
今回は、Lambda@Edge の代わりに CloudFront Functions を使用して Basic 認証を実装します。


## 前提条件

- **AWS アカウント**: 適切な権限を持つ AWS アカウントが必要です。
- **AWS CLI**: AWS CLI がインストールされ、設定されていること。
- **Node.js**: AWS CDK のインストールに必要です。
- **Python**: CDK スクリプトを Python で実装します。
- **GitHub リポジトリ**: デプロイ用のスクリプトを格納するリポジトリが必要です。

## ディレクトリ構成

プロジェクトのディレクトリ構成は以下のようにします。
`sphinx` ディレクトリ内に AWS CDK 用の `cdk` ディレクトリを設けます。

```
sphinx/
├── cdk/
│   ├── app.py
│   ├── requirements.txt
│   └── stack.py
└── ...（他のディレクトリやファイル）
```

- `app.py`: CDK アプリケーションのエントリポイント
- `stack.py`: CDK スタックの定義
- `requirements.txt`: 必要な Python パッケージ

## AWS CDK プロジェクトの初期化

### 1. AWS CDK のインストール

まず、AWS CDK をインストールします。
グローバルにインストールする方法と、プロジェクトごとに仮想環境を使用する方法がありますが、ここでは仮想環境を使用します。

```bash
# グローバルに CDK をインストール
npm install -g aws-cdk

# プロジェクトディレクトリに移動
cd sphinx/cdk

# Python 仮想環境の作成
python3 -m venv .venv
source .venv/bin/activate

# 必要な Python パッケージのインストール
pip install aws-cdk-lib constructs
pip freeze > requirements.txt
```

### 2. CDK アプリケーションの初期化

`app.py` を作成し、CDK アプリケーションを定義します。

```python
# sphinx/cdk/app.py
import aws_cdk as cdk
from stack import SphinxHostingStack
import os
app = cdk.App()

# コンテキストから IP アドレスと認証情報を取得
allowed_ips = os.getenv("ALLOWED_IPS", "0.0.0.0/0")
basic_auth_user = os.getenv("BASIC_AUTH_USER", "admin")
basic_auth_password = os.getenv("BASIC_AUTH_PASSWORD", "password")

SphinxHostingStack(app, "SphinxHostingStack",
    allowed_ips=allowed_ips,
    basic_auth_user=basic_auth_user,
    basic_auth_password=basic_auth_password,
)

app.synth()
```

## S3 バケットと CloudFront ディストリビューションの構築

`stack.py` に S3 バケットと CloudFront ディストリビューションの設定を追加します。

```python
import aws_cdk as cdk
import base64
from aws_cdk import (
    Stack,
    aws_s3 as s3,
    aws_cloudfront as cloudfront,
    aws_cloudfront_origins as origins,
    aws_s3_deployment as s3_deployment,
)
from constructs import Construct

class SphinxHostingStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, *, allowed_ips, basic_auth_user, basic_auth_password, **kwargs):
        super().__init__(scope, construct_id, **kwargs)

        # S3 バケットの作成
        bucket = s3.Bucket(self, "SphinxBucket",
            removal_policy=cdk.RemovalPolicy.DESTROY,
            public_read_access=False,
            block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
        )

        # オリジンアクセスアイデンティティ（OAI）の作成
        origin_access_identity = cloudfront.OriginAccessIdentity(self, "OAI")

        # S3 バケットにCloudFrontの読み取り権限を付与
        bucket.grant_read(origin_access_identity)

        # セキュリティ用のCloudFront Functionを作成
        security_function = self._create_security_function(basic_auth_user, basic_auth_password, allowed_ips)

        # CloudFront ディストリビューションの作成
        distribution = cloudfront.Distribution(self, "Distribution",
            default_behavior=cloudfront.BehaviorOptions(
                origin=origins.S3Origin(bucket, origin_access_identity=origin_access_identity),
                viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.HTTPS_ONLY,
                function_associations=[
                    cloudfront.FunctionAssociation(
                        event_type=cloudfront.FunctionEventType.VIEWER_REQUEST,
                        function=security_function,
                    )
                ],
                allowed_methods=cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
                cached_methods=cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS,
            ),
            default_root_object="index.html",
            log_file_prefix="cloudfront-logs/",
        )

        # S3にSphinxビルドファイルをデプロイ
        s3_deployment.BucketDeployment(self, "DeploySphinxDocs",
            sources=[s3_deployment.Source.asset("../build/html")],
            destination_bucket=bucket,
            distribution=distribution,
            distribution_paths=["/*"],
        )

        # 出力
        cdk.CfnOutput(self, "DistributionDomainName",
            value=distribution.domain_name,
            description="CloudFront Distribution Domain Name",
        )

    def _create_security_function(self, user, password, allowed_ips):
        # Basic認証とIP制限を統合したCloudFront Functionを作成
        basic_auth = f"{user}:{password}"
        basic_auth_encoded = base64.b64encode(basic_auth.encode()).decode()
        ip_list = [ip.strip() for ip in allowed_ips.split(',')]
        allowed_ips_js_array = ', '.join(f'"{ip}"' for ip in ip_list)

        function_code = f"""
function handler(event) {{
    var request = event.request;
    var headers = request.headers;
    var clientIp = event.viewer.ip;

    // IPv4アドレスを整数に変換
    function ipv4ToInt(ip) {{
        return ip.split('.').reduce(function (intIp, octet) {{
            return (intIp << 8) + parseInt(octet, 10);
        }}, 0) >>> 0;
    }}

    // IPアドレスがレンジ内にあるかをチェック
    function ipInRange(ip, range) {{
        var parts = range.split('/');
        var rangeIp = parts[0];
        var maskSize = parts.length > 1 ? parseInt(parts[1]) : 32;
        var ipInt = ipv4ToInt(ip);
        var rangeIpInt = ipv4ToInt(rangeIp);
        var mask = ~(Math.pow(2, 32 - maskSize) - 1);
        return (ipInt & mask) === (rangeIpInt & mask);
    }}

    var allowedRanges = [{allowed_ips_js_array}];

    var ipAllowed = false;
    for (var i = 0; i < allowedRanges.length; i++) {{
        if (ipInRange(clientIp, allowedRanges[i])) {{
            ipAllowed = true;
            break;
        }}
    }}

    if (!ipAllowed) {{
        return {{
            statusCode: 403,
            statusDescription: "Forbidden",
            body: "Access Denied"
        }};
    }}

    var authString = "Basic {basic_auth_encoded}";

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

    return request;
}}
"""

        return cloudfront.Function(self, "SecurityFunction",
            code=cloudfront.FunctionCode.from_inline(function_code),
        )
```

### 詳細説明

- **S3 バケット**: 静的ウェブサイトをホスティングするためのバケットを作成します。公開アクセスをブロックし、CloudFront からのみアクセスできるように設定します。
  
- **Origin Access Identity (OAI)**: CloudFront が S3 バケットにアクセスするための権限を持つ OAI を作成し、バケットポリシーを設定します。
  
- **CloudFront ディストリビューション**: S3 バケットをオリジンとする CloudFront ディストリビューションを作成します。HTTPS リダイレクトを有効にし、CloudFront Functions を使用して Basic 認証を実装します。
  
- **IP アドレス制限**: CloudFront の `ip_address_allow_list` を使用して、特定の IP アドレスからのみアクセスを許可します。
  
- **Basic 認証**: CloudFront Functions を使用して、Basic 認証を実装します。Lambda@Edge の代わりに CloudFront Functions を使用することで、より低レイテンシかつコスト効率の良い認証を実現します。

## IP アドレス制限の設定

IP アドレス制限は、CloudFront の `ip_address_allow_list` プロパティを使用して設定します。
`allowed_ips` はカンマ区切りで GitHub Variables に保存されており、CDK スクリプトで動的に読み込まれます。

```python
def _get_allowed_ips(self, allowed_ips_str):
    # カンマ区切りのIPアドレスをリストに変換
    ip_list = [ip.strip() for ip in allowed_ips_str.split(",")]
    return [cloudfront.IPAddressRange(ip) for ip in ip_list]
```

`ALLOWED_IPS` 変数に以下のように設定します（カンマ区切りで複数の IP を指定）。

```
203.0.113.0,198.51.100.0
```

## CloudFront Functions を用いた Basic 認証の実装

Lambda@Edge の代わりに CloudFront Functions を使用して Basic 認証を実装します。
CloudFront Functions は、軽量な JavaScript ベースの関数であり、低レイテンシかつコスト効率の良い処理が可能です。

### CloudFront Function の作成

`stack.py` 内の `_create_basic_auth_function` メソッドで CloudFront Function を作成します。



## GitHub Actions の設定

GitHub リポジトリでリリースが作成された際に、AWS CDK スクリプトを自動的にデプロイするために GitHub Actions を設定します。

### 1. GitHub Variables の追加

まず、リポジトリの Variables に以下の値を追加します。
これにより、機密情報をコードにハードコーディングすることなく、安全に管理できます。

1. **リポジトリの設定ページに移動**:
   - GitHub リポジトリのページで、「Settings」をクリックします。
2. **Variables に移動**:
   - 左側のメニューから「Secrets and variables」 > 「Variables」を選択します。
3. **新しい Variable の追加**:
   - 「New repository variable」をクリックし、以下の Variables を追加します。

| 変数名                 | 説明                         | 値                               |
|------------------------|------------------------------|----------------------------------|
| `ALLOWED_IPS`          | 許可するIPアドレスのリスト   | `203.0.113.0/24,198.51.100.0/24` |
| `BASIC_AUTH_USER`      | Basic 認証のユーザー名       | `your_username`                   |

**注意**: `ALLOWED_IPS` はカンマ区切りで複数の IP アドレスを指定します。

### 2. GitHub Secrets の追加

さらに、AWS の認証情報を GitHub Secrets に追加します。これにより、AWS へのアクセスを安全に行えます。

1. **リポジトリの設定ページに移動**:
   - GitHub リポジトリのページで、「Settings」をクリックします。
2. **Secrets に移動**:
   - 左側のメニューから「Secrets and variables」 > 「Actions」を選択します。
3. **新しい Secret の追加**:
   - 「New repository secret」をクリックし、以下の Secrets を追加します。

| シークレット名           | 説明                         | 値                               |
|--------------------------|------------------------------|----------------------------------|
| `AWS_ACCESS_KEY_ID`      | AWS アクセスキーID           | `YOUR_AWS_ACCESS_KEY_ID`         |
| `AWS_SECRET_ACCESS_KEY`  | AWS シークレットアクセスキー | `YOUR_AWS_SECRET_ACCESS_KEY`     |
| `AWS_ACCOUNT_ID`         | AWS アカウントID             | `YOUR_AWS_ACCOUNT_ID`            |
| `BASIC_AUTH_PASSWORD`  | Basic 認証のパスワード       | `your_password`                   |

### 3. GitHub Actions ワークフローの作成

`.github/workflows/deploy.yml` を作成し、以下の内容を追加します。

```yaml
name: Deploy CDK on Release

on:
  release:
    types: [published]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: コードのチェックアウト
        uses: actions/checkout@v4

      - name: Node.js のセットアップ
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: CDK CLI のインストール
        run: npm install -g aws-cdk

      - name: Python のセットアップ
        uses: actions/setup-python@v5
        with:
          python-version: '3.13'

      - name: 依存関係のインストールとドキュメントのビルド
        run: |
          # 仮想環境の作成と有効化
          cd cdk
          python -m venv .venv
          source .venv/bin/activate

          # CDKアプリの依存関係のインストール
          pip install -r requirements.txt

          # プロジェクトルートに戻る
          cd ..

          # Sphinxと必要なパッケージのインストール
          pip install sphinx sphinxcontrib-mermaid furo myst-parser

          # ドキュメントのビルド
          make html SPHINXOPTS="-E"

      - name: AWS 認証情報の設定
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1

      - name: CDK ブートストラップ
        run: |
          cd cdk
          source .venv/bin/activate
          cdk bootstrap aws://${{ secrets.AWS_ACCOUNT_ID }}/ap-northeast-1

      - name: CDK デプロイ
        env:
          ALLOWED_IPS: "${{ vars.ALLOWED_IPS }}"
          BASIC_AUTH_USER: ${{ vars.BASIC_AUTH_USER }}
          BASIC_AUTH_PASSWORD: ${{ secrets.BASIC_AUTH_PASSWORD }}
          AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}
        run: |
          cd cdk
          source .venv/bin/activate
          cdk deploy --app "python app.py" --require-approval never
```

### ワークフローの説明

1. **トリガー**: リリースが公開されたとき (`release.published`) にワークフローが実行されます。
2. **ジョブのステップ**:
   - **コードのチェックアウト**: リポジトリのコードを取得します。
   - **Python 環境のセットアップ**: 指定した Python バージョンを設定します。
   - **依存関係のインストール**: 仮想環境を作成し、必要な Python パッケージをインストールします。
   - **AWS 認証情報の設定**: GitHub Secrets に保存された AWS 認証情報を設定します。
   - **CDK ブートストラップ**: 指定リージョン（ap-northeast-1 および us-east-1）に CDK のブートストラップを実行します。
   - **CDK デプロイ**: CDK スクリプトをデプロイします。`--context` オプションを使用して、Variables から取得した値を渡します。

## 最小限のIAMポリシーの作成

AWS CDK を用いてリソースをデプロイする際、使用する IAM ユーザーには必要最低限の権限を付与することが重要です。
これにより、セキュリティを強化し、不必要なリスクを回避できます。以下に、今回の構成に必要な最小限の IAM ポリシーを示します。

### 1. ポリシーの概要

このポリシーは、S3 バケットの作成・管理、CloudFront ディストリビューションの作成・管理、CloudFront Functions の管理、そして必要な IAM 操作を行うための権限を付与します。

### 2. IAM ポリシーの作成手順

#### AWS マネジメントコンソールを使用する場合

1. **IAM コンソールにアクセス**:
   - AWS マネジメントコンソールで「IAM」を検索し、IAM コンソールにアクセスします。

2. **ポリシーの作成**:
   - 左側のメニューから「Policies（ポリシー）」を選択し、「Create policy（ポリシーの作成）」をクリックします。

3. **JSON タブを選択**:
   - 「JSON」タブを選択し、以下のポリシーを貼り付けます。

4. **ポリシーの確認と作成**:
   - 「Review policy（ポリシーの確認）」をクリックし、ポリシー名（例: `CDKMinimalPolicy`）と説明を入力して「Create policy（ポリシーの作成）」をクリックします。

#### ポリシーの内容

以下は、今回のデプロイに必要な最小限の IAM ポリシーです。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:CreateBucket",
                "s3:DeleteBucket",
                "s3:ListBucket",
                "s3:GetBucketPolicy",
                "s3:PutBucketPolicy",
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject",
                "s3:ListAllMyBuckets",
                "s3:PutEncryptionConfiguration",
                "s3:PutLifecycleConfiguration",
                "s3:PutBucketVersioning",
                "s3:PutBucketPublicAccessBlock",
                "s3:DeleteBucketPolicy"
            ],
            "Resource": [
                "arn:aws:s3:::*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "cloudfront:CreateDistribution",
                "cloudfront:DeleteDistribution",
                "cloudfront:GetDistribution",
                "cloudfront:UpdateDistribution",
                "cloudfront:ListDistributions",
                "cloudfront:CreateFunction",
                "cloudfront:DeleteFunction",
                "cloudfront:GetFunction",
                "cloudfront:UpdateFunction",
                "cloudfront:ListFunctions"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "cloudfront:PublishFunction",
                "cloudfront:TestFunction"
            ],
            "Resource": "arn:aws:cloudfront::<YOUR_AWS_ACCOUNT_ID>:function/*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "iam:PassRole",
                "iam:GetRole",
                "iam:DeleteRole",
                "iam:DeleteRolePolicy",
                "iam:DetachRolePolicy",
                "iam:TagRole",
                "iam:UntagRole",
                "iam:CreateRole",
                "iam:AttachRolePolicy",
                "iam:PutRolePolicy",
                "iam:GetRolePolicy"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "sts:AssumeRole"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "cloudformation:DescribeStacks",
                "cloudformation:CreateStack",
                "cloudformation:DeleteStack",
                "cloudformation:UpdateStack",
                "cloudformation:ListStacks",
                "cloudformation:DescribeStackResources",
                "cloudformation:DescribeStackEvents",
                "cloudformation:GetTemplate",
                "cloudformation:CreateChangeSet",
                "cloudformation:DescribeChangeSet",
                "cloudformation:DeleteChangeSet",
                "cloudformation:ExecuteChangeSet"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ecr:CreateRepository",
                "ecr:DeleteRepository",
                "ecr:PutLifecyclePolicy",
                "ecr:SetRepositoryPolicy",
                "ecr:DescribeRepositories"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ssm:PutParameter",
                "ssm:DeleteParameter",
                "ssm:GetParameter",
                "ssm:GetParameters"
            ],
            "Resource": "*"
        }
    ]
}
```

**注意**: `YOUR_AWS_ACCOUNT_ID` を自身の AWS アカウント ID に置き換えてください。

### 3. ポリシーの説明



### 4. IAM ユーザーへのポリシーのアタッチ

作成したポリシーをデプロイに使用する IAM ユーザーにアタッチします。




## セキュリティとベストプラクティス

### 認証情報の管理

- **Secrets の使用**: AWS 認証情報、`BASIC_AUTH_PASSWORD` は GitHub Secrets に安全に保管し、ワークフロー内でのみ使用します。
- **Variables の使用**: `ALLOWED_IPS`, `BASIC_AUTH_USER` は GitHub Variables に設定し、ワークフロー内で使用します。
- **最小権限の原則**: AWS IAM ユーザーには必要最低限の権限を付与します。CDK デプロイに必要な権限のみを許可します。

### Basic 認証の強化

- **パスワードの強化**: Basic 認証のパスワードは強力なものを使用し、定期的に変更します。
- **認証情報の暗号化**: 認証情報はコード内にハードコーディングせず、環境変数や Variables を通じて安全に管理します。

### ネットワークのセキュリティ

- **IP 制限の適用**: 指定された IP アドレスからのみアクセスを許可することで、セキュリティを強化します。
- **HTTPS の強制**: CloudFront で HTTPS を強制し、データの暗号化を確保します。

### モニタリングとロギング

- **アクセスログの有効化**: CloudFront と S3 のアクセスログを有効にし、アクセス状況をモニタリングします。
- **ログの確認**: CloudFront Functions の実行ログは CloudWatch に送信されるため、問題発生時に確認できます。

### コスト管理

- **リソースの最適化**: 使用しないリソースは削除し、コストを最適化します。
- **CloudFront Functions の利用**: Lambda@Edge よりもコスト効率が高いため、軽量な処理には CloudFront Functions を活用します。

## まとめ

本記事では、AWS CDK を使用して S3 にホスティングされた静的サイトを CloudFront で配信し、IP アドレス制限と Basic 認証を CloudFront Functions を用いて実装する方法を解説しました。
また、GitHub Actions を利用して、GitHub リリース作成時に自動的にデプロイを行う設定も紹介しました。

**主なポイント**:

1. **AWS CDK の活用**: インフラをコードとして管理し、再現性の高いデプロイを実現。
2. **CloudFront Functions の使用**: Lambda@Edge よりも低レイテンシかつコスト効率の高い認証処理を実現。
3. **GitHub Actions との連携**: リリース時に自動デプロイを行い、開発フローを効率化。
4. **セキュリティの強化**: Variables と Secrets を適切に管理し、最小権限の原則を遵守。

**注意点**:

- **CloudFront Functions の制約**: JavaScript ベースであり、実行時間やメモリに制限があります。複雑な処理には向きません。
- **認証情報の管理**: 認証情報は必ず安全に管理し、漏洩しないよう注意してください。
- **リージョンの選定**: CloudFront Functions は全リージョンで利用可能ですが、CDK のデプロイ先リージョンは `ap-northeast-1` に設定しています。

これらの手順を踏むことで、安全かつ効率的に静的サイトをホスティングし、アクセス制限や認証を適用することができます。

ぜひ試してみてください！

質問やフィードバックがあれば、コメント欄でお知らせください。

---
### 会社紹介

**株式会社 Mosaica**  
最先端テクノロジーで社会課題を解決し、持続可能な未来を創造する IT カンパニー。
AI ソリューション、クラウド統合、DX 推進、経営コンサルティングなど包括的なサービスでビジネス変革を支援しています。  

詳しくは [公式サイト](https://mosaica.co.jp/) までお気軽にご相談ください。
公式サイト： https://mosaica.co.jp/