Azureデプロイの6つのhacks(バッドノウハウ) ―胃の痛くなるデプロイスクリプトを書いた話―

# はじめに

この記事は、[Microsoft Azure Advent Calendar 2016](http://qiita.com/advent-calendar/2016/azure)の2日目の記事です。

前日は [nntsugu@github](http://qiita.com/nntsugu@github) さんですが、記事内容は… null pointer exceptionですね

# TL;DR

* Azure運用で得たバッドノウハウを紹介します
* 公式で特に説明のない項目について言及しています
* 何がベストなのか、その正解はここには無いです

# 今回の内容

数々のバッドノウハウ(hacks)
1:複数リポジトリのデプロイ方法
2:デプロイスクリプトの待ち時間の伸ばし方
3:portalのCORS設定を使うとヘッダが触れなくなる
4:SiteExtensionの Reverse Proxy は無口である
5:設定値の保持に環境変数を使う
6:Web Appsの無料枠でsslを使う

良くない構成を回避するには

# 要件

よくあるwebサービスは以下のようなものでしょうか。

**バックエンド**

    + PHP
    + composerを使ってパッケージ管理する
    + DB接続あり
    + ユーザー認証あり

**フロントエンド**

    + Node.js
    + npmを使ってパッケージ管理する
    + npmを使ってビルドする

そんな中色々やりくりしたのが以下hacksです。

## hacks01:アンチパターン:サブモジュール地獄

WebApps環境構築用リポジトリ
├フロントエンドのソース
└バックエンドのコード

|repos|Dir|
|---|---|
|WebApps環境構築用リポジトリ|/|
|フロントエンドのサブモジュール(PHP)|/app/|
|バックエンドのサブモジュール(Node.js)|/view/|

公開領域のroot: `/app/public/`

> そもそもサブモジュールにアクセス権ないし、認証に使われるDeployKeyって一つじゃない？

GitHubでsubmodule連携するとき、
WebAppsで生成されたDeployKeyをリポジトリのDeployKeyからいったん削除して、
GitHubユーザのSSH Keysに再登録することになります。
Deploy keyはインスタンス内の `D:\home\.ssh\id_rsa.pub` に吐き出されているので、
それを拾ってきて貼り付けます。

![002.JPG](https://qiita-image-store.s3.amazonaws.com/0/35009/92e883fd-22a5-6cdf-34d1-69f2a0979d5c.jpeg)

尚、ハック的なやり方で手間もかかるのでお勧めはしません！

## hacks02:アンチパターン:複雑すぎるビルドスクリプト

例えばNode.jsとPHPのハイブリッドアプリの場合、

* `npm install` を実行する
* `npm run build` を実行する
* `composer install` を実行する
* 公開領域にビルドしたアプリを更新する
* データベースをmigrateする

などのタスクが考えられます。

それぞれをファイルに分割してコールすることもできますが、

```bat
@echo off

call deploy.node.cmd
call deploy.front.cmd
call deploy.api.cmd

echo Copy frontend components.
call xcopy "%DEPLOYMENT_TARGET%\front\dist" "%DEPLOYMENT_TARGET%\api\public" /E /H /R /Y
```

なんか一気にやっちゃってますが、これどこかでエラー出たら残りのタスクほったらかして死にます。
良くないですね。

綱渡りだし危険なのでやめましょう。

**罠1：package.jsonのアレコレ**

サブモジュールのデプロイ等で、プロジェクトのroot以外にpackage.jsonがある場合、
デプロイスクリプトを書き換えなければいけません。

```bat
:: 2. Install npm packages
IF EXIST "%DEPLOYMENT_TARGET%\package.json" (
  pushd %DEPLOYMENT_TARGET%
  echo Install npm components.
  call npm install --no-optional
  echo Build frontend components.
  call npm run build
  IF !ERRORLEVEL! NEQ 0 goto error
  popd
)
```
これを↓のようにディレクトリ指定するなど

```bat
:: 2. Install npm packages
IF EXIST "%DEPLOYMENT_TARGET%\view\package.json" (
  pushd %DEPLOYMENT_TARGET%\view
  echo Install npm components.
  call npm install --no-optional
  echo Build frontend components.
  call npm run build
  IF !ERRORLEVEL! NEQ 0 goto error
  popd
)
```

また、以下のように`package.json`でNode.jsのバージョンを指定する場合、
`package.json`が`/view/package.json`のように、プロジェクトのrootにない場合も、
ディレクトリの変更を追従しないと、Nodeのバージョンを変更してくれません。

```json
{
  "engines":{
    "node": "^6.9.1"
  }
}
```


**罠2: SCM_COMMAND_TIMEOUT**
アウトプットがないままの待ち時間がある処理 (PHP composer など)
は処理に時間がかかるとデプロイスクリプトのタイムアウトで落とされる。

あとデプロイするバッチの中でY/n確認など入力待ちさせる処理を殺し忘れるとタイムアウトを待つしかなくなる。

`SCM_COMMAND_IDLE_TIMEOUT` の値に秒数を入れると伸ばせるが、諸刃の剣。(長すぎるとデプロイ終わらない地獄)
デフォルトは60が設定されている

## hacks03:アンチパターン:無視されるhttpヘッダ

+ Azure portal で各インスタンスごとにCORSを指定することができる。
+ しかしそこで設定設定してしまうと、カスタムヘッダを蹴るようになります。その方法では `Access-Control-Allow-Credentials` ヘッダを付けられない。

[Document Microsoct Azure | App Service の CORS と Web API の CORS](https://docs.microsoft.com/ja-jp/azure/app-service-api/app-service-api-cors-consume-javascript)

![001.JPG](https://qiita-image-store.s3.amazonaws.com/0/35009/3e4e716e-c17e-ab01-ce0a-3ad3506100fa.jpeg)


CORSを使う上で `credencials` を有効にする場合、各インスタンスのCORSの設定に何もいれないようにします。
その上でアプリケーションでヘッダを書ききるか、 `Web.config` ファイルに書きましょう。

参考：
http://m2wasabi.hatenablog.com/entry/2016/11/30/123538

## hacks04:微妙ポイント:静かなる Reverse Proxy

Site ExtensionでReverseProxyがあるけど、
転送元の情報を何ら付与せずに、転送先サーバへリクエストを書き換えて送っているだけなので、
転送先サーバから見ると**どこからアクセスしてきているのかわからない**。
APIにアクセスさせるWebアプリを分けようとしたときに辛い。

![003.JPG](https://qiita-image-store.s3.amazonaws.com/0/35009/84259db3-7a35-dcf0-9749-7163c097d86c.jpeg)


また、転送先サーバは`Web.config` のrewrite roulesに記述するのですが、
Azure上では、ここに環境設定を組み込むことが難しいので、
直接書かなければいけません。

これが自動化を難しくしています。

Reverse Proxyのソースは公開されていないので、自分で書くしかないかもしれません。
自動デプロイに組み込み可能なナイスなリバースプロキシ拡張があれば紹介してください。

## hacks05:環境変数にサーバ固有の設定を投入する

アプリ設定の項目で、アプリごとの環境変数を指定できます。
環境変数の拾い方は、処理系によって様々です。

![004.JPG](https://qiita-image-store.s3.amazonaws.com/0/35009/f204c7f7-0ceb-e2f0-e33b-ba412f3d2ce9.jpeg)

実際の拾い方は以下です

#### Node.js

process.envから拾うことができます

#### Webpack with Node.js

Webpack definision プラグインで、リテラルを指定することにより、
ビルドした後にも、ビルド時の環境変数を使うことができます。

参考：
http://qiita.com/mikakane/items/5ab96c4c7e187ab6c9f1

#### PHP
phpdotenvを使って、
`.env`ファイルに書き込むのが良いかもしれません。
アプリケーション内では、`.env`ファイルを通して環境設定させ、
ローカル開発環境など他のサーバで動かす場合、`.env`ファイルをかきかえるようにしましょう。

Azure特有の運用方法として、デプロイスクリプトで以下の様に記述して、
azureの場合は環境設定ファイルを参照するようにしてもいいかもしれません。

```ini:.env.azure
APP_ENV=production
DB_HOST=${MYSQL_DB_HOST}
DB_NAME=${MYSQL_DB_NAME}
...
```


```
copy /Y .env.azure .env
```

## hacks06:Web Appsの無料枠でsslを使う

WebAppsのSSLアクセスは有料サービスプランでしか使えないはずですが、
同じサービスプランにApp Service認証がONのインスタンスがあると、
他のインスタンスでもhttpsでのアクセスが有効になっています。

![005.JPG](https://qiita-image-store.s3.amazonaws.com/0/35009/c21350f1-35c6-6b8d-1c89-f733cf6a9505.jpeg)

なので、認証が有効なダミーWebAppsを立てれば、httpsでアクセスできるんじゃないかと。

# 望ましいバックエンド構成

## デプロイスクリプトはできるだけシンプルに。
複数言語のハイブリッドサーバは収拾がつかなくなる(上記でやってしまいましたが…)のでやめましょう。

継続的デプロイをきちんと活用するなら、
利用するアーキテクチャごとでそれぞれインスタンスを立てるべきです。

#### デプロイスクリプトの作成

何の事前知識もなしにカスタムデプロイのスクリプトを記述するのは非常に困難です。
そこでデプロイスクリプトのスキャフォールドはazure-cliを使って作成します。
Node.jsのライブラリなのでエコで環境にやさしいです。

```
npm install -g azure-cli
```

リポジトリのrootに立って、以下コマンドで`.deployment` と `deploy.cmd` が生成されます

Node 

```
azure config mode asm
azure site deploymentscript --node
```

PHP 

```
azure config mode asm
azure site deploymentscript --php
```

DotNet

```
azure config mode asm
azure site deploymentscript --aspWAP pathToYourWebProjectFile.csproj -s pathToYourSolutionFile.sln
```

生成されたファイルをまとめてリポジトリにコミットすると、
Azureにデプロイされたときに自動的にデプロイスクリプトとして動きます。

Node.jsや.NETのサンプルは探せば見つかるのですが、
PHPのサンプルが見当たらなかったので作りました。

[deploy.cmd](https://gist.github.com/m2wasabi/0af38219431a1392cc1152dd65e0424d)

自前で書いたのは以下の部分のみです。

```bat

:: 2. Install composer
IF EXIST "%DEPLOYMENT_TARGET%\composer.json" (
  pushd "%DEPLOYMENT_TARGET%"

  IF NOT EXIST "%DEPLOYMENT_TARGET%\composer.phar" (
    echo Download composer installer
    call :ExecuteCmd php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
    IF !ERRORLEVEL! NEQ 0 goto error
    echo Install Composer
    call :ExecuteCmd php composer-setup.php
    IF !ERRORLEVEL! NEQ 0 goto error
    echo Deleting installer
    call :ExecuteCmd php -r "unlink('composer-setup.php');"
    IF !ERRORLEVEL! NEQ 0 goto error
  )

  echo Composer self update
  call :ExecuteCmd php composer.phar self-update
  IF !ERRORLEVEL! NEQ 0 goto error
  echo Install Composer plugin
  call :ExecuteCmd php composer.phar global require "hirak/prestissimo"
  IF !ERRORLEVEL! NEQ 0 goto error

  echo Composer install
  call :ExecuteCmd php composer.phar install --no-dev
  IF !ERRORLEVEL! NEQ 0 goto error
  popd
)

:: 3. Laravel update
IF EXIST "%DEPLOYMENT_TARGET%\.env.azure" (
  pushd "%DEPLOYMENT_TARGET%"
  echo Copy .env file
  call :ExecuteCmd copy /Y ".env.azure" ".env"
  echo Migrate database
  call :ExecuteCmd php artisan migrate --force
  IF !ERRORLEVEL! NEQ 0 goto error
  popd
)
```

## APIサーバとフロントエンドを別にする

フロントエンドとバックエンドでそれぞれインスタンスを分ける場合、
以下のようなアプリケーション構成が考えられます。

### CASE1:APIサーバはCORSでやりとり

+ 認証周りについて、フロントエンド、バックエンドのそれぞれにスキルが求められる
+ トラフィックに無駄がなくネットワークにやさしい

### CASE2:フロントエンドとAPIサーバはリバースプロキシでやりとり

+ CORSのめんどくさい所はインフラで吸収できる
+ プログラマーから見るとサイト内リンクと同じなので、2000年代スキルのロートルエンジニアでも使える


# 最後に

面倒なこと抜きにしてCIサーバにローカルGitをマネジメントさせるのがいいんじゃないか？
と思いつつ、最良の構成についてはいまだに迷子です。

Azureの闇についてはここにまだまだ書ききれないほどありますが、
その中でもなんとか光明を掴み取って前に進みたいと思います。

あと、 App Service on Linux も便利そうなので使ってみたいけど、
従来のApp Serviceと同居しない&無料枠は存在しないので、お財布と相談しながら使わざるを得ないですね。

# さて、次回のアドベントカレンダーは…

[Microsoft Azure Advent Calendar 2016](http://qiita.com/advent-calendar/2016/azure)
明日は @ksasaki さんです。よろしくお願いします！