Azure Static Web AppsのバックエンドをContainer Appsにしてみる。ついでにAzure DevOpsでCI/CD環境も作る。

# 1. Azure DevOpsでCI/CD環境を作る準備をしておく

## ⓪Azure DevOpsのサインアップ

以下リンクからサインアップします。

https://azure.microsoft.com/ja-jp/products/devops/


## ①Azure Reposでリポジトリの準備

プロジェクトをプライベートで作成します。
Azure Reposをフロントエンド、バックエンド用に2つ作ります
- フロントエンド: Static Web Apps
- バックエンド Container Apps

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/66257a81-da50-ff0d-006a-94b5b015d726.png)


Github DesktopにCloneします。
```Generate Git Credentials```をクリックし、パスワードを発行しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/b4cf18a1-cde0-c3d1-564d-badd08b6ce80.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/4bc2963b-2d18-96e9-02ba-930c557a7258.png)

ユーザ名・パスワードを入力してCloneします。
これでGithub Desktopで管理できるようになります。















---------------------------------------------------------
# 2. バックエンド用のContainer Appsを作成する

先ほど作成したbackendリポジトリをローカルにcloneしました。
ローカルのフォルダ内での作業です。ブランチはmainです。

## ①ソースを用意する

FlaskでHTTPリクエストに応答するソースコードを書き、簡易的なWebサーバを作ります。
- リクエストパス：```/api/test```
- HTTPメソッド：```POST```


上記リクエストに応答するために2ファイル作成します。
- api/app.py
- api/modules/test_module.py


```py:api/app.py
from flask import Flask, request
from modules.test_module import TestModule 

app = Flask(__name__)

@app.route("/api/test", methods=["POST"])
def post_test():
    data = request.json['test_data']
    response = TestModule().test_func(data)
    print(response)

    return response

if __name__ == "__main__":
    app.run(
        host="0.0.0.0",
        port="5000"
    )
```

```py:api/modules/test_module.py
class TestModule:
    def __init__(self):
        pass

    def test_func(self, data):
        return data
```

app.pyを実行後、curlでPOSTリクエストを投げてみます。


```sh:
python api/app.py
curl -X POST localhost:5000

# POSTリクエスト
curl -X POST -H "Content-Type: application/json" -d '{"test_data": "Hello!"}' http://localhost:5000/api/test
This is test.
```

うまく動作しています。requirements.txtを用意します。

```sh:api/requirements.txt
flask
```


## ②Dockerfileを作成する

Dockerfileを作成し、以下を定義します。
- Python実行環境が整ったimageの指定
- ソースのコピー
- requirements.txtのコピー
- python api/app.pyを実行するコマンド


```sh:api/Dockerfile
# 使用するイメージ
FROM python:3.10-alpine

# ソースの配置・パッケージインストール
COPY . .
RUN pip install -r requirements.txt

# コンテナ起動時のコマンド
CMD ["python", "app.py"]
```

フォルダ構成は以下のような形になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/bc47ae81-7604-ab78-006f-69a0b129c94b.png)


コンテナの起動確認を行い、先ほどのcurlコマンドを打ちます。
ポートだけ8000に変更します。

```sh:
cd api

# イメージのビルドとコンテナ起動
docker build -t myapp .
docker run -d -p 8000:5000 myapp  # 8000に来たリクエストを5000にポートフォワード

# curl
curl -X POST -H "Content-Type: application/json" -d '{"test_data": "Hello!"}' http://localhost:8000/api/test
This is test.
```

これでイメージの準備は整いました。
コミットし、pushしておきます。



## ③Container Appsの作成

Container Appsを作成する前に、先ほど作成したイメージをAzure Container Registoryにアップロードします。
まずは、コンテナレジストリーを作成します。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/8e71eca0-23cc-fd7d-3194-4f9f73d3b3b6.png)

左側メニューから**アクセスキー**をクリックし、**管理者ユーザー**を有効化します。
また、ログインサーバ名をコピーしておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/66f778b2-13a9-8008-9295-a9da4303097c.png)


続いて、上記手順で作成したレジストリにイメージをアップロードします。

```sh:
az login
az account set --subscription <your-subscription>
# Dockerfileがあるディレクトリで実行
az acr build --image backend-for-openai-app:v1.0 --registry registryforopenai .
```


Container Appsを作成します。
まずは、コンテナアプリ名を付けます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/7bad9b5e-355f-5ba7-30eb-236cd03f48f9.png)


次に、コンテナを作成する際のイメージとタグを指定します。
先ほどアップロードしたものを入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/dc7e2829-aeff-e0e1-b96d-416a88d6830b.png)


