Denoアプリケーションをデプロイするには?

How to Deploy a Deno Application_
How to Deploy a Deno Application_

Denoで構築されたWebアプリケーションには様々なデプロイオプションが存在する。しかし、サービス・プラットフォームとしてのコンテナ化は、他のデプロイ・オプションに比べて様々な利点があるため、最近では一般的な選択肢となっている。

この記事では、Denoについて、その利点と限界について説明する。さらに、シンプルなDenoアプリを構築し、Back4appコンテナ上にデプロイする。

Denoとは?

Denoは、Node.jsに見られる制限や設計上の欠陥に対処するために作られた、JavaScriptとTypeScriptのためのセキュアでモダンなランタイムである。

Node.jsとは異なり、デフォルトでセキュリティを重視しており、ファイルシステムやネットワークへのアクセスに対してきめ細かなパーミッションが強制される。

さらに、DenoはTypeScriptをネイティブにサポートしており、他の機能の中でも、セットアップやトランスパイルステップを追加する必要がない。

2018年のリリース以来、DenoはNode.jsを上回る改善点から開発者の注目と関心を集めてきた。

しかし、Denoが改善を提供する一方で、Node.jsは依然として、広範なコミュニティ・サポートと膨大なパッケージ・リポジトリを持つ成熟したエコシステムである。

それにもかかわらず、Denoはそのアプローチを高く評価し、その可能性を追求する開発者たちのコミュニティを引きつけている。

Denoの利点

DENOの人気上昇には、いくつかの根本的な理由がある。その一部を紹介しよう。

Node.jsより向上したセキュリティ

Deno は、権限ベースのセキュリティ モデルを実装し、サンドボックス環境でアプリケーションを実行することで、セキュリティの向上を主な利点として提供します。

パーミッションベースのセキュリティモデルを実装しており、ファイルシステムやネットワークなどのリソースへのアクセスには明示的な承認が必要となる。

デフォルトでは、Denoは制限モードで動作し、サンドボックス環境でアプリケーションを実行し、潜在的にリスクのあるアクションを制限し、基礎となるシステムから分離し、機密リソースへの直接アクセスを防止します。

包括的なセキュリティ監査と綿密なコードレビューが、Denoの強固なセキュリティをさらに強化します。これらの対策は、アプリケーションを構築するための信頼できる安全なプラットフォームを提供し、そのセキュリティに信頼を与え、潜在的な脆弱性から保護します。

依存関係の管理

Denoは、Node.jsのような伝統的なJavaScript実行環境と比較して、依存関係管理に対する独自のアプローチを提供します。

集中管理されたパッケージ・レジストリに依存する代わりに、Denoはウェブから直接モジュールをインポートするためにURLを利用する。

このアプローチでは、npmのような別のパッケージマネージャーが不要になり、バージョンの競合や「node_modules」フォルダーの管理の複雑さに関連する懸念が緩和されるため、プロセスが簡素化される。

インポートしたいモジュールの URL を指定することで依存関係を指定でき、コードの共有と配布が容易になります。Denoの依存関係管理に対するこの分散化されたアプローチは、シンプルさを促進し、摩擦を減らし、より合理的な開発体験を保証するのに役立ちます。

TypeScriptをすぐにサポート

DenoはTypeScriptをネイティブかつシームレスにサポートしているため、プロジェクトでTypeScriptを使用したい、または使用する必要がある場合に最適な選択肢となる。

TypeScriptはJavaScriptの型付きスーパーセットであり、JavaScript開発に静的型付けやその他の高度な言語機能をもたらします。Denoを使用すると、TypeScriptを使用するための追加の設定やビルドステップは必要ありません。

DenoにはTypeScriptコンパイラがバンドルされており、TypeScriptのコードを直接書いて実行することができる。

このネイティブサポートにより、TypeScriptのツールチェーンを個別にセットアップする複雑さがなくなり、開発プロセスが簡素化される。

Denoを使用してアプリケーションを構築する際に、TypeScriptの型チェック、改良されたツール、および強化された開発者エクスペリエンスを活用することができます。

Denoの限界

