ツナ缶雑記

ぐうたらSEのブログです。主にマイクロソフト系技術を中心に扱います。

Azure DevOpsでリリース時にappsettings.jsonの中身を書き換える

はじめに

アプリケーションの動作に必要な設定は、appsettings.jsonに定義することが一般的です。 appsettings.jsonには様々な情報を定義することになりますが、データベースの接続文字列など、セキュアな情報を定義したくなるケースも多々あります。 このファイルは、Gitなどの構成管理システム上にのせて管理することになるため、機微な情報をappsettings.jsonに設定しておくのはセキュリティ上好ましくありません。

このようなケースでは、環境変数を用いてappsettings.jsonの設定値を上書きするテクニックがよく活用されます。 Web AppsやAzure Functionsなどは、そういったユースケースに対応するための機能がAzure Portalに組み込まれていたりします。 しかし、アプリケーションをVMやオンプレミスの物理マシンにデプロイするケースでは、環境変数を使う方法は負荷の高い方式になってしまいます。 設定値が変わったり、追加されたりするたびにデプロイ先の全マシンの環境変数を設定しなければなりません。 下手をするとマシンの再起動が必要になることもあります。 サーバーの台数が少なければまだよいですが、数が増えれば増えるほど、その管理は難しくなっていきます。

いろいろ工夫をすれば回避できることもあるのですが、今回はAzure DevOpsとKey Vaultを使ってスマートにこれらの問題に対処してみようと思います。

環境

  • Visual Studio 2019
  • ASP.NET Core 3.0(サンプルとしてMVCのアプリケーションを使います)
  • Azure DevOps (2019/11/28時点)
  • Azure Key Vault

全体的な方式

まず今回作るモノの全体像を示しておきます。

f:id:masatsuna:20191128171150p:plain
全体アーキテクチャ

まず開発作業はローカルPC上で実行し、ローカルPC内で完結できるように構成します。 DBはSQL Server Developerエディションを使用し、既定のインスタンスとして構築したものを使います。

開発したソースコードは、Azure DevOpsのGitリポジトリ(Azure Repos)上で管理します。 アプリケーションのビルドはAzure DevOpsに付属するMicrosoft-hosted agentで実行し、コンパイルしたアプリケーションをBuild ArtifactsとしてAzure DevOpsにアップロードします。

アプリケーションのリリースには、Azure DevOpsのリリース機能を使用します。 今回のデプロイ先はAzure内に置いたVMです。 このVMをDeployment Groupとして登録し、GUIでできる方のRelease(Release pipeline (classic))を使ってリリースします。

YAMLでやる方は、EnvironmentとしてVMが追加できるようにならないと、いろいろゴニョらない限り実行できません。 現時点ではk8sのリソースしかEnvironmentに追加できない*1ので、今回はclassicな方を使っていきます。

2019/11/28時点で以下のサイトにそんなことが書いてあります。 docs.microsoft.com

その打ち消されそうなので、引用して貼っておきます。

While environment at its core is a grouping of resources, the resources themselves represent actual deployment targets. Currently, only Kubernetes resource type is supported, with the roadmap of environments including support for other resources such as virtual machines, databases and more.

実行環境は、前述の通りAzure上のVMをWebアプリケーションサーバーとして使います。 データベースはSQL Databaseを使います。 データベースへの接続文字列は、今回Key Vaultに配置します。 このKey Vaultは、Azure DevOpsから読み取りできるように構成します。 そして、アプリケーションをリリースする際、appsettings.jsonにあるローカルデータベース向けの接続文字列を、SQL Database向けの接続文字列で上書きするようにしていきます。

Azure Key Vaultにセキュアな設定値を定義する

まずは実行環境を構築していきます。 VMSQL Databaseは普通に作成してください。 データベースはテーブルが1つあればよいので、適当に作っておきましょう。 またローカルのSQL Serverにも、同様のデータベース/テーブルを構築しておいてください。 今回特に解説しませんが、VMの方はNSGの設定を、SQL Databaseの方はファイアウォールの設定を適切に行うようにしてください。

今回のポイントとなるKey Vaultも作っていきます。 今回はSQL Databaseへの接続文字列をKey Vault管理させたいので、Key Vaultのリソースを作ったら、 [シークレット] を追加します。 Azure Portalからも以下の通り操作可能です。

f:id:masatsuna:20191128153022p:plain
シークレット

最初は何もない状態なので、そのまま [+生成/インポート] ボタンを押下します。

f:id:masatsuna:20191128153228p:plain
シークレットの生成/インポート

