tekitoumemo’s diary

.NET CoreとVue、Angularなどの技術ブログを書いています。みんなの洋楽ランキングを運営しています。

.NET CoreアプリをGitHub ActionsでAzureにデプロイした

mygkrnk.com

個人サービスをkuduデプロイからGitHub Actionsへ、.NET Core2.2から3.1に上げた。
NET Coreバージョンアップは別の記事で書く。

kuduとは

Azure Webサイトで利用されているGitデプロイエンジン。要はオープンソースのCIなのだが、非常に使い勝手が悪い、というか結局シェルスクリプトゴリゴリなのであまり恩恵がない上、github上で連携出来ないのでAzure portalを見に行かなければならず非常に不便だった。さらに.NET Coreを使う上で大きな問題がある

github.com

.NET Coreを使う上でのkuduの問題点

kuduはホスティング上でビルドするのでホスティング環境が整ってないとビルドが出来ないことがある。AppServiceではnodeは8(記憶上だと)と古く、.NET Coreで言えばビルドするために必要なdotnet cliが使えない(厳密には使えるが最新のバージョンをサポートしてない)。dotnet cliSDKに含まれてるのでAzureのサービス上でビルド出来ないのだ。AppServiceなど3.1のruntimeはあるのにビルド出来ずにバージョンが上げれず、いつSDKがリリースされるのか告知も無い。そのようなことがあり継続的なバージョンアップが出来ないと判断し、GitHub Actionsに移行した。あと、MSがgithubを買収したところからkudu廃止してGitHub Actionsに移行する方向で進んでそう。

GitHub Actionsを選んだ理由

無料枠多すぎ、Microsoftひいきしすぎ(ドキュメント豊富)、期待感すごい。

ワークフローを作る

[new workflow]から.NET Coreを選択。
f:id:tekitoumemo:20200416232820p:plain

[set up this workflow]をクリックするとテンプレートが表示される。2020/4時点はこれ↓

name: .NET Core

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: Setup .NET Core
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: 3.1.101
    - name: Install dependencies
      run: dotnet restore
    - name: Build
      run: dotnet build --configuration Release --no-restore
    - name: Test
      run: dotnet test --no-restore --verbosity normal

以下、テンプレから変えたところ。

checkout

git submodulesを使ってるリポジトリだったのでcheckout@v1を選択。submodulesをv2で実現するのはちょっと面倒なので。

+    - uses: actions/checkout@v1
+      with:
+        submodules: true
-    - uses: actions/checkout@v2

submodulesでsshだと取得出来ないのでhttpsに変更した。
.gitmodules

[submodule "library/DapperSlackOff"]
 path = library/DapperSlackOff
+ url = https://github.com/ikuosaito1989/DapperSlackOff.git
- url = git@github.com:ikuosaito1989/DapperSlackOff.git
setup-dotnet
    - name: Setup .NET Core
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: 3.1.100
    - name: Install dependencies
      run: dotnet restore
    - name: Build
      run: dotnet publish "{path}" --configuration Release -o hoge

テストは作ってないので削除。

コードフォーマッター
.NET Coreではdotnet formatというフォーマッタがある。prittierとか使ってるとちょっとしょぼいように感じるが、無いよりましなので追加する。
github.com

導入は下記参照。
.NET ローカル ツールの使い方 - Qiita

マニフェストファイルがないとdotnet toolコマンドが有効にならないので注意

    - name: Restore Tool
      run: dotnet tool restore
    - name: Check code format 
      run: dotnet format --check --dry-run

--checkはフォーマットせずチェックする仕組みで--dry-runは失敗したときに0以外の終了コードを返すオプション。

webapps-deploy

Azureにデプロイするためのworkflows。

デプロイするための認証情報を取得する必要があるので[発行プロファイルの取得]を押下。githubのSecretにてダウンロードしたファイルを以下で貼り付け。packageはdotnet publishで出力したディレクトリを指定する。
f:id:tekitoumemo:20200417120113p:plain

- uses: azure/webapps-deploy@v1
   with:
     publish-profile: ${{ secrets.publish_profile }}
     package: './hoge' 
masterにpushされたら自動デプロイする
push:
    branches: [ master ]

f:id:tekitoumemo:20200417120808p:plain

AppServiceの設定

AzureのUIが違うかもしれないが、、
[デプロイセンター]からgithubを押下
f:id:tekitoumemo:20200417121303p:plain

github actionsを選択
f:id:tekitoumemo:20200417121350p:plain