しかし、Denoにはその普及に影響を及ぼしているいくつかの限界がある。そのいくつかは以下の通りである。

未成熟な生態系

Denoの限界の1つは、そのエコシステムの成熟度である。より長く存在しているNode.jsと比べると、Denoのエコシステムはまだ比較的新しく、進化している。

これは、Deno 専用に設計されたサードパーティのライブラリ、フレームワーク、およびツールが少ない可能性があることを意味します。特定の機能をゼロから構築するか、既存のNode.jsパッケージをDenoで使用するために適応させる必要があるかもしれません。

コミュニティの規模が小さいということは、確立されたNode.jsエコシステムに比べて、利用できるリソース、チュートリアル、コミュニティ・サポートが少ない可能性があるということでもあります。

しかし、Denoの人気と普及が進むにつれて、そのエコシステムは成長・成熟し、将来的にはより広範なライブラリとツールを提供することが期待される。

急な学習曲線

Denoのもう一つの限界は、Node.jsのような他のJavaScriptランタイム環境からの移行に伴う学習曲線である。

Denoは、新しいコンセプト、API、およびパターンを導入しており、これらに慣れる必要があるかもしれません。これには、Denoのモジュールシステム、パーミッションベースのセキュリティモデル、および他のランタイムと比較した場合の特定の機能の実装方法の違いが含まれます。

すでにNode.jsに習熟している開発者は、Deno特有の機能や慣習を学び、それに適応するために時間と労力を費やす必要があるかもしれない。

しかし、学習曲線は、Denoの公式ドキュメントを参照し、Denoコミュニティに参加し、利用可能な学習リソースを探索することによって管理することができます。

Node.jsライブラリとの互換性

Node.jsライブラリとの互換性もDenoの制限事項の1つです。モジュールシステムや実行環境の違いにより、すべてのNode.jsライブラリやモジュールがそのままDenoで使用できるわけではありません。

DenoはモジュールシステムとしてESモジュール(ECMAScriptモジュール)を使用していますが、Node.jsは伝統的にCommonJSモジュールを使用しています。このモジュール形式の違いは、DenoでNode.js固有のモジュールをインポートして使用する際に、非互換性を引き起こす可能性があります。

開発者は、Denoのモジュールシステムで動作するように特別に設計された代替ライブラリを調整したり、探したりする必要があるかもしれません。

Denoは、いくつかのNode.jsモジュールを実行するための互換性レイヤーを提供しますが、すべてのケースをカバーできるわけではなく、手作業による修正や適応が必要になる場合があります。

Denoの展開オプション

Denoアプリにはいくつかのデプロイメント・オプションがあり、その中には以下のようなものがある。

インフラストラクチャー・アズ・ア・サービス(IaaS)

IaaS(Infrastructure-as-a-Service)は、仮想化されたコンピューティングリソースを提供するクラウドコンピューティングモデルです。IaaSでは、クラウドプロバイダーから仮想マシン、ストレージ、ネットワークを従量課金で借りることができる。これにより、物理的なハードウェアに投資することなく、独自の仮想化インフラを構築・管理できる。

IaaSオプションは、仮想マシン上でDenoアプリケーションを実行することを可能にします。AWS、Google Cloud、Microsoft Azureのような一般的なIaaSプラットフォームは、柔軟でスケーラブルなソリューションを提供し、アプリケーション固有のニーズに応じてインフラストラクチャを構成することができます。

しかし、IaaSはより大きなコントロールとリソースの分離を可能にする一方で、サーバーのプロビジョニング、セキュリティの更新、監視などのタスクを含む、より多くの手作業によるセットアップと管理が要求される。

したがって、IaaSは、インフラを広範囲にコントロールする必要があり、その複雑さを効果的に処理する専門知識がある場合に有効な選択肢となる。

コンテナ・アズ・ア・サービス(CaaS)

コンテナ・アズ・ア・サービス(CaaS)は、コンテナ化されたアプリケーションのデプロイと管理を簡素化するクラウド・コンピューティング・モデルである。

CaaSを利用すれば、基盤となるインフラを気にすることなく、アプリケーションの構築とデプロイに集中できる。

