Nuxt TypeScript（Composition API）、Django REST Framework で開発したアプリを、Azure Static Web Apps、Heroku にリリースする

開発環境についてはこちら。

https://zenn.dev/jqinglong/articles/e901ca698f2158

ソースはこちら
https://github.com/JQinglong/JQRally

こんな感じで作っているアプリを、リリースしてみようという内容です。
タイトルも長ければ、中身も無駄に長くてすみません・・・
また、このJQRallyというアプリを作る過程をそのまま書いているので、アプリ名関連のところは、自分が作ろうとするアプリ名に置き換えながら見ていただければと思います。

元々は、Django でのAPIは、Heroku に、そこから呼び出す MySQL は ClearDB に、そして、Nuxtアプリは netlify にリリースするつもりでした。
が、「祝 Azure Static Web Apps GA！Static Web Apps のあなたの『いち推し』ポイント、教えてください」というイベントをやっているということで、Nuxtアプリについては、Static Web Apps を使ってみたいと思います。

ということで、本題は、Static Web Apps ですが、先にAPI側のリリースから行います。


## Django REST API の Herokuへのリリース

### 先にsettings対応
最初は、settings.py にDB接続情報を書いて、ソース管理対象外としていますが、ローカルとリリース環境での切り替えを可能にする形で変更し、環境変数を読み込む形にしたsettings.pyをソース管理に含めます。

```py:jq-rally-api/config/local_settings.py（これはソース管理対象外）
import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

SECRET_KEY = 'xxx'

# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'rally_database',
        'USER': 'xxx',
        'PASSWORD': 'xxx',
        'HOST': 'db',
        'PORT': 3306,
        'OPTIONS': {
            'charset': 'utf8mb4',
            'sql_mode': 'TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY',
        },
    }
}

DEBUG = True

```

```py:jq-rally-api/config/settings.py
"""
Django settings for config project.

Generated by 'django-admin startproject' using Django 3.2.

For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
"""
DEBUG = False

# from pathlib import Path
import os
import sys
try:
    import urlparse
except ImportError:
    import urllib.parse as urlparse

# Build paths inside the project like this: BASE_DIR / 'subdir'.
# BASE_DIR = Path(__file__).resolve().parent.parent
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!

# SECURITY WARNING: don't run with debug turned on in production!
# DEBUG = True

ALLOWED_HOSTS = ['localhost', '127.0.0.1', '.herokuapp.com']


# Application definition

INSTALLED_APPS = [
    'rest_framework_swagger',
    'rest_framework',
    'jqrally_api',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'config.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'config.wsgi.application'


# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases

# DATABASES = {
#     'default': {
#         'ENGINE': 'django.db.backends.mysql',
#         'NAME': 'rally_database',
#         'USER': 'jq',
#         'PASSWORD': 'password',
#         'HOST': 'db',
#         'PORT': 3306,
#         'OPTIONS': {
#             'charset': 'utf8mb4',
#             'sql_mode': 'TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY',
#         },
#     }
# }

urlparse.uses_netloc.append('mysql')


# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/

LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/

STATIC_URL = '/static/'

STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

REST_FRAMEWORK = {'DEFAULT_SCHEMA_CLASS':'rest_framework.schemas.coreapi.AutoSchema' }

REST_FRAMEWORK = {
        'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
        # 'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAuthenticated',),
        'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.AllowAny',),
        'DEFAULT_AUTHENTICATION_CLASSES': (
            'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
            'rest_framework.authentication.SessionAuthentication',
            'rest_framework.authentication.BasicAuthentication',
        ),
        'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
}

CORS_ORIGIN_WHITELIST = (
    'http://localhost:3000',
    'https://jqrally-web.netlify.app',
)

try:
    from .local_settings import *
    print('import')
except ImportError as e:
    print('ImportError')
    print(e)
    if not DEBUG:
        SECRET_KEY = os.environ['SECRET_KEY']

try:

    # Check to make sure DATABASES is set in settings.py file.
    # If not default to {}

    if 'DATABASES' not in locals():
        DATABASES = {}

    if 'DATABASE_URL' in os.environ:
        url = urlparse.urlparse(os.environ['DATABASE_URL'])

        # Ensure default database exists.
        DATABASES['default'] = DATABASES.get('default', {})

        # Update with environment configuration.
        DATABASES['default'].update({
            'NAME': url.path[1:],
            'USER': url.username,
            'PASSWORD': url.password,
            'HOST': url.hostname,
            'PORT': url.port,
        })


        if url.scheme == 'mysql':
            DATABASES['default']['ENGINE'] = 'django.db.backends.mysql'
except Exception:
    # print 'Unexpected error:', sys.exc_info()
    pass
```

```sh
git add .
git commit -m "settings.py 環境切り替え対応"
git push
```


###　Heroku アプリケーション環境構築

Heroku へのデプロイ方法は、いくつか用意されています。

