開発環境についてはこちら。
ソースはこちら
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をソース管理に含めます。
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
"""
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
git add .
git commit -m "settings.py 環境切り替え対応"
git push
### Heroku アプリケーション環境構築
Heroku へのデプロイ方法は、いくつか用意されています。
今回は、Container Registry を使ったデプロイを行います。
基本の流れはこんな感じですね。
- heroku login
- heroku container:login
- heroku create
- heroku addons:create
- heroku container:push web
- heroku container:release web
- heroku open
こちらも参考にさせていただきました。
$ 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サイトのダッシュボードでも確認
ClearDB MySQL 作成
こちらを参考にさせていただきました。
$ 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でまとめてできそうに思いましたが、
これだとエラー
$ heroku container:push web --recursive
▸ Cannot read property 'length' of undefined
こちらを参考に
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.
で、Hello, world. が表示されました。
ただし、
のデザインが反映されていない。すなわち、staticfilesが参照できていない
$ 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 に変更
が見えました。
Nuxtアプリの Azure Static Web へのリリース
今回の本題はこちら。
APIを呼び出す、フロント側を、Azure Static Web へリリースします。
Azure Static Web Apps リソースの作成
まず、基本として、こちら参照。
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.
こちらを参照
outというフォルダ名が悪い訳でもなさそうでしたが、dist にして、
さらに
"build": "nuxt generate",
に変更しました。
さらに、これらを参照して、こんな変更も
buildModules: [
// https://go.nuxtjs.dev/typescript
'@nuxt/typescript-build',
// https://go.nuxtjs.dev/vuetify
'@nuxtjs/vuetify',
'@nuxtjs/composition-api/module'
],
/module を追記
これで開くようになりました。
なるほど。Azureの設定画面で入力した内容に基づいて、
.github/workflows/azure-static-web-apps-gentle-sand-0b8386900.yml
というファイルを作ってくれて、それがGitHub Actions で動くということですね。
Azure Static Web Apps における環境変数の参照
まだ、API呼び出しはできていません。
axios: {
baseURL: process.env.BASE_URL || 'http://localhost:8000/api/',
},
このようにしておいて、ローカル開発環境とAzure実行環境で参照先を変えようと思います。
今回一番苦労したところです。
まずは、
BASE_URL="https://jq-rally.herokuapp.com/api/"
を作成して、参照が有効なことを確認しておきます。
Netlify のノリだと、これを使ってできるのかなと思いましたが、
ダメでした。
まあ、書いてあるとおりですが
この記事では、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変更。
- 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 の中の、この部分に、
env:
BASE_URL: ${{ secrets.BASE_URL }}
を追加する、ということです。
今回はこんなところで。
(追記)
その後BASE_URL以外の変数も追加しながら開発する中で、あるタイミングから、環境変数が読み取れなくなりました。
対応は、nuxt.config.ts へのenv項目の追加です。
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 とちょっと違っているのが面白いです。