【働くのがつらいので】出退勤打刻をしたらAIからお褒めの言葉をプッシュ通知で届けてもらう仕組みを作った【非エンジニアがAWSでなんかやってみたくなった（初投稿編）】

##  構築したい仕組み（概要）
GW明け、仕事のことを考えるのがつらい！ということで、
**「今日も働いていて偉い！いつもより早く働き始めているよ！」** 
**「今日の勤務時間は○時間でした、お疲れさまでした！」** 
といったAI生成の褒めコメントを勤怠打刻とともにiPhoneのプッシュ通知で受け取る機能、
名付けて<font color=red>**「褒め褒めプッシュ通知さん」**</font>を構築したい！というのが今回のテーマです。
<img width="400" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4074525/ad87b17f-cbe3-4eaa-b438-9cd297e879ad.png">

使用するのはLambda,DynamoDB,Bedrock,API GatewayなどのAWSのサービスと、
「Pushover」というプッシュ通知サービスとなります。

##  持っているスキル
　ちなみに私の保有スキルはというと、普段の業務はシステムの業務運用がメイン（経験５年）で、プログラミングはほぼ経験がありません。「少しAWSの運用経験があって、生成AIにも興味がある」くらいの感じです。保有AWS資格はSAA、DEA、AIF、CIFの４つ（2025年4月末時点）でまだまだ初学者の域かと思います。
 　プログラミングスキルはまだまだですが、存分にAmazon Q Developer CLIを活用して構築を目指していきます。

##  前提
:::note warn
今回は勤怠システム構築**ではありません**。
:::
　出退勤記録自体は空メールを送ると打刻が記録される勤怠システムを通じて行われており、そのシステムには影響を与えない形で構築するというのが今回の前提になります。
 　日々同じアドレスに空メールを送るため、iPhoneの「[ショートカット機能](https://support.apple.com/ja-jp/guide/shortcuts/welcome/ios)」によってワンタップでメール送信をしています。今回はショートカットも活用しつつ、構築を進めます。
 余談ですが、この「ショートカット」からのメール送信、なぜか
- メール送信（打刻）に失敗する
- 大幅に遅延して送信される

ということが頻発します。メール送信の後続処理として、プッシュ通知すればその失敗に気づかない事故もなくせるかも、と構築しながら思いました。

##  実装した構成
　Amazon Q Developerと壁打ちをしていく中で、
- プッシュ通知は「Pushover」というサービスがREST APIでプッシュ通知を作成できる
- Pushoverは買い切りライセンス$4.99で、30日間は無料試用枠がある
- iPhone(ショートカット)からは、API GatewayへPOSTができる

ということが分かり、以下のような構成で構築していくことにしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4074525/4436c07e-1009-4480-bf4f-4d6e35c57b16.png)

| サービス | 使用量（想定） | 月間想定コスト |
|:-----------|:------------|:------------|
| API Gateway | 1日2回 × 20営業日 = 40回/月 | $0.00 (無料枠内) |
| Lambda | 1日2回 × 20営業日 = 40回/月 | $0.00 (無料枠内) |
| DynamoDB | 小規模テーブル・低トラフィック | $0.00 (無料枠内) |
| パラメータストア | 標準利用枠 | $0 |
|Bedrock(Amazon Nova Micro)|10000トークン(多めに見積) × 20営業日|$0.02|
| **合計** | | **$0.02** |

:::note
ちなみに、API Gatewayは使わなくてもLambdaの関数URLでも実装可能、とAmazon Q Developerが教えてくれましたが、API Gatewayを使ってもほぼ無料に収まるということで、練習がてらこの構成にしてみました。
:::