次に、インバウンド通信に関する設定を行います。
ポートフォワードの設定を行います。テスト環境のため、全てのインバウンドを許可します。
**セキュリティで保護されていない接続を許可**はチェックしていますが、外して大丈夫です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/7e155e7c-af1f-dc1b-2167-c9b65b74e65b.png)


以上の設定で作成します。
作成後、アプリケーションURLをコピーします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/12b9244f-3bcd-5d5e-298e-aacd8fd771c4.png)

このURL+/api/testに対して、POSTリクエストを送って動作確認を行います。

```sh:
curl -X POST -H "Content-Type: application/json" -d '{"test_data": "Hello!"}' https://backend-container-for-openai-app.wittybay-28470bf8.eastus.azurecontainerapps.io/api/test

# 出力
This is test.
```

先ほどと同じ出力が得らればバックエンドのコンテナ化は完了です。


## ④CI/CD環境の設定

**2023年5月25日現在は、Azure DevOpsには対応していません。**
今回は設定項目だけ紹介します。


Containers Appsの左側メニューの**継続的デプロイ**をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/01cebb65-3b2f-ea1f-b440-feb007d80f74.png)


githubアカウントの情報を入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/9bc6bdb1-8963-4f2e-b755-6ae1b2595249.png)

mainブランチにpushが発生したタイミングでイメージがACRに格納され、ビルドされます。
イメージの格納先を入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/9def7dbb-cbcc-674f-53c2-45facaa1dffa.png)



Azure ADへのアプリケーション登録とサービスプリンシパルの作成が必要です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/dfa255c4-f817-5c4d-e932-45a64219b3cd.png)


https://learn.microsoft.com/ja-jp/azure/active-directory/develop/howto-create-service-principal-portal








--------------------------------------------------------------------
# 3. Static Web Appsを作成し、Container Appsと繋ぐ 

## ①雛形プロジェクト作成
手順2で作成したリポジトリをCloneし、そのフォルダ内での作業です。
まずは、雛形プロジェクトを作成します。

```sh:
# インストール
npm install -g @vue/cli
vue --version

# 雛形作成
vue create app

# 起動確認
cd app
npm run serve

# HTTPリクエスト用のモジュールを追加
npm install axios 
```



## ②POSTリクエストを投げれるようにする

続いて、ボタンを押したらバックエンドにPOSTリクエストを投げる部分を実装します。
以下の2ファイルを編集します。

```vue:app/src/components/PostTest.vue
<template>
    <div>
        <button @click="post_test()">POSTテスト</button>
        <h2>{{ response }}</h2>
    </div>
</template>


<script>
    const axios = require('axios').create()

    export default{
        name: "PostTest",
        data: () => ({
            post_body:{
                test_data:"Hello!"
            },
            response: ""
        }),
        methods:{
            post_test: async function (){
                console.log(this.post_body.test_data)
                const container_url = "https://test-cotainer-app.calmcoast-b2bb2cd5.eastus.azurecontainerapps.io"
                const api_path = "/api/test"
                const res = await axios.post(container_url+api_path, this.post_body)
                console.log(res)

                this.response = res.data
            }
        }
    }
</script>
```


```vue:app/src/App.vue
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <!-- <HelloWorld msg="Welcome to Your Vue.js App"/> -->
    <PostTest/>
  </div>
</template>

<script>
// import HelloWorld from './components/HelloWorld.vue'
import PostTest from './components/PostTest.vue';

export default {
  name: 'App',
  components: {
    // HelloWorld,
    PostTest
}
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
```

編集が完了したら起動確認を行います。

```sh:
npm run serve
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/3e48f6ed-fd71-0b28-0f5f-4d8472cac460.png)


## ③Container Apps側でJavascriptからのリクエストを受け取れるようにする
Container AppsのCORSを有効化します。
※テストのため、配信元制限はかけていません。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/513a0d4a-879f-b2c4-58e4-da7fc70bf250.png)



以下のようにバックエンドのコンテナから返答があれば確認完了です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/670a3694-4187-743f-3218-af6ffe30f1af.png)




## ④Static Web Appsを作成する

上記手順で作成したコードを用いてStatic Web Appsを作成します。
**APIエンドポイントを編集した後**、手順1で作成したリポジトリのmainブランチにpushしておきます。

Static Web Apps × Container Appsの場合、
/**api**/*のリクエストをContainer Appsに流してくれます。
そのため、先ほどはContainer AppsのURLを指定していましたが修正します

- app/src/components/PostTest.vue

```vue:
<template>
    <div>
        <button @click="post_test()">POSTテスト</button>
        <h2>{{ response }}</h2>
    </div>
</template>


