🦕

Deno Deployではdotenvを使えない

2021/07/06に公開

Deno公式のCDN Edgeサービス、Deno Deployを勉強中です。

https://deno.com/deploy

Deno Deployへ上げるためのコード内で、以下のdotenvモジュールを使用して.envファイルに定義した環境変数にアクセスしようとしたところ、エラーが発生したので対処法をまとめておきます。
https://deno.land/x/dotenv@v2.0.0

タイトルに書いていますが、Deno Deployを使いたい場合、dotenvモジュールは(通常のままでは)使用できません。

発生したエラー

Deno Deployで動くコード中にてdotenvが使用されているとこんなエラーが出ます。

❯ deployctl run ./server.ts
Check file:///Users/kawarimidoll/ghq/github.com/kawarimidoll/deno-github-contributions-api/$deno$eval.ts
error: TS2339 [ERROR]: Property 'readFileSync' does not exist on type 'typeof Deno'.
    return parse(new TextDecoder("utf-8").decode(Deno.readFileSync(filepath)));
                                                      ~~~~~~~~~~~~
    at https://deno.land/x/dotenv@v2.0.0/mod.ts:76:55

TS2339 [ERROR]: Property 'errors' does not exist on type 'typeof Deno'.
    if (e instanceof Deno.errors.NotFound) return {};
                          ~~~~~~
    at https://deno.land/x/dotenv@v2.0.0/mod.ts:78:27

Found 2 errors.

dotenvが環境変数を読み取るためにファイルを開こうとするのですが、そこで
Deno.readFileSync(filepath)なんて無いよ」と言われています。

Deno deploy用の対処

issueが上がっていました。
https://github.com/denoland/deployctl/issues/35

「Deno Deployではファイルシステム関連の機能はサポートしてない」とのことです。
なるほど。それならエラー出ますよね。

環境変数の設定

ということで対処法の紹介です。
まず、Deno Deployで動く可能性のあるコードからはdotenvを取り除きます。

ローカルでの設定

deployctlのドキュメントにありますが、--envオプションを使うことで.envファイルを読み込むことができます。

https://deno.com/deploy/docs/deployctl

deployctl run --env=.env ./server.ts

Webでの設定

https://deno.com/deploy/docs/projects#environment-variables

実際のDeno Deployに上げる場合は、Settings画面から設定を行います。

サイドバーのEnvironment Variablesから設定しましょう。

なお、こちらで設定した環境変数はshowボタンで後から確認できます。


また、現在のバージョンでは設定後に 再度Deployしないと環境変数が読み込まれない ようです。
Web GUI上ではRedeployを行う機能が見当たらないので、READMEを適当に変更してpushするなどして再起動する必要があります。

環境変数の使用

コード中でDeno.env.get()を呼べば使えます。

.env
SECRET_TOKEN=xxxxx
main.ts
console.log(Deno.env.get("SECRET_TOKEN"))

問題なく.env内のデータを使えるようになりました。めでたしめでたし。
もちろん、実際はトークンをconsole.log()なんてやっちゃ駄目ですよ。

通常のスクリプトとの共存

これでDeno Deploy環境ではエラーが出なくなります。

ところが、同じコードをdeno runでも動かしたい場合、コード内に環境変数を読み込む箇所がなくなるため、失敗(Deno.env.get("SECRET_TOKEN")undefined)となります。
しかし、前述の通り、コード内で外部ファイルを読み込むとDeno Deploy非対応となってしまいます。

したがって、「Denoの実行コード外で環境変数を設定する」必要があります。

実行時に環境変数を渡す

deno run実行時に環境変数を渡すことで、環境変数を使用できます。

❯ SECRET_TOKEN=xxxxx deno run --allow-env main.ts

ただし、毎回これを行うのは面倒です。入力はコピペで楽をするとしても、端末の表示に生のトークンが残ってしまうのはあまり気持ちよくありません。

Velociraptorを使って環境変数を設定する

個人的にオススメの方法はこちら。タスクランナーのVelociraptorの力を借ります。
VelociraptorのenvFileオプションを使うと、指定したファイルから環境変数を読み込み、Deno.envに設定してくれます [1]