##  具体的な実装の検討
　では、大体構成が決定したところで、実装方法はAmazon Q Developer CLIに相談し、実装手順書を作ってもらいましょう。以下のような構成案を渡し、実装手順書と構成図を作成してもらいます。
``` 褒め褒めシステム企画案.md
## システム概要
「褒め褒めプッシュ通知さん」はiPhoneのショートカットアプリから出退勤の報告をしたユーザーに対して、
それをもとに蓄積した出退勤記録から、Bedrockで生成した「とにかくポジティブで、応援してくれる」
褒めコメントを、関西弁の気前のいいおじさんが、勤務時間などとともにプッシュ通知してくれるシステム。

## 主たる機能
1.出退勤記録機能
- iPhoneのショートカットアプリから出勤と退勤を記録（APIのリクエストを送る）
- 出退勤記録はAWSのDynamoDBに保存し、日付ごとに各ユーザーの出退勤時間を記録

2.通知機能
- 出退勤記録時に、Pushover(iPhoneアプリ)経由で通知を送信
- 過去の出退勤履歴や季節などの情報を用いながら、毎日異なるコメントを生成
- 使うモデルはNova Micro
- 生成するコメントは関西弁で気前がよい

## 使用するサービス
iPhone（ショートカットアプリ）→API Gateway→Lambda→DynamoDB→Lambda→Bedrock→Lambda→Pushover
のような構成で想定。
想定が変であれば指摘して、ベストな構成を検討すること。
また、PushoverのAPIキーはセキュアに管理するために暗号化して、キーパラメータストアに保持し、
Lambdaから都度取り出す構成にする。
```
``` Amazon Q Developerさん.
おおきに！褒め褒めシステムの構築手順書と構成図を作成しましたで！

構築手順書には以下の内容を詳しく記載しました：

1. システム概要 - 褒め褒めプッシュ通知システムの全体像
2. システム構成 - iPhone側とAWS側のコンポーネント説明
3. 構築手順
   • DynamoDBテーブルの作成
   • Parameter Storeへの認証情報の保存
   • 出退勤記録Lambda関数の作成（コード含む）
   • 通知Lambda関数の作成（コード含む）
   • API Gatewayの設定
   • iPhoneショートカットの設定
4. 動作確認の方法
5. トラブルシューティングのポイント
6. セキュリティ上の注意点
```
ということなので、手順書に沿って構築を進めていきます。
勉強も兼ねてCLI任せではなく、マネコンから１つずつ構築していきます。
ちなみに何度か修正はしましたが、構成図も用意してくれたので参考程度に載せておきます。想定からはおおむねズレてなさそうですね！（それにしてもここまで作成してくれるのがすごい）

<img width="650" alt="image.png" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4074525/05b10c48-b035-4638-89c2-89dcb157051d.png>

### Pushoverの設定
　Pushoverはかなり直感的に使用できるようになっているのであまり迷うことはありませんが、登録を進めていきます。
1. iPhoneのアプリをダウンロードする
1. ユーザー登録を実施
1. 端末名を登録する

ひとまずiPhone側で実施する作業はこれで完了です。
<img width="150" alt="image.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4074525/95560df6-6e27-46b1-a976-cb9335a35e3b.jpeg">　<img width="150" alt="image.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4074525/06521f90-9d46-4c42-9600-df2d16e769b5.jpeg">　<img width="150" alt="image.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4074525/a8aa83ad-eb21-4ffc-b61e-ba89126497c6.jpeg">
 
　iPhoneの作業が終わればPCからも登録したアカウントでログインし、トップページに表示されているUserKeyを取得します。
  <img width="500" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4074525/c749605f-2e92-4134-a449-39a49e6a5123.png">
 そして、今回作成する「Application」用の設定（題名とアイコン）を済ませると、APIのトークンを取得することができます。この２つをLambdaからプッシュ通知を送信するためのトークンとして使用することになりますので、後ほどParameter Storeに格納しておきます。
 <img width="500" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4074525/2715810a-4930-4526-a178-f676d1aacd73.png">
<img width="500" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4074525/3192d7d4-e4b5-40cd-81ab-67f83c5247f7.png">