[シークレットの作成] 画面では、以下の通り SQL Database への接続文字列を設定します。 [名前] はなんでもいいですが、今回は適当に [ConnectionString] としておきます。 [値] には SQL Database の [概要] メニュー画面からコピーした接続文字列を貼り付けます。

f:id:masatsuna:20191128153516p:plain
シークレットの作成(接続文字列の保存)

設定が終わったら、最下部の [作成] ボタンを押下してシークレットを作成しておきましょう。

Deployment Groupに本番環境のマシンを追加する

続いて、実行環境として作成したVMを、Azure DevOpsに登録します。 今回はリリース先のサーバーとして登録したいので、Deployment Groupに登録します。 Powershellスクリプトを流すだけなので非常に簡単です。 以下に手順があるので参考にしてください。

docs.microsoft.com

またサーバーがプロキシ内にある場合は以下も参考にしてください。

tsuna-can.hateblo.jp

アプリケーションを作成する

今回は.NET Core 3.0のMVCアプリケーションを作り、データアクセスのコードを追加してみます。 データアクセスはEntity Framework Core 3.0を使うことにします。 先ほど作ったテーブルに対してアクセスするDbContextの実装クラス(TestDbContext)を追加しておきましょう。 そして、Startup.csでTestDbContextをDIできるように実装しておきます。

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();

        // 以下を追加
        services.AddDbContext<TestDbContext>(options =>
            options.UseSqlServer(this.Configuration.GetConnectionString(nameof(TestDbContext))));
    }

    // 後略
}

ここでは接続文字列をIConfiguration.GetConnectionStringメソッドを用いて取得しています。 このメソッドを呼び出すと、appsettings.jsonから*2接続文字列の情報を取得します。 ローカルマシンの環境では、ローカルに構築したSQL Serverにアクセスしたいので、そのデータベースに対する接続文字列をappsettings.jsonに設定しておきましょう。 最終的にリリースパイプラインの方で、この接続文字列の値をSQL Database用のものに差し替えることになります。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "TestDbContext": "Data Source=localhost;Initial Catalog=test-db;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"
  }
}

新たにコントローラークラス(ProductController)を追加して、TestDbContextを使ったデータアクセスコードを追加します。 今回はProductsテーブルの総レコード数をViewBagに追加するという雑な処理を実装してあります。 ビューの方は割愛しますが、ViewBag.ProductsCountの値を表示するようにしておきます。

public class ProductController : Controller
{
    private readonly ILogger<ProductController> logger;
    private readonly TestDbContext dbContext;

    public ProductController(ILogger<ProductController> logger, TestDbContext dbContext)
    {
        this.logger = logger;
        this.dbContext = dbContext;
    }

    public IActionResult Index()
    {
        ViewBag.ProductsCount = dbContext.Products.Count();
        return View();
    }
}

一旦ローカルでビルド、実行して、ローカル環境で動作することを確認しておきましょう。 また作成したアプリケーションをAzure ReposのGitリポジトリにプッシュしておきましょう。

VMの構築

今回はWindows Server 2019のVMを用意しておきました。 最終的にIISのインプロセスモードで実行できるようにしていきます。

Azureの場合、VM作成後は英語OSとなっているので、日本語化の作業を実施しています。 またIISの有効化、.NET Coreホスティングバンドルのインストールを実施して、.NET Core 3.0のアプリを実行できるようにします。 詳細な手順は以下にありますので、そちらを参照してください。

docs.microsoft.com

そして、アプリケーションを実行するWebアプリケーションを作成しておきます。 今回は [Default Web Site] の下にアプリケーションを1つ追加して、そこに配置するようにします。

f:id:masatsuna:20191128173449p:plain
Web アプリケーションの構成

アプリケーションプールの設定については、以下の節の項番4以降の手順を参照してください。

https://docs.microsoft.com/ja-jp/aspnet/core/host-and-deploy/iis/?view=aspnetcore-3.0#create-the-iis-site

ビルドパイプラインを構築する

続いて作成したアプリケーションをビルドするため、ビルドパイプラインを作成します。 今回はYAMLを用いてビルドパイプラインを構築します。 特に自動テストは組み込んでいないので、単純にdotnet publishでアプリケーションをビルドして、Build Artifactsとして公開するだけです。

trigger:
- master

pool:
  vmImage: 'windows-2019'

variables:
  buildConfiguration: 'Release'

