Deno in Dockerなアプリ開発

# はじめに
練習がてら、タイトルの技術を使用した簡単なアプリを作ってみました。
ソースコードは[こちら（UI）](https://github.com/LeftLetter/your-memory)と[こちら（サーバ）](https://github.com/LeftLetter/your-memory-server)です。

**アプリそのものというより、構築方法に主眼を置いた記事です。**
以下が本稿で説明する内容です。

* DenoコンテナをVS Code Remote Containersで開発するための環境構築方法
* DenoコンテナをGitHubからAzureに自動デプロイする方法


# アプリの概要
Qiitaで良い記事を見つけると、ストックをしますが見返すことがほぼありません。これではストックの意味がないので、過去にストックした記事をランダムで表示させることで、過去に覚えた（はず）の知識のおさらいをするアプリです。

作成したアプリは[こちら](https://leftletter.github.io/your-memory/)です。ユーザ名欄に任意のユーザ（ユーザ自身を想定）を入力し、表示ボタンを何回か押してみてください。

構築の練習が目的のため、アプリ自体はかなりしょぼいのでご了承ください。
またAzure Web Appsの無料枠を使っているため初回アクセス時は重いです。

## 主な使用技術
* Azure App Service　・・・　サーバアプリのデプロイ先
* Azure DevOps　・・・　サーバアプリの自動デプロイ
* Deno　・・・　サーバアプリ作成に使用
* VS Code Remote Containers（Docker）　・・・　開発環境として使用
* GitHub Pages　・・・　画面のデプロイ先[^1]
* React　・・・　画面作成に使用[^1]

[^1]: 本稿では触れません。

## 構成
UIとサーバのリポジトリを分けており、UIはGitHub Pages、サーバはコンテナ化してAzure DevOpsのPipelinesで自動デプロイするようにしています。

また、開発環境は二つともVS Code Remote Containersで開発するようにしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227999/ca4ce092-528e-fffd-afd3-c43e546baf91.png)


# DenoコンテナをVS Code Remote Containersで開発するための環境構築方法
重要な部分だけかいつまんで説明します。コードの全体は[こちら](https://github.com/LeftLetter/your-memory-server)にあります。

## VS Code Remote Containersの設定をする
`devcontainer.json`を以下のように設定します。拡張機能の`denoland.vscode-deno`をインストールし、`"deno.enable": true`とするところがポイントです。

```json:.devcontainer/devcontainer.json
{
  "name": "Your-Memory-Server",
  "dockerFile": "Dockerfile",
  "shutdownAction": "none",
  "appPort": "5000:5000",
  "extensions": [
    "denoland.vscode-deno",
    "eamodio.gitlens",
    "mhutchie.git-graph"
  ],
  "settings": {
    "terminal.integrated.shell.linux": "/bin/bash",
    "editor.formatOnSave": true,
    "editor.tabSize": 2,
    "editor.detectIndentation": false,
    "editor.insertSpaces": true,
    "deno.enable": true
  }
}
```

Dockerfileは以下のようにしました。denoのインストールは[公式](https://deno.land/)の通りですが、私の環境ではパスを設定しないと`deno`コマンドが動きませんでした。また、開発用にgitやcurlがほしかったためベースイメージを`buildpack-deps`としています。

```dockerfile:.devcontainer/Dockerfile
FROM buildpack-deps:stretch

RUN curl -fsSL https://deno.land/x/install/install.sh | sh
ENV DENO_INSTALL /root/.deno
ENV PATH $DENO_INSTALL/bin:$PATH
```

この状態でVS Codeの`Remote-Containers: Reopen in Container`をクリックすることでコンテナ内に入り、開発が可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227999/e32c928c-b7b6-f273-881b-d1f81f73afed.png)

## VS Codeでdenoアプリをデバッグする

`launch.json`を以下のように設定し、`F5`でデバッグ可能です。

```json:.vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "runtimeExecutable": "deno",
      "runtimeArgs": [
        "run",
        "--inspect-brk",
        "--allow-net",
        "--allow-read",
        "-A",
        "index.ts"
      ],
      "port": 9229
    }
  ]
}
```

`--allow-net`はサーバ起動、`--allow-read`は`.env`読み込みのために指定しています。

## 本番ビルド用のDockerfileを作成する
上記のDockerfileはあくまで開発用のコンテナを作成するためのものなので、実際にAzureにデプロイするコンテナを作成するDockerfileも別で用意します。

```dockerfile:Dockerfile
FROM buildpack-deps:stretch-curl

ENV DIRPATH /opt/app
WORKDIR $DIRPATH

RUN apt-get update && apt-get install -y --no-install-recommends \
    unzip

RUN curl -fsSL https://deno.land/x/install/install.sh | sh
ENV DENO_INSTALL /root/.deno
ENV PATH $DENO_INSTALL/bin:$PATH

COPY . $DIRPATH

ENTRYPOINT deno run --allow-net --allow-read --allow-env index.ts

```

本番環境では.envを介さずに直接環境変数を読み込むため、`--allow-env`を追加しています。


# コード
リクエストパラメータとしてユーザ名を投げるとそのユーザがストックした記事一覧をレスポンスとして返す簡単なサーバです。deno標準のhttpライブラリを使用していますが、どうやらルーティングやリクエストパラメータのパース機能が用意されていないようなので、しょうがなく自前実装しています。


本格的にやるなら以下のようなライブラリを使った方がよさそうです。

* [servest](https://github.com/keroxp/servest)
* [oak](https://github.com/oakserver/oak)
* [pogo](https://deno.land/x/pogo)

また、ストック記事は100件しか取得していませんしキャッシュもしていません。これは、単に実装の手を抜いたためです。

```typescript:index.ts
import { serve } from "https://deno.land/std/http/server.ts";
import { config } from "https://deno.land/x/dotenv/mod.ts";

let env = config();
if (Deno.env.get("API_KEY")) {
  env.API_KEY = Deno.env.get("API_KEY")!;
}

for await (const req of serve({ port: 5000 })) {
  if (req.method === "GET" && req.url.split("?")[0] === "/api/v1/article") {
    let params: { [key: string]: any } = {};
    const paramAry = req.url.split("?")[1].split("&");
    for (const param of paramAry) {
      const [key, value] = param.split("=");
      params[key] = value;
    }
    const res = await fetch(
      `https://qiita.com/api/v2/users/${params.username}/stocks?per_page=100`,
      {
        headers: {
          Authorization: `Bearer ${env.API_KEY}`,
        },
      },
    );
    const json = res.json();
    const data = await json;
    req.respond({
      status: 200,
      headers: new Headers({
        "Content-Type": "application/json",
      }),
      body: JSON.stringify(data),
    });
  } else {
    req.respond({
      status: 404,
      body: JSON.stringify({}),
    });
  }
}

```


# DenoコンテナをGitHubからAzureに自動デプロイする方法

## デプロイ先のAzure Web Appsを作成する

はじめに、Azureポータルでデプロイ先のコンテナを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227999/71f53f24-fa5c-13f3-5842-fa70c4ec53a6.png)

ここの詳細な手順は省略します。

## 自動デプロイ設定をする
1. 作成したAzure Web Appsで、デプロイセンターを選択し、Get startedをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227999/3743ead9-6f11-eaed-f24a-f359ee8c31e9.png)

1. 最初にデプロイ元を選択します。ここではGitHubを選択し、Nextをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227999/c1a7313c-f520-15bd-f465-328531044032.png)

1. 次にGitHubの中から対象のリポジトリを選択します。Authorizeをクリックし、GitHubにログインします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227999/07544bb3-ff86-c34c-cec2-dc100d010a59.png)

1. 対象のリポジトリとブランチを選択し、Nextをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227999/5f9421db-683d-201a-8266-c6a1932fd8ea.png)

1. 次に、対象のリポジトリからビルド用のDockerfileと公開ポートを選択します。
今回は開発用と本番用で二種類のDockerfileがあるため、本番用を選択するように気をつけます。Portにはコンテナ内のアプリのポートを指定します（仮に5000とすると、`docker run -p 80:5000`になるイメージ）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227999/df50ee99-bbeb-abdb-41f6-ae32905e33c1.png)

1. Azure DevOpsのプロジェクト名などを入力し、最後にDoneをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227999/5d578af9-f8ae-122e-300f-2548e5abdcbc.png)

完了すると、Azure DevOpsにデプロイ用のパイプラインが作成されます。これにより、指定したGitHubリポジトリのmasterブランチからビルドされたDockerイメージがAzure Container RegistryにPushされ、Azure App Serviceにアプリがデプロイされます。



# おわりに
AzureはUIが綺麗で好きです。次はこの構成でちゃんとしたアプリを作成したいですね。

ちなみに、この構成を作るにあたって結構はまりました。
結果的には大した事のない問題だったのですが、一応記事にしてあります。
↓↓↓↓
[Azure App Serviceに環境変数が設定されなくて困った。と思ったら原因が違った話](https://qiita.com/LeftLetter/items/15afcf776efc446bc7af)