AWS初心者ハンズオン - Lambda+API Gateway+DynamoDBでメモアプリAPIを作ろう【Windows対応】

AWS Basic
スポンサーリンク
スポンサーリンク

はじめに

「AWSのサーバーレスを試してみたい」「Lambda・API Gateway・DynamoDBを組み合わせて何か作ってみたい」と思っていませんか?

この記事では、AWS SAM(Serverless Application Model) を使って、メモの作成・取得・更新・削除ができるREST APIをゼロから構築するハンズオンを紹介します。

このハンズオンで実際に作るもの:

インターネット
  ↓ HTTPS
API Gateway(Prod ステージ)
  ↓ プロキシ統合
Lambda(Python 3.12)
  ↓ boto3
DynamoDB(メモデータを保存)

この記事の特徴:

  • Macユーザー向けの記事が多い中、Windows(VSCode)での実際の操作手順を詳細に解説
  • 実際にハマった Windowsならではのエラーと対処法 を丁寧に紹介
  • VSCode上でCMD・PowerShell・Git Bashを使い分ける際の注意点も解説
  • ハンズオン完了後のリソース削除手順まで含めた完全ガイド

Amazon検索[本 AWS 開発]

前提条件

以下のツールがインストール・設定済みであることを確認してください。

必要なツール

ツール確認コマンド最低バージョン目安
AWS CLI v2aws --version2.x
AWS SAM CLIsam --version1.x
Pythonpython --version3.12推奨
Gitgit --version-
VSCode-最新版推奨

AWS認証の確認

aws sts get-caller-identity

上記コマンドでアカウントIDが表示されれば認証設定済みです。表示されない場合は aws configure で認証情報を設定してください。


アーキテクチャと使用サービス

使用するAWSサービス

サービス役割料金
API GatewayHTTPリクエストの受付・ルーティング100万リクエスト/$3.50
Lambdaビジネスロジックの実行(Python)100万リクエスト無料枠あり
DynamoDBメモデータの永続化(NoSQL)PAY_PER_REQUESTで使った分だけ
S3SAMのデプロイパッケージ置き場自動作成・少量なのでほぼ無料

学習目的の短時間ハンズオンであれば、ほぼ無料枠内で収まります。
ただし、ハンズオン完了後は必ずリソースを削除してください。

APIエンドポイント設計

メソッドパス説明
POST/memosメモ作成
GET/memosメモ一覧取得
GET/memos/{id}メモ1件取得
PUT/memos/{id}メモ更新
DELETE/memos/{id}メモ削除

Step 1: プロジェクトフォルダの作成

フォルダ構造

memo-api-dynamodb/
├── template.yaml       # SAMテンプレート(インフラ定義)
├── samconfig.toml      # デプロイ設定(gitignore対象・手動作成が必要)
├── README.md
└── src/
    ├── app.py          # Lambda関数(CRUD処理)
    └── requirements.txt

作業ディレクトリを開く

VSCodeでターミナルを開き、プロジェクトを管理したいディレクトリに移動します。

cd C:\my-aws\aws-learning-projects
mkdir memo-api-dynamodb
cd memo-api-dynamodb
mkdir src

Step 2: SAMテンプレートの作成(template.yaml)

memo-api-dynamodb/template.yaml を作成します。このファイルがAWSリソース全体の設計図です。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Memo API with Lambda, API Gateway, and DynamoDB

Globals:
  Function:
    Runtime: python3.12
    Timeout: 30
    MemorySize: 128
    Environment:
      Variables:
        # Lambda関数からDynamoDBテーブル名を参照するための環境変数
        TABLE_NAME: !Ref MemosTable

