Azure Web App + Flask + Github Actionsで認証ページ付きポートフォリオをデプロイする方法
はじめに
以前Firebaseでデプロイしたときをベースに書いてますので、先にこちらを見て下さい。 tmyoda.hatenablog.com
Azure クラウド サービス | Microsoft Azure にはとんでもない数のサービスがありますが 、 今回はポートフォリオを公開するために、WebのApp Serviceを使用しました。分類的にはPaaSですかね。
そしてなにより、1Gメモリ F1を選択すると無料で使えます。 また、学生でしたら、登録したときに1万円分のサブスククーポンを貰えます。
概要
- AzureでWebページをデプロイ
- AzureではBasic認証が使えないので、ログインページを用意(Flask)
- 以前Firebaseでデプロイしたhtml,css,jsを使いまわしたいのでサブモジュールにて保持
参考にさせて頂いた記事
Azure登録まで
参考記事に従ってAzureを登録します。 このとき、Flaskを使いたいのでランタイムスタックをPython3にします。
しばらくするとインスタンスが作成されます。
Flaskで認証ページを実装
Firebase Hostingでデプロイしたときは、Basic認証が使えたので、実質静的ページの公開と同じでした。
しかし、AzureはBasic認証が使えないようなので、Flaskでかんたんなログインを実装しようと考えました。 参考記事に従いながら実装していると‥
なんと、Flaskでhtmlを表示するにはjinja2のテンプレートに対応した記述が必要です。
しかし、サブモジュールでhtmlを保持しているため、書き換え等はしたくない、、、
かといってFlaskのstaticフォルダに格納したら直接が見られるが認証がかけられない、、、
そこで、
@app.route('/portfolio/')
にGETが来たら、直接htmlをテキストとして読み出しResponse
型で返す- その他のcss,jsへのアクセスが来たら、
send_file
する
といった方針でひとまずやりたいことは実現しました。
解決策
from flask import Flask, request, Response, abort, render_template, redirect, url_for, send_file from flask_login import LoginManager, login_user, logout_user, login_required, UserMixin from collections import defaultdict import os import logging app = Flask(__name__) login_manager = LoginManager() login_manager.init_app(app) app.config['SECRET_KEY'] = なんらかしらのキーを設定 # logging setting logging.basicConfig(level=logging.DEBUG) #logging ex # app.logger.info("Hello World %s", variable) # path setting currnet_dir = os.path.dirname( os.path.abspath(__file__) ) static_dir = 'portfolio/functions/static/' class User(UserMixin): def __init__(self, id, name, password): self.id = id self.name = name self.password = password # ログイン用ユーザー作成 users = { 1: User(1, "user01", "password"), 2: User(2, "user02", "password") } # ユーザーチェックに使用する辞書作成 nested_dict = lambda: defaultdict(nested_dict) user_check = nested_dict() for i in users.values(): user_check[i.name]["password"] = i.password user_check[i.name]["id"] = i.id @login_manager.user_loader def load_user(user_id): return users.get(int(user_id)) # css, js ,imgなどのコンポーネント呼び出し用 @app.route('/portfolio/<path:path>/<string:filename>', methods=["GET"]) @login_required def portfolio_content(path, filename): return send_file(static_dir + path + "/" + filename) # index.html呼び出し # .htmlはこの方法じゃないとだめみたい @app.route('/portfolio/') @login_required def portfolio(): with open(currnet_dir + '/' + static_dir + 'index.html') as f: txt = f.read() return Response(txt) # ログインパス @app.route('/', methods=["GET", "POST"]) def login(): if(request.method == "POST"): # ユーザーチェック if(request.form["username"] in user_check and request.form["password"] == user_check[request.form["username"]]["password"]): # ユーザーが存在した場合はログイン login_user(users.get(user_check[request.form["username"]]["id"])) return redirect(url_for('portfolio')) else: return abort(401) else: return render_template("login.html") # ログアウトパス @app.route('/logout/') @login_required def logout(): logout_user() return Response(''' logout success!<br /> <a href="/login/">login</a> ''') if __name__ == '__main__': app.run(threaded=True)
pass,idベタ書きしてますが、本当はDBに格納してパスワードもハッシュ化しないといけませんね。 まあ最悪全世界に晒されても良い内容なので、今回はこの程度で良いでしょう。
/portfolio に来るアクセスはindex.htmlを返して、 /portfolio/img/, css/ とかはsend_fileで返却します。
フォルダ階層
. ├── Pipfile ├── Pipfile.lock ├── app.py ├── portfolio │ └── functions │ └── static │ ├── css │ ├── img │ ├── index.html │ ├── js │ └── vendor ├── requirements.txt └── templates └── login.html
この工夫点として、portfolioフォルダは別リポジトリのサブモジュールです。 なので、どっちかでポートフォリオをアップデートしたら、もう片方がアップデートされます。
また、 staticフォルダを作成していない点もポイントです。(staticは外から見える)
そして、Pythonは、*.py、requirements.txt、または runtime.txt
が最初に呼び出されるらしいので、
名前をちゃんと変えておきましょう。
デプロイ
ここまできたらいよいよです。今回はGithub Actionsでのデプロイが目標ですので、Azure portalのデプロイメントから、 Githubを選択して、Actionsを選択します。(要認証)
すると勝手にworkflowが作成されるので、次々と進むと勝手にCI/CDが走ってデプロイ完了です。 非常にかんたんですね!
注意点として、再起動しないと認識されないので注意です。
サブモジュール関連のトラブル
サブモジュール使ってない人はパスして下さい。
サブモジュールがpullされておらず、htmlがNot Foundになりました。 その解決方法として、追記したworkflow/ymlを晒します。
steps: - uses: actions/checkout@master with: submodules: true token: ${{ secrets.PORTFOLIO_ACCESS_TOKEN }} - name: Sparse-Checkout run: | echo /functions/static >> .git/modules/portfolio/info/sparse-checkout cd portfolio git config core.sparsecheckout true git read-tree -mu HEAD git checkout master cd ..
まず、プライベートリポジトリでしたので、アクセストークンを取得して設定する必要があります。
以下を参考に作成して、サクッと設定しましょう。見ての通り、token: ${{ secrets.PORTFOLIO_ACCESS_TOKEN }}
で埋め込めます。
また、私の場合はhtmlに関係するファイルのみ欲しかったので、sparse-checkoutしています。 一括でうまくやってくれるworkflowファイルを見つけられなかったので、runでベタ書きしました。
とりあえず、これで上手く動いてくれているので良かったです。 (すぐデプロイするつもりがなんだかんだ一日かかっちゃいました‥)
注意点
- 再起動しないとデプロイが反映されない
- privateリポジトリをサブモジュールで持つなら、Access Tokenが必要
- FlaskはStatic以下は常に晒されている
- Secretkey pass, idなどはちゃんとDB管理
追記
Azureは適当にhtmlファイル置くだけでも表示されるらしいので、なんかのWebサーバが裏で動いているんですかね、わかりません。
そしてなんと、Azure Static Web Appというより気軽なサービスが最近追加されたらしいです‥ こっち使ったほうが早かったですね