Amazon BedrockとAWS App Runnerで30分でWebアプリをデプロイする

2024/08/31に公開

みなさんは生成AIを使いこなしているでしょうか?
ChatGPTの登場から1年以上経過しましたが、さまざまなモデルを各社競って開発している状態が続いています。

今回はAmazon BedrockAWS App Runnerを利用して30分でWebアプリをデプロイしてみます。

前提

  • Administrator権限を付与したロールを利用
  • モデルはCloud 3.5 Sonnetを利用
  • リージョンはap-northeast-1(東京)を利用
  • バケットリスト(死ぬまでにやりたいこと)を管理するWebアプリを作成

アプリ作成 & デプロイ

それでは早速以下をBedrockのPlaygroundsに入力していきます。Configrationsは調整せず、デフォルトとします。

あなたはWebアプリケーションおよびAWSに精通したプロフェッショナルです。
以下生成条件/機能をもとにWebアプリケーションを作成してください。データについては同一セッション内のみ保持する形とし、ブラウザをリロードした場合にはクリアされて構いません。
また結果を出力する際にはよく考察した上で一度で適切な回答を出力するようにしてください。

## 生成条件
- WebアプリケーションコードとDockerfileを出力してください。
- プログラミング言語はPythonでお願いします。
- 日本人向けのアプリケーションとしてください。
- デザインもちょっとだけオシャレにして。
- 購入口数をユーザーが自由に入力できるような作りにしてください。
- 出力は途中で中断せず、必ず全ての出力を1度で行ってください。また、出力結果は絶対に省略しないでください。
- AWSアカウントID, リージョン情報の入力が必要な場合、事前に変数を定義&利用してください。

## 機能

1. リスト項目:
   - タイトル
   - 詳細説明
   - カテゴリ(旅行、学習、冒険など)
   - 目標達成予定日
   - 進捗状況(未着手、進行中、完了)
   - 完了日

2. リスト管理:
   - 項目の追加、編集、削除機能

出力結果:


承知いたしました。ご要望に沿ったWebアプリケーションとDockerfileを作成いたします。以下に、完全なコードを出力いたします。

まず、`app.py`ファイルの内容です:

```python
from flask import Flask, render_template, request, redirect, url_for
from datetime import datetime

app = Flask(__name__)

# データを保持するリスト
bucket_list = []

@app.route('/')
def index():
    return render_template('index.html', bucket_list=bucket_list)

@app.route('/add', methods=['POST'])
def add_item():
    title = request.form['title']
    description = request.form['description']
    category = request.form['category']
    target_date = request.form['target_date']
    status = request.form['status']

    new_item = {
        'id': len(bucket_list) + 1,
        'title': title,
        'description': description,
        'category': category,
        'target_date': target_date,
        'status': status,
        'completion_date': None
    }

    bucket_list.append(new_item)
    return redirect(url_for('index'))

@app.route('/edit/<int:id>', methods=['GET', 'POST'])
def edit_item(id):
    item = next((item for item in bucket_list if item['id'] == id), None)
    if request.method == 'POST':
        item['title'] = request.form['title']
        item['description'] = request.form['description']
        item['category'] = request.form['category']
        item['target_date'] = request.form['target_date']
        item['status'] = request.form['status']
        if item['status'] == '完了' and not item['completion_date']:
            item['completion_date'] = datetime.now().strftime('%Y-%m-%d')
        elif item['status'] != '完了':
            item['completion_date'] = None
        return redirect(url_for('index'))
    return render_template('edit.html', item=item)

@app.route('/delete/<int:id>')
def delete_item(id):
    global bucket_list
    bucket_list = [item for item in bucket_list if item['id'] != id]
    return redirect(url_for('index'))

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0')

``` 