Resources:

  # DynamoDB テーブル
  MemosTable:
    Type: AWS::DynamoDB::Table
    Properties:
      # スタック名をテーブル名に含めることで、複数環境でも名前が衝突しない
      TableName: !Sub "${AWS::StackName}-Memos"
      # PAY_PER_REQUEST: アクセスがない時間は課金ゼロ(学習・開発に最適)
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: memoId
          AttributeType: S   # S = String
      KeySchema:
        - AttributeName: memoId
          KeyType: HASH      # パーティションキー(一意のID)

  # Lambda 関数(全エンドポイントを1つの関数で処理)
  MemoFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/
      Handler: app.lambda_handler
      Description: CRUD handler for Memo API
      Policies:
        # DynamoDBCrudPolicy: SAM組み込みのポリシーテンプレート
        # GetItem / PutItem / UpdateItem / DeleteItem / Scan / Query が自動付与される
        - DynamoDBCrudPolicy:
            TableName: !Ref MemosTable
      Events:
        # POST /memos - メモ作成
        CreateMemo:
          Type: Api
          Properties:
            Path: /memos
            Method: post
        # GET /memos - メモ一覧取得
        ListMemos:
          Type: Api
          Properties:
            Path: /memos
            Method: get
        # GET /memos/{id} - メモ1件取得
        GetMemo:
          Type: Api
          Properties:
            Path: /memos/{id}
            Method: get
        # PUT /memos/{id} - メモ更新
        UpdateMemo:
          Type: Api
          Properties:
            Path: /memos/{id}
            Method: put
        # DELETE /memos/{id} - メモ削除
        DeleteMemo:
          Type: Api
          Properties:
            Path: /memos/{id}
            Method: delete

# Outputs: デプロイ後に表示される情報
Outputs:
  MemoApiUrl:
    Description: "Memo API エンドポイント URL"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod"
  MemoFunctionArn:
    Description: "Lambda 関数 ARN"
    Value: !GetAtt MemoFunction.Arn
  DynamoDBTableName:
    Description: "DynamoDB テーブル名"
    Value: !Ref MemosTable

template.yaml のポイント解説

Transform: AWS::Serverless-2016-10-31
CloudFormationをSAM用に拡張する宣言。これがないと AWS::Serverless::Function などのSAMリソースが使えません。

BillingMode: PAY_PER_REQUEST
DynamoDBの課金モード。リクエストがない時間は課金ゼロなので、学習・開発用途に最適です。

DynamoDBCrudPolicy
SAMが提供する組み込みポリシーテンプレート。自分でIAMポリシーをゼロから書く必要がなく、必要最小限の権限(CRUD操作のみ)が自動付与されます。


Step 3: Lambda関数の作成(src/app.py)

memo-api-dynamodb/src/app.py を作成します。

"""
メモアプリ API - Lambda 関数
Lambda + API Gateway + DynamoDB による CRUD API の実装
"""
import json
import os
import uuid
from datetime import datetime, timezone

import boto3

# DynamoDB クライアント初期化
# テーブル名は環境変数 TABLE_NAME から取得(template.yaml で設定)
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['TABLE_NAME'])


# -------------------------------------------------------
# ヘルパー関数
# -------------------------------------------------------

def success(body, status_code=200):
    """成功レスポンスを生成する共通関数"""
    return {
        'statusCode': status_code,
        'headers': {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*',  # CORS対応
        },
        'body': json.dumps(body, ensure_ascii=False),
    }


def error(message, status_code=400):
    """エラーレスポンスを生成する共通関数"""
    return {
        'statusCode': status_code,
        'headers': {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*',
        },
        'body': json.dumps({'error': message}, ensure_ascii=False),
    }


def now_iso():
    """現在時刻を ISO 8601 形式(UTC)で返す"""
    return datetime.now(timezone.utc).isoformat()


# -------------------------------------------------------
# CRUD 処理
# -------------------------------------------------------

def create_memo(body):
    """POST /memos - メモを新規作成する"""
    if not body:
        return error('リクエストボディが空です', 400)

    data = json.loads(body)
    title = data.get('title', '').strip()
    content = data.get('content', '').strip()

    if not title:
        return error('title は必須です', 400)

    memo_id = str(uuid.uuid4())  # UUID でユニークなIDを自動生成
    timestamp = now_iso()

    item = {
        'memoId': memo_id,
        'title': title,
        'content': content,
        'createdAt': timestamp,
        'updatedAt': timestamp,
    }
    table.put_item(Item=item)

    return success(item, 201)


