AzureコンテナーインスタンスでNode.jsアプリケーションを動かそう

Node.jsで開発したアプリケーションをAzureコンテナーインスタンスで動かす方法について解説します。
規模が大きい場合は、Azure Kubernetes サービス(AKS)を使うといいと思いますが、規模が小さい場合、開発中などは、Azureコンテナーサービスを利用するとサクッといけていいです。
現在、プレビュー中のAzure Dev Spaceを使うと言う手もあります。まだ、プレビューだからでしょうか。うまく動かない事が多いので、諦めました(2019年4月現在)

# データベース
Azureポータルから、すべてのリソースで、SQLデータベースを作成します。
開発用として利用するのであれば、一番安いプランで十分です。
本番用として利用するのであれば、提供するサービスに合わせてプランを選択してください。

# アプリケーション
以下の3層で構成するアプリケーションを想定しています。
開発環境は、VisualStudio Codeです。

| レイヤ | 環境 |
|:-------|:--------|
| UI層 | Angular7、GraphQL(Client) |
| API層 | Node.js、NestJS、TypeORM、GraphQL(Server) |
| Database層 | SQL Server 2017 |

# Dockerイメージの作成、登録
開発したアプリケーションを、Docker化して、Azureコンテナーレジストリに登録します。
この手順については、下記を見てください。

https://qiita.com/kmatae/items/924fdcb2f2a7f5131e7f

# DockerイメージからAzureコンテナーインスタンスを実行(API層)
Azureコンテナーレジストリに登録すると、それをベースにAzureコンテナーインスタンスを実行する事ができます。

## Azureコンテナーレジストリのリポジトリを選択
Azureポータルのすべてのリソースから作成したAzureコンテナーレジストリを選択します。
さらに、実行したいリポジトリを選択します。

![acr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334727/5a52ff95-75d0-abe9-c88b-fc5e44d99ee1.png)

## リポジトリ内の対象タグを実行
リポジトリ内のタグの一覧が表示されます。実行したいタグの`...`をクリックして、実行インスタンスを選択します。

![exec.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334727/7b7d6d16-e0dd-1d48-6890-77407088248e.png)

## コンテナー情報の設定
以下の項目を設定し、OKボタンをクリックします。

* コンテナー名
* コンテナーイメージ名 ･･･ Azureコンテナーリポジトリ名.azurecr.io/リポジトリ名:タグ
* OSの種類
* サブスクリプション
* リソースグループ
* 場所
* コア数
* メモリ
* パブリックIPアドレス
* ポート

![create.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334727/b86c7fcc-c83b-331a-3f25-a9a79583cf9e.png)

これでしばらくすると、コンテナーインスタンスが実行できます。

![resource.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334727/20d9c69a-33be-7d22-2da7-b988a4cafd7f.png)

## 実行されたインスタンスにアクセス
これで、指定したものが実行されます。パブリックIPアドレスに「はい」を指定するとグローバルIPアドレスが振られますので、ブラウザでアクセスしてください。

![cointaner.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334727/48104097-88c2-6c01-b7a6-1ef9be5f8a8a.png)


簡単！！
これ以上、何もする必要がないアプリケーションなら、ここでおわりです。

# 環境変数の設定
ここでは、SQL Server 2017にアクセスするアプリケーションと言う設定になっています。
すると、SQL Server2017に接続するための情報が必要になりますよね？
データベースサーバー名、ユーザID、パスワード、データベース名･･･どれも知られるとまずいですよね？
ソース管理を通して、Dockerイメージを作成する訳ですが、ソース管理にこういう機微な情報を乗せるのは、絶対に避けたいところです。
そこで、作成したコンテナーインスタンスに環境変数を設定していきます。Node.jsで言うと、process.env.XXXXXとしてアクセスできる環境変数です。

## テンプレートのダウンロード
作成したコンテナーインスタンスのテンプレートをダウンロードします。
コンテナーインスタンスのテンプレートのエクスポートを選択し、ダウンロードしてください。
これでコンテナーインスタンスの設定情報がJSON形式で取得できます(zipファイルとしてダウンロードされるので、解凍してください)

![template.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334727/60bcd56d-b18e-6938-a9ac-18d518140968.png)

## テンプレートファイルの編集
テンプレートファイル(template.json)をテキストエディタで開いてください。
環境変数は、environmentVariablesのところに、以下のような形式で定義していきます。

```json
environmentVariables: [
    {
        "name": "変数名",
        "value": "値"
    }
]
```

あと、Azureコンテナーレジストリのパスワードが抜けていますので、パスワードを追加する必要があります(imageRegistryCredentialsのpasswordを追加)

