アレがアレでアレ

(できれば)プログラミング関係のことを書きたい

ASP.NET CoreをDocker使ってHerokuにデプロイ & PostgreSQLを使う

devcenter.heroku.com

ASP.NET CoreはHerokuにデプロイできないものだと思っていたのですが、
HerokuのDockerコンテナを使ったデプロイ方法であれば、ASP.NET Coreも動くようなので試してみました。

Dockerを使用したHerokuへのデプロイ方法は2つあるようです。

  1. 自分でビルドしたDockerイメージをHerokuにデプロイする方法
  2. heroku.ymlという設定ファイルを使ってHeroku上でDockerイメージをビルドし、デプロイする方法

1の方法は他の記事でも見ましたが、2の方法を書いているところがなさそうだったのでheroku.ymlを使った方法でデプロイしたいと思います。

環境

  • .NET Core 3.1
  • macOS Catalina 10.15.1

サンプルプロジェクト

Razor Pagesの公式チュートリアルを元に、Herokuデプロイ用のサンプルプロジェクトを用意しました。

github.com

このプロジェクトは、生徒の情報をCRUDできるRazor Pagesアプリです。
データベースはSQLiteが使われています。

このプロジェクトを使って進めていきますので、GitHubからリポジトリをクローンしておいてください。

Nugetパッケージの復元

クローンしたらまずはNugetパッケージを復元します。
ターミナル上でHerokuDeploySample.csprojがあるディレクトリまで移動し、以下のコマンドを実行。

dotnet restore

HerokuのPostgreSQLへ接続するための設定

Heroku上の本番環境ではPostgreSQLを使い、開発環境ではSQLiteを使うようにコードを修正していきます。

EF CoreのPostgreSQLライブラリをNugetから追加します。

dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL

追加できたら、環境によってSQLitePostgreSQLか接続を分けるようにStartup.csを修正します。

public Startup(IConfiguration configuration, IWebHostEnvironment env)
{
    Configuration = configuration;
    Env = env;
}

public IConfiguration Configuration { get; }
public IWebHostEnvironment Env { get; }

StartupコンストラクタにIWebHostEnvironmentインターフェースを追加して、
Envプロパティから環境情報にアクセスできるようにしました。

次に、Envプロパティを使って接続先を分けます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();

    if (Env.IsDevelopment())
    {
        services.AddDbContext<SchoolContext>(options =>
        options.UseSQLite(Configuration.GetConnectionString("SchoolContext")));
    }
    else
    {
        var uri = new Uri(Configuration["DATABASE_URL"]);
        var userInfo = uri.UserInfo.Split(":");
        (var user, var password) = (userInfo[0], userInfo[1]);
        var db = Path.GetFileName(uri.AbsolutePath);

        var connStr = $"Host={uri.Host};Port={uri.Port};Database={db};Username={user};Password={password};Enlist=true";
        services.AddDbContext<SchoolContext>(options => options.UseNpgsql(connStr));
    }
}

ConfigureServicesメソッド内でEnv.IsDevelopmentを使用して接続先を分岐させます。
ifのtureの部分が開発時(SQLite)で、falseの部分がHeroku(PostgreSQL)になっています。

このあと設定しますが、HerokuのPostgresアドオンを追加すると、DATABASE_URLというPostgreSQLの接続先URLが入った環境変数がHerokuに追加されます。

上のConfiguration["DATABASE_URL"]はその環境変数にアクセスし、接続先URLを取得するコードです。
これをそのままUseNpgsqlに渡しても、接続文字列のフォーマットが異なるため接続できません。
そのため接続先URLから情報を抜き出し、ライブラリに渡す用の文字列を作っています。
ロジックが結構書かれちゃってますが、サンプルなのでこのままで。

修正後のStartup.cs全体のコード

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.EntityFrameworkCore;
using HerokuDeploySample.Data;
using System.IO;

namespace HerokuDeploySample
{
    public class Startup
    {
        public Startup(IConfiguration configuration, IWebHostEnvironment env)
        {
            Configuration = configuration;
            Env = env;
        }