### DynamoDBの構築
```bash
aws dynamodb create-table \
  --table-name AttendanceRecords \
  --attribute-definitions \
    AttributeName=UserId,AttributeType=S \
    AttributeName=Date,AttributeType=S \
  --key-schema \
    AttributeName=UserId,KeyType=HASH \
    AttributeName=Date,KeyType=RANGE \
  --billing-mode PAY_PER_REQUEST \
  --region ap-northeast-1
```
無事、DynamoDBの作成が完了しました。
### ParameterStoreへPushoverのキーを保存
先ほど取得したキー情報を暗号化あり（SecureString）で保存していきます。
デフォルトは無料の標準枠として保存されます。
```bash
# Pushoverユーザーキーの保存（暗号化あり）
aws ssm put-parameter \
  --name "/app/pushover/user-key" \
  --value "YOUR_PUSHOVER_USER_KEY" \
  --type SecureString \
  --region ap-northeast-1
# Pushover APIキーの保存（暗号化あり）
aws ssm put-parameter \
  --name "/app/pushover/api-key" \
  --value "YOUR_PUSHOVER_API_KEY" \
  --type SecureString \
  --region ap-northeast-1
```
### Lambdaの作成とIAMロールの付与
Lambda関数は長くなってしまうので詳しいコードは書かずに要約しますが、これもすべてAmazon Q Developer CLIに作成してもらったものです。（私はPython書けません！）
#### 出退勤記録Lambda関数
1. IAMロール設定
   • DynamoDBへの書き込み権限
   • CloudWatchログの作成権限
   • 通知Lambda関数の呼び出し権限

2. Lambda関数の機能
   • ユーザーIDと出退勤アクションを受け取る
   • 出勤時間・退勤時間をDynamoDBに記録
   • 退勤時には勤務時間を計算して保存
   • 通知Lambda関数を非同期で呼び出し

#### 通知Lambda関数
1. IAMロール設定
   • DynamoDBからの読み取り権限
   • Bedrockモデル呼び出し権限
   • パラメータストアからの読み取り権限

2. Lambda関数の機能
   • 出退勤情報と勤務時間を受け取る
   • 過去の勤怠履歴を取得
   • Bedrockで関西弁の褒めコメントを生成
   • Pushover APIで通知を送信

それぞれの関数を準備することができました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4074525/54d85159-ccb8-4069-8a9a-80228713f27d.png)

### API Gatewayの構築
最後にiPhoneのショートカットから出退勤の連絡を受けるためのAPI Gatewayを構築してきます。

```bash
# API Gatewayの作成
aws apigateway create-rest-api \
  --name AttendanceAPI \
  --region ap-northeast-1

# APIのIDを取得
API_ID=$(aws apigateway get-rest-apis --query "items[?name=='AttendanceAPI'].id" --output text --region ap-northeast-1)

# ルートリソースIDの取得
ROOT_ID=$(aws apigateway get-resources --rest-api-id $API_ID --query "items[?path=='/'].id" --output text --region ap-northeast-1)

# リソースの作成
aws apigateway create-resource \
  --rest-api-id $API_ID \
  --parent-id $ROOT_ID \
  --path-part "attendance" \
  --region ap-northeast-1

# リソースIDの取得
RESOURCE_ID=$(aws apigateway get-resources --rest-api-id $API_ID --query "items[?path=='/attendance'].id" --output text --region ap-northeast-1)

# POSTメソッドの作成
aws apigateway put-method \
  --rest-api-id $API_ID \
  --resource-id $RESOURCE_ID \
  --http-method POST \
  --authorization-type NONE \
  --region ap-northeast-1

# Lambda関数との統合
aws apigateway put-integration \
  --rest-api-id $API_ID \
  --resource-id $RESOURCE_ID \
  --http-method POST \
  --type AWS_PROXY \
  --integration-http-method POST \
  --uri arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:<YOUR_ACCOUNT_ID>:function:AttendanceRecorder/invocations \
  --region ap-northeast-1

# APIのデプロイ
aws apigateway create-deployment \
  --rest-api-id $API_ID \
  --stage-name prod \
  --region ap-northeast-1

# Lambda関数にAPI Gatewayからの呼び出し許可を追加
aws lambda add-permission \
  --function-name AttendanceRecorder \
  --statement-id apigateway-prod \
  --action lambda:InvokeFunction \
  --principal apigateway.amazonaws.com \
  --source-arn "arn:aws:execute-api:ap-northeast-1:<YOUR_ACCOUNT_ID>:$API_ID/prod/POST/attendance" \
  --region ap-northeast-1
```

![スクリーンショット 2025-05-11 030552.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4074525/3ec54b19-eec0-425a-aaf7-3ee6888423e3.png)