jobs:
- job:
  displayName: 'Application Build'
  steps:
  - task: DotNetCoreCLI@2
    displayName: '発行ビルドの実行'
    inputs:
      command: 'publish'
      publishWebProjects: true
      modifyOutputPath: true
      arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory) --self-contained --runtime win-x64'
      projects: |
        $(Build.Repository.LocalPath)/<*.csprojへのパス>

  - task: PublishBuildArtifacts@1
    displayName: 'ビルド成果物のアップロード'
    inputs:
      PathtoPublish: '$(Build.ArtifactStagingDirectory)'
      ArtifactName: '<Build Artifactsの名前。動作確認が目的なら適当な名前でOK。>'
      publishLocation: 'Container'

ビルドパイプラインが完成したら、後程使うので一度ビルドを実行しておきましょう。

Azure DevOpsとKey Vaultを接続して、シークレットの参照に必要な情報を定義する

Key Vaultの値は、事前に登録したユーザーまたはアプリケーションからしか操作することができません。 リリースパイプライン内でKey Vaultの値を操作する方法はいくつかあるのですが、Azure DevOpsの機能を用いるのが最も簡単です。

まずAzure DevOpsの組織にログインし、 [Pipelines] → [Library] のメニューを開きます。 そして [+Variable group] のボタンを押下します。

プロパティの設定画面で、 [Link secrets from an Azure key vault as variables] を有効にして、準備したKey Vaultと接続するようにAzureサブスクリプションと構築したKey Vaultを選択します。 うまく認証が終わったら、最下部の [+Add] ボタンを押下して、Key Vaultに定義したシークレットを追加します。

f:id:masatsuna:20191128220710p:plain
Azure DevOpsとKey Vaultを接続

これでAzure DevOpsとKey Vaultの接続が完了します。

Azure Portalで接続設定したKey Vaultを開いて、どのような権限が追加されているかを確認しておきましょう。 Key Vaultの画面を開き、 [アクセスポリシー] のメニューを開くと、Azure DevOpsのアプリケーションが追加されていることが確認できます。 権限も [取得] と [一覧] という最小限の設定になっており、問題なさそうです。

f:id:masatsuna:20191128221541p:plain
Key Vaultのアクセスポリシー

リリースパイプラインを構築する(appsettings.jsonの値を差し替える)

ここまで作成してきたものをリリースパイプラインの中で統合していきます。 まず [Pipelines] → [Releases] のメニューを開いて [+New] ボタンを押下し、リリースパイプラインを新規追加します。 今回はDeployment Groupに登録したマシンにリリースをするので、 [Select a template] では [IIS Website deployment] のテンプレートを選択してください。

f:id:masatsuna:20191128222203p:plain
IIS Website deploymentsを選択

とりあえずまっさらな状態だと以下のようになります。 勝手にStageの設定画面が出てきますが、一旦無視して以下の画面に戻りましょう。

f:id:masatsuna:20191128222503p:plain
初期状態のリリースパイプライン

まずはリリースするモジュールを設定していきます。 左側の [Artifacts] の横にある [+Add] ボタンを押下します。 リリースする対象物を選択する画面になるので、ビルドパイプラインで作っておいたモジュールを使うように選択します。 設定が終わったら [Add] ボタンを押下します。

f:id:masatsuna:20191128223151p:plain
Artifactsの設定

続いて [Tasks] タブのドロップダウンから [Stage1] を選択します。 既にステージの名前を変更してしまっている場合は、その名前を選択してください。

f:id:masatsuna:20191128223431p:plain
ステージの選択

初期状態では、以下のようなタスクになっているはずです。

f:id:masatsuna:20191128223547p:plain
初期状態のタスク

今回は事前にIIS上でWebサイトとアプリケーションを構築してあるので、1つ目の [IIS Web App Manage] のタスクは消してしまいましょう。 続いて、 [IIS Deployment] の項目を選択して、Deployment Groupの設定を行います。 作成済のDeployment Groupをドロップダウンリストから選択します。

f:id:masatsuna:20191128224254p:plain
Deployment Groupの設定

次に [IIS Web App Deploy] のタスクを選択して、配置するモジュールと配置先のWebサイト、アプリケーションを設定します。 今回は [Default Web Site] にリリースするため、 [Website Name] の設定を変更する必要はありません*3。 必要に応じて変更してください。 続いて配置先のアプリケーション名を [Virtual Application] に設定します。 VMの構築時に作成したアプリケーション名を設定します。 次に、 [File Transforms & Variable Substitution Options] のセクションを開いて、 [JSON variable substitution] の項目に、 [appsettings.json] を設定してください。 これが、appsettings.jsonを書き換えるための設定です。 その他の設定は、要件に合わせて変更してください。