Denoアプリケーションはコンテナ内にデプロイすることができ、一貫性と分離を保証します。Back4appコンテナは、Denoをデプロイするための一般的なCaaSオプションです。

CaaSプラットフォームはスケーラビリティとリソースの分離を提供し、各アプリケーションは独自のコンテナで実行されるため、セキュリティと安定性が向上する。

コンテナの一貫性により、Denoアプリケーションはコンテナをサポートするどのプラットフォームにも容易にデプロイできる。

CaaSソリューションには学習曲線があるが、複数のノードやクラスタにまたがる動的なスケーリングやデプロイメントを必要とするアプリケーションには大きなメリットがある。

Denoのインストールプロセス

Denoを使用する前に、ダウンロードしてインストールする必要があります。Denoのインストール方法はOSによって異なります。

macOSとLinuxでは、以下のコマンドを実行することでDenoをインストールできる:

curl -fsSL <https://deno.land/x/install/install.sh> | sh

Windowsでは、以下のコマンドを実行することで、Powershellを使用してDenoをインストールすることができます:

irm <https://deno.land/install.ps1> | iex

インストールが成功したことを確認するには、以下のコマンドを実行すれば、バージョン番号がターミナルに表示されるはずだ。

deno --version

バージョン番号が表示されない場合は、Denoを再度インストールしてみてください。

Denoプロジェクトの設定

DenoでシンプルなAPIを作成するには、ルーター、サーバー、データベースが必要です。

以下の手順を行う前に、プロジェクトのルート・ディレクトリにsrcフォルダを作成してください。このフォルダには、プロジェクトのすべてのソース・ファイルが格納されます。

ステップ1:依存ファイルの作成

Node.jsとは異なり、DenoはNPMやYarnのようなパッケージ・マネージャーを使用しない。むしろ、パッケージはURLから直接インポートされる。

package.jsonファイルの機能を模倣するには、プロジェクトのルート・ディレクトリにdeps.tsを作成し、そこに以下のコード・ブロックを追加する。

export { Application, Router } from "https://deno.land/x/[email protected]/mod.ts";
export type { RouterContext} from "https://deno.land/x/[email protected]/mod.ts";
export { config as dotenvConfig } from "https://deno.land/x/[email protected]/mod.ts";
export { Client } from "https://deno.land/x/[email protected]/mod.ts";

上のコードブロックは、OakからApplicationRouterRouterContexをインポート(インストール)し、エクスポートしています。

ステップ2:サーバーの作成

このステップでは、Oakを使ってシンプルなHTTPサーバーを作成します。Oakは、Expressに似ているがより軽量なNode.js用フレームワークであるKoa.jsをベースにしたDenoのHTTPサーバー用ミドルウェアである。

OakでHTTPサーバーを作成するには、srcに server.tsファイルを作成し、以下のコードブロックを追加する。

import { Application } from "../deps.ts";
import config from "../config/default.ts";

import router from "./router.ts";

const app = new Application();

app.use(router.routes());
app.use(router.allowedMethods());

await app.listen({ port: config.port });

上のコードブロックは、OakでHTTPサーバーを作成し、すべての受信トラフィックを処理するためにルーターを登録する。

app.use(router.routes()) 行は、ルーターのルートを Oak アプリケーションのミドルウェアとして登録します。すべての受信リクエストは登録されたルートと照合され、一致が見つかった場合に対応するハンドラーが実行されます。

マッチするメソッドが見つからない場合、app.use(router.allowedMethods())行は、404 not foundや405 not allowedといった適切なレスポンスを送信することで、それらを処理する。

ステップ3:環境変数の管理

APIキーやデータベースの認証情報などの機密データをプレーンテキストで保存することは、セキュリティリスクをもたらします。キーや認証情報を手に入れた者は、アプリケーションに無制限にアクセスできる可能性があります。これは、他の可能性のある悪用の中でも、データ損失やデータ盗難につながる可能性があります。

このような事態を避けるため、機密データを環境変数に保存するのは良い習慣とされている。

プロジェクトのルート・フォルダーに.envファイルを作成し、データベース認証情報やその他の機密情報をそのファイルに保存します。