https://velociraptor.run/docs/environment-variables/

以下のvelociraptor.ymlを用意することで、vr mainmain.tsを、vr serverserver.tsを実行でき、その両方で.envファイルから環境変数を読み込んで使用できます。

velociraptor.yml
envFile:
  - .env

scripts:
  main:
    desc: Runs main script
    cmd: main.ts
  server:
    desc: Starts local server
    cmd: deployctl run --env=.env server.ts

Velociraptorに関しては以下の記事でも説明しています。

https://zenn.dev/kawarimidoll/articles/1c48c097020cbc

要検討:env.tsで一元管理する

以下は実行時に渡すか、Velociraptorを使うかしてDeno.envに環境変数が設定されている場合の環境変数の管理方法の検討です。

上で紹介したテンプレートの記事にも載せていたのですが、これまではこういうenv.tsで環境変数を一元管理していました。

env.ts
import { config } from "https://deno.land/x/dotenv@v2.0.0/mod.ts";

const {
  SECRET_TOKEN,
} = config({ safe: true });

export { SECRET_TOKEN };

使うときは以下のようにenv.tsから定数をimportします。

main.ts
import { SECRET_TOKEN } from "./env.ts";
console.log(SECRET_TOKEN)

ただ、今後Deno Deployを使うことを考えると、この形式は採用できません。
しかし、各所でDeno.env.get()を呼び出すのもなんかちょっとな…という気がします。環境変数を定義し忘れているときにthrow new Error()してもらいたいというのもあるので。
ということで、env.tsの代替案を考えてみました。

案1

env.ts
const SECRET_TOKEN = Deno.env.get("SECRET_TOKEN") ?? "";

if (!SECRET_TOKEN) {
  throw new Error("No token: SECRET_TOKEN");
}

export { SECRET_TOKEN };
main.ts
import { SECRET_TOKEN } from "./env.ts";
console.log(SECRET_TOKEN)
  • 👍 使う側のコードをこれまでと変える必要がない
  • 👍 必要な環境変数をenv.ts内で確認できる
  • 👎 env.ts内に環境変数名が5回も出現する 扱う環境変数が複数になるとしんどい

案2

env.ts
const env: { [key: string]: string } = {};
const envNames = [
  "SECRET_TOKEN",
];

envNames.forEach((envName) => {
  const envValue = Deno.env.get(envName);

  if (!envValue) {
    throw new Error(`No token: ${envName}`);
  }

  env[envName] = envValue;
});

export default env;
main.ts
import env from "./env.ts";
const { SECRET_TOKEN } = env
console.log(SECRET_TOKEN)
  • 👍 環境変数名をenvNamesに追加すれば読み込み可能
  • 👍 必要な環境変数をenv.ts内で確認できる
  • 👎 exportされている環境変数名をTSコンパイラが認識できないため、使用する際に変数env(名前は任意)を経由する必要がある

案3

env.ts
export default function (envName: string) {
  const envValue = Deno.env.get(envName);

  if (!envValue) {
    throw new Error(`No token: ${envName}`);
  }

  return envValue;
}
main.ts
import env from "./env.ts";
console.log(env("SECRET_TOKEN"))
  • 👍 実質Deno.env.get()のラッパー
    • 存在確認と型限定が追加された感じ
  • 👎 必要な環境変数を一覧できる場所がない
  • 👎 使用感がけっこう変わる

ちょっと使い方が変わるものの、案3が一番良いかな…と考えています。

ボツ案

Deno DeployのWeb上ではDENO_DEPLOYMENT_IDという環境変数が設定されるのでこれで分岐とかできないかなーとか思いましたがローカルでdeployctl使うときには設定されないようだったので諦めました

おわりに

Deno Deployは現状beta1なのでこのあたりの仕様が変わる可能性はあります。
とはいえ、今後盛り上がっていくことは確実なので、Deno deployで使いやすい設定に寄せて行ったほうがよさそうです。

脚注
  1. 実際は環境変数を読み込んでDeno.run()envオプションに渡しています:https://github.com/jurassiscripts/velociraptor/blob/main/src/run_commands.ts ↩︎

Discussion