def list_memos():
    """GET /memos - メモ一覧を取得する"""
    result = table.scan()  # テーブル全件取得
    items = result.get('Items', [])

    # createdAt の降順(新しい順)でソート
    items.sort(key=lambda x: x.get('createdAt', ''), reverse=True)

    return success({'memos': items, 'count': len(items)})


def get_memo(memo_id):
    """GET /memos/{id} - 指定IDのメモを1件取得する"""
    result = table.get_item(Key={'memoId': memo_id})
    item = result.get('Item')

    if not item:
        return error(f'memoId={memo_id} のメモが見つかりません', 404)

    return success(item)


def update_memo(memo_id, body):
    """PUT /memos/{id} - 指定IDのメモを更新する"""
    if not body:
        return error('リクエストボディが空です', 400)

    # 更新前に存在確認
    check = table.get_item(Key={'memoId': memo_id})
    if not check.get('Item'):
        return error(f'memoId={memo_id} のメモが見つかりません', 404)

    data = json.loads(body)
    title = data.get('title', '').strip()
    content = data.get('content', '').strip()

    if not title:
        return error('title は必須です', 400)

    timestamp = now_iso()

    result = table.update_item(
        Key={'memoId': memo_id},
        UpdateExpression='SET title = :title, content = :content, updatedAt = :updatedAt',
        ExpressionAttributeValues={
            ':title': title,
            ':content': content,
            ':updatedAt': timestamp,
        },
        ReturnValues='ALL_NEW',  # 更新後の全属性を返す
    )

    return success(result['Attributes'])


def delete_memo(memo_id):
    """DELETE /memos/{id} - 指定IDのメモを削除する"""
    # 削除前に存在確認
    check = table.get_item(Key={'memoId': memo_id})
    if not check.get('Item'):
        return error(f'memoId={memo_id} のメモが見つかりません', 404)

    table.delete_item(Key={'memoId': memo_id})

    return success({'message': f'memoId={memo_id} を削除しました'})


# -------------------------------------------------------
# エントリーポイント(ルーター)
# -------------------------------------------------------

def lambda_handler(event, context):
    """
    API Gateway からのリクエストを受け取り、
    HTTPメソッドとパスに応じて適切な関数に振り分ける
    """
    http_method = event.get('httpMethod', '')
    path = event.get('path', '')
    path_parameters = event.get('pathParameters') or {}
    body = event.get('body')

    try:
        if http_method == 'POST' and path == '/memos':          # POST /memos - メモ作成
            return create_memo(body)

        elif http_method == 'GET' and path == '/memos':         # GET /memos - メモ一覧取得
            return list_memos()

        elif http_method == 'GET' and 'id' in path_parameters:  # GET /memos/{id} - メモ1件取得
            return get_memo(path_parameters['id'])

        elif http_method == 'PUT' and 'id' in path_parameters:  # PUT /memos/{id} - メモ更新
            return update_memo(path_parameters['id'], body)

        elif http_method == 'DELETE' and 'id' in path_parameters:  # DELETE /memos/{id} - メモ削除
            return delete_memo(path_parameters['id'])

        else:
            return error('エンドポイントが存在しません', 404)

    except json.JSONDecodeError:
        return error('リクエストボディのJSONが不正です', 400)
    except Exception as e:
        print(f'Unexpected error: {e}')  # CloudWatch Logs に記録
        return error('サーバー内部エラーが発生しました', 500)

Step 4: requirements.txtの作成

memo-api-dynamodb/src/requirements.txt を作成します。

# boto3 is included in the Lambda runtime, no need to list it here.
# Add external libraries here if needed.
# Example: requests==2.31.0

【Windows注意点】requirements.txt に日本語コメントを書いてはいけない

requirements.txt に日本語コメントを書くと、sam build で以下のエラーが発生します:

Build Failed
Error: PythonPipBuilder:ResolveDependencies - 'cp932' codec can't decode byte 0x84 in position 43: illegal multibyte sequence

原因: Windows の SAM CLI(PythonPipBuilder)は requirements.txtcp932(Shift-JIS)で読み込もうとします。UTF-8で書かれた日本語文字をcp932として解釈しようとするとエラーになります。