https://devcenter.heroku.com/ja/categories/deployment

今回は、Container Registry を使ったデプロイを行います。

https://devcenter.heroku.com/ja/articles/container-registry-and-runtime

基本の流れはこんな感じですね。  
- heroku login
- heroku container:login
- heroku create
- heroku addons:create
- heroku container:push web
- heroku container:release web
- heroku open

こちらも参考にさせていただきました。

https://qiita.com/sho7650/items/9654377a8fc2d4db236d

```sh
$ heroku logout
Logging out... done

$ heroku update
heroku: Updating CLI from 7.53.0 to 7.56.1... done
heroku: Updating CLI... done
heroku: Updating plugins... done

$ heroku login
heroku: Press any key to open up the browser to login or q to exit: 
Opening browser to https://cli-auth.heroku.com/auth/cli/browser/xxx
Logging in... done
Logged in as xxx@gmail.com

$ heroku container:login
Login Succeeded

$ heroku create jq-rally
Creating ⬢ jq-rally... done
https://jq-rally.herokuapp.com/ | https://git.heroku.com/jq-rally.git

$ heroku list
```
今作成されたアプリが確認できます。

Webサイトのダッシュボードでも確認

https://dashboard.heroku.com/apps

### ClearDB MySQL 作成

こちらを参考にさせていただきました。

https://qiita.com/chatrate/items/b2703cb7854259bf343e


```sh
$ heroku addons:create cleardb:ignite --app jq-rally
Creating cleardb:ignite on ⬢ jq-rally... free
Created cleardb-shaped-74302 as CLEARDB_DATABASE_URL
Use heroku addons:docs cleardb to view documentation

$ heroku config --app jq-rally | grep CLEARDB
CLEARDB_DATABASE_URL: mysql://xxx:xxx@us-cdbr-east-04.cleardb.com/heroku_8ac0d72e3d23c46?reconnect=true

$ heroku config:set --app jq-rally DATABASE_URL='mysql://xxx:xxx@us-cdbr-east-04.cleardb.com/heroku_8ac0d72e3d23c46?reconnect=true'
$ heroku config:set --app jq-rally DEBUG_COLLECTSTATIC='1'
$ heroku config:set --app jq-rally SECRET_KEY='xxx'

```

最後のコマンドで環境変数DATABASE_URL＝settings.pyで参照している環境変数にも登録しておきます。
ついでに、必要な環境変数も追加しておきます。
DEBUG_COLLECTSTATIC
SECRET_KEY


### APIリリース

docker-compose 構成の場合、recursiveでまとめてできそうに思いましたが、
これだとエラー

```sh
$ heroku container:push web --recursive
 ▸    Cannot read property 'length' of undefined
```

こちらを参考に

http://adliblog.hatenablog.com/entry/2018/06/15/042834


```yml:heroku.yml
build:
  docker:
    web: jq-rally-api/Dockerfile
run:
  # web: python3 manage.py runserver 0.0.0.0:$PORT
  web: gunicorn config.wsgi --bind 0.0.0.0:$PORT
```

これで、commit・push。

heroku管理画面で、GitHubと接続

Deploy タブ
↓
Deployment method
→GitHub
→リポジトリ選択
　Connect
→Manual deploy
　Deploy Branch
Your app was successfully deployed.

https://jq-rally.herokuapp.com/

で、Hello, world. が表示されました。

ただし、

https://jq-rally.herokuapp.com/api/

のデザインが反映されていない。すなわち、staticfilesが参照できていない

```sh
$ docker exec -it jq-rally-api sh
# pwd
/code
# python manage.py collectstatic
import

168 static files copied to '/code/staticfiles'.
```

これで、commit・pushすると、スタイルも適用されました。

### DB作成
Mac環境では、TablePlus　が便利かと思います。
https://tableplus.com/

ローカル環境に接続し、export
ClearDBに接続して、import
utf8mb4_0900_ai_ci は使えないと言われるので、
utf8mb4_general_ci に変更

https://jq-rally.herokuapp.com/api/memo/

が見えました。

## Nuxtアプリの Azure Static Web へのリリース

今回の本題はこちら。
APIを呼び出す、フロント側を、Azure Static Web へリリースします。

### Azure Static Web Apps リソースの作成

まず、基本として、こちら参照。

https://docs.microsoft.com/ja-jp/azure/static-web-apps/deploy-nuxtjs#create-an-azure-static-web-apps-resource


> [App location](アプリの場所) のボックスに「 ./ 」と入力します。


とありますが、今回の構成だと

```
/jq-rally-web/jq-rally
```
を入力します。

デプロイ完成後、URLに移動しても、しばらくは、
Your Azure Static Web App is live and waiting for your content
が表示されます。
Github側では、Actionのページで、「ci: add Azure Static Web Apps workflow file」が動いています。
しばらくしてエラーになりました。

```
---End of Oryx build logs---
The app build failed to produce artifact folder: 'out'. Please ensure this property is configured correctly in your workflow file.
```