f:id:masatsuna:20191128224836p:plain
IIS Web App Deployの設定

最後に、appsettings.jsonの値をどの値に書き換えるかの定義を行います。 いろいろやり方はあるのですが、最も簡単なのは [Variables] タブで変数を定義する方法です。 早速 [Variables] タブを開いてみましょう。

f:id:masatsuna:20191128230156p:plain
Variables タブの初期状態

初期状態では何も定義されていません。 まずは [Library] で定義した [Variable group] をこのリリースパイプライン内で参照できるようにします。 [Variable groups] のメニューを選択して、 [Link variable group] のボタンを押下します。

f:id:masatsuna:20191128233129p:plain
Variable group の追加

[Library] に定義した [Variable group] の一覧が表示されるので、作成したグループと参照するスコープを選択して [Link] ボタンを押下します。

f:id:masatsuna:20191128233259p:plain
Variable group の選択

これにより、選択したスコープ内で、 [Variable group] に設定してある変数が利用できるようになります。 今回は [ConnectionString] という名前の変数が定義されているので、リリースパイプライン内で [$(ConnectionString)] と書くと、その値を参照できます。

次に、appsettings.jsonの中身を書き換えるための設定を行っていきます。 appsettings.jsonの中身を書き換えるには、 [JSON variable substitution] の項目を定義した上で、 Variables に書き換える値を定義することで実現します。 書き換える項目は、 Variables の変数名によってマッピングします。 例えば今回は、appsettings.jsonConnectionStrings オブジェクトの TestDbContext という項目に設定してある接続文字列を書き換えたいので、Variablies のキー名は [ConnectionStrings.TestDbContext] とします。 要するに、JSONオブジェクトの階層構造を、半角のピリオドで表現してキー名とします。 このような名前の変数に対して値を設定すると、リリースパイプラインの中でappsettings.jsonの値を差し替えてくれるのです。

今回接続文字列は [$(ConnectionString)] という変数で参照できます。 ですので、先ほど作成した [ConnectionStrings.TestDbContext] の値に、[$(ConnectionString)] を設定することで、Key Vaultの値をappsettings.jsonに差し込めるようになります。

変数の定義は [Variables] タブの [Pipeline variables] メニューで行います。 前述の変数を追加定義しておきましょう。 Variables にはその変数を使用できるスコープも定義できます。 こちらは適宜設定すればよいと思います。

f:id:masatsuna:20191128231752p:plain
Variables の設定後

以上でリリースパイプラインの設定は完了です。 パイプラインを保存して、リリースを実行してみましょう。

動作確認

まずはリリースパイプラインのログを見てみましょう。

f:id:masatsuna:20191128234907p:plain
リリースパイプラインの動作ログ

右側のログの中に、 [Download secrets: xxxx] という項目が見えます。 これがリリースパイプライン内でKey Vaultにアクセスして、接続文字列の値を取得している処理そのものです*4。 今回は Deployment grounp に登録したVM上でこの処理を実行しています。 ですので、VMからAzure DevOpsとKey Vaultに対するアクセスができないと、リリースが失敗してしまいます。

続いて実行環境のVMにログインして、配置されたモジュールからappsettings.jsonを探し、開いてみましょう。 [TestDbContext] の値が、Key Vaultに定義した接続文字列に差し変わっていることがわかると思います。

最後に、実際に配置したアプリケーションにアクセスして、期待通りの結果が得られるか確認してみてください。

f:id:masatsuna:20191129002341p:plain
実行結果

自動生成コードをほとんど変更していないのでお見苦しいでしょうが、このような形で正常に処理が実行されることが確認できます。

まとめ

今回はAzure DevOpsの機能と、Azure Key Vaultの機能をうまく連携して、環境変数を用いないシークレットの管理方法について、詳細な手順を解説しました。 時代はCaaS/PaaS/FaaSに移り変わってきていますが、まだまだIaaS/オンプレミスの案件はゴロゴロあります。 少しでも楽に、そして安全な、シークレットの管理方法を模索してみてはいかがでしょうか。

*1:そのうち追加すると言ってますが、いつになるのかは調べてません!

*2:正確には違うんですが、説明のために簡略化して記載しています。

*3:[IIS Website deployment] のテンプレートを使うと、勝手に [Stage1] に各タスクで共通に設定する値がリンクされます。変更する場合はそちらを変更するか、リンクを無効にしてください。

*4:どのようなアクセスを行っているか知りたい場合は、リリースパイプラインの Variables に [System.Debug] というキーを追加し、値を [true] に設定して再度実行してみてください。