リポジトリをよしなに入力。ワークフローはさっきつくったので[既存のワークフロー定義を使用して構成する]を選択。新規で作ると勝手にコミットされるので、僕は嫌いです。
f:id:tekitoumemo:20200417122057p:plain

完了したらあとはpushしてワークフロー流したらデプロイ完了。ちなみにたまにコンフリクト起こしてデプロイ失敗するので、リトライすれば大体成功する。

個人サービス公開して二年経ったので振り返る

去年の振り返り ※この記事は振り返りの続きです。
tekitoumemo.hatenablog.com

このブログに何度も登場してるサービスですが、2018年の4/1に公開してから二年経ったので振り返ります。

結論から言うと、

うまく周ってます、黒字になりました!

2019/5 投稿が少しずつ利用される

SEOが全くうまくいかなかったこの時期。投稿機能を使ってもらった効果によって自分の中で一定の基準(1万PV)に達していたので、続けてライターのイチオシ曲を投稿出来る機能を作成。
f:id:tekitoumemo:20200410215917p:plain
これが全然うまくいかず(笑)後述するレビュー機能と類似機能になってしまい、近々削除する予定。このときは高まっていて勢いで作ってしまった。技術的にもサービス的にもなにもうまくいかず、ただただ負債になってしまった。勢いに乗っているときこそ冷静に。

2019/6 各ページにページビューを追加

そこそこまともなPV数になってきたので、ページビュー数を追加。なぜこれを追加したかというと、会員投稿のモチベーションを下げたくなかったから。1万程度だと、赤字なので会員に還元するわけにもいかず、どの方法で記事を書くモチベーションをあげられるかずっと考えていた。結果的に「こんなにみてる人がいるんですね!」と喜びの声もいただき、さらにその影響か不明だが、PVが多いページが多く閲覧される傾向がでてきた。

2019/8 アドセンス停止

adsenseから不正なクリックを検出したとのことで1ヶ月停止をうける。正直自分で何回か押しちゃってたし、心当たりありまくりなので自戒。adsenseすごいね、そこまで激しいクリックしたつもりないけど、わかっちゃうんだねー。8月は17000PVとそこまで大きくないのであまり影響なかったのも救いだったけど、いろんな壁乗り越えてきたからそこまで焦らず冷静にいられた。
f:id:tekitoumemo:20200410230600p:plain

2019/9 レビュー機能追加

f:id:tekitoumemo:20200411120221p:plain
7月から実装開始したレビュー機能をリリースした。この時期は子どもが生まれたり鬼のように忙しかったのでほとんど手がつけられず、2ヶ月もかかってしまった。さらにこのときフロントエンドフレームワークに葛藤があり、機能の大半をSSRで済むような画面である以上はJQueryなどライトにかけるスクリプトの方が生産性が上がるのではないか?と思い、JQueryで実装した。結果的に40行程度のjsファイルで完結したが、可読性が著しく低下し、変更に弱いコードが出来上がってしまった。JQueryはDOMを加工した物をデータと見立てて、そのデータを加工しDOMに戻す作業なので、本質のコード以外にやることが多くて結果的に可読性が著しく低下するということがわかった。さらに3大フロントエンドフレームワークアンチパターンとして王道なDOM操作をバンバンやるので、疑心暗鬼になりながらでも後戻り出来ず、モチベーションが低下し、個人サービスとしては最悪なコンディションになることがわかった。このレビューに関しては、あまり使ってもらえてはないがライト会員の利用率は比較的高いなという印象。

2019/10 パフォーマンス改善&バージョンアップ

tekitoumemo.hatenablog.com
全ては過去に書いたブログに書いてあるが、完全にパフォーマンス対策した。画像の最適化から圧縮、遅延ロードまで。結果的にめちゃくちゃよくなって、実際のユーザー体験も向上したと思う。バージョンアップと同じでいつかはやりたいなぁと思ってたので、一旦落ち着いたこの時期に出来てよかった。
さらに.Net Coreを2.2へ、Reactを16.8へとあげた。

2019/11 ~ 12 なにもせず

ここらへんは仕事でいろいろあったり、からだ壊して入院したりとそれどころじゃない状況。たまーにバグ直したりする程度で主にNuxtの学習がてらプロフィールサイト作ったり、他のところに時間を費やしていた。PVも8月あたりから2~3万を一定を保つ感じで推移していて、特に何もやる必要がないなと感じていた。昨年は変動が大きかったりといろいろ戸惑ったけど、どんな状況でも怖くなくなったことがでかいと思う。12月は過去最高レベルのPVを記録。
f:id:tekitoumemo:20200411003535p:plain

2020/01 アクセスが右肩上がり

