Skip to content
This repository was archived by the owner on Jun 23, 2026. It is now read-only.

Latest commit

 

History

History
423 lines (332 loc) · 19.4 KB

File metadata and controls

423 lines (332 loc) · 19.4 KB
awscape

English · 日本語

Always Free Python 3.13 AWS Lambda 30 min hands-on

30 分で動く小さくて実用的な AWS パイプラインを作るハンズオン。 終わるころには S3 / Lambda / DynamoDB / IAM / CloudWatch / SNS / Budgets / AWS CLI を一通り触っている状態になる。 全部 Always Free 枠の中 で完結するように設計してあるので、 6 ヶ月の無料クレジットが切れた後も請求は 0 円のまま。

TL;DR

git clone https://github.com/kanywst/awscape.git && cd awscape
cp config.env.example config.env  # BUCKET と EMAIL を書き換える
./scripts/setup.sh                # パイプラインを一発構築
./scripts/cleanup.sh              # 全部削除

作るもの

graph LR
    User[👤 自分の PC]

    S3[S3 バケット<br/>uploads/]
    Lambda[Lambda 関数<br/>Python 3.13]
    DDB[DynamoDB テーブル<br/>UploadLog]
    Logs[CloudWatch Logs<br/>+ Metrics]
    Alarm[CloudWatch Alarm<br/>+ Budgets]

    User -->|1 ファイルアップロード| S3
    S3 -->|2 イベント通知| Lambda
    Lambda -->|3 アイテム書き込み| DDB
    Lambda -.->|4 ログ出力| Logs
    Logs -.->|5 監視| Alarm

    classDef user fill:#fef3c7,stroke:#b45309,color:#000,stroke-width:2px
    classDef s3 fill:#dbeafe,stroke:#1d4ed8,color:#000,stroke-width:2px
    classDef compute fill:#fed7aa,stroke:#c2410c,color:#000,stroke-width:2px
    classDef db fill:#dcfce7,stroke:#16a34a,color:#000,stroke-width:2px
    classDef mon fill:#fce7f3,stroke:#be185d,color:#000,stroke-width:2px
    class User user
    class S3 s3
    class Lambda compute
    class DDB db
    class Logs,Alarm mon
Loading

S3 にファイルを置く → Lambda が発火 → DynamoDB にレコードが入る → CloudWatch にログとメトリクスが残る。 Lambda が失敗すれば SNS からメールが飛ぶ。 上限を超えなければコストは 0 円。

2 つの走らせ方

モード こういうとき やり方
手動 初回、 もしくはちゃんと理解したいとき 下の「ハンズオン」 セクションを上から順にコピペで実行する
自動 一度やった、 再度立てるだけでよいとき cp config.env.example config.env で値を書き換えて ./scripts/setup.sh

どちらでも片付けは ./scripts/cleanup.sh で一発。

始める前に

必要なもの。

  • AWS アカウント (Free Plan で OK、 詳細は次節)
  • Root ユーザに MFA 設定済み、 アクセスキーは作らない
  • 管理者権限を持つ IAM ユーザ + そのユーザの MFA 設定済み (作業はこちらでログイン、 Root では作業しない)
  • AWS Budgets で最低でも Zero spend アラートを設定
  • AWS CLI v2 をインストールして aws configure 済み
  • 操作する Region を 1 つに固定 (AWS_REGION)

上から 3 つは必須。 アクセスキーを GitHub に commit してしまって 「AWS から数十万円請求が来た」 の事故は毎月どこかで起きている。

2026 年の AWS 無料プラン (60 秒で)

2025-07-15 にモデルが変わった。 それ以降に作ったアカウントは新しいクレジット制プラン。 それ以前は旧 12 ヶ月無料枠。 どちらの下にも Always Free が走っている。