こちらを参照

https://ja.nuxtjs.org/docs/2.x/deployment/deployment-azure-static-web-apps

outというフォルダ名が悪い訳でもなさそうでしたが、dist にして、
さらに

```json:jq-rally-web/jq-rally/package.json
"build": "nuxt generate",
```

に変更しました。


さらに、これらを参照して、こんな変更も

https://shiro-changelife.com/nuxt-js-module-should-export-composition-api/

https://github.com/nuxt-community/composition-api/issues/465

```ts:jq-rally-web/jq-rally/nuxt.config.ts
  buildModules: [
    // https://go.nuxtjs.dev/typescript
    '@nuxt/typescript-build',
    // https://go.nuxtjs.dev/vuetify
    '@nuxtjs/vuetify',
    '@nuxtjs/composition-api/module'
  ],
```

/module　を追記


https://gentle-sand-0b8386900.azurestaticapps.net/

これで開くようになりました。

なるほど。Azureの設定画面で入力した内容に基づいて、
.github/workflows/azure-static-web-apps-gentle-sand-0b8386900.yml
というファイルを作ってくれて、それがGitHub Actions で動くということですね。



### Azure Static Web Apps　における環境変数の参照

まだ、API呼び出しはできていません。

```ts:jq-rally-web/jq-rally/nuxt.config.ts
  axios: {
    baseURL: process.env.BASE_URL || 'http://localhost:8000/api/',
  },
```

このようにしておいて、ローカル開発環境とAzure実行環境で参照先を変えようと思います。

今回一番苦労したところです。

まずは、

```jq-rally-web/jq-rally/.env
BASE_URL="https://jq-rally.herokuapp.com/api/"
```

を作成して、参照が有効なことを確認しておきます。


Netlify のノリだと、これを使ってできるのかなと思いましたが、

https://docs.microsoft.com/ja-jp/azure/static-web-apps/application-settings#using-the-azure-portal

ダメでした。
まあ、書いてあるとおりですが
> この記事では、Azure Functions でバックエンド API のアプリケーション設定を管理する方法について説明します。

ということです。

では、どうするかというと、GitHub　で secrets を作っておいて、GitHub Actions のymlファイルで、その secrets の値を、docker run の環境変数引数として渡されるようにする、ということになります。

まずは、GitHub の、Settings > Secrets > New repository secret で、変数を追加します。
追加すると、Repository secrets の欄に、既存の「AZURE_STATIC_WEB_APPS_API_TOKEN_GENTLE_SAND_0B8386900」　に並んで、「BASE_URL」が追加されていることが確認できます。

最初、Environment secrets　の方に登録しましたが、それではうまくいかなかった気がします。

その上で、yml変更。

```yml:.github/workflows/azure-static-web-apps-gentle-sand-0b8386900.yml
      - name: Build And Deploy
        id: builddeploy
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_GENTLE_SAND_0B8386900 }}
          repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
          action: "upload"
          ###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
          # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
          app_location: "/jq-rally-web/jq-rally" # App source code path
          api_location: "" # Api source code path - optional
          output_location: "dist" # Built app content directory - optional
          ###### End of Repository/Build Configurations ######
        env:
          BASE_URL: ${{ secrets.BASE_URL }}
```

steps の中の、この部分に、

```yml
        env:
          BASE_URL: ${{ secrets.BASE_URL }}
```

を追加する、ということです。

今回はこんなところで。

（追記）
その後BASE_URL以外の変数も追加しながら開発する中で、あるタイミングから、環境変数が読み取れなくなりました。
対応は、nuxt.config.ts へのenv項目の追加です。

```ts:nuxt.config.ts
  env: {
    BASE_URL: process.env.BASE_URL || '',
    FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID || 'jqrally',
    FIREBASE_API_KEY: process.env.FIREBASE_API_KEY || '',
    FIREBASE_AUTH_DOMAIN: process.env.FIREBASE_AUTH_DOMAIN || '',
    FIREBASE_STORAGE_BUCKET: process.env.FIREBASE_STORAGE_BUCKET || '',
    FIREBASE_MESSAGING_SENDER_ID: process.env.FIREBASE_MESSAGING_SENDER_ID || '',
    FIREBASE_APP_ID: process.env.FIREBASE_APP_ID || ''
  },

```


## 「いち推し」ポイント

肝心の「いち推し」ポイント、書いてませんでした。

GitHub Actions をなんとなく敬遠していましたが、ここまでお膳立てしてくれるのであれば、なんのストレスもなく、利用できるのが良いかと思います。
その構成に乗って仕舞えば、上記の環境変数の件のように、あとは、GitHub Actions に関する、豊富な知見を参照させていただくことで、やりたいことに素早く近づけるのではないかと。

今回試してみませんでしたが、ステージング環境に関する考え方も、Netlify とちょっと違っているのが面白いです。