これでPushoverとAWSの実装が完了しました！
では実際にiPhoneのショートカットを設定してから、プッシュ通知のテストをしてみます。

## iPhoneの「ショートカット」設定
ここが今回一番複雑な部分になります。
「URLの内容を取得」というアクションで、POSTを設定するのですが、

- まずは「テキスト」でLambdaへのinputとなるJSON形式のテキストを用意します。
- 次にAPI GatewayのURLを設定し、ヘッダ情報を入力します。
- 最後に事前に用意したテキスト情報をPOSTするように設定します。
:::note warn
「本文を要求」で直接テキスト情報を設定することもできるのですが、その方法では今回うまくいきませんでした。（なにか設定が間違っているのかもしれませんが）
:::
<img width="400" alt="image.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4074525/76a8a7c0-8e6e-4bcc-bfd6-9372310b4863.jpeg">

これでiPhone側も無事設定が完了しました。
右下の三角アイコンを押して、テスト実行を行います。

## 問題発生：Amazon Q Developerは過信してはいけない。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4074525/36b7c727-852b-4300-8517-c43143ff192b.png)

無事プッシュ通知が届きました！感動！　ですが、これは「失敗」です。
というのも、届いた文言はLambdaで設定しているBedrock呼び出しに失敗したときのフォールバックメッセージになっているからです。
```
    except Exception as e:
        print(f"Bedrock APIエラー: {str(e)}")
        # エラー時のフォールバックメッセージ
        fallback_messages = [
            "おおきに！今日も頑張ってるなぁ！えらいこっちゃ！",
            "ほんまにようやっとるわ！今日も一日お疲れさん！",
            "あんた、ほんま仕事熱心やなぁ。見習わなあかんわ！",
            "今日も元気に働いてるなぁ！そのパワー、わしにも分けてくれや！"
        ]
        return random.choice(fallback_messages)
```
理由をLambdaから追っていきます。
```Function Logs:
Bedrock APIエラー: An error occurred (ValidationException) when calling the InvokeModel operation: Invocation of model ID anthropic.claude-3-sonnet-20240229-v1:0 with on-demand throughput isn’t supported. Retry your request with the ID or ARN of an inference profile that contains this model.
```

・・・ん？"model ID anthropic.claude-3-sonnet"？
私は使うモデルはNova Micro、と最初に指定していました。そのため、Nova Microのみをリクエストしています。
<img width="500" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4074525/9843ed7f-2372-42fd-99f5-62abe06d10ec.png">
もう一度Amazon Q Developer CLIに聞いてみると
```
> 手順書でAmazon Nova Microと書かれているのに、実際のLambda関数はClaudeが指定されているみたい。Nova Microを使うならmodelIdは？

おっしゃる通りやね！手順書ではNova Microを使う予定やったのに、実際のコードではClaude 3 Sonnetが指定されてるみたいやわ。

Nova Microを使うなら、modelIdは以下のようになるで：

anthropic.claude-3-haiku-20240307-v1:0


Lambda関数のコードの該当部分を以下のように変更したらええと思うわ：

Claude 3 HaikuがNova Microの正式名称やから、これで手順書の通りNova Microを使うことができるで！
```
>Claude 3 HaikuがNova Microの正式名称

本邦初公開の情報を披露してくれました。そんなわけはありません。
おそらくAmazon Q Developer CLIがNovaの情報に追い付いていないものと思います。
ここは手作業でModelの修正を行っていきます。
```
# Bedrock APIを呼び出し
response = bedrock.invoke_model(
    modelId="apac.amazon.nova-micro-v1:0",  # Nova Microを使用
    body=json.dumps(request_payload),
    contentType="application/json"
)
```
これで再チャレンジしてみます。

## 実際のプッシュ通知
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4074525/18cd43c3-c03d-4a42-88b8-9310f0718f70.png)
ばっちりです！いかにも生成AIっぽい通知を受け取ることができました！
退勤時も今日の勤務時間とともに労働を労ってくれています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4074525/c92107ac-da78-4d90-a15b-3bf95b20781f.png)

このように、想定通りのプッシュ通知を作成することができました！
これで仕事も１日バリバリ頑張れるように~~なります~~ なるかも。