こんな感じだ:

#.env
DB_URI = <YOUR_POSTGRES_DB_URI>
PORT = 8000

をあなたのデータベース認証情報に置き換えてください。

次に、プロジェクトのルート・フォルダーにconfigフォルダーを作成し、configフォルダー内にdefault.tsファイルを作成して、以下のコード・ブロックを追加します。

//default.ts
import { dotenvConfig } from "../deps.ts";

dotenvConfig({
  export: true,
  path: "../.env",
});

const config = {
  db: {
    dbUri: Deno.env.get("DB_URI"),
  },
  port: 3000
};

export default config;

上記のコードブロックは、.envファイルに保存されている値を安全に取得し、アプリケーションの残りの部分に公開します。

ステップ3:データベースへの接続

このステップでは、アプリケーションをPostgresデータベースに接続します。アプリケーションのデータを保存したり取得したりするためにデータベースが必要になります。

srcフォルダにdb.tsファイルを作成し、以下のコードブロックを追加する。

//db.ts
import { Client } from "../deps.ts";
import config from "../config/default.ts";

let postgresConfiguration = config.db.dbUri;

const client = new Client(postgresConfiguration);

await client.connect();

export default client;

上記のコードブロックは、.envファイルで指定したURIを使用して、アプリケーションをPostgresデータベースに接続しようとします。

ステップ4:データベース・リポジトリの作成

srcフォルダにblogRepositoryファイルを作成し、以下のコードを追加してください。

//blogRepository.ts
import client from "./db.ts";

class BlogRepository {
  async createBlogTable() {
    const blog = await client.queryArray(
      `CREATE TABLE IF NOT EXISTS blogs (id SERIAL PRIMARY KEY, title VARCHAR(255), body VARCHAR(255), author VARCHAR(255))`
    );

    return blog;
  }

  async getAllBlogs() {
    const allBlogs = await client.queryArray("SELECT * FROM blogs");

    return allBlogs;
  }

  async getBlogById(id: string) {
    const blog = await client.queryArray(
      `SELECT * FROM blogs WHERE id = ${id}`
    );

    return blog;
  }

  async createBlog(title: string, body: string, author: string) {
    const blog = await client.queryArray(
      `INSERT INTO blogs (title, body, author) VALUES ('${title}', '${body}', '${author}')`
    );

    return blog;
  }

  async updateBlog(id: string, title: string, body: string, author: string) {
    const blog = await client.queryArray(
      `UPDATE blogs SET title = '${title}', body = '${body}', author = '${author}' WHERE id = ${id}`
    );

    return blog;
  }

  async deleteBlog(id: string) {
    const blog = await client.queryArray(`DELETE FROM blogs WHERE id = ${id}`);

    return blog;
  }
}

export default new BlogRepository();

上記のコードブロックは、生のSQLクエリを抽象化し、Postgresデータベースとやりとりするために使用できるシンプルなメソッドを公開することで、すべてのデータベース操作を処理します。

ステップ 5: ルートハンドラの作成

このステップでは、アプリケーションの単純なCRUD関数を処理するルートハンドラを作成します。サポートされるルートは次のとおりです:

  • GET /api/blogs:データベース内のすべてのブログを返す
  • GET /api/blog/:id:URLパラメータで指定されたidにマッチするブログを1つ返します。
  • POST /api/blog/new: データベースに新しいブログを作成します。
  • PUT /api/blog/:id:URLパラメータで指定されたidにマッチするブログを更新する。
  • DELETE /api/blog/:id:URLパラメータで指定されたidにマッチするブログを削除する。

srcフォルダにrouter.tsファイルを作成し、以下のインポートを追加する。

import { Router, RouterContext } from "../deps.ts";
import blogRepository from "./blogRepository.ts";

次に、以下のコード・ブロックを追加してルーター・インスタンスを作成する:

const router = new Router();

ルーターハンドラーを登録するには、ルーターインスタンスにチェーンする必要がある。

例えば(GET /api/blogs):

