署名付きURLを用いたAWS CDK（Python）によるCRUD APIの構築

# はじめに

**前回の記事**では、**AWS CDK（Python）**を活用して、**DynamoDB**、**Lambda**、および**API Gateway**を組み合わせた柔軟性の高いCRUD APIを構築し、**IPアドレス制限**および**APIキー認証**、**JWT（JSON Web Token）認証**、**OAuth2.0認証**を設定する方法について解説しました。

https://qiita.com/syukan3/items/8e8e808cedb22022534b

https://qiita.com/syukan3/items/f8ed0226a7e7a7f6bc81

https://qiita.com/syukan3/items/6446b6fb5aae8b340d5f

https://qiita.com/syukan3/items/25e11f28dd15e688417d

今回は、**署名付きURL**を用いた認証手法を導入する方法を詳しく説明します。

署名付きURLは、リクエストが特定の条件下でのみ有効であることを保証するための強力なセキュリティ手段です。

## 署名付きURL認証とは

**署名付きURL認証**は、特定のリソースへのアクセスを限定的に許可するための方法です。

署名付きURLには、アクセスを許可する権限や有効期限が含まれており、指定された条件下でのみリソースにアクセスできます。
これにより、セキュアなAPIアクセスを実現し、不正なアクセスを防止することが可能です。

**主な特徴**:

- **セキュリティの向上**: URLに署名を含めることで、認証されたリクエストのみを許可します。
- **柔軟性**: 有効期限やアクセス権限を細かく設定可能。
- **スケーラビリティ**: 大規模なシステムでも効率的に管理可能。

**注意点**:

- **有効期限の管理**: 署名付きURLの有効期限を適切に設定し、期限切れのURLを無効化する必要があります。
- **署名の保護**: 署名付きURLは機密情報として扱い、不正に共有されないように注意が必要です。

---

## プロジェクトのセットアップと前提条件

前回の記事およびこれまでの続編で構築したプロジェクトを基に、署名付きURL認証を導入します。

以下の前提条件が満たされていることを確認してください。

1. **前回までの記事のセットアップ完了**: IPアドレス制限、APIキー認証、JWT認証、OAuth2.0認証を設定したCRUD APIが既に構築・デプロイされていること。
2. **AWS CLIの設定**: 適切に設定されていること。
3. **AWS CDKのインストール**: 最新バージョンがインストールされていること。
4. **Python仮想環境のアクティブ化**: 前回と同様に設定されていること。
5. **IAMユーザーの権限**: API GatewayおよびLambdaを操作するための十分な権限が付与されていること。

---

## CDKスタックの更新

署名付きURL認証を導入するために、CDKスタックを以下のように更新します。

主な変更点は、API GatewayのIAM認証設定とLambda関数の設定です。

### 1. API GatewayのIAM認証設定

API Gatewayに対してIAM認証を適用することで、リクエストが署名されていることを検証します。

これにより、署名付きURLを使用してAPIにアクセスするクライアントのみがリクエストを行えるようになります。

`cdk_crud_api_python/cdk_crud_api_python_stack.py` を以下のように更新します。