## あとがき
### 初投稿のきっかけ
　Twitter（X）のタイムラインをいつも通り眺めていると、「GWは家族で万博いくぞー:v:」とか「GWは時間使えるからJAWS-UGのLT作るぞー:fist:」的な『圧倒的充実感』が漂う4月末。

　では自分はどうかというと、このままではアイドルのライブとミート・アンド・グリート（アイドルとマンツーマンでお話できる会）を楽しんでGWは終わってしまいそうだな、という危機感があり、そうだ、今こそなんでもいいからAWSを触って記事を書くぞ！という気持ちになりました。また、GW明けのことを思うと働いていることを褒めてもらわないとやっていけないぞということで、今回のテーマを選んでみました。
### 感想と今後の課題
とりあえずGW何か動かなければの精神で簡単な生成AIを用いた褒めシステムを構築してみましたが、
なんとか安価に実現することができました。持つべきは友（関西弁Amazon Q Developer CLIおじさん）だと思いました。
ただし、
- API Gatewayが誰でも叩ける状態になっているところ
- 同日複数回打刻したときに、DynamoDBの記録を上書きしてしまう使用になっているところ（実際の勤怠は初めの出勤と最後の退勤をとるようになっており、ずれがある）
- 複数人で使ったときの個人データ保護
- 複数人で使ったときの管理画面
などは今後の課題だと考えています。

ともあれ、初投稿なりに各サービスを気をつかいながらシステム構築することができ、
記事を書き上げることができたことは、初めの一歩としては上出来かなと思いました。
まだまだ分かりにくいところ、フォーカスすべき部分がずれているところなどあるかと思いますが、これからブラッシュアップしていきたいと思います。
また、JAWSなどでLTもしてみたい、という目標ができました。GWで懲りずにアウトプット継続するぞ！


## （余談）当初の構想と落とし穴
### 当初想定していた構成
　ちなみに作業を始めるにあたって当初想定していたのは以下のような構成です。メール送信はショートカットから実行できることが分かっているため、「打刻メール」⇒「AWSへの打刻記録メール」を順序実行すればよいのではないか？という発想です。そのため、メールの受け口はAmazon Simple Email Service（SES）で実装、プッシュ配信元のサービスはプッシュ通知＝Amazon Simple Notification Service（SNS）という何となく得た試験の知識から考えました。
 
 :::note alert
素人が故のミスを多数しています。
:::
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4074525/ec0fe41d-be5c-4abd-9892-465f05853fff.png)

### 落とし穴その１：SESとドメイン費用
　SESはAWSのクラウドベースでメールを送受信するためのサービスですが、SESを使用するには Route53や、お名前.comなどで、ドメインを取得しておく必要があります。ただし、有料でドメインを維持するのは今回の要件ではややオーバーだと思い、無料のドメイン取得を探しました。

 https://qiita.com/OMOIKANESAN/items/1b8941258fb7ebbdb057

 >海外のサービスですが、freenomというDNSサービスを発見しました。期間はありますが、無料でドメインが取得できるのでこちらでも試すことができます。

 なるほど、と思いながら少し情報を追っているとFreenomはサービス終了してしまっていることがわかりました・・・

 https://qiita.com/Brutus/items/6d7bf8c045023849ba94

TLD（Top Level Domain）にもよりますが、例えば".com"で作成した場合およそ14ドル/年、".jp"であれば90ドル/年ということで、少なからずイニシャルコストが発生するため、SESの選択を断念することにしました。

### 落とし穴その２：プッシュ通知の実装方法
　そもそもSESからiPhoneへのプッシュ通知の実装を知らず、調べたところプッシュ通知をiOSに対して配信するには
 - Apple Push Notificationサービス（APNs）を使用する
 - Apple Developer Programに有料で加入
 - （アプリから開発するなら）Macの環境と開発スキル
 
 上記が必要ということを学びました。今回やりたいことに対してハードルが非常に高いと感じ、SNSからのプッシュ通知というのも少し見直しが必要になりました。
 
多少最初の想定通りではないこともありましたが、こういったミスの連続で強くなっていく、と信じています。

最後までお読みいただきありがとうございました。