        public IConfiguration Configuration { get; }
        public IWebHostEnvironment Env { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();

            if (Env.IsDevelopment())
            {
                services.AddDbContext<SchoolContext>(options =>
                options.UseSqlite(Configuration.GetConnectionString("SchoolContext")));
            }
            else
            {
                var uri = new Uri(Configuration["DATABASE_URL"]);
                var userInfo = uri.UserInfo.Split(":");
                (var user, var password) = (userInfo[0], userInfo[1]);
                var db = Path.GetFileName(uri.AbsolutePath);

                var connStr = $"Host={uri.Host};Port={uri.Port};Database={db};Username={user};Password={password};Enlist=true";
                services.AddDbContext<SchoolContext>(options => options.UseNpgsql(connStr));
            }
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
        }
    }
}

Dockerfileを作成

次に、Heroku上で動かすコンテナの元となるDockerfileを作ります。

Startup.csと同じディレクトリに以下の内容のDockerfileを作成してください。

※2020/11/26 追記

DockerfileにEXPOSEを書いていましたがherokuではサポートしておらず、使用するポートは$PORTに渡されるので不要でした。

また、dotnet publishrestorebuildも暗黙的に実行されるため不要なコマンド行を削除しました。

FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app

FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS publish
WORKDIR "/src/HerokuDeploySample"
COPY . .

RUN dotnet publish "HerokuDeploySample.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
CMD ASPNETCORE_URLS=http://*:$PORT dotnet HerokuDeploySample.dll

このDockerfileはVisual StudioでDockerfile追加したときにデフォルトで書かれている内容を、Heroku用に書き換えたものです。

参考: Deploy ASP.NET Core 3.1 Web API to Heroku with Docker – Jakub Wajs

heroku.yml

heroku.ymlとは

devcenter.heroku.com

Dockerを使ったHerokuアプリを構築するための設定ファイルです。
Heroku上でイメージをビルドでき、コマンドや環境変数なども設定できるみたいです。

heroku.ymlを作成

GitHubからクローンしたサンプルプロジェクトのルートに以下の内容のheroku.ymlを作ります。

ディレクトリ構造

root/
 ├ HerokuDeploySample/
 │ ├  Data/
 │ ├  Models/
 │ ├  ...../
 │ └ ....../
 └ heroku.yml

heroku.yml

build:
  docker:
    web: HerokuDeploySample/Dockerfile

中身は簡単で、Dockerfileの場所を指定してあげるだけでOKです。

Herokuにはリポジトリをプッシュしてデプロイするので、
ここまでの修正をコミットしておいてください。

Herokuへデプロイ

Herokuアプリを構築してデプロイします。

Herokuの操作はすべてHeroku CLIを使って行います。
ターミナルなどではカレントディレクトリをプロジェクトのルート(heroku.ymlを作った場所)にしておきます。

1. Herokuへログイン

heroku login

2. Herokuアプリの作成
デプロイ先が作られ、gitのリモートにherokuが追加されます。

heroku create

3. Postgresアドオンを追加
HerokuアプリでPostgreSQLを使うためアドオンを追加します。
これで環境変数DATABASE_URLが追加され、HerokuでPostgreSQLを使う準備ができました。
環境変数の中身はコマンドで見れます。

heroku addons:create heroku-postgresql:hobby-dev

# DATABASE_URLの中身を表示するコマンド
# heroku config:get DATABASE_URL

4. アプリをDockerコンテナで動かすように設定

heroku stack:set container

5. Herokuへリポジトリをプッシュ

git push heroku master

プッシュするとHeroku上で、追加したDockerfileを元にイメージのビルドが始まります。

しばらく待ち、「remote: Verifying deploy... done.」と表示されたら完了です。
heroku openコマンドでアプリを開いて、動いているか確認しましょう。

成功していれば、/Studentsに生徒情報が一覧表示されています。

f:id:eiken7kyuu:20200805205041p:plain

PCにPostgreSQLがインストールされていれば、Heroku上のデーテベースもコマンドから中身を確認できます。

heroku pg:psql

=> SELECT * FROM "Student";
 ID | LastName  | FirstMidName
----+-----------+--------------
  1 | Alexander | Carson
  2 | Alonso    | Meredith
  3 | Anand     | Arturo
  4 | Barzdukas | Gytis
  5 | Li        | Yan
  6 | Justice   | Peggy
  7 | Norman    | Laura
  8 | Olivetto  | Nino
(8 rows)

修正後のプロジェクト

同じリポジトリのafterブランチに修正後のプロジェクトがあるのでこちらも参照してください。

GitHub - eiken7kyuu/AspNetCoreHeroku at after

その他参考にした記事

ASP.NET Core 3.1のREST-APIをVisualStudio2019とDockerでHerokuにデプロイする手順 - Qiita