f:id:tekitoumemo:20200411005444p:plain
SEOが順調に上がって約8万PVを記録。2019年のSEOはずっと調子よかったものの、順位が数位上がるだけでこの効果。初めて黒字化達成。1月はグラミー賞もあり、新年で新しい情報を知りたいユーザーが多かったことが重なった。この考察は以前のブログに書いた。
tekitoumemo.hatenablog.com
一番よかったのが、会員に還元出来たこと。過去にキャンペーンとして還元したことあるが、会員が書いてくれた記事をみたユーザーが広告を踏んで得た利益を会員に還元するといった循環が出来たことが何よりも嬉しい。黒字になった部分を全て還元してるので結果的にトントンだが、確実に誰かが必要としているサービスに育ったと実感した。

2020/02 初の10万PVを達成

f:id:tekitoumemo:20200411005639p:plain
昨年の約24倍。昨年1万PVいったときの高揚感はなく、サイト設立当初に計画していたアクセス数と同じような結果がでたなーという感じで冷静だった。昨年の7月にはいつかこの結果になるだろうという確信を持っていたと思う。実際に想定アクセス数が15万〜20万と書いてあるし、なんとなく書き残してるってことはそれなりの自信があったのだろう。
tekitoumemo.hatenablog.com
この月からパートナープログラムを試験的に開始した。投稿の仕組み的に大量のユーザーが書くことがないので得た利益分をうまく回せている。まだ手動だけど、拡大するようであれば今後スケーリングさせる。

2020/03 引き続き10万PV達成

f:id:tekitoumemo:20200411112441p:plain
2月よりは少し落ちてしまったけど平均3000PVはあるので安定したと思う。4月の緊急事態宣言が発表されてからアクセス数が増加しているので、この状況下でたくさんの人にみてもらえ、不安を払拭してくれたらこんなに嬉しいはない。ただ、アクセス数は上昇しているが、モチベーションが下がっていてもう1ヶ月以上更新せず。もともと想定していたアクセス数に近づいたというのとそろそろ限界だろうなというところが見えてきた。おそらくもう少し順位が上がっても30万PVが限度だろうなと思う。モチベーションが下がったとはいえ、利益は出て、多くのユーザーに閲覧されているので今年はバージョンアップやリファクタ、デザインの変更等ユーザー体験が向上するところに重きを置くだろう。


結果的には成功。adsenseの広告収入をユーザーに全還元して他の収入でインフラ代になれば最高かなと思ってるので最後に


広告掲載について | みんなの洋楽ランキング


マジで広告募集してるので連絡ください!!

VeeValidateで一括検証+スクロールするmixins

v3ではValidationObserverが追加され、一括検証が楽になった(v2知らない😇)
一括検証+エラーの要素にスクロールできれば嬉しいはずなので、そのmixinsを書く。
スクロールはお任せで、vue-scrolltoとかいいんじゃないかしら。

page/hoge.vue

<ValidationObserver ref='observer'>
    <ValidationProvider/>
    <ValidationProvider/>
</ValidationObserver>

<script>
import Validate from '~/mixins/validate'
export default {
    mixins: [ Validate ],
    methods: {
        async onSubmit() {
            if (!await this.validateAll()) {
                // Do something
            }
        }
    },
}
</script>

mixins/validate.js