graph TB
    subgraph New["新フリープラン (2025-07-15 以降に作成)"]
        FreePlan[Free Plan<br/>= クレジット制 + 6 ヶ月期限]
        PaidPlan[Paid Plan<br/>= フル機能 + 通常課金]
    end

    subgraph Legacy["レガシー (2025-07-15 以前に作成)"]
        Legacy12[12 ヶ月無料枠<br/>EC2 750h、 S3 5 GB など]
    end

    subgraph AF["Always Free (全アカウント共通)"]
        Items[Lambda 100 万 req/月<br/>DynamoDB 25 GB<br/>S3 5 GB<br/>CloudWatch 10 メトリクス<br/>... 30 以上のサービス]
    end

    FreePlan -.->|6 ヶ月後 expire<br/>アカウント自動クローズ| AF
    Legacy12 -.->|12 ヶ月後 expire<br/>アカウントは残る| AF
    PaidPlan --> AF

    classDef new fill:#dbeafe,stroke:#1d4ed8,color:#000,stroke-width:2px
    classDef legacy fill:#fef3c7,stroke:#b45309,color:#000,stroke-width:2px
    classDef always fill:#dcfce7,stroke:#16a34a,color:#000,stroke-width:3px
    class FreePlan,PaidPlan new
    class Legacy12 legacy
    class Items always
Loading

このリポジトリは Always Free のみで完結する設計。 2026 年時点の枠は以下のとおり。

サービス Always Free 枠
Lambda 100 万 req + 40 万 GB 秒
DynamoDB 25 GB + 25 RCU + 25 WCU (プロビジョンド Standard クラスのみ、 オンデマンドは対象外)
S3 5 GB + 20,000 GET + 2,000 PUT
CloudWatch メトリクス/アラーム カスタムメトリクス 10 + アラーム 10 + API 100 万
CloudWatch Logs 取込 5 GB + アーカイブ 5 GB + Logs Insights スキャン 5 GB
SNS 100 万 publish

DynamoDB の注釈に注意。 Always Free は プロビジョンドモードのみ。 オンデマンド (PAY_PER_REQUEST) は無料枠の対象外。 このリポジトリは 25/25 のプロビジョンドで作る。

ハンズオン (上から順に実行)

Region と グローバル一意なバケット名を決めて、 シェルに export する。

export AWS_REGION=ap-northeast-1
export BUCKET=awscape-handson-YOUR_HANDLE
export TABLE=UploadLog
export FUNCTION=HandleUpload
export ROLE_NAME=HandleUploadRole
export TOPIC_NAME=awscape-alarms
export EMAIL=you@example.com
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
echo "$ACCOUNT_ID"

1. S3 バケットを作る

aws s3 mb "s3://${BUCKET}" --region "$AWS_REGION"

新規バケットは Block Public Access がデフォルト ON のはず。 確認する。

aws s3api get-public-access-block --bucket "$BUCKET"

4 つとも true なら OK。 1 つでも false だったら全部 ON にする。

aws s3api put-public-access-block --bucket "$BUCKET" \
    --public-access-block-configuration "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

2. DynamoDB テーブルを作る

プロビジョンド 25 WCU / 25 RCU、 Standard クラス。 これがちょうど Always Free 枠と一致する。

aws dynamodb create-table \
    --table-name "$TABLE" \
    --attribute-definitions AttributeName=ObjectKey,AttributeType=S \
    --key-schema AttributeName=ObjectKey,KeyType=HASH \
    --billing-mode PROVISIONED \
    --provisioned-throughput ReadCapacityUnits=25,WriteCapacityUnits=25 \
    --table-class STANDARD \
    --region "$AWS_REGION"

aws dynamodb wait table-exists --table-name "$TABLE" --region "$AWS_REGION"

3. Lambda 用の IAM Role を作る

Lambda 関数は自分のクレデンシャルを持たない。 関数が起動するたびに Lambda サービスが IAM Role を Assume して一時クレデンシャルを取り、 それを boto3 等が使う。 つまり Role には以下が必要。

  • Trust Policy: 「Lambda サービスはこの Role を引き受けてよい」 を書く (リポジトリの policies/trust-policy.json)
  • Permission Policy: 引き受けたあと何ができるか (AWS マネージドの AWSLambdaBasicExecutionRole で CloudWatch Logs 書込み + 自前 inline で dynamodb:PutItems3:GetObject)
aws iam create-role \
    --role-name "$ROLE_NAME" \
    --assume-role-policy-document file://policies/trust-policy.json

aws iam attach-role-policy \
    --role-name "$ROLE_NAME" \
    --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

