ハンズオン: Cogtnio、API Gateway、Lambda、DynamoDB ~AWS認定デベロッパーアソシエイト(DVA-C02)~
このブログは2026年6月29日翔泳社さんより発売される「AWS教科書 AWS認定デベロッパーアソシエイト テキスト&問題集」で扱う内容を体験していただくためのハンズオンガイドです。
このハンズオンガイドでは、Cognitoユーザープール、Cognito IDプール、API Gateway(IAM認証)、Lambda、DynamoDBを組み合わせて、シンプルなニュースサイトのバックエンドとフロントエンドを構築します。ゲストユーザーはニュース一覧を閲覧でき、サインインしたユーザーはニュースの追加・編集ができます。
Cognito ユーザープールとIDプールの2つの機能を手を動かして体験できます。
目次
全体構成
東京リージョンで構築してください。
完成する構成は以下のとおりです。
(1) DynamoDBにニュースデータを保存するテーブルを作成する
(2) Lambda関数を作成する(一覧取得、追加、編集)
(3) API Gatewayを作成する(IAM認証で保護)
(4) Cognitoユーザープールを作成する(ホストされたUIでサインアップ・サインイン)
(5) Cognito IDプールを作成する(認証済みロール/ゲストロール)
(6) CloudShellからPythonで動作確認する(ゲスト→一覧のみ、認証済み→追加可能)
(7) S3静的サイトにフロントエンドを配置する
一連のフロー:
– ゲストユーザー:IDプール(未認証) → 一時認証情報取得 → API Gateway(GET /news) → Lambda → DynamoDB
– 認証済みユーザー:ユーザープール(サインイン) → IDプール(認証済み) → 一時認証情報取得 → API Gateway(GET/POST/PUT) → Lambda → DynamoDB
準備:IAMポリシーの作成とアタッチ
ハンズオンで使用するIAMユーザーまたはIAMロールに、以下のポリシーをアタッチしてください。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
{ "Version": "2012-10-17", "Statement": [ { "Sid": "DynamoDBAccess", "Effect": "Allow", "Action": [ "dynamodb:CreateTable", "dynamodb:DeleteTable", "dynamodb:Describe*", "dynamodb:PutItem", "dynamodb:Get*", "dynamodb:List*", "dynamodb:Scan", "dynamodb:UpdateItem" ], "Resource": "*" }, { "Sid": "LambdaAccess", "Effect": "Allow", "Action": [ "lambda:CreateFunction", "lambda:DeleteFunction", "lambda:Get*", "lambda:List*", "lambda:InvokeFunction", "lambda:UpdateFunctionCode", "lambda:AddPermission", "lambda:RemovePermission", "lambda:*Version", "lambda:*Alias*" ], "Resource": "*" }, { "Sid": "APIGatewayAccess", "Effect": "Allow", "Action": [ "apigateway:*" ], "Resource": "*" }, { "Sid": "CognitoAccess", "Effect": "Allow", "Action": [ "cognito-idp:*", "cognito-identity:*" ], "Resource": "*" }, { "Sid": "IAMAccess", "Effect": "Allow", "Action": [ "iam:CreateRole", "iam:DeleteRole", "iam:AttachRolePolicy", "iam:DetachRolePolicy", "iam:PutRolePolicy", "iam:DeleteRolePolicy", "iam:PassRole", "iam:Get*", "iam:List*", "iam:UpdateAssumeRolePolicy", "iam:CreatePolicy" ], "Resource": "*" }, { "Sid": "S3Access", "Effect": "Allow", "Action": [ "s3:CreateBucket", "s3:DeleteBucket", "s3:PutObject", "s3:Get*", "s3:DeleteObject", "s3:List*", "s3:PutBucketPolicy", "s3:DeleteBucketPolicy", "s3:PutBucketWebsite", "s3:DeleteBucketWebsite", "s3:PutBucketPublicAccessBlock" ], "Resource": "*" }, { "Sid": "CloudShellAccess", "Effect": "Allow", "Action": [ "cloudshell:*", "cloudfront:*", "wafv2:*", "cloudwatch:*", "logs:*" ], "Resource": "*" } ] } |
タスク 1:DynamoDBテーブルを作成する
目的
ニュース記事を保存するDynamoDBテーブルを作成しましょう。
手順
(1) DynamoDBコンソールにアクセスし、[テーブルの作成]をクリック。
(2) 以下の設定を行います:
| 項目 | 設定値 |
|---|---|
| テーブル名 | HandsonNews |
| パーティションキー | newsId (文字列) |
| テーブル設定 | 設定をカスタマイズ |
| 読み込み/書き込みキャパシティモード | オンデマンド |
(3) [テーブルの作成]をクリック。
(4) テーブルがアクティブになったら、サンプルデータを投入します。CloudShellで以下を実行します:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
aws dynamodb put-item \ --table-name HandsonNews \ --item '{ "newsId": {"S": "news-001"}, "title": {"S": "サービスリリースのお知らせ"}, "body": {"S": "本日、新しいサービスをリリースしました。"}, "createdAt": {"S": "2026-06-01T10:00:00+09:00"} }' \ --region ap-northeast-1 aws dynamodb put-item \ --table-name HandsonNews \ --item '{ "newsId": {"S": "news-002"}, "title": {"S": "メンテナンスのお知らせ"}, "body": {"S": "6月15日にメンテナンスを実施します。"}, "createdAt": {"S": "2026-06-03T09:00:00+09:00"} }' \ --region ap-northeast-1 |
ポイント
- パーティションキーnewsIdで各ニュース記事を一意に識別します。
- オンデマンドモードはリクエスト数に応じた課金なので、開発やテストにも適しています。
タスク 2:Lambda関数を作成する
目的
ニュースの一覧取得、追加、編集をそれぞれ個別のLambda関数として作成しましょう。関数ごとに最小権限のIAMポリシーを設定します。
手順
GetNewsFunction(一覧取得)
(1) Lambdaコンソールで[関数の作成]をクリック。
(2) 以下の設定を行います:
| 項目 | 設定値 |
|---|---|
| 作成方法 | 一から作成 |
| 関数名 | GetNewsFunction |
| ランタイム | Python 3.14 |
| アーキテクチャ | x86_64 |
(3) [関数の作成]をクリック。
(4) [設定]タブ-[アクセス権限]をクリックし、ロール名のリンクをクリックしてIAMコンソールを開きます。
(5) [許可を追加]-[インラインポリシーを作成]をクリックし、以下のポリシーを貼り付けます:
|
1 2 3 4 5 6 7 8 9 10 11 |
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "dynamodb:Scan", "Resource": "arn:aws:dynamodb:ap-northeast-1:*:table/HandsonNews" } ] } |
(6) ポリシー名をGetNewsPolicyとして保存します。
(7) Lambdaコンソールに戻り、[コード]タブで以下のコードを貼り付けます:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import json import boto3 dynamodb = boto3.client('dynamodb', region_name='ap-northeast-1') def lambda_handler(event, context): result = dynamodb.scan(TableName='HandsonNews') items = [] for item in result['Items']: items.append({ 'newsId': item['newsId']['S'], 'title': item['title']['S'], 'body': item['body']['S'], 'createdAt': item['createdAt']['S'] }) items.sort(key=lambda x: x['createdAt'], reverse=True) return response(200, items) def response(status_code, body): return { 'statusCode': status_code, 'headers': { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type,Authorization,X-Amz-Date,X-Amz-Security-Token', 'Access-Control-Allow-Methods': 'GET,POST,PUT,OPTIONS' }, 'body': json.dumps(body, ensure_ascii=False) } |
(8) [Deploy]をクリック。
(9) バージョンを発行します。[バージョン]タブ-[新しいバージョンを発行]をクリックし、[発行]をクリック。
(10) エイリアスを作成します。[エイリアス]タブ-[エイリアスを作成]をクリックし、名前にprod、バージョンに1を設定して[保存]をクリック。
PostNewsFunction(追加)
(9) 同様に[関数の作成]をクリックし、以下の設定で作成します:
| 項目 | 設定値 |
|---|---|
| 関数名 | PostNewsFunction |
| ランタイム | Python 3.14 |
| アーキテクチャ | x86_64 |
(10) IAMロールにインラインポリシーを追加します:
|
1 2 3 4 5 6 7 8 9 10 11 |
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "dynamodb:PutItem", "Resource": "arn:aws:dynamodb:ap-northeast-1:*:table/HandsonNews" } ] } |
(11) ポリシー名をPostNewsPolicyとして保存します。
(12) [コード]タブで以下のコードを貼り付けて[Deploy]をクリック:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import json import boto3 import uuid from datetime import datetime, timezone, timedelta dynamodb = boto3.client('dynamodb', region_name='ap-northeast-1') def lambda_handler(event, context): body = json.loads(event['body']) jst = timezone(timedelta(hours=9)) news_id = f"news-{uuid.uuid4().hex[:8]}" dynamodb.put_item( TableName='HandsonNews', Item={ 'newsId': {'S': news_id}, 'title': {'S': body['title']}, 'body': {'S': body['body']}, 'createdAt': {'S': datetime.now(jst).isoformat()} } ) return response(201, {'message': 'ニュースを追加しました', 'newsId': news_id}) def response(status_code, body): return { 'statusCode': status_code, 'headers': { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type,Authorization,X-Amz-Date,X-Amz-Security-Token', 'Access-Control-Allow-Methods': 'GET,POST,PUT,OPTIONS' }, 'body': json.dumps(body, ensure_ascii=False) } |
(13) バージョンを発行し、エイリアスprodを作成します(GetNewsFunctionと同じ手順)。
PutNewsFunction(編集)
(13) 同様に[関数の作成]をクリックし、以下の設定で作成します:
| 項目 | 設定値 |
|---|---|
| 関数名 | PutNewsFunction |
| ランタイム | Python 3.14 |
| アーキテクチャ | x86_64 |
(14) IAMロールにインラインポリシーを追加します:
|
1 2 3 4 5 6 7 8 9 10 11 |
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "dynamodb:UpdateItem", "Resource": "arn:aws:dynamodb:ap-northeast-1:*:table/HandsonNews" } ] } |
(15) ポリシー名をPutNewsPolicyとして保存します。
(16) [コード]タブで以下のコードを貼り付けて[Deploy]をクリック:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
import json import boto3 dynamodb = boto3.client('dynamodb', region_name='ap-northeast-1') def lambda_handler(event, context): news_id = event['pathParameters']['id'] body = json.loads(event['body']) dynamodb.update_item( TableName='HandsonNews', Key={'newsId': {'S': news_id}}, UpdateExpression='SET title = :t, body = :b', ExpressionAttributeValues={ ':t': {'S': body['title']}, ':b': {'S': body['body']} } ) return response(200, {'message': 'ニュースを更新しました', 'newsId': news_id}) def response(status_code, body): return { 'statusCode': status_code, 'headers': { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type,Authorization,X-Amz-Date,X-Amz-Security-Token', 'Access-Control-Allow-Methods': 'GET,POST,PUT,OPTIONS' }, 'body': json.dumps(body, ensure_ascii=False) } |
(17) バージョンを発行し、エイリアスprodを作成します(GetNewsFunctionと同じ手順)。
ポイント
- 各Lambda関数を個別に作成することで、単一責任の原則に沿った設計になります。
- IAMポリシーも関数ごとに最小権限(Scanのみ、PutItemのみ、UpdateItemのみ)を設定しています。
- バージョンを発行してprodエイリアスを作成することで、コード更新時にエイリアスの向き先を変えるだけでリリースやロールバックが可能になります。
- CORSヘッダーにX-Amz-DateやX-Amz-Security-Tokenを含めているのは、IAM認証(署名バージョン4)のリクエストヘッダーを許可するためです。
タスク 3:API Gatewayを作成する(IAM認証)
目的
Lambda関数を呼び出すREST APIを作成し、IAM認証で保護しましょう。
手順
(1) API Gatewayコンソールにアクセスし、[APIの作成]をクリック。
(2) REST APIの[構築]をクリック。
(3) 以下の設定を行います:
| 項目 | 設定値 |
|---|---|
| APIの詳細 | 新しいAPI |
| API名 | HandsonNewsAPI |
| APIエンドポイントタイプ | リージョン |
(4) [APIを作成]をクリック。
(5) [リソースを作成]をクリックし、リソース名にnewsと入力、[CORS(クロスオリジンリソース共有)]にチェックを入れて[リソースを作成]をクリック。
(6) /newsリソースを選択した状態で、さらに[リソースを作成]をクリック。リソースパスに{id}と入力して[リソースを作成]をクリック。
(7) /newsリソースを選択し、[メソッドを作成]をクリック。以下の設定でGETメソッドを作成します:
| 項目 | 設定値 |
|---|---|
| メソッドタイプ | GET |
| 統合タイプ | Lambda関数 |
| Lambdaプロキシ統合 | オン |
| Lambda関数 | GetNewsFunction:prod |
※ GetNewsFunctionのARNを選択して後ろに :prod を書き加えます。
(8) [メソッドを作成]をクリック。
(9) 作成したGETメソッドを選択し、[メソッドリクエスト]タブの[編集]をクリック。[認可]でAWS IAMを選択して[保存]をクリック。
(10) 同様に/newsリソースにPOSTメソッドを作成します。Lambda関数にはPostNewsFunction:prodを指定します。
(11) POSTメソッドの[メソッドリクエスト]タブで[認可]をAWS IAMに設定します。
(12) /news/{id}リソースを選択し、PUTメソッドを作成します。Lambda関数にはPutNewsFunction:prodを指定します。
(13) PUTメソッドの[メソッドリクエスト]タブで[認可]をAWS IAMに設定します。
(14) [APIをデプロイ]をクリック。ステージ名をprodとして[デプロイ]をクリック。
(15) 表示されるURLの呼び出しをメモします。
例:https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod
ポイント
- IAM認証を設定すると、APIリクエストに署名バージョン4の署名が必要になります。
- 署名がないリクエストや、権限のないIAMエンティティからのリクエストは拒否されます。
- Cognito IDプールから取得した一時認証情報で署名することで、ゲスト/認証済みの権限に応じたアクセス制御ができます。
タスク 4:Cognitoユーザープールを作成する
目的
ユーザーのサインアップ・サインインを管理するCognitoユーザープールを作成しましょう。マネージドログイン(ホストされたUI)を使ってブラウザからサインインできるようにします。
手順
(1) Cognitoコンソールにアクセスし、[ユーザープールを作成]をクリック。
(2) [アプリケーションの定義]で以下を設定します:
| 項目 | 設定値 |
|---|---|
| アプリケーションタイプ | モバイルアプリ |
| アプリケーション名 | handson-news-client |
(3) [オプションの設定]で以下を設定します:
| 項目 | 設定値 |
|---|---|
| サインイン識別子のオプション | Eメール |
| サインアップに必要な属性 | email(デフォルト) |
(4) [リターンURLの追加]にhttps://example.com/callbackと入力します(あとでS3静的サイトのURLに変更します)。
(5) [ユーザーディレクトリを作成する]をクリック。
(6) ユーザープールとアプリクライアントが作成されます。[概要に移動]をクリック。
(7) ユーザープールIDをメモします(形式: ap-northeast-1_XXXXXXXXX)。
(8) 左メニューの[アプリケーションクライアント]をクリックして、handson-news-clientのクライアントIDをメモします。
(9) handson-news-clientをクリックして詳細を開き、[認証フロー]セクションにALLOW_USER_PASSWORD_AUTHが含まれていることを確認します。含まれていない場合は[編集]から追加して保存します。
(10) [マネージドログインページ]セクションの[編集]をクリックし、以下を確認・設定します:
– [OAuth 2.0の許可タイプ]に[Implicit grant]が含まれていることを確認。含まれていなければ追加する。
– [OpenID Connectのスコープ]に[openid]と[email]が含まれていることを確認。
– [許可されているリターンURL]にhttps://example.com/callback/index.htmlが含まれていることを確認(末尾の/index.htmlを忘れずに)。
– 保存する。
(11) マネージドログインのドメインを確認します。左メニューの[ドメイン]を開くと、自動生成されたCognitoドメインが表示されます。このドメインをメモしておきます。
ポイント
- 「モバイルアプリ」を選択するとクライアントシークレットなし(パブリッククライアント)で作成されます。CLIやブラウザから直接認証(USER_PASSWORD_AUTHフロー)が可能です。
- マネージドログイン(旧ホストされたUI)を使うことで、サインアップ・サインインのフォームを自前で実装する必要がありません。
- リターンURLは認証後のリダイレクト先です。あとでS3静的サイトのURLに変更します。
タスク 5:Cognito IDプールを作成する
目的
ゲストユーザーと認証済みユーザーにそれぞれ異なるAWS権限を付与するCognito IDプールを作成しましょう。
手順
(1) Cognitoコンソールの左メニューから[IDプール]を選択します。
(2) [IDプールを作成]をクリック。
(3) [IDプールの信頼を設定]で以下を設定します:
| 項目 | 設定値 |
|---|---|
| 認証されたアクセス | チェックを入れる |
| 認証されたアクセス-IDタイプ | Amazon Cognitoユーザープール |
| ゲストアクセス | チェックを入れる |
(4) [次へ]をクリック。
(5) [アクセス許可を設定]で、認証済みロールとゲストロールをそれぞれ[新しいIAMロールを作成]で以下のロール名を入力して[次へ]をクリック。
| 項目 | 設定値 |
|——|——–|
| 認証されたロール-IAMロール名 | handson-news-auth-role |
| ゲストロール-IAMロール名 | handson-news-guest-role |
(6) [IDプロバイダーを接続]で以下を設定します:
| 項目 | 設定値 |
|---|---|
| ユーザープールID | タスク4でメモしたユーザープールID |
| アプリクライアントID | タスク4でメモしたクライアントID |
(7) ロールの設定は[デフォルトのロール]のまま[次へ]をクリック。
(8) [プロパティを設定]でIDプール名にhandson-news-idpoolと入力します。
(9) [次へ]をクリックして、確認画面で[IDプールを作成]をクリック。
(10) 作成されたIDプールを開き、IDプールIDをメモします(形式: ap-northeast-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)。
(11) 次に、ゲストロールの権限を設定します。IAMコンソールにアクセスし、handson-news-guest-roleを開きます。
(12) [許可を追加]-[インラインポリシーを作成]をクリックし、以下のポリシーを貼り付けます(API GatewayのIDは自分のものに置き換えてください):
|
1 2 3 4 5 6 7 8 9 10 11 |
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "execute-api:Invoke", "Resource": "arn:aws:execute-api:ap-northeast-1:*:{APIのID}/prod/GET/news" } ] } |
(13) ポリシー名をHandsonNewsGuestPolicyとして保存します。
(14) 次に、handson-news-auth-roleを開きます。
(15) [許可を追加]-[インラインポリシーを作成]をクリックし、以下のポリシーを貼り付けます:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "execute-api:Invoke", "Resource": [ "arn:aws:execute-api:ap-northeast-1:*:{APIのID}/prod/GET/news", "arn:aws:execute-api:ap-northeast-1:*:{APIのID}/prod/POST/news", "arn:aws:execute-api:ap-northeast-1:*:{APIのID}/prod/PUT/news/*" ] } ] } |
(16) ポリシー名をHandsonNewsAuthPolicyとして保存します。
ポイント
- IDプールは、ユーザープールで認証されたユーザーと未認証のゲストユーザーに、それぞれ異なるIAMロールを割り当てます。
- ゲストロールはGET /newsのみ実行可能で、ニュース一覧の閲覧だけができます。
- 認証済みロールはGET、POST、PUTが実行可能で、ニュースの追加・編集もできます。
- execute-api:InvokeアクションのResourceでAPIのメソッドとパスを細かく制御できます。
タスク 6:CloudShellからPythonで動作確認する
目的
CloudShellからPythonスクリプトを実行して、ゲストユーザーと認証済みユーザーでAPIの動作が異なることを確認しましょう。
手順
(1) CloudShellを開いて、以下のPythonスクリプトを実行します。変数を自分の値に置き換えてください:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
python3 << 'EOF' import boto3 import json import hashlib import hmac import urllib.request from datetime import datetime # ------------------------------------------------------- # 設定値(自分の値に置き換えてください) # ------------------------------------------------------- REGION = 'ap-northeast-1' IDENTITY_POOL_ID = 'ap-northeast-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' USER_POOL_ID = 'ap-northeast-1_XXXXXXXXX' CLIENT_ID = 'xxxxxxxxxxxxxxxxxxxxxxxxxx' API_URL = 'https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod' # ------------------------------------------------------- # 署名バージョン4のヘルパー関数 # ------------------------------------------------------- def sign(key, msg): return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest() def get_signature_key(key, date_stamp, region, service): k_date = sign(('AWS4' + key).encode('utf-8'), date_stamp) k_region = sign(k_date, region) k_service = sign(k_region, service) return sign(k_service, 'aws4_request') def signed_request(method, url, credentials, body=None): from urllib.parse import urlparse parsed = urlparse(url) host = parsed.hostname path = parsed.path now = datetime.utcnow() amz_date = now.strftime('%Y%m%dT%H%M%SZ') date_stamp = now.strftime('%Y%m%d') headers_to_sign = f'host:{host}\nx-amz-date:{amz_date}\n' signed_headers = 'host;x-amz-date' if credentials.get('SessionToken'): headers_to_sign += f'x-amz-security-token:{credentials["SessionToken"]}\n' signed_headers += ';x-amz-security-token' payload = body if body else '' payload_hash = hashlib.sha256(payload.encode('utf-8')).hexdigest() canonical_request = '\n'.join([ method, path, '', headers_to_sign, signed_headers, payload_hash ]) credential_scope = f'{date_stamp}/{REGION}/execute-api/aws4_request' string_to_sign = '\n'.join([ 'AWS4-HMAC-SHA256', amz_date, credential_scope, hashlib.sha256(canonical_request.encode('utf-8')).hexdigest() ]) signing_key = get_signature_key(credentials['SecretKey'], date_stamp, REGION, 'execute-api') signature = hmac.new(signing_key, string_to_sign.encode('utf-8'), hashlib.sha256).hexdigest() auth_header = ( f'AWS4-HMAC-SHA256 Credential={credentials["AccessKeyId"]}/{credential_scope}, ' f'SignedHeaders={signed_headers}, Signature={signature}' ) req_headers = { 'x-amz-date': amz_date, 'Authorization': auth_header, } if credentials.get('SessionToken'): req_headers['x-amz-security-token'] = credentials['SessionToken'] if body: req_headers['Content-Type'] = 'application/json' req = urllib.request.Request(url, data=body.encode('utf-8') if body else None, headers=req_headers, method=method) try: with urllib.request.urlopen(req) as resp: return resp.status, json.loads(resp.read().decode()) except urllib.error.HTTPError as e: return e.code, e.read().decode() # ------------------------------------------------------- # ゲストユーザーとして認証情報を取得 # ------------------------------------------------------- cognito_identity = boto3.client('cognito-identity', region_name=REGION) guest_id = cognito_identity.get_id(IdentityPoolId=IDENTITY_POOL_ID) guest_creds = cognito_identity.get_credentials_for_identity(IdentityId=guest_id['IdentityId']) guest_credentials = { 'AccessKeyId': guest_creds['Credentials']['AccessKeyId'], 'SecretKey': guest_creds['Credentials']['SecretKey'], 'SessionToken': guest_creds['Credentials']['SessionToken'] } print("=== ゲストユーザーでテスト ===") status, data = signed_request('GET', f'{API_URL}/news', guest_credentials) print(f"GET /news: ステータス {status}") if status == 200: print(f" 記事数: {len(data)}件") status, data = signed_request('POST', f'{API_URL}/news', guest_credentials, json.dumps({'title': 'テスト', 'body': 'ゲストから投稿'})) print(f"POST /news: ステータス {status} (ゲストは拒否されるはず)") # ------------------------------------------------------- # 認証済みユーザーとして認証情報を取得 # ------------------------------------------------------- cognito_idp = boto3.client('cognito-idp', region_name=REGION) auth_result = cognito_idp.initiate_auth( ClientId=CLIENT_ID, AuthFlow='USER_PASSWORD_AUTH', AuthParameters={ 'USERNAME': 'testuser@example.com', 'PASSWORD': 'TestP@ss1234' } ) id_token = auth_result['AuthenticationResult']['IdToken'] # IDプールに認証済みとしてリクエスト provider_key = f'cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}' auth_id = cognito_identity.get_id( IdentityPoolId=IDENTITY_POOL_ID, Logins={provider_key: id_token} ) auth_creds = cognito_identity.get_credentials_for_identity( IdentityId=auth_id['IdentityId'], Logins={provider_key: id_token} ) auth_credentials = { 'AccessKeyId': auth_creds['Credentials']['AccessKeyId'], 'SecretKey': auth_creds['Credentials']['SecretKey'], 'SessionToken': auth_creds['Credentials']['SessionToken'] } print("\n=== 認証済みユーザーでテスト ===") status, data = signed_request('GET', f'{API_URL}/news', auth_credentials) print(f"GET /news: ステータス {status}") if status == 200: print(f" 記事数: {len(data)}件") status, data = signed_request('POST', f'{API_URL}/news', auth_credentials, json.dumps({'title': '新しいお知らせ', 'body': '認証済みユーザーから投稿しました'})) print(f"POST /news: ステータス {status}") if status == 201: print(f" {data}") EOF |
このスクリプトを実行する前に、テストユーザーを作成しておきます:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
USER_POOL_ID="ap-northeast-1_XXXXXXXXX" CLIENT_ID="xxxxxxxxxxxxxxxxxxxxxxxxxx" aws cognito-idp sign-up \ --client-id $CLIENT_ID \ --username testuser@example.com \ --password "TestP@ss1234" \ --user-attributes Name=email,Value=testuser@example.com \ --region ap-northeast-1 aws cognito-idp admin-confirm-sign-up \ --user-pool-id $USER_POOL_ID \ --username testuser@example.com \ --region ap-northeast-1 |
(2) テストユーザーを作成してから、Pythonスクリプトを実行します。以下のような出力が表示されれば成功です:
|
1 2 3 4 5 6 7 8 9 10 11 |
=== ゲストユーザーでテスト === GET /news: ステータス 200 記事数: 2件 POST /news: ステータス 403 (ゲストは拒否されるはず) === 認証済みユーザーでテスト === GET /news: ステータス 200 記事数: 2件 POST /news: ステータス 201 {'message': 'ニュースを追加しました', 'newsId': 'news-xxxxxxxx'} |
ポイント
- ゲストユーザーはIDプールから未認証のIAMロールの一時認証情報を取得します。GET /newsのみ許可されているため、POSTは403で拒否されます。
- 認証済みユーザーはユーザープールのIdTokenをIDプールに渡して、認証済みIAMロールの一時認証情報を取得します。
- 一時認証情報で署名バージョン4のリクエストを作成することで、API Gateway(IAM認証)にアクセスできます。
- IAMポリシーのResourceレベルでHTTPメソッドとパスを制限しているため、ロールに応じた細かいアクセス制御が実現されています。
タスク 7:CloudFront + S3でフロントエンドを配置する
目的
S3にHTMLを配置し、CloudFrontでHTTPS配信して、ブラウザからゲスト閲覧とサインイン後の記事追加を体験しましょう。
手順
(1) S3コンソールにアクセスし、[バケットを作成]をクリック。
(2) 以下の設定を行います:
| 項目 | 設定値 |
|---|---|
| バケット名 | handson-news-site-{自分の名前など一意の文字列} |
| リージョン | ap-northeast-1 |
| パブリックアクセスをすべてブロック | チェックを入れたまま(デフォルト) |
(3) [バケットを作成]をクリック。
(4) CloudFrontコンソールにアクセスし、[ディストリビューションを作成]をクリック。
(5) ステップ1 [Choose a plan]で[Free]を選択して[次へ]をクリック。
(6) ステップ2 [Get started]で以下を設定します:
| 項目 | 設定値 |
|---|---|
| Distribution name | handson-news-distribution |
| Distribution type | Single website or app |
| Domain | 空欄のまま(Route 53ドメインは使用しない) |
(7) [次へ]をクリック。
(8) ステップ3 [Specify origin]で以下を設定します:
| 項目 | 設定値 |
|---|---|
| オリジンドメイン | handson-news-site-{自分の名前}.s3.ap-northeast-1.amazonaws.com |
| Settings | Allow private S3 bucket access to CloudFront – Recommended |
(9) [次へ]をクリック。
(10) ステップ4 [Enable security]はデフォルトのまま[次へ]をクリック。デフォルトでAWS WAFのWebACLが自動作成され、以下の3つのマネージドルールが適用されます:
| ルール | 保護内容 |
|——–|———-|
| AWSManagedRulesAmazonIpReputationList | ボットネットや既知の悪意あるIPアドレスからのリクエストをブロックします。 |
| AWSManagedRulesCommonRuleSet | SQLインジェクション、XSS(クロスサイトスクリプティング)などOWASP Top 10の一般的な脅威から保護します。 |
| AWSManagedRulesKnownBadInputsRuleSet | Log4j(CVE-2021-44228)などの既知の脆弱性を悪用するリクエストパターンをブロックします。 |
(11) ステップ5 [Review and create]で確認して[ディストリビューションを作成]をクリック。
(12) 画面上部に「S3バケットポリシーを更新する必要があります」というバナーが表示される場合は、[ポリシーをコピー]をクリックしてバケットポリシーをコピーし、(13)に進みます。バナーが表示されない場合は、ディストリビューションの[オリジン]タブを開き、作成したオリジンを選択して[編集]をクリック。[バケットポリシーをコピー]をクリックしてポリシーをコピーします。
(13) S3コンソールに移動し、作成したバケットの[アクセス許可]タブで[バケットポリシー]の[編集]をクリックしてコピーしたポリシーを貼り付けて保存します。
(14) CloudFrontコンソールに戻り、ディストリビューションのドメイン名(例: d1234567890abc.cloudfront.net)をメモします。デプロイが完了するまで数分待ちます。
(15) デフォルトルートオブジェクトを設定します。ディストリビューションの[一般]タブの[設定]セクションで[編集]をクリックし、[デフォルトルートオブジェクト]にindex.htmlと入力して保存します。
(16) CognitoのリターンURLを更新します。Cognitoコンソールでユーザープールを開き、左メニューの[アプリケーションクライアント]からhandson-news-clientを開きます。[マネージドログインページ]セクションの[編集]をクリックし、[許可されているリターンURL]にhttps://{CloudFrontドメイン名}/index.htmlを追加して保存します(末尾の/index.htmlを忘れずに)。
(17) CloudShellで3つのファイル(config.js、app.js、index.html)を作成します。まずconfig.jsを作成します(設定値は自分のものに置き換えてください):
|
1 2 3 4 5 6 7 8 9 10 11 12 |
cat > /tmp/config.js << 'EOF' const CONFIG = { region: 'ap-northeast-1', userPoolId: 'ap-northeast-1_XXXXXXXXX', clientId: 'xxxxxxxxxxxxxxxxxxxxxxxxxx', identityPoolId: 'ap-northeast-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', apiUrl: 'https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod', cognitoDomain: 'https://xxxxxxxx.auth.ap-northeast-1.amazoncognito.com', redirectUri: 'https://d1234567890abc.cloudfront.net/index.html' }; EOF |
(18) app.jsを作成します:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
cat > /tmp/app.js << 'EOF' let currentCredentials = null; let isAuthenticated = false; window.onload = async function() { AWS.config.region = CONFIG.region; const hash = window.location.hash; if (hash && hash.includes('id_token')) { await checkAuth(); } else { await initGuest(); } await loadNews(); }; async function initGuest() { AWS.config.credentials = new AWS.CognitoIdentityCredentials({ IdentityPoolId: CONFIG.identityPoolId }); await AWS.config.credentials.getPromise(); currentCredentials = AWS.config.credentials; isAuthenticated = false; } function login() { const callbackUrl = encodeURIComponent(CONFIG.redirectUri); const loginUrl = `${CONFIG.cognitoDomain}/login?client_id=${CONFIG.clientId}&response_type=token&scope=openid+email&redirect_uri=${callbackUrl}`; window.location.href = loginUrl; } function logout() { window.location.hash = ''; window.location.reload(); } async function checkAuth() { const hash = window.location.hash.substring(1); const params = new URLSearchParams(hash); const idToken = params.get('id_token'); if (idToken) { const providerKey = `cognito-idp.${CONFIG.region}.amazonaws.com/${CONFIG.userPoolId}`; AWS.config.credentials = new AWS.CognitoIdentityCredentials({ IdentityPoolId: CONFIG.identityPoolId, Logins: { [providerKey]: idToken } }); await AWS.config.credentials.getPromise(); currentCredentials = AWS.config.credentials; isAuthenticated = true; document.getElementById('authStatus').textContent = 'ログイン済み'; document.getElementById('loginBtn').style.display = 'none'; document.getElementById('logoutBtn').style.display = 'inline'; document.getElementById('addForm').style.display = 'block'; } } async function apiRequest(method, path, body) { const endpoint = new AWS.Endpoint(CONFIG.apiUrl + path); const request = new AWS.HttpRequest(endpoint, CONFIG.region); request.method = method; request.headers['Host'] = endpoint.host; request.headers['Content-Type'] = 'application/json'; if (body) request.body = JSON.stringify(body); const signer = new AWS.Signers.V4(request, 'execute-api'); signer.addAuthorization(AWS.config.credentials, new Date()); const response = await fetch(CONFIG.apiUrl + path, { method: method, headers: request.headers, body: body ? JSON.stringify(body) : undefined }); return { status: response.status, data: await response.json() }; } async function loadNews() { try { const { status, data } = await apiRequest('GET', '/news'); const listDiv = document.getElementById('newsList'); if (status === 200 && Array.isArray(data)) { listDiv.innerHTML = data.map(item => ` <div class="news-item"> <h3>${item.title}</h3> <p>${item.body}</p> <span class="date">${item.createdAt}</span> </div> `).join(''); } else { listDiv.innerHTML = '<p>ニュースの取得に失敗しました。</p>'; } } catch (e) { document.getElementById('newsList').innerHTML = '<p>エラーが発生しました。</p>'; } } async function addNews() { const title = document.getElementById('newsTitle').value; const body = document.getElementById('newsBody').value; const statusDiv = document.getElementById('status'); if (!title || !body) { statusDiv.textContent = 'タイトルと本文を入力してください。'; statusDiv.className = 'error'; return; } try { const { status, data } = await apiRequest('POST', '/news', { title, body }); if (status === 201) { statusDiv.textContent = data.message; statusDiv.className = 'success'; document.getElementById('newsTitle').value = ''; document.getElementById('newsBody').value = ''; await loadNews(); } else { statusDiv.textContent = '投稿に失敗しました。'; statusDiv.className = 'error'; } } catch (e) { statusDiv.textContent = 'エラーが発生しました。'; statusDiv.className = 'error'; } } EOF |
(19) index.htmlを作成します:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
cat > /tmp/index.html << 'EOF' <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ニュースサイト</title> <style> body { font-family: sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; } h1 { color: #232F3E; border-bottom: 2px solid #FF9900; padding-bottom: 8px; } .auth-section { background: #f0f0f0; padding: 16px; border-radius: 8px; margin-bottom: 24px; } .news-item { border: 1px solid #ddd; padding: 16px; margin-bottom: 12px; border-radius: 4px; } .news-item h3 { margin: 0 0 8px 0; } .news-item p { margin: 0; color: #666; } .news-item .date { font-size: 12px; color: #999; } .add-form { background: #e8f5e9; padding: 16px; border-radius: 8px; margin-bottom: 24px; display: none; } input, textarea { width: 100%; padding: 8px; margin: 4px 0 12px 0; box-sizing: border-box; } button { padding: 10px 24px; background: #FF9900; color: #fff; border: none; border-radius: 4px; cursor: pointer; margin-right: 8px; } button:hover { background: #e88a00; } button.secondary { background: #545B64; } #status { margin-top: 12px; padding: 8px; border-radius: 4px; } .success { background: #d4edda; color: #155724; } .error { background: #f8d7da; color: #721c24; } </style> </head> <body> <h1>ニュースサイト</h1> <div class="auth-section"> <span id="authStatus">未ログイン(ゲスト閲覧モード)</span> <button id="loginBtn" onclick="login()">サインイン</button> <button id="logoutBtn" onclick="logout()" style="display:none;" class="secondary">サインアウト</button> </div> <div class="add-form" id="addForm"> <h2>ニュースを追加</h2> <label>タイトル</label> <input type="text" id="newsTitle" placeholder="タイトルを入力"> <label>本文</label> <textarea id="newsBody" rows="3" placeholder="本文を入力"></textarea> <button onclick="addNews()">投稿</button> <div id="status"></div> </div> <h2>ニュース一覧</h2> <div id="newsList">読み込み中...</div> <script src="https://sdk.amazonaws.com/js/aws-sdk-2.1692.0.min.js"></script> <script src="config.js"></script> <script src="app.js"></script> </body> </html> EOF |
(20) 3ファイルをS3バケットにアップロードします:
|
1 2 3 4 5 6 7 8 9 |
BUCKET_NAME="handson-news-site-{自分の名前}" aws s3 cp /tmp/config.js s3://$BUCKET_NAME/config.js \ --content-type "application/javascript" --region ap-northeast-1 aws s3 cp /tmp/app.js s3://$BUCKET_NAME/app.js \ --content-type "application/javascript" --region ap-northeast-1 aws s3 cp /tmp/index.html s3://$BUCKET_NAME/index.html \ --content-type "text/html" --region ap-northeast-1 |
(21) ブラウザでCloudFrontのHTTPS URLにアクセスします(デプロイ完了まで数分かかる場合があります)。
例:https://d1234567890abc.cloudfront.net/index.html
(22) ゲスト状態でニュース一覧が表示されることを確認します。
(23) [サインイン]をクリックして、Cognitoのマネージドログイン画面に遷移します。
(24) タスク6で作成したtestuser@example.com / TestP@ss1234でサインインします(もしくは新規サインアップもできます)。
(25) リダイレクト後、「ログイン済み」と表示され、ニュース追加フォームが表示されることを確認します。
(26) タイトルと本文を入力して[投稿]をクリックし、ニュースが追加されることを確認します。
ポイント
- config.js、app.js、index.htmlの3ファイルに分離することで、設定変更時にconfig.jsだけ修正すれば済みます。
- CloudFrontを使うことでHTTPS配信が可能になり、CognitoのマネージドログインのリターンURLに設定できます。
- OAC(Origin Access Control)により、S3バケットへの直接アクセスを防ぎ、CloudFront経由のみでアクセスできるようにしています。
- AWS SDK for JavaScriptを使ってブラウザからCognito IDプールの一時認証情報を取得しています。
- AWS.Signers.V4でAPIリクエストに署名バージョン4の署名を付けています。これによりIAM認証のAPI Gatewayにアクセスできます。
- マネージドログインのImplicit Grant(response_type=token)フローにより、サインイン後にURLフラグメントでIdTokenが返されます。
- ゲスト状態ではIDプールのUnauth Roleが使われ、GETのみ許可されます。サインイン後はAuth Roleに切り替わり、POST/PUTも許可されます。
トラブルシューティング:CORSエラーでAPIにアクセスできない場合
ブラウザのコンソールに以下のようなエラーが表示される場合があります:
|
1 2 |
Access to fetch at '...' has been blocked by CORS policy: Request header field x-amz-user-agent is not allowed by Access-Control-Allow-Headers in preflight response. |
AWS SDK for JavaScriptがリクエストにX-Amz-User-Agentヘッダーを自動的に付与するため、API GatewayのOPTIONSメソッド(プリフライトレスポンス)でこのヘッダーを許可する必要があります。
対処方法:
– API Gatewayコンソールで/newsリソースおよび/news/{id}リソースの[CORSを有効にする]を開く。
– Access-Control-Allow-Headersの値を以下に設定する:Content-Type,Authorization,X-Amz-Date,X-Amz-Security-Token,X-Amz-User-Agent
– APIを再デプロイする。
Lambda関数のレスポンスヘッダーではなく、OPTIONSメソッド(MOCK統合)のレスポンスヘッダーがプリフライトの許可判定に使われる点に注意してください。
クリーンアップ
ハンズオンで作成したリソースをすべて削除します。以下の順番で削除してください。
(1) CloudFrontディストリビューションの削除
CloudFrontコンソールでディストリビューションを選択し、[無効化]をクリックして無効化完了後に[削除]します。
(2) WebACLの削除
AWS WAFコンソール(リージョンを[Global(CloudFront)]に切り替え)で、CloudFront作成時に自動生成されたWebACLを削除します。先にCloudFrontディストリビューションとの関連付けが解除されている必要があります(ディストリビューション削除後であれば問題ありません)。
(3) S3バケットの削除
S3コンソールでhandson-news-site-{自分の名前}バケットのバケットポリシーを削除し、オブジェクトを削除してから、バケットを削除します。
(4) API Gatewayの削除
API GatewayコンソールでHandsonNewsAPIを削除します。
(5) Lambda関数の削除
Lambdaコンソールで以下の関数を削除します:
– GetNewsFunction
– PostNewsFunction
– PutNewsFunction
(6) DynamoDBテーブルの削除
DynamoDBコンソールでHandsonNewsテーブルを削除します。
(7) Cognito IDプールの削除
Cognito IDプールコンソールでhandson-news-idpoolを削除します。
(8) Cognitoユーザープールの削除
Cognitoコンソールでhandson-news-poolを削除します。
(9) IAMロールの削除
IAMコンソールで以下のロールを削除します:
– handson-news-auth-role
– handson-news-guest-role
– GetNewsFunction-role-xxxxxxxx
– PostNewsFunction-role-xxxxxxxx
– PutNewsFunction-role-xxxxxxxx
(10) CloudWatch Logsロググループの削除
CloudWatchコンソールの[ロググループ]で、Lambda関数のロググループを削除します:
– /aws/lambda/GetNewsFunction
– /aws/lambda/PostNewsFunction
– /aws/lambda/PutNewsFunction
(11) ハンズオン用ポリシーの削除
ハンズオン用に作成したIAMポリシーがあれば削除します。
まとめ
| タスク | サービス | 役割 |
|---|---|---|
| タスク 1 | DynamoDB | ニュース記事の保存先 |
| タスク 2 | Lambda | ニュースの一覧取得・追加・編集のバックエンド処理 |
| タスク 3 | API Gateway | IAM認証で保護されたREST APIエンドポイント |
| タスク 4 | Cognitoユーザープール | ユーザーのサインアップ・サインイン管理 |
| タスク 5 | Cognito IDプール | ゲスト/認証済みユーザーへのIAMロール割り当て |
| タスク 6 | CloudShell(Python) | ゲスト・認証済みの動作確認 |
| タスク 7 | CloudFront + S3 + フロントエンド | HTTPS配信とブラウザからの操作体験 |
このハンズオンで体験した認証・認可の特徴をまとめます。
- Cognitoユーザープールでサインアップ・サインイン機能を簡単に構築できます。ホストされたUIを使えばフォームの実装も不要です。
- Cognito IDプールにより、認証済みユーザーとゲストユーザーにそれぞれ異なるIAMロールを割り当てられます。
- API GatewayのIAM認証とIAMポリシーのResourceレベルの制御により、HTTPメソッドとパスに応じた細かいアクセス制御が実現できます。
- 署名バージョン4でリクエストに署名することで、IAM認証のAPIにセキュアにアクセスできます。
- ゲストユーザーにもIDプール経由で制限付きの一時認証情報を発行できるため、未認証でも安全にAPIを利用させることが可能です。
最後までお読みいただきましてありがとうございました!
「AWS認定資格試験テキスト&問題集 AWS認定ソリューションアーキテクト - プロフェッショナル 改訂第2版」という本を書きました。
「AWS認定資格試験テキスト AWS認定クラウドプラクティショナー 改訂第3版」という本を書きました。
「AWS認定資格試験テキスト AWS認定AIプラクティショナー」という本を書きました。
「ポケットスタディ AWS認定 デベロッパーアソシエイト [DVA-C02対応] 」という本を書きました。
「要点整理から攻略するAWS認定ソリューションアーキテクト-アソシエイト」という本を書きました。
「AWSではじめるLinux入門ガイド」という本を書きました。
開発ベンダー5年、ユーザ企業システム部門通算9年、ITインストラクター5年目でプロトタイプビルダーもやりだしたSoftware Engineerです。
質問はコメントかSNSなどからお気軽にどうぞ。
出来る限りなるべく答えます。
このブログの内容/発言の一切は個人の見解であり、所属する組織とは関係ありません。
このブログは経験したことなどの共有を目的としており、手順や結果などを保証するものではありません。
ご参考にされる際は、読者様自身のご判断にてご対応をお願いいたします。
また、勉強会やイベントのレポートは自分が気になったことをメモしたり、聞いて思ったことを書いていますので、登壇者の意見や発表内容ではありません。
関連記事
-
-
freeeで確定申告書類を作成しました
2019年度の確定申告を、2018年度に引き続き、freeeを使って申告書類を作 …
-
-
Sculpt Ergonomic Keyboard for Business USB Port 5KV-00006の3回目の購入
マイクロソフト キーボード ワイヤレス/人間工学デザイン Sculpt Ergo …
-
-
MacBook Proのバッテリーがスリープ中に消費されていたので停止した
起こったこと MacBook Proのバージョンアップ後、フル充電から移動してリ …
-
-
食べログの「ブログに店舗情報を貼る」便利ですね
もうひとつ書いてるブログがありまして、主に自転車の走行記録を書いてたのですが、最 …
-
-
WordPressのPHPを7から8にしたらプラグインのエラー”Uncaught TypeError: implode(): Argument #2 ($array) must be of type ?”
WordPressのPHPを7から8にしたら次のエラーが発生しました。 PHP …
-
-
初めてのe-Tax確定申告(freee)
環境 freee会計スタータープラン利用 MacBook Pro (13-inc …
-
-
2022年もありがとうございました!!
2022年サマリー 2022年もありがとうございました! 会社コース実施日数 1 …
-
-
「要点整理から攻略する『AWS認定ソリューションアーキテクト-アソシエイト』」を執筆しました
要点整理から攻略する 『AWS認定ソリューションアーキテクト-アソシエイト』とい …
-
-
スターバックスのMOBILE ORDER & PAYがものすごく楽でした
この動画、2020年8月に公開されてるってことは去年からあったのでしょうか。 知 …
-
-
来年から青色申告するためにfreee開業から開業届と青色申告承認申請書を電子提出しました
freee会計を使用していますので、半自動で会計処理が完了しています。 そして青 …