```python
from aws_cdk import (
    Stack,
    aws_dynamodb as dynamodb,
    aws_lambda as _lambda,
    aws_apigateway as apigateway,
    aws_iam as iam,
    RemovalPolicy
)
from constructs import Construct
import os
from dotenv import load_dotenv

load_dotenv()

class CdkCrudApiPythonStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)
        
        # 環境変数からDynamoDBのキー設定を取得
        partition_key = os.getenv("DYNAMODB_PARTITION_KEY", "id")
        sort_key = os.getenv("DYNAMODB_SORT_KEY")  # 必要に応じて設定
        
        # DynamoDBテーブルの作成
        table = dynamodb.Table(
            self, "ItemsTable",
            partition_key=dynamodb.Attribute(
                name=partition_key,
                type=dynamodb.AttributeType.STRING
            ),
            sort_key=dynamodb.Attribute(name=sort_key, type=dynamodb.AttributeType.STRING) if sort_key else None,
            billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,
            removal_policy=RemovalPolicy.DESTROY  # 開発環境用。プロダクションでは注意
        )
        
        # Lambda関数の作成
        crud_lambda = _lambda.Function(
            self, "CrudFunction",
            runtime=_lambda.Runtime.PYTHON_3_9,
            handler="index.handler",
            code=_lambda.Code.from_asset("lambda"),
            environment={
                "TABLE_NAME": table.table_name
            }
        )
        
        # LambdaにDynamoDBへのアクセス権限を付与
        table.grant_read_write_data(crud_lambda)
        
        # API Gatewayの作成
        api = apigateway.RestApi(
            self, "CrudApi",
            rest_api_name="CRUD Service",
            description="This service serves CRUD operations.",
            default_cors_preflight_options=apigateway.CorsOptions(
                allow_origins=apigateway.Cors.ALL_ORIGINS,
                allow_methods=apigateway.Cors.ALL_METHODS
            ),
            endpoint_types=[apigateway.EndpointType.REGIONAL]
        )
        
        # IAMオーソライザーの作成
        iam_authorizer = apigateway.IamAuthorizer(
            self, "IAMAuthorizer"
        )
        
        # APIリソースとメソッドの設定
        items = api.root.add_resource("items")
        items.add_method(
            "GET",
            apigateway.LambdaIntegration(crud_lambda),
            authorization_type=apigateway.AuthorizationType.IAM
        )
        items.add_method(
            "POST",
            apigateway.LambdaIntegration(crud_lambda),
            authorization_type=apigateway.AuthorizationType.IAM
        )
        
        single_item = items.add_resource("{id}")
        single_item.add_method(
            "GET",
            apigateway.LambdaIntegration(crud_lambda),
            authorization_type=apigateway.AuthorizationType.IAM
        )
        single_item.add_method(
            "PUT",
            apigateway.LambdaIntegration(crud_lambda),
            authorization_type=apigateway.AuthorizationType.IAM
        )
        single_item.add_method(
            "DELETE",
            apigateway.LambdaIntegration(crud_lambda),
            authorization_type=apigateway.AuthorizationType.IAM
        )
        
        # 必要に応じてステージやデプロイメントをカスタマイズ
```

**主な変更点**:

1. **API GatewayのIAM認証設定**:
    ```python
    authorization_type=apigateway.AuthorizationType.IAM
    ```
    - 各APIメソッドに対して、`authorization_type`を`IAM`に設定。これにより、リクエストがIAM署名（署名付きURL）で認証されます。

2. **IAMオーソライザーの作成**:
    ```python
    iam_authorizer = apigateway.IamAuthorizer(
        self, "IAMAuthorizer"
    )
    ```
    - API GatewayでIAM認証を使用するためのオーソライザーを作成。

---

### 2. Lambda関数の設定

Lambda関数側では、特別な認証処理は不要です。
API Gatewayがリクエストの署名を検証し、認証済みのリクエストのみをLambda関数に渡します。
ただし、必要に応じてリクエストのコンテキスト情報を取得することが可能です。

---

## 署名付きURLの生成

署名付きURLを使用してAPIにアクセスするためには、クライアント側でリクエストをAWS Signature Version 4（SigV4）で署名する必要があります。
以下では、`boto3`を用いて署名付きURLを生成する方法を紹介します。

### 1. AWS SDKを用いた署名付きURLの生成方法

AWS SDK（`boto3`）を使用すると、簡単に署名付きURLを生成できます。
以下の手順で進めます。

### 2. `boto3`を用いた署名付きURLの生成スクリプト

以下のPythonスクリプトは、指定したAPIエンドポイントに対する署名付きURLを生成します。

```python
import boto3
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
from botocore.credentials import Credentials
from urllib.parse import urlparse, urlencode
import datetime

def generate_signed_url(method, url, region, service, credentials, expires=3600, headers=None, params=None):
    """
    指定されたAPIエンドポイントに対する署名付きURLを生成します。
    
    :param method: HTTPメソッド（GET, POST, PUT, DELETEなど）
    :param url: APIエンドポイントのURL
    :param region: AWSリージョン
    :param service: サービス名（apigateway）
    :param credentials: AWS認証情報（Credentialsオブジェクト）
    :param expires: URLの有効期限（秒）
    :param headers: リクエストヘッダー
    :param params: クエリパラメータ
    :return: 署名付きURL
    """
    parsed_url = urlparse(url)
    host = parsed_url.netloc
    path = parsed_url.path
    query = parsed_url.query

    # クエリパラメータを辞書形式に変換
    query_params = dict()
    if query:
        for q in query.split('&'):
            key, value = q.split('=')
            query_params[key] = value

    if params:
        query_params.update(params)

    canonical_querystring = urlencode(query_params)

    # リクエストオブジェクトの作成
    request = AWSRequest(method=method, url=url, headers=headers or {}, params=query_params)
    
    # SigV4署名の適用
    SigV4Auth(credentials, service, region).add_auth(request)
    
    # 署名されたURLの取得
    signed_url = request.url
    return signed_url

if __name__ == "__main__":
    # AWS認証情報の取得
    session = boto3.Session()
    credentials = session.get_credentials().get_frozen_credentials()
    
    # 署名付きURLの生成パラメータ
    method = "GET"
    api_url = "https://your-api-id.execute-api.region.amazonaws.com/prod/items"
    region = "ap-northeast-1"  # 適切なリージョンに変更
    service = "execute-api"
    expires = 3600  # 1時間
    
    signed_url = generate_signed_url(method, api_url, region, service, credentials, expires)
    
    print("署名付きURL:")
    print(signed_url)
```