対処: requirements.txt のコメントは必ず英語で書きましょう。template.yaml のコメントはSAMがpipに直接渡さないため日本語でも問題ありません。


Step 5: Gitリポジトリの初期化

cd C:\my-aws\aws-learning-projects\memo-api-dynamodb
git init

.gitignore を作成します:

# SAM関連(デプロイ設定・ビルド成果物)
samconfig.toml
.aws-sam/

# Python
__pycache__/
*.pyc
.venv/

# テスト用の一時ファイル
test_*.json

samconfig.toml はGit管理外にする理由: リージョン名やスタック名など環境固有の設定が含まれるためです。チームで開発する場合も、各自が自分の環境に合わせて作成します。

初期コミット:

git add template.yaml src/app.py src/requirements.txt .gitignore README.md
git commit -m "memo-api-dynamodb: プロジェクトフォルダと基本ファイルを作成"

Step 6: samconfig.tomlの作成

samconfig.toml.gitignore で管理外になっているため、手動で作成します。memo-api-dynamodb/samconfig.toml を新規作成し、以下を貼り付けてください:

version = 0.1

[default.deploy.parameters]
stack_name = "memo-api-dynamodb-stack"
resolve_s3 = true
s3_prefix = "memo-api-dynamodb-stack"
region = "ap-northeast-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
disable_rollback = true
image_repositories = []

[default.global.parameters]
region = "ap-northeast-1"

【注意】samconfig.toml の置き場所

samconfig.tomlmemo-api-dynamodb/ フォルダの直下 に置く必要があります。
別のフォルダに置いてしまうと、sam deploy で以下のエラーが発生します:

Error: Missing option '--stack-name', 'sam deploy --guided' can be used to provide and save needed parameters for future deploys.

Step 7: sam build(ビルド)

sam build

成功すると以下のように表示されます:

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Validate SAM template: sam validate
[*] Invoke Function: sam local invoke
[*] Test Function in the Cloud: sam sync --stack-name {stack-name} --watch
[*] Deploy: sam deploy --guided

Step 8: sam deploy(デプロイ)

samconfig.tomlがある場合

sam deploy

初回に対話式で設定する場合(--guided)

samconfig.toml を作成していない場合は --guided オプションで対話式に設定できます。自動的に samconfig.toml が生成されます:

sam deploy --guided

以下のように順番に聞かれます:

Stack Name [sam-app]: memo-api-dynamodb-stack   ← これを入力
AWS Region [ap-northeast-1]: Enter(そのままEnter)
Confirm changes before deploy [Y/n]: Y(そのままEnter)
Allow SAM CLI IAM role creation [Y/n]: Y(そのままEnter)
Disable rollback [y/N]: y    ← yを入力
Save arguments to configuration file [Y/n]: Y(そのままEnter)
SAM configuration file [samconfig.toml]: Enter(そのままEnter)
SAM configuration environment [default]: Enter(そのままEnter)

最後に変更内容が表示されて Deploy this changeset? と聞かれるので y を入力します。

デプロイ完了の確認

デプロイが完了すると、ターミナルに Outputs が表示されます:

CloudFormation outputs from deployed stack
---------------------------------------------------------------------------------------------------------------
Outputs
---------------------------------------------------------------------------------------------------------------
Key                 MemoApiUrl
Description         Memo API エンドポイント URL
Value               https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod
---------------------------------------------------------------------------------------------------------------

この URL を控えておいてください。 テストで使います。


Step 9: 動作テスト

デプロイで取得したURLを使ってAPIをテストします。

VSCode上のターミナルの選択肢

VSCodeでは複数のターミナルが使えますが、コマンドの書き方が異なります:

ターミナル変数定義変数参照curl特徴
CMDset VAR=値%VAR%curlWindowsデフォルト
PowerShell$VAR = "値"$VARcurl.exePowerShell組み込みcurlと区別が必要
Git BashVAR="値"${VAR}curlbash構文が使える

CMD でのテスト手順

まずAPI URLを変数にセットします:

set API_URL=https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod

POST /memos — メモを作成する