router
  .get("/api/blogs", async (ctx: RouterContext<"/api/blogs">) => {
    const data = await blogRepository.getAllBlogs();
    console.log(data);

    //format data
    const allBlogs = data.rows.map((blog) => {
      return {
        id: blog[0],
        title: blog[1],
        body: blog[2],
        author: blog[3]
      };
    });

    ctx.response.body = allBlogs;
  })

上のコードブロックは、ハンドラロジックをルーターインスタンスにチェーンして、GET /api/blogsのルートハンドラを作成します。

残りのルートを登録するために、先にチェーンしたメソッドにチェーンする。このように:

GET /api/blog/:id:

.get("/api/blog/:id", async (ctx: RouterContext<"/api/blog/:id">) => {
    try {
      const data = await blogRepository.getBlogById(ctx.params.id);
      console.log(data);

      //format data
      const blog = data.rows.map((blog) => {
        return {
          id: blog[0],
          title: blog[1],
          body: blog[2],
          author: blog[3]
        };
      });

      ctx.response.body = blog;
    } catch (error) {
      ctx.response.status = 500;
      ctx.response.body = {
        msg: "Error getting blog",
        error,
      };
    }
  })

POST /api/blog/new

.post("/api/blog/new", async (ctx: RouterContext<"/api/blog/new">) => {
    const resBody = ctx.request.body();
    const blog = await resBody.value;

    if (!blog) {
      ctx.response.status = 400;
      ctx.response.body = { msg: "Invalid data. Please provide a valid blog." };
      return;
    }

    const { title, body, author } = blog;

    if (!(title && body && author)) {
      ctx.response.status = 400;
      ctx.response.body = {
        msg: "Title or description missing. Please provide a valid blog.",
      };
      return;
    }

    try {
      await blogRepository.createBlog(title, body, author);

      ctx.response.status = 201;
      ctx.response.body = {
        msg: "blog added successfully",
      };
    } catch (error) {
      ctx.response.status = 500;
      ctx.response.body = {
        msg: "Error adding blog",
        error,
      };
    }
  })

PUT /api/blog/:id:

.put("/api/blog/:id", async (ctx: RouterContext<"/api/blog/:id">) => {
    try {
      const resBody = ctx.request.body();
      const blog = await resBody.value;

      if (!blog) {
        ctx.response.status = 400;
        ctx.response.body = {
          msg: "Invalid data. Please provide a valid blog.",
        };
        return;
      }

      const { title, body, author } = blog;

      if (!(title && body && author)) {
        ctx.response.status = 400;
        ctx.response.body = {
          msg: "Title or description missing. Please provide a valid blog.",
        };

        return;
      }

      await blogRepository.updateBlog(ctx.params.id, title, body, author);

      ctx.response.status = 200;
      ctx.response.body = {
        msg: "blog updated successfully",
      };
    } catch (error) {
      console.log(error);
      ctx.response.status = 500;
      ctx.response.body = {
        msg: "Error updating blog",
        error: error.message,
      };
    }
  })

DELETE /api/blog/:id:

.delete("/api/blog/:id", async (ctx: RouterContext<"/api/blog/:id">) => {
    await blogRepository.deleteBlog(ctx.params.id);

    ctx.response.status = 200;
    ctx.response.body = {
      msg: "blog deleted successfully",
    };
  });

次に、ルーターのインスタンスをエクスポートします。このように:

export default router;

最後に、アプリケーションの初回起動時にブログ・データベースを作成するようにserver.tsファイルを修正する。

こんな感じだ:

import { Application } from "../deps.ts";
import config from "../config/default.ts";
import blogRepository from "./blogRepository.ts";

import router from "./router.ts";

const app = new Application();

(async () => {
  await blogRepository.createBlogTable();
})();

app.use(router.routes());
app.use(router.allowedMethods());

await app.listen({ port: config.port });

修正したコードでは、データベースに新しいブログテーブルを作成するIIFEをserver.tsファイルに追加しています。

DenoアプリをBack4appコンテナにデプロイする

DenoアプリをBack4appコンテナにデプロイするには、以下の手順が必要です:

ステップ1:Dockerfileの作成