export default {
  methods: {
    async validateAll() {
        const result = await this.$refs.observer.validate()
        const ref = Object.values(this.$refs.observer.refs)
                .filter((ref) => ref.errors.length > 0)[0]
        // refに対してスクロールやらなんやら
        return result
    }
}

universalモードでのaxiosのエラーハンドリングを共通化する

universalモードの場合、SPAのハンドリング、SSRのハンドリングをしなければならない。

SPAの場合はビューで制御して
ビュー - NuxtJS

SSRの場合はnginxのerror_pageディレクティブとか使って制御する必要がある

フロントもSSR、SPAを判定してエラーハンドリングをしなければいけないのですが、めんどくさいのでそこらへんの判定を無視する雑なハンドリング方法を書く。雑実装なので非推奨

static/error.html

<body>
    エラーっす
</body>

plugins/axios.js

export default function({ $axios, redirect }) {
  $axios.onError((err) => {
    const code = parseInt(err.response && err.response.status)
    redirect(code, '/static/error.html')
  })
}

nuxt.config.js

plugins: [
    { src: '~/plugins/axios.js' },
]

あとはnginxとかでerror_pageディレクティブとか使ってハンドリングする。errorメソッドでハンドリングしても良いが、axiosの例外はSSRのときに500で拾ってしまうので正しいステータスコードをサーバーに伝えるため無理やり指定のstatusコードにredirectしてる。

雑すぎるのでおすすめしない。

[参考]
https://axios.nuxtjs.org/helpers

[NustJS]外部ファイルからstoreにアクセスする

NuxtJSではコンポーネントやplugins、middrewareなどなど、contextにアクセス出来ないことがある。つまりstoreにアクセス出来ない。

やり方はいろいろあるが、自分なりの落とし所を書く。

globalで使う(やばい)

.eslintrc.js

globals: {
    $store: true,
    ...
}

plugins/global_store.js

export default function({ store }) {
  global.$store = store
}

nuxt.config.js

plugins: [
    { src: '@/plugins/global_store.js', ssr: false },
]

これでこう使う

export default class Hoge {
    static hoge() {
        $store.dispatch('hogehoge');
    }
}

シャローコピーだしまぁ良いかなーと思ったけど、状態のように常に変わるようなものをグローバルに置くのはまぁヤバイ。コンポーネントではthis.$store$storeどっちも使えるのがシビれる(悪い意味で

onNuxtReadyで初期化

let store
if (process.client) {
  window.onNuxtReady(({$store}) => {
    store = $store
  })
}

export default class Hoge {
    static hoge() {
        store.dispatch('hogehoge');
    }
}

なんか絶対よくない気がするけど、ググってもonNuxtReadyがなんだかわからない。公式のドキュメントがあれば読みたい。却下。

constructorで初期化

export default class Hoge {
  constructor(store) {
    this._store = store
  }
  get $store() {
     return this._store
  }
}

無難。staticで使えないので却下。

シンプルにインスタンス作る

export default class Hoge {
    static get $store() {
        if (!this._store) {
            this._store = new Vuex.Store(...)
        }
        return this._store
    }
}

無難でシンプル、が結果的には最初の実装と同じようにインスタンスはグローバルに。インスタンスメソッドからはthis.constructor.$storeで使う。this.$storeインスタンスが変わるのでシュチュエーションによっては悪い例だけど、今回は特定のclass経由でのみstoreをいじるのでまぁおっけいかも?

pluginsでstoreを注入

plugins/hoge.js

import Vue from 'vue'
export default ({ store }) => {
    Hoge.$store = store
    // Vueインスタンスとして使いたければ
    // Vue.prototype.$hoge = Hoge
    // Vue.prototype.$hoge.$store = store
}
export default class Hoge {
    static hoge() {
        $store.dispatch('hogehoge');
    }
}

Nuxtの初期化フェーズでクリーンなcontextとして扱えるのでこの方法が一番しっくりくる。別インスタンス作ってしまうとchromeのvuedevtoolでstateが確認出来ないので困る。

[参考]
Accessing the store from an external file · Issue #2005 · nuxt/nuxt.js · GitHub

正規表現を使ってディレクトリ内のファイルをrequire出来るようにする

requireでは、ファイル名を直接指定する必要がある。

require

const hogehoge = require('./hoge/hogehoge.js')

たまーに動的に使いたいときがある(NuxtJSでVeeValidateでカスタムルール作るときとか)。
こう使ってたりすることが多い。

names.forEach(name => require(`./hoge/${name}.js`))

これだとnamesはどこで作るのか迷う。余談だがNuxtJSは設定より規約を重視してるので規約をつくった方がよい。だからnames作りたくない

require.context

独自のコンテキストを作れる。早速使い方。

const context = require.context('./hoge/', true, /\.js$/)
context.keys().forEach((key) => {
  // const hoge = require(`./hoge/{key}`) これとほぼ同じ(これじゃ実際動かない
  const hoge = context(key).default
})

左からディレクトリパス、サブディレクトリも検索するかどうか、ファイルを照合する正規表現

これをうまく使えばNuxtJSのpagesでroutingを生成するように規約ベースでなんやかんや出来る!

[参考]
Dependency Management | webpack

dotnet build、runで起きるエラーの対処法

いつも忘れるいつもググるから備忘録

コピーできません

warning MSB3026: "obj/Debug/{app}""bin/Debug/{app}" にコピーできませんでした。1000 ミリ秒以内に 1 回目の再試行を開始します。

多分どっかのプロセスが握ってるので、ブチ消す。

rm -rf bin

Kestrel動きません

crit: Microsoft.AspNetCore.Server.Kestrel[0]
      Unable to start Kestrel.
System.InvalidOperationException: HTTPS endpoints can only be configured using KestrelServerOptions.Listen().

使いたいポートが空いてない。開ける

lsof -i:5000
kill PID