curl -s -X POST "%API_URL%/memos" -H "Content-Type: application/json" -d "{\"title\":\"初めてのメモ\",\"content\":\"DynamoDBに保存されます\"}"

レスポンス例:

{
  "memoId": "6150433b-4834-4cfe-81f5-266d3dad9e57",
  "title": "初めてのメモ",
  "content": "DynamoDBに保存されます",
  "createdAt": "2026-02-20T10:00:00.000000+00:00",
  "updatedAt": "2026-02-20T10:00:00.000000+00:00"
}

返ってきた memoId を控えておいてください。 以降のコマンドで使います。

GET /memos — メモ一覧を取得する

curl -s "%API_URL%/memos"

GET /memos/{id} — メモ1件を取得する

curl -s "%API_URL%/memos/6150433b-4834-4cfe-81f5-266d3dad9e57"

PUT /memos/{id} — メモを更新する

curl -s -X PUT "%API_URL%/memos/6150433b-4834-4cfe-81f5-266d3dad9e57" -H "Content-Type: application/json" -d "{\"title\":\"更新後タイトル\",\"content\":\"内容も更新しました\"}"

【Windows CMD 注意点】PUT/DELETEでのURL変数エラー

以下のように %MEMO_ID% を使って変数でIDを指定しようとするとエラーが発生することがあります:

set MEMO_ID=6150433b-4834-4cfe-81f5-266d3dad9e57
curl -s -X PUT "%API_URL%/memos/%MEMO_ID%" ...
400 ERROR - Bad request (Generated by cloudfront)

原因: CMDでは変数を set してもターミナルセッションを閉じると消えます。新しいターミナルを開いて %MEMO_ID% を使うと変数が未設定のまま %MEMO_ID% という文字列がURLに含まれてしまいます。CloudFrontは % をパーセントエンコーディングの開始として解釈するため、不正なURLとして400エラーを返します。

対処: UUIDを直接URLに貼り付けて実行してください。

DELETE /memos/{id} — メモを削除する

curl -s -X DELETE "%API_URL%/memos/6150433b-4834-4cfe-81f5-266d3dad9e57"

エラー確認(404テスト)

削除済みのIDで取得を試みて404が返ることを確認:

curl -s "%API_URL%/memos/6150433b-4834-4cfe-81f5-266d3dad9e57"
{"error": "memoId=6150433b-4834-4cfe-81f5-266d3dad9e57 のメモが見つかりません"}

Git Bash でのテスト手順(参考)

bash構文が使えるので、より読みやすく書けます:

API_URL="https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod"
MEMO_ID="6150433b-4834-4cfe-81f5-266d3dad9e57"

# POST
curl -s -X POST "${API_URL}/memos" \
  -H "Content-Type: application/json" \
  -d '{"title":"初めてのメモ","content":"テスト"}'

# GET一覧
curl -s "${API_URL}/memos"

# PUT
curl -s -X PUT "${API_URL}/memos/${MEMO_ID}" \
  -H "Content-Type: application/json" \
  -d '{"title":"更新後タイトル","content":"内容も更新"}'

# DELETE
curl -s -X DELETE "${API_URL}/memos/${MEMO_ID}"

VSCodeでGit Bashターミナルを使う方法:
ターミナルパネルの右上にある + ボタンの横の をクリック → Git Bash を選択


Step 10: AWS管理コンソールで確認

テスト後、AWSマネジメントコンソールからも各サービスの状態を確認できます。

DynamoDB

DynamoDB → テーブル → memo-api-dynamodb-stack-Memos → 「テーブルアイテムの探索」

作成したメモのデータが保存されていることを確認できます。

Lambda

Lambda → 関数 → memo-api-dynamodb-stack-MemoFunction-XXXX

関数の設定・環境変数・モニタリングが確認できます。

CloudWatch Logs(エラー確認)

Lambda → 対象関数 → 「モニタリング」タブ → 「CloudWatch Logs を表示」

エラーが発生した場合はここでログを確認します。print() で出力した内容もここに記録されます。


Step 11: リソースの削除

課金を止めるために、ハンズオン完了後は必ずリソースを削除してください。