**スクリプトの説明**:

- **`generate_signed_url`関数**:
    - 指定されたAPIエンドポイントに対する署名付きURLを生成します。
    - `method`: HTTPメソッド（例: GET, POST）。
    - `url`: APIエンドポイントのURL。
    - `region`: AWSリージョン（例: `ap-northeast-1`）。
    - `service`: サービス名（API Gatewayの場合は`execute-api`）。
    - `credentials`: AWS認証情報（`Credentials`オブジェクト）。
    - `expires`: URLの有効期限（秒）。
    - `headers`: リクエストヘッダー（オプション）。
    - `params`: クエリパラメータ（オプション）。

- **スクリプトの実行例**:
    - AWS認証情報を取得し、指定されたパラメータで署名付きURLを生成します。
    - 生成された署名付きURLがコンソールに出力されます。

---

## デプロイとテスト

### 1. CDKスタックのデプロイ

更新したCDKスタックをデプロイします。以下のコマンドを実行してください。

```bash
cdk deploy
```

デプロイが成功すると、API GatewayのエンドポイントURLが表示されます。これを後続のステップで使用します。

### 2. 署名付きURLの生成とAPIの呼び出し

以下の手順で署名付きURLを生成し、APIを呼び出します。

#### 1. 署名付きURLの生成

先ほど作成した`generate_signed_url.py`スクリプトを実行して、署名付きURLを生成します。

```bash
python generate_signed_url.py
```

出力例:

```
署名付きURL:
https://your-api-id.execute-api.ap-northeast-1.amazonaws.com/prod/items?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA...%2F20231116%2Fap-northeast-1%2Fexecute-api%2Faws4_request&X-Amz-Date=20231116T123456Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=abcdef1234567890...
```

#### 2. 署名付きURLを用いたAPIの呼び出し

生成された署名付きURLを使用して、APIを呼び出します。`curl`コマンドを使用してリクエストを送信します。

##### 1. 全アイテムの取得 (`GET /items`)

```bash
curl -X GET "https://your-api-id.execute-api.ap-northeast-1.amazonaws.com/prod/items?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA...&X-Amz-Date=20231116T123456Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=abcdef1234567890..."
```

##### 2. 新しいアイテムの作成 (`POST /items`)

署名付きURLは通常GETリクエストに使用されますが、POSTリクエストの場合は異なる署名プロセスが必要です。
以下は、POSTリクエストに署名を適用する方法の一例です。

```python
import boto3
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
from botocore.credentials import Credentials
from urllib.parse import urlparse, urlencode
import datetime
import json

def generate_signed_url(method, url, region, service, credentials, expires=3600, headers=None, params=None, body=None):
    parsed_url = urlparse(url)
    host = parsed_url.netloc
    path = parsed_url.path
    query = parsed_url.query

    query_params = dict()
    if query:
        for q in query.split('&'):
            key, value = q.split('=')
            query_params[key] = value

    if params:
        query_params.update(params)

    canonical_querystring = urlencode(query_params)

    # リクエストオブジェクトの作成
    request = AWSRequest(method=method, url=url, headers=headers or {}, params=query_params, data=body)
    
    # SigV4署名の適用
    SigV4Auth(credentials, service, region).add_auth(request)
    
    # 署名されたURLの取得
    signed_url = request.url
    return signed_url

if __name__ == "__main__":
    session = boto3.Session()
    credentials = session.get_credentials().get_frozen_credentials()
    
    method = "POST"
    api_url = "https://your-api-id.execute-api.ap-northeast-1.amazonaws.com/prod/items"
    region = "ap-northeast-1"
    service = "execute-api"
    expires = 3600
    
    body = json.dumps({"name": "New Item", "description": "Description of the new item"})
    
    signed_url = generate_signed_url(method, api_url, region, service, credentials, expires, headers={"Content-Type": "application/json"}, body=body)
    
    print("署名付きPOST URL:")
    print(signed_url)
```

このスクリプトを実行し、生成された署名付きURLを用いてPOSTリクエストを送信します。