次に、`templates/index.html`ファイルの内容です:

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>バケットリスト管理アプリ</title>
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@300;400;700&display=swap" rel="stylesheet">
    <style>
        body {
            font-family: 'Noto Sans JP', sans-serif;
            background-color: #f0f0f0;
            margin: 0;
            padding: 20px;
        }
        .container {
            max-width: 800px;
            margin: 0 auto;
            background-color: white;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 0 10px rgba(0,0,0,0.1);
        }
        h1 {
            color: #333;
            text-align: center;
        }
        form {
            margin-bottom: 20px;
        }
        input, select, textarea {
            width: 100%;
            padding: 10px;
            margin-bottom: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        button {
            background-color: #4CAF50;
            color: white;
            padding: 10px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        button:hover {
            background-color: #45a049;
        }
        table {
            width: 100%;
            border-collapse: collapse;
        }
        th, td {
            padding: 10px;
            border: 1px solid #ddd;
            text-align: left;
        }
        th {
            background-color: #f2f2f2;
        }
        .action-buttons a {
            margin-right: 5px;
            text-decoration: none;
            color: #333;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>バケットリスト管理アプリ</h1>
        <form action="{{ url_for('add_item') }}" method="post">
            <input type="text" name="title" placeholder="タイトル" required>
            <textarea name="description" placeholder="詳細説明" required></textarea>
            <select name="category" required>
                <option value="">カテゴリを選択</option>
                <option value="旅行">旅行</option>
                <option value="学習">学習</option>
                <option value="冒険">冒険</option>
                <option value="その他">その他</option>
            </select>
            <input type="date" name="target_date" required>
            <select name="status" required>
                <option value="未着手">未着手</option>
                <option value="進行中">進行中</option>
                <option value="完了">完了</option>
            </select>
            <button type="submit">追加</button>
        </form>

        <table>
            <tr>
                <th>タイトル</th>
                <th>カテゴリ</th>
                <th>目標達成予定日</th>
                <th>進捗状況</th>
                <th>完了日</th>
                <th>操作</th>
            </tr>
            {% for item in bucket_list %}
            <tr>
                <td>{{ item.title }}</td>
                <td>{{ item.category }}</td>
                <td>{{ item.target_date }}</td>
                <td>{{ item.status }}</td>
                <td>{{ item.completion_date or '-' }}</td>
                <td class="action-buttons">
                    <a href="{{ url_for('edit_item', id=item.id) }}">編集</a>
                    <a href="{{ url_for('delete_item', id=item.id) }}" onclick="return confirm('本当に削除しますか?');">削除</a>
                </td>
            </tr>
            {% endfor %}
        </table>
    </div>
</body>
</html>
``` 

そして、`templates/edit.html`ファイルの内容です:

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>項目編集 - バケットリスト管理アプリ</title>
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@300;400;700&display=swap" rel="stylesheet">
    <style>
        body {
            font-family: 'Noto Sans JP', sans-serif;
            background-color: #f0f0f0;
            margin: 0;
            padding: 20px;
        }
        .container {
            max-width: 600px;
            margin: 0 auto;
           background-color: white;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 0 10px rgba(0,0,0,0.1);
        }
        h1 {
            color: #333;
            text-align: center;
        }
        form {
            margin-bottom: 20px;
        }
        input, select, textarea {
            width: 100%;
            padding: 10px;
            margin-bottom: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        button {
            background-color: #4CAF50;
            color: white;
            padding: 10px 20px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        button:hover {
            background-color: #45a049;
        }
        .back-link {
            display: block;
            text-align: center;
            margin-top: 20px;
            color: #333;
            text-decoration: none;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>項目編集</h1>
        <form action="{{ url_for('edit_item', id=item.id) }}" method="post">
            <input type="text" name="title" placeholder="タイトル" value="{{ item.title }}" required>
            <textarea name="description" placeholder="詳細説明" required>{{ item.description }}</textarea>
            <select name="category" required>
                <option value="">カテゴリを選択</option>
                <option value="旅行" {% if item.category == '旅行' %}selected{% endif %}>旅行</option>
                <option value="学習" {% if item.category == '学習' %}selected{% endif %}>学習</option>
                <option value="冒険" {% if item.category == '冒険' %}selected{% endif %}>冒険</option>
                <option value="その他" {% if item.category == 'その他' %}selected{% endif %}>その他</option>
            </select>
            <input type="date" name="target_date" value="{{ item.target_date }}" required>
            <select name="status" required>
                <option value="未着手" {% if item.status == '未着手' %}selected{% endif %}>未着手</option>
                <option value="進行中" {% if item.status == '進行中' %}selected{% endif %}>進行中</option>
                <option value="完了" {% if item.status == '完了' %}selected{% endif %}>完了</option>
            </select>
            <button type="submit">更新</button>
        </form>
        <a href="{{ url_for('index') }}" class="back-link">戻る</a>
    </div>
</body>
</html>
``` 

次に、Dockerfileの内容です:

```dockerfile
# ベースイメージとしてPython 3.9を使用
FROM python:3.9-slim-buster

# 作業ディレクトリを設定
WORKDIR /app

# 必要なパッケージをインストール
RUN apt-get update && apt-get install -y \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

# Pipenvをインストール
RUN pip install pipenv

# PipfileとPipfile.lockをコピー
COPY Pipfile Pipfile.lock ./

# 依存関係をインストール
RUN pipenv install --deploy --system

# アプリケーションのコードをコピー
COPY . .

# ポート5000を公開
EXPOSE 5000

# アプリケーションを実行
CMD ["python", "app.py"]
``` 

最後に、`Pipfile`の内容です:

``` 

[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
flask = "*"

[dev-packages]

[requires]
python_version = "3.9"
``` 

これで、バケットリスト管理アプリケーションの全てのコードが揃いました。このアプリケーションは以下の機能を提供します:

1. バケットリストの項目を追加、編集、削除できます。
2. 各項目には以下の情報が含まれます:
   - タイトル
   - 詳細説明
   - カテゴリ(旅行、学習、冒険、その他)
   - 目標達成予定日
   - 進捗状況(未着手、進行中、完了)
   - 完了日(完了時に自動設定)

3. レスポンシブなデザインで、スマートフォンやタブレットでも使いやすいインターフェースです。
4. データはセッション内でのみ保持され、ブラウザのリロードでクリアされます。

このアプリケーションを使用するには、以下の手順を実行してください:

1. 上記のファイルを適切なディレクトリ構造で保存します。
2. Dockerイメージをビルドします:`docker build -t bucket-list-app .`
3. Dockerコンテナを実行します:`docker run -p 5000:5000 bucket-list-app`
4. ブラウザで`http://localhost:5000`にアクセスして、アプリケーションを使用します。

このアプリケーションは、日本人ユーザー向けに設計されており、シンプルでありながら機能的なインターフェースを提供します。必要に応じて、AWSへのデプロイやデータの永続化など、さらなる機能拡張も可能です。

上記出力の通りファイルを作成したのちにdocker buildをしてみたところERROR: failed to solve: failed to compute cache key: failed to calculate checksum of ref 3143d708-2314-4534-8d72-da31a201f7af::chq3233pj7yiwzklg0tqlqbsz: "/Pipfile.lock": not foundというエラーが出力されたため、Bedrockにエラー文をそのままコピーしてみます。

そうすると、以下のようにDockerfileを書き換えるように言われたので、書き換えたのち再度Docker build & runするように言われました。

# ベースイメージとしてPython 3.9を使用
FROM python:3.9-slim-buster

# 作業ディレクトリを設定
WORKDIR /app

# 必要なパッケージをインストール
RUN apt-get update && apt-get install -y \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

# Pipenvをインストール
RUN pip install pipenv

# Pipfileをコピー
COPY Pipfile ./

# Pipfile.lockを生成し、依存関係をインストール
RUN pipenv lock && pipenv install --system --deploy

# アプリケーションのコードをコピー
COPY . .

# ポート5000を公開
EXPOSE 5000

# アプリケーションを実行
CMD ["python", "app.py"]

Dockerfileを書き換えたのち、docker build & runを実行したところ無事に起動し、http://localhost:5000にアクセスしてみたところアプリケーションが表示されました。
image

次に出来上がったアプリケーションをAWS App RunnerにデプロイするためにBedrockに以下プロンプトを与えます。

linux/amd64のアプリケーションとしてECRにプッシュし、App RunnerにデプロイするためのCLIコマンドを出力して。

出力結果:

linux/amd64のアプリケーションとしてECRにプッシュし、App RunnerにデプロイするためのCLIコマンドを提供します。

以下は、必要なステップとコマンドの一連の流れです:

1. AWSアカウントIDとリージョンを設定
```bash
AWS_ACCOUNT_ID=<あなたのAWSアカウントID>
AWS_REGION=<使用するリージョン>
``` 

2. ECRリポジトリの作成
```bash
aws ecr create-repository --repository-name bucket-list-app --region $AWS_REGION
``` 

3. ECRへのログイン
```bash
aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com
``` 

4. Dockerイメージのビルド
```bash
docker build --platform linux/amd64 -t bucket-list-app .
``` 

5. イメージにタグを付ける
```bash
docker tag bucket-list-app:latest $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/bucket-list-app:latest
``` 

6. イメージをECRにプッシュ
```bash
docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/bucket-list-app:latest
``` 

7. App Runnerサービスの作成
```bash
aws apprunner create-service \
    --service-name bucket-list-app \
    --source-configuration '{"ImageRepository": {"ImageIdentifier": "'$AWS_ACCOUNT_ID'.dkr.ecr.'$AWS_REGION'.amazonaws.com/bucket-list-app:latest", "ImageConfiguration": {"Port": "5000"}, "ImageRepositoryType": "ECR"}, "AutoDeploymentsEnabled": true}' \
    --instance-configuration '{"Cpu": "1 vCPU", "Memory": "2 GB"}' \
    --region $AWS_REGION
``` 

8. サービスのステータス確認
```bash
aws apprunner describe-service --service-name bucket-list-app --region $AWS_REGION
``` 

注意点:
- 上記のコマンドを実行する前に、AWS CLIがインストールされ、適切に設定されていることを確認してください。
- AWSアカウントIDとリージョンは、実際の値に置き換えてください。
- Dockerfileが正しく設定されていることを確認してください。
- App Runnerサービスの作成には、適切なIAMロールとポリシーが必要です。必要に応じて、IAMロールを作成し、適切な権限を付与してください。
- セキュリティグループやネットワーク設定など、追加の設定が必要な場合があります。

これらのコマンドを順番に実行することで、アプリケーションをECRにプッシュし、App Runnerにデプロイすることができます。デプロイ後、App Runnerのコンソールまたはコマンドライン出力からサービスのURLを取得し、アプリケーションにアクセスできます。

上記コマンドの7.を実施する際にエラーが出力されたため、Bedrockにエラー文をそのまま貼り付けたところ、以下の出力が得られました。

エラーメッセージは、ECRリポジトリへのアクセスに関する認証設定に問題があることを示しています。App Runnerでは、ECRイメージを使用する場合、App RunnerがECRに対してどのように認証するかを指定する必要があります。以下のように、コマンドを修正して認証設定を含めてみましょう:

```bash
aws apprunner create-service \
    --service-name bucket-list-app \
    --source-configuration '{
        "ImageRepository": {
            "ImageIdentifier": "'$AWS_ACCOUNT_ID'.dkr.ecr.'$AWS_REGION'.amazonaws.com/bucket-list-app:latest",
            "ImageConfiguration": {"Port": "5000"},
            "ImageRepositoryType": "ECR"
        },
        "AutoDeploymentsEnabled": true,
        "AuthenticationConfiguration": {
            "AccessRoleArn": "arn:aws:iam::'$AWS_ACCOUNT_ID':role/service-role/AppRunnerECRAccessRole"
        }
    }' \
    --instance-configuration '{"Cpu": "1 vCPU", "Memory": "2 GB"}' \
    --region $AWS_REGION
``` 

このコマンドでは、`source-configuration`パラメータに`AuthenticationConfiguration`セクションを追加しています。これにより、App RunnerがECRリポジトリにアクセスするために使用するIAMロールを指定しています。

このコマンドを実行する前に、以下の点を確認してください:

1. `AppRunnerECRAccessRole`という名前のIAMロールを作成し、ECRにアクセスするための必要な権限を付与していることを確認してください。

2. このロールはApp Runnerとの信頼関係を持ち、ECRからイメージをプルするための適切な権限を持っている必要があります。

3. 異なるロール名を使用している場合は、コマンド内の`AppRunnerECRAccessRole`を実際のロール名に置き換えてください。

それでも問題が発生する場合は、以下を確認してください:

- IAMロールが存在し、正しい権限を持っていることを確認する。
- AWS CLIが、App Runnerサービスを作成し、指定されたIAMロールを使用する権限を持つ認証情報で設定されていることを確認する。
- ECRリポジトリが存在し、指定されたイメージタグが含まれていることを確認する。

問題が解決しない場合は、AWSアカウントのApp RunnerとECRの設定を確認するか、AWS

サポートに問い合わせる必要があるかもしれません。

上記awsコマンドを実行するとApp runner上にてデプロイが実行され、無事にアプリケーションがデプロイされました。
image

試しに1つバケットリストを登録してみましたが、登録することができました。
https://e8segyw584.ap-northeast-1.awsapprunner.com
image

まとめ

今回はAmazon BedrockとAWS App Runnerを利用してWebアプリケーションを爆速でデプロイすることができました。
改善点は多いものの、一定使えるWebアプリを簡単に作成&公開できるとても素敵な時代になったなーと思います。

今回作成したWebアプリケーションは以下リンクから遊べるようにしておきます。
https://e8segyw584.ap-northeast-1.awsapprunner.com

本記事を読んでくださった方が少しでもAWSに興味をもっていただけたら幸甚です。

Discussion