cat > /tmp/inline-policy.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "dynamodb:PutItem",
      "Resource": "arn:aws:dynamodb:${AWS_REGION}:${ACCOUNT_ID}:table/${TABLE}"
    },
    {
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::${BUCKET}/*"
    }
  ]
}
EOF

aws iam put-role-policy \
    --role-name "$ROLE_NAME" \
    --policy-name HandleUploadInline \
    --policy-document file:///tmp/inline-policy.json

ROLE_ARN=$(aws iam get-role --role-name "$ROLE_NAME" --query 'Role.Arn' --output text)

inline ポリシーで「自分のテーブルへの PutItem だけ」「自分のバケットからの GetObject だけ」 に絞っている。 これが最小権限。

4. Lambda 関数をデプロイする

関数のソースは lambda-src/handler.py。 知っておくべき小ネタ:

  • boto3 は AWS の Python SDK。 Lambda Python ランタイムに最初から同梱されている ので pip install 不要
  • urllib.parse.unquote_plus は S3 イベントに入る URL エンコード済みオブジェクトキーを元に戻す関数 (空白が +、 日本語が %E3%83%... の状態から戻す)
(cd lambda-src && zip -q ../function.zip handler.py)

# IAM Role の伝播を数秒待ってから次の API を叩く
sleep 10

aws lambda create-function \
    --function-name "$FUNCTION" \
    --runtime python3.13 \
    --role "$ROLE_ARN" \
    --handler handler.lambda_handler \
    --zip-file fileb://function.zip \
    --environment "Variables={TABLE_NAME=${TABLE}}" \
    --region "$AWS_REGION"

S3 イベントを模した payload で動作確認。

cat > /tmp/test-event.json <<EOF
{
  "Records": [{
    "s3": {
      "bucket": {"name": "${BUCKET}"},
      "object": {"key": "uploads/manual-test.txt", "size": 12}
    }
  }]
}
EOF

aws lambda invoke \
    --function-name "$FUNCTION" \
    --payload fileb:///tmp/test-event.json \
    --cli-binary-format raw-in-base64-out \
    /tmp/response.json --region "$AWS_REGION"

cat /tmp/response.json
aws dynamodb scan --table-name "$TABLE" --region "$AWS_REGION"

5. S3 → Lambda の自動発火を繋ぐ

aws lambda add-permission \
    --function-name "$FUNCTION" \
    --statement-id AllowS3Invoke \
    --action lambda:InvokeFunction \
    --principal s3.amazonaws.com \
    --source-arn "arn:aws:s3:::${BUCKET}" \
    --region "$AWS_REGION"

LAMBDA_ARN=$(aws lambda get-function --function-name "$FUNCTION" \
    --query 'Configuration.FunctionArn' --output text --region "$AWS_REGION")

cat > /tmp/notification.json <<EOF
{
  "LambdaFunctionConfigurations": [{
    "LambdaFunctionArn": "${LAMBDA_ARN}",
    "Events": ["s3:ObjectCreated:*"],
    "Filter": {
      "Key": {
        "FilterRules": [{ "Name": "prefix", "Value": "uploads/" }]
      }
    }
  }]
}
EOF

aws s3api put-bucket-notification-configuration \
    --bucket "$BUCKET" \
    --notification-configuration file:///tmp/notification.json

パイプライン全体を試す。

echo "auto trigger" > /tmp/memo.txt
aws s3 cp /tmp/memo.txt "s3://${BUCKET}/uploads/memo.txt" --region "$AWS_REGION"
sleep 5
aws dynamodb scan --table-name "$TABLE" --region "$AWS_REGION"

6. CloudWatch Logs とアラーム

Lambda の print() は自動で CloudWatch Logs に流れる。 確認。

aws logs tail "/aws/lambda/${FUNCTION}" --since 10m --region "$AWS_REGION"

アラームをメールに飛ばすには SNS トピックを作って自分のメアドを subscribe する。

TOPIC_ARN=$(aws sns create-topic --name "$TOPIC_NAME" \
    --region "$AWS_REGION" --query 'TopicArn' --output text)

aws sns subscribe \
    --topic-arn "$TOPIC_ARN" \
    --protocol email \
    --notification-endpoint "$EMAIL" \
    --region "$AWS_REGION"

# 受信ボックスに AWS からの確認メールが届くので、 Confirm リンクをクリック

aws cloudwatch put-metric-alarm \
    --alarm-name "${FUNCTION}-Errors" \
    --metric-name Errors \
    --namespace AWS/Lambda \
    --statistic Sum \
    --period 60 \
    --evaluation-periods 1 \
    --threshold 1 \
    --comparison-operator GreaterThanOrEqualToThreshold \
    --dimensions Name=FunctionName,Value="$FUNCTION" \
    --treat-missing-data notBreaching \
    --alarm-actions "$TOPIC_ARN" \
    --region "$AWS_REGION"

Confirm リンクをクリックしないと、 アラームは ALARM 状態になってもメールは届かない。

片付け

スクリプトを叩く。 リソースがなくてもエラーにしない作りなので、 何度叩いても安全。

./scripts/cleanup.sh

または手動で:

aws s3 rm "s3://${BUCKET}" --recursive --region "$AWS_REGION"
aws s3 rb "s3://${BUCKET}" --region "$AWS_REGION"
aws lambda delete-function --function-name "$FUNCTION" --region "$AWS_REGION"
aws dynamodb delete-table --table-name "$TABLE" --region "$AWS_REGION"
aws cloudwatch delete-alarms --alarm-names "${FUNCTION}-Errors" --region "$AWS_REGION"
aws logs delete-log-group --log-group-name "/aws/lambda/${FUNCTION}" --region "$AWS_REGION"
aws sns delete-topic --topic-arn "$TOPIC_ARN" --region "$AWS_REGION"
aws iam delete-role-policy --role-name "$ROLE_NAME" --policy-name HandleUploadInline
aws iam detach-role-policy --role-name "$ROLE_NAME" \
    --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam delete-role --role-name "$ROLE_NAME"

翌日に Cost Explorer を覗いて $0.00 になっていれば完璧。

結局なにを触ったのか

レイヤ サービス 学んだこと
認証認可 IAM ユーザ + Role + Trust Policy + Permission Policy Lambda はクレデンシャルを持たない。 起動時に Role を Assume する
ストレージ S3 + Block Public Access クラウドオブジェクトストア。 新規バケットは非公開、 解除しない
コンピュート Lambda + Python 3.13 サーバレス。 ランタイムに boto3 同梱、 自分が書くのはハンドラだけ
データベース DynamoDB (プロビジョンド) マネージド NoSQL。 Always Free に乗せるならプロビジョンド
連携 S3 → Lambda イベント通知 サービス同士をイベントで疎結合に繋ぐ AWS の標準パターン
可観測性 CloudWatch Logs/Metrics/Alarms + SNS 動いているか確認 + 失敗時の自動通知
コスト AWS Budgets + Always Free サプライズ請求への二段の防御線

「S3 で入力を受ける → Lambda で処理 → DynamoDB に記録 → CloudWatch で監視」 という形は実戦でも頻出。 ここから API Gateway を足して REST API にする、 EventBridge / SQS で分岐させる、 CloudFormation / CDK で IaC 化、 と発展できる。

リポジトリ構成

awscape/
├── README.md                       # 英語版 (デフォルト)
├── README.ja.md                    # このファイル
├── config.env.example              # config.env にコピーして値を書き換える
├── lambda-src/
│   └── handler.py                  # Lambda 関数のコード
├── policies/
│   └── trust-policy.json           # Lambda の Trust Policy
└── scripts/
    ├── setup.sh                    # 一発セットアップ
    └── cleanup.sh                  # 一発片付け

ハマりポイント

症状 原因 対策
何やっても AccessDenied 権限の狭い IAM ユーザで作業している 学習中は admin ユーザに切り替え
BucketAlreadyExists バケット名はグローバル一意 BUCKET にランダムな文字列を足す
Lambda 作成で InvalidParameterValueException IAM Role がまだ伝播していない 10 秒待ってリトライ
S3 にアップロードしたのに Lambda が動かない add-permission 忘れ、 もしくは prefix がずれている 手順 5 を再実行
Lambda は動くが DynamoDB 書き込みが失敗 inline ポリシーに dynamodb:PutItem が無い inline ポリシーを再度 apply
アラームは ALARM になるのにメールが来ない SNS subscribe を Confirm していない AWS から届いたメールの Confirm リンクをクリック
翌月いきなり請求 片付け忘れ cleanup.sh を実行、 Cost Explorer で何が課金されたか確認
6 ヶ月後にアカウントが消えた Free Plan の期限切れ 期限前に Paid Plan に切り替えるか、 新規アカウントを作り直す

参考