sam delete --stack-name memo-api-dynamodb-stack --region ap-northeast-1

対話式で確認が入ります:

        Are you sure you want to delete the stack memo-api-dynamodb-stack in the region ap-northeast-1 ? [y/N]: y
        Are you sure you want to delete the folder memo-api-dynamodb-stack in S3 which contains the artifacts? [y/N]: y

両方 y で進めると数分で削除が完了します。

削除完了の確認

aws cloudformation describe-stacks --stack-name memo-api-dynamodb-stack --region ap-northeast-1
An error occurred (ValidationError) when calling the DescribeStacks operation: Stack with id memo-api-dynamodb-stack does not exist

このメッセージが表示されれば削除完了です。

注意: DynamoDB テーブル内のデータも同時に削除されます。必要なデータは事前にエクスポートしてください。


トラブルシューティング一覧

実際のハンズオン中に遭遇したエラーと対処法をまとめました。

症状原因対処
sam build'cp932' codec can't decode... エラーrequirements.txt に日本語コメントが含まれているコメントを英語にする
sam deployMissing option '--stack-name' エラーsamconfig.toml がない、または置き場所が違うmemo-api-dynamodb/ 直下に作成する
PUT/DELETE で CloudFront 400 Bad RequestURL に %MEMO_ID% が未展開のまま含まれているUUIDを直接URLに貼り付ける
CMD で 'API_URL' は認識されていませんbash構文(API_URL="..." )をCMDで実行したCMDでは set API_URL=... 形式を使う
PowerShell で curl がエラーPowerShell組み込みの Invoke-WebRequest が呼ばれているcurl.exe と明示的に .exe を付ける
API が 500 を返すLambda内部エラーCloudWatch Logsでエラー内容を確認

まとめ

今回のハンズオンで実現したこと:

  • SAMテンプレート1ファイルでLambda・API Gateway・DynamoDBの3サービスを一括定義
  • CRUD 5エンドポイント(POST/GET/GET{id}/PUT/DELETE)を持つREST APIを構築
  • sam buildsam deploysam deleteサーバーレスアプリのライフサイクルを体験
  • Windows特有のハマりポイント(cp932エラー・CMDの変数・curlの書き方の違い)を解消

Macで書かれたAWSチュートリアルをWindowsで試してエラーが出た経験がある方も多いと思います。この記事でそのハマりポイントが解決できれば幸いです。


SAMがやってくれていたことをコンソールで確認する

「SAMは便利だけど、裏で何をやっているのかわからない」と感じた方向けに、全く同じAPIをAWSコンソールのみで手動構築する手順を比較記事としてまとめました。

コンソールで手動構築すると何が起きるか(一部抜粋):

SAMの記述1行コンソールでやること
DynamoDBCrudPolicyIAMロール作成 + インラインポリシーのJSON作成
Events: Type: Api(5行)API Gatewayのリソース5個 + メソッド5個 + デプロイ
sam delete4サービスを依存関係の逆順に個別手動削除

操作ステップ数はSAMの約5ステップに対してコンソール手動は約50〜60クリック。SAMの価値をコンソール操作を通じて実感できます。

  • AWSコンソールだけでメモアプリAPIを構築する手順【SAMとの比較付き】
AWSコンソールだけでメモアプリAPIを構築する手順【Lambda+API Gateway+DynamoDB / SAMとの比較付き】
はじめにこの記事は、SAM版ハンズオン記事の比較版です。SAM版ではコマンド数本でインフラが完成しましたが、「その裏で何が起きているのか?」「SAMを使わない場合はどれだけ手間がかかるのか?」 を実感してもらうために、全く同じメモアプリAP...

次のステップ

このハンズオンを完成させたら、以下の発展的な内容にもチャレンジしてみてください:

  • API Key認証の追加 — 外部からの不正アクセスを防ぐ
  • Cognito認証の追加 — ユーザーごとにメモを管理する
  • フロントエンドと連携 — React/Vue.jsからこのAPIを呼び出す
  • CI/CDパイプラインの構築 — GitHub ActionsでSAMデプロイを自動化

Amazon検索[本 AWS 開発]

コメント