```json:template.json
{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "containerGroups_servicename_api_st_name": {
            "defaultValue": "servicename-api-st",
            "type": "String"
        }
    },
    "variables": {},
    "resources": [
        {
            "type": "Microsoft.ContainerInstance/containerGroups",
            "apiVersion": "2018-04-01",
            "name": "[parameters('containerGroups_servicename_api_st_name')]",
            "location": "japaneast",
            "properties": {
                "containers": [
                    {
                        "name": "[parameters('containerGroups_servicename_api_st_name')]",
                        "properties": {
                            "image": "acrname.azurecr.io/servicename-api:64",
                            "ports": [
                                {
                                    "port": 443
                                }
                            ],
                            "environmentVariables": [
                            	{
                            		"name": "NODE_ENV",
                            		"value": "production"
                            	},
                            	{
                            		"name": "DATABASE_URL",
                            		"value": "mssql://userid:passwod@servicename_st:1433/databasename"
                            	}
                            ],
                            "resources": {
                                "requests": {
                                    "memoryInGB": 1.5,
                                    "cpu": 1
                                }
                            }
                        }
                    }
                ],
				"imageRegistryCredentials": [
				  {
				    "server": "acrname.azurecr.io",
				    "username": "acrname",
				    "password": "acrpassword"
				  }
				],
                "ipAddress": {
                    "ports": [
                        {
                            "protocol": "TCP",
                            "port": 80
                        }
                    ],
                    "ip": "000.000.000.000",
                    "type": "Public"
                },
                "osType": "Linux"
            }
        }
    ]
}
```

## テンプレートのデプロイ
作成したtemplate.jsonを使ってデプロイします。
コンテナインスタンスの、テンプレートのエクスポートのデプロイをクリックします。

![deploy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334727/9161daae-6ae4-7139-afcc-bb5bc9342251.png)

カスタムデプロイ画面に切り替わりますので、「テンプレートの...」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334727/f39061e3-1772-53ef-f932-36b03f4031c9.png)

ファイルの読み込みをクリックし、準備しておいたtemplate.jsonを選択します。内容を確認してエラーがなければ、保存をクリックします(エラーは赤の下線が表示されます)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334727/77e5a64a-afbc-4bc0-2c95-0e2ef22e1d65.png)

リソースグループを選択し、購入をクリックします。

![buy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334727/9ba38b1b-39dc-21b2-20b9-8e4d611593d7.png)

しばらくすると、デプロイが完了します。

コンテナーインスタンスの、コンテナー、プロパティの環境変数に設定したものが登録されていることを確認します。

# DockerイメージからAzureコンテナーインスタンスを実行(UI層)
API層と同じ手順でUI層のDockerイメージをコンテナーインスタンスとして実行します。
ここでは、Angular7アプリケーションをDockerで動作させますので、Dockerイメージのビルドは以下のステップを踏みます。

* node:10.15.3-alpineを元にAngular7アプリケーションをng buildする(バージョンは環境に合わせてください。alpineでなくてもいいですが軽量なので、特別な理由がない限り、alpineでいいです)
* 開発環境ではng serveで実行しますが、本番環境ではnginxを使う(nginx:1.15.11-alpine)

## Dokcerfile

```file:Dockerfile
### STAGE 1: Build ###

# We label our stage as ‘builder’
FROM node:10.15.3-alpine as builder

COPY package.json package-lock.json ng-add-pug-loader.js ./

## Storing node modules on a separate layer will prevent unnecessary npm installs at each build

RUN npm --unsafe-perm ci 
RUN mkdir /ng-app && mv ./node_modules ./ng-app

WORKDIR /ng-app

COPY . .

## Build the angular app in production mode and store the artifacts in dist folder

RUN npm run ng build --aot --prod --output-path=dist


### STAGE 2: Setup ###

FROM nginx:1.15.11-alpine

# ポート番号の指定
EXPOSE 80

## Copy our default nginx config
COPY nginx/default.conf /etc/nginx/conf.d/

## Remove default nginx website
RUN rm -rf /usr/share/nginx/html/*

## From ‘builder’ stage copy over the artifacts in dist folder to default nginx public folder
COPY --from=builder /ng-app/dist /usr/share/nginx/html

CMD ["nginx", "-g", "daemon off;"]
```

## nginxの構成ファイル
nginxの構成ファイルは以下の通りです。

```conf:nginx/default.conf
server {

  listen 80;

  sendfile on;

  default_type application/octet-stream;


  gzip on;
  gzip_http_version 1.1;
  gzip_disable      "MSIE [1-6]\.";
  gzip_min_length   1100;
  gzip_vary         on;
  gzip_proxied      expired no-cache no-store private auth;
  gzip_types        text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
  gzip_comp_level   9;


  root /usr/share/nginx/html;


  location / {
    try_files $uri $uri/ /index.html =404;
  }

}
```

これで、ui層のAzureコンテナーインスタンスのURLにアクセスすると、Angular7で作ったページが開きます。