<script>
    const axios = require('axios').create()

    export default{
        name: "PostTest",
        data: () => ({
            post_body:{
                test_data:"Hello!"
            },
            response: ""
        }),
        methods:{
            post_test: async function (){
                console.log(this.post_body.test_data)

                // const container_url = "https://test-cotainer-app.calmcoast-b2bb2cd5.eastus.azurecontainerapps.io"
                // const api_path = "/api/test"
                // const res = await axios.post(container_url+api_path, this.post_body)
                
                const endpoint = "/api/test"
                const res = await axios.post(endpoint, this.post_body)

                console.log(res)

                this.response = res.data
            }
        }
    }
</script>
```

編集後、mainブランチへpushしておきます。


Azureポータルに移り、Static Web Appsを作成していきます。
**デプロイの詳細**という項目に注意します。


- その他を選択 (Azure DevOpsだとエラーが出たので手動で設定しました、、)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/34877742-a333-2e5a-c5aa-fb9ddc0ba3da.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/a96003de-d063-d737-52fd-4b6180781a4a.png)


## ⑤Azure Pipelineの作成

frontendのリポジトリの画面から、**set up build**をクリックし、
**Node.js with Vue**を選択します。



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/b0ab7678-2614-452d-b70c-815bc1549355.png)



yamlが生成されます。以下のように書き換えます。

```yaml:
trigger:
- main

pool:
  vmImage: ubuntu-latest

steps:
- checkout: self
  submodules: true
- task: AzureStaticWebApp@0
  inputs:
    app_location: 'app/dist'
    api_location: ''
    output_location: ''
    skip_app_build: true
  env:
    azure_static_web_apps_api_token: $(deployment_token)
```

npm installをする際のディレクトリの指定方法が分からなかったです。
そのため、build済みのフォルダを使ってAzure Static Web Appsにデプロイするyamlに書き換えています。

↑により、**app/.gitignore**の修正が必要です。

```:app/.gitignore
/dist
#/dist
```

distフォルダが差分として検出されるようになるので、mainブランチにpushしておきます。
**staticwebapp.config.json**で認証やルーティング設定を行う場合は、distフォルダに含める必要があることに注意します。

https://learn.microsoft.com/ja-jp/azure/static-web-apps/build-configuration?tabs=azure-devops

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/8af1ff57-9c53-6079-08ee-8bc24129a653.png)



ここで一度AzureポータルからStatic Web Appsの画面に行き、デプロイトークンをコピーします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/fa6e38c0-da07-885e-56be-329cb23e3055.png)


Azure DevOps Pipelineに戻り、先ほど作成したパイプラインの変数にデプロイトークンを設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/43f0d11f-6b87-66cc-c4a0-9cd118944b8d.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/4c04bf78-0249-51d6-3458-ecd4871d6808.png)


**規約変更により、無償利用枠の場合は申請をしなければなりません。**
下記のようなエラーが出たため、[こちら](https://aka.ms/azpipelines-parallelism-request)から利用申請を行います。
```sh:
##[error]No hosted parallelism has been purchased or granted. To request a free parallelism grant, please fill out the following form https://aka.ms/azpipelines-parallelism-request
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/3b040168-beee-748d-58b4-3fd3266fc056.png)



申請が通るとメールが来ます。
メールを確認したのち、pipelineを実行します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/bfc5ec05-b7d4-c02c-cc8e-9386044d3034.png)

Jobが終了したらStatic Web Appsの画面からURLをコピーし、ブラウザから開きます。
先ほど作成した画面と同じものが出力されればPipelineの構築は完了です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/559351fb-1fe6-fb71-5419-f4259ee32267.png)

まだバックエンドとのつなぎこみを行っていないため、ボタンを押しても返答がありません。

## ⑥Static Web AppsとContainer Appsを連携する

Static Web Appsの左側のメニューから**API**を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/83364670-a1ac-3fe4-1b08-7901c7941e91.png)


**リンク**をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/079d5851-b6e9-4467-8586-8c2c2f09f99e.png)


下記3項目を設定し、リンクをクリックします。

- **バックエンドリソースの種類**：コンテナーアプリ
- **サブスクリプション**：お使いのサブスクリプション
- **リソース名**：Container Appsのリソース名


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/8c62e683-b01a-ee10-c34e-8b0463923a2c.png)

リンクに成功すると、以下のように運用に反映されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/5f6c0169-7291-26ae-35d0-19df8e949ba8.png)



再度URLからアプリケーションを開き、ボタンを押すとContainer Appsへリクエストが流れていることを確認できます。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/670a3694-4187-743f-3218-af6ffe30f1af.png)






# まとめ
Azure DevOpsはAzureポータルから作るものと思っていました、、。
あと、azure pipelineのドキュメント全然わからない。
パイプラインの勉強しないとですね。