Dockerfileには、Dockerイメージを構築するための具体的な手順が記述されています。これらの指示は、イメージの構築プロセスをガイドします。

以下のコマンドを実行してDockerfileを作成する:

touch Dockerfile

上のコマンドは、プロジェクトのルート・ディレクトリにDockerfileを作成する。

次に、以下のコードブロックをDockerfileに追加する:

FROM denoland/deno:latest

EXPOSE 8000

WORKDIR /app

COPY deps.ts .
RUN deno cache deps.ts

COPY . .

RUN deno cache src/server.ts

CMD ["run", "--allow-net", "--allow-env", "--allow-read", "src/server.ts"]

上記の Dockerfile は、Deno アプリケーションを実行するためのコンテナ環境をセットアップする。アプリケーションの依存関係とエントリーポイントをキャッシュし、コンテナの起動時に指定されたパーミッションでDenoアプリケーションを実行する。

アプリケーションは8000番ポートをリッスンすることになっているが、実際のポートマッピングはコンテナの実行時に行う必要がある。

最後に、コードをGitHubにプッシュする

ステップ2: 新しいBack4appアプリケーションを作成する

Back4appアプリケーションを作成するには、Back4app公式ウェブサイトをご覧ください。Back4appのランディングページの右上にあるサインアップボタンをクリックします。サインアップボタンをクリックすると、登録フォームが表示されます。Eメールアドレス、ユーザー名、パスワードなどの必要事項を入力してください。正確な情報を入力してください。フォームに記入後、送信してください。

すでにアカウントをお持ちの場合は、代わりにログインをクリックしてください。

Back4appアカウントの設定が完了したら、ログインしてアカウントダッシュボードにアクセスしてください。そこから “NEW APP “ボタンを探してクリックします。

このアクションを実行すると、新しいアプリを作成するためのさまざまなオプションが表示されるページに移動します。コンテナ化を使用してデプロイすることを意図しているので、“Containers as a Service“オプションを選択します。

次に、GitHubアカウントをBack4appアカウントに接続します。Back4appはアカウント内の全てのリポジトリ、または特定のリポジトリにアクセスすることができます。

デプロイしたいアプリケーション(この場合は、このチュートリアルでビルドしたアプリケーション)を選択し、[Select]をクリックします。

デプロイするリポジトリを選択する

選択ボタンをクリックすると、アプリの名前、ブランチ、ルートディレクトリ、自動デプロイオプション、ポート、ヘルス、環境変数などの情報を入力するページに移動します。

アプリケーションが正しく動作するために必要な環境変数をすべて入力してください。必要な情報の入力が完了したら、「Create App」ボタンをクリックします。

アプリの詳細を入力

これでデプロイプロセスが開始され、しばらくするとデプロイの準備が整います。デプロイプロセスに時間がかかっている場合は、デプロイ時にエラーが発生していないかログを確認するか、Back4appのトラブルシューティングガイドを参照してください。

結論

この記事では、Denoについて、その利点、制限、一般的なデプロイオプション、Denoでアプリをビルドする方法、そしてBack4appコンテナを使用してアプリをデプロイする方法について説明した。

その制限にもかかわらず、そのセキュリティモデルにより、Denoは非常にセキュアで機密性の高いアプリケーションを構築するのに理想的である。さらに、ネイティブのTypeScriptをサポートしているため、プロジェクトでTypeScriptをセットアップする手間が省ける。

本記事で説明するステップに従うことで、Back4appコンテナ上にDenoアプリケーションを簡単に作成し、デプロイすることができる。

よくあるご質問

Denoとは何ですか?

Deno は、開発者がサーバー側およびクライアント側のアプリケーションを構築できるようにする、安全で最新の JavaScript/TypeScript ランタイムです。

Deno アプリケーションをデプロイするには?

1. Deno アプリを作成する
2. Dockerfile を作成する
3. Deno アプリを GitHub にプッシュし、GitHub アカウントを Back4app アカウントに接続します。
4. Back4app で CaaS アプリを作成する
5. リポジトリのリストから Deno アプリを選択する
6. Deno アプリをデプロイする


Leave a reply

Your email address will not be published.