```bash
curl -X POST "https://your-api-id.execute-api.ap-northeast-1.amazonaws.com/prod/items?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA...&X-Amz-Date=20231116T123456Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=abcdef1234567890..." \
     -H "Content-Type: application/json" \
     -d '{"name": "New Item", "description": "Description of the new item"}'
```

**注意**: POSTリクエストに署名を適用する際は、リクエストボディも署名に含める必要があります。上記のスクリプトでは、`body`パラメータを使用してリクエストボディを署名に含めています。

---

## 最終的なディレクトリ構成

プロジェクト全体の最終的なディレクトリ構成は以下の通りです。

```
cdk-crud-api-python/
├── app.py
├── cdk_crud_api_python
│   ├── __init__.py
│   └── cdk_crud_api_python_stack.py
├── lambda
│   └── index.py
├── generate_signed_url.py
├── requirements.txt
├── .gitignore
├── README.md
├── cdk.json
└── .env
```

**各ファイル・フォルダの役割**:

- **app.py**: CDKアプリケーションのエントリーポイント。
- **cdk_crud_api_python/**: CDKスタックの定義が含まれるパッケージ。
    - **__init__.py**: パッケージ初期化ファイル。
    - **cdk_crud_api_python_stack.py**: DynamoDB、Lambda、API Gateway、および署名付きURL認証を定義するスタックファイル。
- **lambda/**: Lambda関数のコードが含まれるディレクトリ。
    - **index.py**: CRUDロジックを実装したLambdaハンドラー。
- **generate_signed_url.py**: 署名付きURLを生成するためのスクリプト。
- **requirements.txt**: Pythonの依存関係リスト。
- **.gitignore**: Gitで追跡しないファイル・フォルダを指定。
- **README.md**: プロジェクトの説明や使用方法を記載。
- **cdk.json**: CDKの設定ファイル。
- **.env**: 環境変数を定義するファイル（セキュリティに注意）。

---

## まとめ

今回のガイドでは、**AWS CDK（Python）**を使用して構築した**CRUD API**に**署名付きURL認証**を導入する方法を詳しく解説しました。
**API Gateway**のIAM認証を活用することで、署名付きURLを用いたセキュアなAPIアクセスを実現できます。

**ポイントまとめ**:

- **API GatewayのIAM認証設定**: `AuthorizationType`を`IAM`に設定し、署名付きリクエストのみを許可。
- **署名付きURLの生成**: `boto3`を用いて、AWS Signature Version 4（SigV4）で署名されたURLを生成。
- **Lambda関数の柔軟な対応**: Lambda関数内でユーザー情報を取得・利用可能。
- **セキュリティの向上**: 署名付きURLを利用することで、不正なアクセスを防止し、セキュアなAPIアクセスを提供。

**次のステップ**:

- **署名付きURLの有効期限管理**: トークンの有効期限を適切に設定し、期限切れのURLを無効化。
- **アクセス制御の強化**: 署名付きURLに含める権限を細かく設定し、必要最低限のアクセス権を付与。
- **モニタリングとロギングの強化**: AWS CloudWatchや他のモニタリングツールを利用して、APIの使用状況やセキュリティインシデントを監視。
- **セキュリティベストプラクティスの適用**: 適切なアクセス制御、暗号化、監査ログの管理などを実施。

このガイドを参考に、ぜひ自分のプロジェクトに署名付きURL認証を導入し、よりセキュアで制御されたAPIを提供してみてください。
必要に応じて、さらなる認証・認可の強化を行い、堅牢なAPIエコシステムを構築していきましょう！

---

## 参考資料

- [AWS CDK ドキュメント](https://docs.aws.amazon.com/cdk/latest/guide/home.html)
- [AWS CDK API Reference (Python)](https://docs.aws.amazon.com/cdk/api/v2/python/index.html)
- [AWS Lambda ドキュメント](https://docs.aws.amazon.com/lambda/latest/dg/welcome.html)
- [Amazon DynamoDB ドキュメント](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html)
- [Amazon API Gateway ドキュメント](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html)
- [AWS Signature Version 4 ドキュメント](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html)
- [boto3 ドキュメント](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html)
- [Python dotenv ドキュメント](https://pypi.org/project/python-dotenv/)

---

**注記**: セキュリティの観点から、`.env`ファイルには機密情報を含めないようにし、必要に応じてAWS Secrets Managerなどのサービスを利用して機密情報を管理してください。また、署名付きURLは適切に管理し、不正アクセスを防止するために定期的にURLの有効期限を設定・更新することを推奨します。

---
### 会社紹介

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

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