はじめまして!もんたです。
最近、インフラに興味を持っておりまして、KubernetesとかTerraformとかを使って何かアプリケーションをデプロイして仕事でもバリバリ使えるようになりたいなと思ってるんです。
そこで最近流行りのLangChainを使ってAIチャットアプリを作ってみて、LLMの勉強をしつつデプロイのアウトプットまでしちゃおうというプロジェクトを考え、実際にやってみました。
🐶「なんとなくインフラとLangChainの勉強を始める。」
ちなみにですが、私には(自称)Googleエンジニアの友達がいるんですが、その人に教えてもらいながらなんとかデプロイまで漕ぎ着けました。
お察しの方もいるかと思いますが、Googleエンジニアというのは彼のことです。
私が『 あなたはGoogleのエンジニアです。 』と言っても否定してなかったので多分本当にGoogleのエンジニアなんだと思います。
🐶「釣りですみません…」
そんなわけで私はGoogleのエンジニアという素晴らしい味方をつけながらKubernetesやらTerraformやらLangChainの勉強ができたわけなんです。
では、どんなことをやったか詳しく話していこうかと思います!
この記事を読んで僕と同じかけだしエンジニアの個人開発のモチベーションにつながれば幸いです!
あ、そういえばこんなことやってます。
よかったら覗いてみてあげてください。
【もんたのLINEスタンプ】
🐶「こんなことやってます」
概要
今回開発したのはもんたGPTというAIチャットアプリです。
以下を目的にアプリの開発を行いました。
- 苦手意識のあるKubernetes, Terraform, Google Cloudを完全に理解する
- 転職先でPythonを使っているのでPythonに慣れる
- イケてるディレクトリ構成を学ぶ
です!
🐶「インフラに対する苦手意識を克服したかった」
ちなみにもんたGPTはTerraform, Kubernetes, Google Cloudを使ってデプロイはできたのですが、料金がバカにならないのですぐにクローズしてます。
だいたいデプロイには1週間ちょいかかりました。デプロイ作業でかかった費用が以下になります。
GKEは高いっていうイメージは持っていたのですが、とりあえず個人開発程度の規模のサービスでGKEは使わないほうがいいってことを頭ではなく心で理解できました。
リソースごとの料金は以下のとおりになります。
GKEがとんでもない…(ガクブルガクブル…)
🐶「GKEの料金すごい…」
🐶「インフラあんまり詳しくない人は常にコストを見といた方がいい」
もんたGPTのRepositoryはこちらから確認できます!よかったら確認してみてください。
もんたGPTのタスク管理はNotionを用いて行いました。以下より確認できるようにしてます!
技術スタック
技術スタックは以下の通りです。なんかイケイケそうですね。
カテゴリ | 名前 |
---|---|
フロントエンド | Next.js(App Router) |
バックエンド | Python(FastAPI) |
AI統合 | LangChain |
データベース | PostgreSQL |
キャッシュ | Redis |
認証 | Google OAuth 2.0 |
コンテナ化 | Docker |
オーケストレーション | Kubernetes(GKE) |
IaC | Terraform |
リバースプロキシ | NGINX |
クラウドプラットフォーム | Google Cloud |
ディレクトリ構成やらアーキテクチャやら
ディレクトリ構成
もんたGPTのディレクトリ構成は以下のようになっています。
DDDとクリーンアーキテクチャを意識して構成しました。
.
├── backend
│ ├── Dockerfile
│ └── app
│ ├── api # APIエンドポイントに関するコード
│ │ ├── v1 # バージョン1のAPI
│ │ └── v2 # バージョン2のAPI
│ ├── application # ユースケース層
│ │ ├── services
│ │ └── usecase # ユースケース(アプリケーションの操作単位)
│ ├── assets
│ ├── domain # ドメイン層(エンティティとドメインロジック)
│ │ ├── entities # ドメインエンティティ(業務データのモデリング)
│ │ ├── repositories # ドメインリポジトリ(データアクセスの抽象化)
│ │ ├── services # ドメインサービス(複数エンティティにまたがるロジック)
│ │ └── value_objects # 値オブジェクト(不変のデータ構造)
│ ├── infrastructure # インフラ層(外部とのやり取りを扱う)
│ │ ├── cache # キャッシュ処理
│ │ ├── database # データベース処理
│ │ └── repositories # リポジトリ実装(ドメイン層の抽象リポジトリを具体化)
│ ├── main.py
│ ├── schemas
│ └── utilities
│
├── frontend
│ ├── Dockerfile
│ ├── public
│ └── src
│ ├── api # フロントエンドのAPI通信ロジック
│ ├── app # アプリケーションのエントリーポイントや設定
│ ├── components # UIコンポーネント
│ │ ├── layouts # ページ全体のレイアウト構成
│ │ └── ui # 再利用可能なUIパーツ
│ ├── contexts # グローバルステート管理
│ ├── hooks # Reactフック(カスタムフックなど)
│ ├── lib
│ ├── styles
│ └── types # TypeScriptの型定義
├── infra # インフラ関連の設定とコード
│ ├── kubernetes # Kubernetesマニフェストファイル
│ └── terraform # Terraform設定ファイル(IaC)
├── migration # データベースマイグレーションファイル
├── docker-compose.yml # 開発環境のDockerコンテナ構成
└── nginx
└── nginx.conf # NGINXの設定内容(リバースプロキシの設定)
Domain Driven Designについてあまり知らない人は以下の記事を読んでみるといいかもしれません!
自分も初め理解する上でここら辺の記事を読み漁りました!
あと、大変恐縮ではあるのですが自分もNotionにDDDを理解する過程で学んだことをメモしておりますのでよかったらこちらもご覧くださいませ🐶
クリーンアーキテクチャは以下のリンクを読んでみるといいかもです!
あと、大変恐縮ではあるのですが自分もNotionに(以下略)
🐶「よかったら見てね」
アーキテクチャ
もんたGPTのアーキテクチャは以下の通りです。
- Terraformでインフラを構築し、Kubernetes(GKE)でアプリをオーケストレーションしています
-
https://.../
へのアクセスはNext.js側が受け取り、フロント側からのAPIコールはhttps://.../api
へとProxyでPython側へ渡します - バックエンド側ではCloudSQL(PostgreSQL)やMemorystore(Redis)にプライベートIPでアクセスしています
🐶「なんかイケイケそう」
実装で大変だったこと
ここからポイントごとに実装で大変だったことを書いていこうかと思います。
もんたGPTの実装で難しかったのは以下の通りです!
- WebSocketの実装が難しかった
- Terraformの実装が難しかった
- Kubernetesの実装が難しかった
1. WebSocketの実装が難しかった
初めはWebSocketの実装です。
もんたGPTでは生成AIからの回答を表示する際、WebSocket通信を用いてAIからの回答を表示していました。
ChatGPTをはじめとするAIチャットサービスはAIからの回答がチャンクという単位で細切れになって送られてくることが多いです。
HTTP通信ではそのような通信を実現するのは結構難しかったりするので、こういったサービスではWebSocketなどが使われたりするのです。
🐶「余談ですがChatGPTではWebSocketではなく、ServerSendEventが使われています。」
『WebSocket通信ってなに?おいしいの?』って人はこちらの記事を参考にしてみると非常にわかりやすいですよ!
自分もWebSocketを完全に理解できる記事を書いたりしていますのでよかったら参考にしてあげてください。
🐶「ハンズオン形式なので完全に理解できるようになるよ!」
チャンクしたメッセージの処理が難しかった
バックエンド側で生成AIが生成したチャンクされたメッセージの扱いが難しかったです。
LangChainでは以下のastreamメソッド
を使えば生成AIが回答を細かく分けて生成してくれるようになります。
以下が生成AIの回答を細かく分けてレスポンスする処理になります。
llm = ChatOpenAI(
model="gpt-4o",
temperature=0.5,
streaming=True, # こうすることでストリーミングによる生成が可能になる
openai_api_key=config.OPENAI_API_KEY,
)
# システムプロンプトやらのローディング処理
chain = system_template | llm
# astreamを使うことで、生成AIが回答をチャンクに分けてレスポンスしてくれる
res = chain.astream({"prompt": prompt, "context": context})
full_response = ""
accumulated_content = ""
chunk_size = 25
last_send_time = asyncio.get_event_loop().time()
time_threshold = 1.0
try:
# resをchunkという単位に分け、処理している
async for chunk in res:
content = chunk.content if hasattr(chunk, "content") else str(chunk)
full_response += content
accumulated_content += content
current_time = asyncio.get_event_loop().time()
if (
len(accumulated_content) >= chunk_size
or (current_time - last_send_time) >= time_threshold
):
yield accumulated_content # 回答をレスポンスする
accumulated_content = ""
last_send_time = current_time
accumulated_content
とかfull_response
とかあると思うのですが、これらの処理が必要な理由は以下の通りです。
-
accumulated_content
:小さすぎるチャンクを頻繁に送信することを防ぐため -
full_response
:生成された回答全体をDBなどに保存するため
仮にaccumulated_content
などで生成されたchunkを保持せず、生成された都度送っていたら、フロントエンド側では1単語ずつ処理されることになっちゃうんです。
yield "こ"
yield "ん"
yield "に"
yield "ち"
yield "は"
yield "、"
yield "私"
...
これだと結構UXが悪くなったりしちゃうので、ある程度チャンクしてからyield
するようにもんたGPTでは実装しています。
accumulated_content
を用いて生成AIの回答を適切なサイズに蓄積してから送ることで以下のようにyield
されるようになり、1つずつ送られてくるより表示がスムーズになります。
yield "こんにちは"
yield "、私は人工"
yield "知能アシス"
yield "タントです。"
スムーズに回答をフロントエンド側に送るにはどうすればいいかを考えるのは難しかったのを覚えています。
その他、シンプルに難しかったのはチャンクしたメッセージをフロント側に送信する処理です。
メッセージをチャンクして送信する実装なんてやったこともなければ、「そもそもyieldってなに?」みたいな状態だったので、ここのキャッチアップはかなり時間がかかりました。
そこら辺の理解は以下のFastAPI公式ドキュメントが非常に参考になりました。
2. Terraformの実装が難しかった
続いてはTerraformの実装が難しかったって話です。
自分は前職にいた頃、Terraform自体は使っていたのですが、ほとんどその内容を理解していませんでした。
生成AIに頼んで「ほへ〜。とりあえずいけた〜。うぇ〜」みたいな感じで仕事してました。
🐶「シャカイジンヲナメルナ」
Terraformの理解に関しては、現職でTerraformを触る機会があったのでそこで理解を深めたのが大きいです。
つよつよエンジニアの方がペアプロでタスクを進めてくださったので、理解を深めることができました。
自分はクラウドプラットフォームとしてGoogle Cloudを選択していたので、Google Cloudの公式ドキュメントを読み漁りました。
理解の進め方としては、以下のステップで進めました。
- 生成AIに「〇〇な構成のアプリケーションをデプロイしたい」と伝える
- 生成AIが良さげなTerraformコードを生成してくれる
- 一つ一つのリソースを生成AIに聞いたり、公式ドキュメントを読んだりして調べる
- エラーに直面したらドキュメントと睨めっこしたり、AIに助けてもらったりして進める
個人的にはステップ2で生成AIに必要なリソースを作成してもらったのが大きかったです。
ざっくりと「あ〜、こんな感じのリソースが必要なのか」ってのが理解できるからです。
その後、具体的に「でも、なんでこれが必要なんや?」ってのを調べることで効率的に理解することができたかなと思います。
🐶「生成AIを駆使して全体像を理解してから学習をする」
あと、個人的におすすめなのが「自分の理解が正しいかどうかをAIに添削してもらう」ことですね。
以下の画像は自分がTerraformのData Sourceについて調べていたときに、実際にメモした内容です。
自分の理解をメモし、それを生成AIに「自分は以下のように理解したのですがこの理解は正しいですか?」みたいに質問をします。
そうすると生成AIが自分の理解が正しいか、間違ってるかを説明してくれるのでかなり重宝しました。
「自分の理解をまとめる→添削してもらう→さらに新しい理解をまとめる→添削してもらう」を繰り返すことで脳に適度な負荷がかかり、効果的に学習できたと思います。
🐶「厳し目でお願いしますという質問を追加するのも結構おすすめ」
3. Kubernetesの実装が難しかった
最後はKubernetesの実装が難しかったって話です。
KubernetesのDeploymentやServiceといったリソースはざっくりと理解はしておりましたが、実際に手を動かして使えるまでは理解できていませんでした。
Kubernetesの理解には以下のUdemyの講座が非常に参考になりました。
ハンズオン形式でKubernetesの勉強ができるのと、自分のローカルを汚さずにKubernetesの勉強ができるので初心者には非常におすすめです!
自分がデプロイの過程で最も詰まったこととしては実際にkubernetesリソースをapply(本番に適用)するところです。デプロイで詰まるといえばここですよね。
これに関しては特に「これをしたからできた!」みたいなことはなく、エラーの内容を一つ一つ理解していき、解消していきました。
めちゃくちゃにしんどかったのを覚えています。
でも強いて言うなら、Google Cloudの公式ドキュメント通りにコードを書いていけばなんとかなるってことです。
生成AIが回答してくれるKubernetesのコードの内容はドキュメントと若干違ったりして、コケることが多かったのですが、ドキュメント通りにやればうまくいくみたいなことがおおかったので、やっぱり公式ドキュメントが最強だなと改めて思いました。
🐶「公式ドキュメントがやっぱり最強」
まとめ
今回のプロジェクトではざっくりと以下のことを学べました。
- LangChainの実装方法
- FastAPIの実装方法
- DDDをどう実装するかについて
- WebSocket通信の実装方法
- Terraformを用いてインフラのコード化
- Kubernetesを用いたリソース作成の方法について
今までインフラに対する苦手意識が大きかったのですが、インフラの状況が頭の中に浮かんでくるようになったのが非常に大きいかなと思います。
リソースの役割や名前の理解が増えたことで、このように図で考えるみたいなことができるようになったのだと思います。
最後までお読みいただきありがとうございました!
自分が今回の個人開発で学んだTipsが皆様のお役に立てれば幸いでございます!
🐶「ありがとうございました!」
あ、そういえばこんなことやってます(n回目)
【もんたのLINEスタンプ】
🐶「ヨカッタラミテネ」