ツナ缶雑記

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

個人的によく使うAzure Pipelinesの定義済み変数まとめ

f:id:masatsuna:20200530011052p:plain

Azure Pipelines で YAML を書いていると、毎回のように定義済み変数の名前を検索することになります。 変数名が長すぎて覚えきれない。 今回は YAML を書くとき私が個人的にめちゃくちゃよく使う定義済み変数をまとめておきます。

よく使う定義済み変数

変数の名前 意味 値の例
Agent.BuildDirectory または Pipeline.Workspace ビルドエージェント内に作成されるこのビルドパイプライン用のルートディレクトリ。どちらを使っても同じ値。 c:\agent\_work\1
Build.SourcesDirectory ソースコードをダウンロードしてくるディレクト c:\agent\_work\1\s
Build.BinariesDirectory ビルドしたバイナリファイルの出力先として使うディレクトリ。ここにバイナリを一旦出力して、 Zip 圧縮後に Build.ArtifactStagingDirectory に配置することが多い。ビルド開始時に勝手にクリーンしてくれない。 c:\agent\_work\1\b
Build.ArtifactStagingDirectory または Build.StagingDirectory ビルド成果物をパイプラインアーティファクトとしてアップロードする前にコピーして置いておくディレクトリ。どちらを使っても同じ値。ビルド開始時に勝手にクリーンしてくれる。 c:\agent\_work\1\a
Build.Reason このビルドを実行するに至ったイベント。手動で実行したとき、プルリクエストを契機に実行したとき、などが判定できる。 Stage の Conditions で使うことが多い。 IndividualCI など(「よく使う Build.Reason のとる値」参照)
Build.SourceBranch ビルドを実行したブランチ。完全名となる文字列が渡ってくる。Stage の Conditions で使うことが多い。 refs/heads/master refs/tags/タグ名など
Build.SourceBranchName ビルドを実行したブランチ。ブランチのパスの最後の文字列が渡ってくる。Stage の Conditions で使うことが多い。 master タグ名 など
Build.BuildId ビルドID。パイプラインアーティファクトにアップロードする Zip ファイルの名前とかによく使う。 105

よく使う Build.Reason のとる値

意味
Manual 手動でビルドをキューに入れた。
IndividualCI ブランチをプッシュした。ただし CI トリガーの batch が false に設定されている場合(既定値)。
BatchedCI ブランチをプッシュした。ただし CI トリガーの batch が true に設定されている場合。
Schedule 設定したスケジュールで実行された。
PullRequest ブランチポリシーで「Build validation」を設定したブランチに対するプルリクエストが提出された。

全定義済み変数

以下のページにまとまっています。

docs.microsoft.com

GitHub のリポジトリに Boost Software License 1.0 を適用する方法

f:id:masatsuna:20200509005616p:plain

GitHub で公開リポジトリを作成する際、最初にライセンスを選択すると思います。 GitHub のヘルプを読むと、「Boost Software License 1.0」 が選択できるようなのですが、 UI 上このライセンスが選択できません。

f:id:masatsuna:20200509002544p:plain
ライセンスの選択

今回は「Boost Software License 1.0」を選択する方法を紹介します。

この手順は 2020/05/08 時点の手順であり、今後変更される可能性があります。

リポジトリの作成

まずはライセンスなし(None)でリポジトリを作成します。

f:id:masatsuna:20200509003527p:plain
ライセンスは一旦 None にする

ライセンスの追加

リポジトリ作成後、以下の URL にアクセスします。

https://github.com/<ユーザー名>/<リポジトリ名>/community/license/new?branch=master&template=bsl-1.0

以下のように、「Boost Software License 1.0」を選択できるようになります。

f:id:masatsuna:20200509004123p:plain
Boost Software License 1.0 を追加

右側にある [Review and submit] ボタンを押下します。 次の画面でコミットメッセージを入力してコミットしましょう。

Azure Pipelines で Windows サービスの停止/開始を安全に行うテンプレートを作る

f:id:masatsuna:20200508164333p:plain

Windows サービスをデプロイするリリースパイプラインを作ろうとすると、リリースパイプライン内で Windows サービスを停止したり開始したりする必要があります。 Windows サービスは常駐プロセスなので、一度停止しないとモジュールの交換ができません。 ということで、今回は Windows サービスの停止/開始を行う Azure Pipelines の YAML テンプレートのご紹介です。

環境

前提条件

今回紹介するビルド/リリースパイプラインは、インストール済みの Windows サービスを置き換えるためのパイプラインです。 初回の配置は手動で行うことを前提としています。

またリリース先の VM は、 Environments に登録しておいてください。 Environment の登録については以下の記事を参考にして下さい。

tsuna-can.hateblo.jp

リリースパイプラインの全体像

まずはリリースパイプラインの本体を作っていきます。 まずは全体を先に示しておきます。

trigger: none

pool:
  vmImage: 'windows-latest'

variables:
  solution: '**/Samples.sln'
  windowsServiceProject: '**/Samples.WinService.csproj'
  buildPlatform: 'AnyCPU'
  buildConfiguration: 'Release'

stages:
  - stage: 
    jobs:
      - job:
        steps:
          - task: NuGetToolInstaller@1
            inputs:
              versionSpec: 
              checkLatest: true
          - task: NuGetCommand@2
            inputs:
              restoreSolution: '$(solution)'
          - task: VSBuild@1
            inputs:
              solution: '$(windowsServiceProject)'
              platform: '$(buildPlatform)'
              configuration: '$(buildConfiguration)'
              msbuildArgs: '/p:OutPutPath="$(Build.Artifactstagingdirectory)/WinService"'
          - task: PublishBuildArtifacts@1
            inputs:
              PathtoPublish: '$(Build.Artifactstagingdirectory)/WinService'
              ArtifactName: 'WinService'
              publishLocation: 'Container'

  - stage: 
    variables:
      winServiceModulePath: '<Windows サービスの配置先>'
      winServiceName: '<Windows サービスの名前>'
    jobs:
      - deployment: 
        environment:
          name: '<リリース先 Environment の名前>'
          resourceType: VirtualMachine
        strategy:
          runOnce:
            preDeploy:
              steps:
              - download: none
              - template: stop-windows-service-template.yml
                parameters:
                  windowsServiceName: '$(winServiceName)'
            deploy:
              steps:
              - download: none
              - task: DownloadPipelineArtifact@2
                inputs:
                  buildType: 'current'
                  artifactName: 'WinService'
                  targetPath: '$(winServiceModulePath)'
            on:
              success:
                steps:
                - template: start-windows-service-template.yml
                  parameters:
                    windowsServiceName: '$(winServiceName)'

ステージは 2 つ準備しておきます。 1 つ目のステージでは、 Windows サービスのアプリケーションをビルドし、パイプラインアーティファクトとしてアップロードしています。 今回のサンプルでは NuGet パッケージのリストアも実装してあります。 2 つ目のステージで Windows サービスのアプリケーションを配置しています。 配置をする前に、対象の Windows サービスを停止するテンプレートを呼び出しています。 また配置が成功した後、 Windows サービスを開始するテンプレートを呼び出しています。

Windows サービスを配置する場合は、 preDeployon をうまく活用して、 Windows サービスの停止/開始を組み込んであげる必要があります。

Windows サービスを停止する YAML テンプレート

続いて Windows サービスを停止するテンプレートを見ていきます。

stop-windows-service-template.yml

parameters:
- name: windowsServiceName
  type: string
  default: ''
- name: timeoutSec
  type: number
  default: '40'

steps:
  - task: PowerShell@2
    displayName: 'パラメーターチェック'
    inputs:
      targetType: 'inline'
      script: |
        If ( "${{ parameters.windowsServiceName }}" -eq "" ) {
            Write-Host "##vso[task.logissue type=error;]テンプレートパラメーター \"windowsServiceName\" が指定されていません。"
            Write-Host "##vso[task.complete result=Failed;]"
            exit
        }
  - task: PowerShell@2
    displayName: '${{ parameters.windowsServiceName }} の停止'
    inputs:
      targetType: 'inline'
      script: |
        $SvcName = '${{ parameters.windowsServiceName }}'
        $Timeout = New-TimeSpan -Seconds ${{ parameters.timeoutSec }}
        $SvcCtrl = Get-Service | Where-Object { $_.Name -eq $SvcName }
        If ($SvcCtrl -eq $null) {
            Write-Host "##vso[task.logissue type=error;]"$SvcName" という名前の Windows サービスが見つかりません。"
            Write-Host "##vso[task.complete result=Failed;]"
            exit
        }

        Write-Host "サービス (" $SvcCtrl.Name "|" $SvcCtrl.DisplayName ") が見つかりました ( ステータス:" $SvcCtrl.Status ") 。"
        If ($SvcCtrl.Status -eq "StartPending") {
            Write-Host "サービス (" $SvcCtrl.Name "|" $SvcCtrl.DisplayName ") が開始処理中のため待機しています ( ステータス:" $SvcCtrl.Status ") 。"
            try {
                $SvcCtrl.WaitForStatus("Running", $Timeout)
            } catch {
                Write-Host "##vso[task.logissue type=warning;]"$SvcName" が開始するのを待機しましたが、タイムアウトしました。"
                Write-Host $_.Exception.ToString()
            }
            $SvcCtrl.Refresh()
        }

        If ($SvcCtrl.Status -eq "Running") {
            Write-Host "サービス (" $SvcCtrl.Name "|" $SvcCtrl.DisplayName ") の終了処理を実行します ( ステータス:" $SvcCtrl.Status ") 。"
            try {
                $SvcCtrl.Stop()
            } catch {
                Write-Host "##vso[task.logissue type=warning;]"$SvcName" の終了に失敗しました。"
                Write-Host $_.Exception.ToString()
            }
            $SvcCtrl.Refresh()
        }

        If ($SvcCtrl.Status -eq "StopPending") {
            Write-Host "サービス (" $SvcCtrl.Name "|" $SvcCtrl.DisplayName ") が終了処理中のため待機しています ( ステータス:" $SvcCtrl.Status ") 。"
            try {
                $SvcCtrl.WaitForStatus("Stopped", $Timeout)
            } catch {
                Write-Host "##vso[task.logissue type=warning;]"$SvcName" が終了するのを待機しましたが、タイムアウトしました。"
                Write-Host $_.Exception.ToString()
            }
            $SvcCtrl.Refresh()
        }

        If ($SvcCtrl.Status -eq "Stopped") {
            Write-Host "サービス (" $SvcCtrl.Name "|" $SvcCtrl.DisplayName ") が終了しました ( ステータス:" $SvcCtrl.Status ") 。"
            exit
        }
        Else {
            Write-Host "##vso[task.logissue type=error;]"$SvcName" が終了しませんでした。"
            Write-Host "サービス (" $SvcCtrl.Name "|" $SvcCtrl.DisplayName ") が終了しませんでした ( ステータス:" $SvcCtrl.Status ") 。"
            Write-Host "##vso[task.complete result=Failed;]"
            exit
        }

Windows サービスを安全に停止するためには、対象の Windows サービスの状態をよく確認して処理を行わなければなりません。 停止している Windows サービスに対して停止処理を行うと例外が発生してしまいます。 そのため、テンプレート内ではステータスを逐一チェックしながら、 Windows サービスが安全に停止するように実装しています。

とにかく Windows サービスが停止すれば良いので、細かく try-catch をかけて、何か問題があればコンソールに出力するようにしてあります。 Azure Pipelines にビルドの警告やエラーを通知する場合、 PowerShell なら以下のような実装を使います。

Write-Host "##vso[task.logissue type=warning;]<警告メッセージ>"
Write-Host "##vso[task.logissue type=error;]<エラーメッセージ>"

このような実装を行っておくと、 Azure Pipelines のサマリーページに以下のようなメッセージを出力できるようになります。

f:id:masatsuna:20200508125758p:plain
エラーメッセージ

f:id:masatsuna:20200508125737p:plain
警告メッセージ

そして、最終的に Windows サービスが停止しなかったときだけ、このタスクを失敗にするよう実装してあります。 タスクを明示的に失敗させたい場合は、以下のように実装します。

Write-Host "##vso[task.complete result=Failed;]"

Windows サービスを開始する YAML テンプレート

続いて Windows サービスを停止するテンプレートを見ていきます。

start-windows-service-template.yml

parameters:
- name: windowsServiceName
  type: string
  default: ''
- name: timeoutSec
  type: number
  default: '5'

steps:
  - task: PowerShell@2
    displayName: 'パラメーターチェック'
    inputs:
      targetType: 'inline'
      script: |
        If ( "${{ parameters.windowsServiceName }}" -eq "" ) {
            Write-Host "##vso[task.logissue type=error;]テンプレートパラメーター \"windowsServiceName\" が指定されていません。"
            Write-Host "##vso[task.complete result=Failed;]"
            exit
        }
  - task: PowerShell@2
    displayName: '${{ parameters.windowsServiceName }} の起動'
    inputs:
      targetType: 'inline'
      script: |
        $SvcName = '${{ parameters.windowsServiceName }}'
        $Timeout = New-TimeSpan -Seconds ${{ parameters.timeoutSec }}
        $SvcCtrl = Get-Service | Where-Object { $_.Name -eq $SvcName }
        If ($SvcCtrl -eq $null) {
            Write-Host "##vso[task.logissue type=error;]"$SvcName" という名前の Windows サービスが見つかりません。"
            Write-Host "##vso[task.complete result=Failed;]"
            exit
        }

        Write-Host "サービス (" $SvcCtrl.Name "|" $SvcCtrl.DisplayName ") が見つかりました ( ステータス:" $SvcCtrl.Status ") 。"
        try {
            Write-Host "サービス (" $SvcCtrl.Name "|" $SvcCtrl.DisplayName ") を起動します ( ステータス:" $SvcCtrl.Status ") 。"
            $SvcCtrl.Start()
        } catch {
            Write-Host "##vso[task.logissue type=warning;]"$SvcName" の起動に失敗しました。"
            Write-Host $_.Exception.ToString()
        }

        $SvcCtrl.Refresh()
        If ($SvcCtrl.Status -eq "StartPending") {
            Write-Host "サービス (" $SvcCtrl.Name "|" $SvcCtrl.DisplayName ") が起動するのを待機しています ( ステータス:" $SvcCtrl.Status ") 。"
            try {
                $SvcCtrl.WaitForStatus("Running", $Timeout)
            } catch {
                Write-Host "##vso[task.logissue type=warning;]"$SvcName" が起動するのを待機しましたが、タイムアウトしました。"
                Write-Host $_.Exception.ToString()
            }
            $SvcCtrl.Refresh()
        }
                                
        If ($SvcCtrl.Status -eq "Running") {
            Write-Host "サービス (" $SvcCtrl.Name "|" $SvcCtrl.DisplayName ") が起動しました ( ステータス:" $SvcCtrl.Status ") 。"
            exit
        }
        Else {
            Write-Host "##vso[task.logissue type=error;]"$SvcName" が起動しませんでした。"
            Write-Host "サービス (" $SvcCtrl.Name "|" $SvcCtrl.DisplayName ") が起動しませんでした ( ステータス:" $SvcCtrl.Status ") 。"
            Write-Host "##vso[task.complete result=Failed;]"
            exit
        }

Windows サービスの開始処理も、停止処理と同様、ステータスを参照しながら処理を行っています。 そして最終的にサービスが起動したことを確認するまで処理を行っています。

手元で動作確認をした限りでは、 Windows サービスの開始処理を実行し、処理が戻ってきた直後は、ステータスが StartPending の状態に一瞬なっているようです。 このテンプレートでは、このあたりの動作を反映した実装を行っています。

タイムアウトについて

今回紹介したテンプレートでは、 .NET Framework の ServiceController クラス を使用して Windows サービスの停止/開始処理を行っています。 このクラスを用いて Windows サービスの停止を行った場合、 Windows サービスが停止するのを待たずに処理が返ってきます。 逆に Windows サービスの開始を行った場合、 Windows サービスが開始するのを待ってから処理が返ってきます。 どちらのテンプレートも、パラメーターとしてタイムアウト時間を指定できるようになっていますが、上記の仕様の都合上、タイムアウト時間の意味が異なる点に注意してください。

まとめ

Windows サービスのアプリケーションを Azure Pipelines で配置するために、 Windows サービスの開始/停止を行うテンプレートを作ってみました。 マーケットプレースにも同じようなタスクがいくつか存在しますので、そういったタスクを使うか、このように自前で実装するか、選択していただければと思います。

本稿で解説した YAML について

原本は以下の GitHub リポジトリに公開してあります。

AzurePipelinesYAMLSample/WindowsServices at master · tsuna-can-se/AzurePipelinesYAMLSample · GitHub

Visual Studioでファイルを簡単にネスト表示する方法

f:id:masatsuna:20200430011629p:plain

Visual Studio を使っていると、一部のファイルがネスト表示されることに気づくと思います。

f:id:masatsuna:20200430001528p:plain
ネスト表示されるファイル

物理フォルダを見ると、これらのファイルはフラットに配置されていることがわかります。

f:id:masatsuna:20200430001705p:plain
ファイルはフラットに配置されている

Visual Studio のソリューションエクスプローラー上の表示がネスト表示されているだけなんですよね。 このネスト表示、既存のファイルを読み込んだりすると、壊れてしまうことって結構あります。 壊れてしまうと、プロジェクトファイルを手修正する必要があって、すごくめんどくさいなーとか思ったことありませんか?

あと ASP.NET Web アプリケーションプロジェクトを作っていると、 JavaScript ファイルや CSS ファイルがダラーっとフラットに並んでしまってうざったい。 そういう時に使える拡張機能を紹介したいと思います。

前提条件

File Nesting を使う

この拡張機能を使ってみてください。 marketplace.visualstudio.com

上記のサイトから拡張機能のパッケージをダウンロードしてインストールします。 拡張機能をインストールする際、 Visual Studio は終了しておく必要があるので注意して下さい。

簡単な使い方

前回解説した記事内で、App.configとApp.Release.configを作りました。 これをWeb.configと同様に、ネスト表示してみたいと思います。

f:id:masatsuna:20200430002917p:plain
ネスト表示対象のファイル

階層を掘りたい方のファイルを右クリックします。 今回の例だと App.config の下に App.Release.config を配置したいので、 App.Release.config を右クリックします。 コンテキストメニューから [File Nesting] → [Nest item] を選択します。

f:id:masatsuna:20200430003153p:plain

[File Nesting] ダイアログが表示されます。 そこで、このファイルの親ファイルを [Select parent] のドロップダウンから選択して [OK] ボタンを押下します。 今回は App.Release.config の親を App.config にしたいので、App.config を選択しています。

f:id:masatsuna:20200430003303p:plain

これでファイルがネスト表示できるようになりました。

f:id:masatsuna:20200430003519p:plain

使いどころ

私が特によく使うのは、 JavaScriptCSS のファイル群です。 ASP.NET MVC 5 のプロジェクトテンプレートを使うと、 JavaScriptCSS ファイルがすべてフラットに並んでしまいます。

f:id:masatsuna:20200430004015p:plain
JavaScript がすべてフラットに並ぶ

ここにこの拡張機能を用いて適切にネスト表示させてあげることで、ソリューションエクスプローラー上のすっきり度が大きく向上します。

f:id:masatsuna:20200430004725p:plain
ファイルをネストしてすっきりさせた状態

全部開くとこんな感じになります。

f:id:masatsuna:20200430004645p:plain
全部開いたとき

App.config に対して XML 変換を行う Azure Pipelines の YAML テンプレートを作る

f:id:masatsuna:20200422160213p:plain

ASP.NET のプロジェクトを作成すると、 Web.config ファイルが生成されます。 それと同時に、Web.Debug.config と Web.Release.config が作成され、アプリケーションの発行を行う際の構成にあわせて、 Web.config の変換を行ってくれます。 今回はこの構成ファイルの書き換え機能を App.config に対して実行できるような Azure Pipelines のテンプレートを作ってみようと思います。

環境

  • Visual Studio 2019
  • .NET Framework 4.8
  • コンソールアプリケーション(App.config のあるプロジェクトなら同じことができます)

プロジェクトを準備する

まずは適当にコンソールアプリケーションを作成します。 超シンプルに、 AppSettings から値を取得してコンソールに表示するだけにしておきます。

using System;
using System.Configuration;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine($"Setting1: {ConfigurationManager.AppSettings["Setting1"]}");
        }
    }
}

App.config はこのような形で、 AppSettings に 1 つだけキーを足しておきます。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="Setting1" value="LocalDebugSetting"/>
  </appSettings>
</configuration>

実行するとこんな感じです。

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

差し替える構成ファイルを準備する

さて、ここから本番です。 今回は App.config に設定されている Settings1 の値を、ビルド時に差し替えるような XML 変換のファイルを作っていきます。 Web.config の場合 Release で発行したときは Web.Release.config の XML 変換が適用されます。 似たような構成にするため、以下のような App.Release.config を作ります。

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <appSettings>
    <add key="Setting1" value="ReleaseSetting" xdt:Transform="Replace" xdt:Locator="Match(key)"/>
  </appSettings>
</configuration>

ファイルのプロパティは以下のように設定しておきます。

f:id:masatsuna:20200422142534p:plain
App.Release.config のプロパティ

ソリューション上は下記のようになります。

f:id:masatsuna:20200422142419p:plain
App.Release.config を追加する

YAML テンプレートを準備する

続いて今回のキモとなる、 YAML のテンプレートを作成します。 .git ディレクトリのあるルートディレクトリ内に、 pipelines-template ディレクトリを作成し、その中に以下の YAML を配置します。 ファイル名は publish-desktop-app-template.yml としておきました。

parameters:
- name: projectRootDirectory
  type: string
  default: ''
- name: projectFileName
  type: string
  default: '*.csproj'
- name: outputPath
  type: string
  default: ''
- name: buildPlatform
  type: string
  default: 'AnyCPU'
- name: buildConfiguration
  type: string
  default: 'Release'

steps:
  - task: Bash@3
    displayName: 'パラメーターチェック'
    inputs:
      targetType: 'inline'
      script: |
        if [ -z "$PROJECTROOTDIRECTORY" ]; then
          echo "##vso[task.logissue type=error;]テンプレートパラメーター \"projectRootDirectory\" が指定されていません。"
          echo "##vso[task.complete result=Failed;]"
        fi
        if [ -z "$OUTPUTPATH" ]; then
          echo "##vso[task.logissue type=error;]テンプレートパラメーター \"outputPath\" が指定されていません。"
          echo "##vso[task.complete result=Failed;]"
        fi
    env:
      PROJECTROOTDIRECTORY: ${{ parameters.projectRootDirectory }}
      OUTPUTPATH: ${{ parameters.outputPath }}
  - task: FileTransform@2
    displayName: 'App.config 変換 - ${{ parameters.projectRootDirectory }}'
    inputs:
      folderPath: '${{ parameters.projectRootDirectory }}'
      xmlTransformationRules: '-transform "**\App.${{ parameters.buildConfiguration }}.config" -xml "**\App.config"'
  - task: VSBuild@1
    displayName: 'アプリケーションのビルド - ${{ parameters.projectRootDirectory }}'
    inputs:
      solution: '${{ parameters.projectRootDirectory }}/**/${{ parameters.projectFileName }}'
      msbuildArgs: '/p:OutPutPath="${{ parameters.outputPath }}"'
      platform: '${{ parameters.buildPlatform }}'
      configuration: '${{ parameters.buildConfiguration }}'

ここまで作成したら Azure DevOps のリポジトリにファイル一式をプッシュしておきます。

ビルドパイプラインを作成する

続いて Azure DevOps から、ビルドパイプラインの YAML を作成していきます。 作成したテンプレートを呼び出すようにしましょう。 また動作確認のため、最終的にビルドしたアプリケーションを Pipeline Artifacts に登録しておきます。

trigger: none

pool:
  vmImage: 'windows-latest'

steps:
- template: 'pipelines-template/publish-desktop-app-template.yml'
  parameters:
    projectRootDirectory: '$(Build.SourcesDirectory)/ConfigTransformationSample/ConsoleApp1'
    outputPath: '$(Build.Artifactstagingdirectory)/ConsoleApps/ConsoleApp1'
- task: PublishPipelineArtifact@1
  inputs:
    targetPath: '$(build.ArtifactStagingDirectory)\ConsoleApps'
    artifact: 'ConsoleApplications'
    publishLocation: 'pipeline'

結果確認

上記のビルドパイプラインを実行すると、構成ファイルの変換が実行され、Pipeline Artifacts には以下の構成ファイルを含むアプリケーションが配置されているはずです。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <appSettings>
        <add key="Setting1" value="ReleaseSetting" />
    </appSettings>
</configuration>

これで ASP.NET のアプリケーションと同じような方法で、構成ファイルの XML 変換を実行できるようになりました。

まとめ

今回は ASP.NET Web アプリケーションの発行のような形で、デスクトップアプリケーションのビルドができる YAML テンプレートを作ってみました。 カスタマイズして使ってみてください。

注意点

テンプレートに渡す sourceDirectory の配下には、 プロジェクトを 1 つだけ置くようにしましょう。 ここにルートディレクトリを渡してしまうと、 App.config の書き換えが思ったようにいかなくなります。 これは FileTransform のタスクの制約なのであきらめるしかなさそうです。

本稿で解説した YAML について

以下の GitHub リポジトリに公開してあります。

AzurePipelinesYAMLSample/AppConfigTransform at master · tsuna-can-se/AzurePipelinesYAMLSample · GitHub

なお一部のコードについて、本稿で解説した物とは異なる箇所があります。

Azure Pipelines の YAML テンプレートにある buildPlatform の設定に注意しろ

f:id:masatsuna:20200415160341p:plain

地味にはまったので供養のためメモを残しておきます。

本稿は 2020/4/15 現在の情報を示すものであり、 Azure Pipelines および Visual Studio の更新によって、将来当てはまらなくなる可能性を含んでいることに注意してください。

結論から言うと。。。

Azure Pipelines の ASP.NET や デスクトップ .NET アプリケーションの YAML テンプレートを使うとき、テンプレートとして出力される YAML を必ず見直しましょう。 具体的には以下の 3 つのテンプレートのことを言っています。

Azure DevOps での操作時、以下の画面で選択している方が多いのではないかと思います。

f:id:masatsuna:20200415152445p:plain
対象となるテンプレート

これらのテンプレートを使うと、 YAML に記載されている buildPlatform の変数値が

Any CPU

に設定されていると思います。 しかし、正しくは

AnyCPU

です。 Any と CPU の間に半角のスペースは不要です。

f:id:masatsuna:20200415152917p:plain
この部分が間違っている

この設定がリンクする先

この設定値は、プロジェクトファイル内に記載されている値と合わせなければなりません。 例えば Visual Studio 2019 でコンソールアプリケーションのプロジェクトを作成すると、以下のようなプロジェクトファイルが出力されます。

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{8686833F-8175-4D1C-B8E4-6DD111FC417A}</ProjectGuid>
    <OutputType>Exe</OutputType>
    <RootNamespace>ConsoleApp1</RootNamespace>
    <AssemblyName>ConsoleApp1</AssemblyName>
    <TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>

良く設定を見てほしいのですが、ビルドプラットフォームの設定値は、どこも AnyCPU となっています。 Any と CPU の間に半角スペースが入っていません。 この設定が間違っていると、せっかくプロジェクトファイルに設定している上記の設定が生きなくなってしまいます。

なんでこんなのに気付いたのか

私がこの問題に気づいたのは ASP.NET の Web アプリケーションをプロジェクト指定でビルドしようとしたときに遭遇したエラーメッセージからでした。 ASP.NETYAML テンプレートを編集して、プロジェクトファイルを指定してビルドするように変更して実行しました。

- task: VSBuild@1
  inputs:
    solution: '**/WebApp.csproj'
    msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildPackageLocation="$(Build.Artifactstagingdirectory)\WebApp.zip"'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

YAML 上変更しているのは solution の値だけで、他のはテンプレート通りです。 これを実行すると、以下のような警告メッセージが出てきます。

Warning : The OutputPath property is not set for project 'WebApp.csproj'. Please check to make sure that you have specified a valid combination of Configuration and Platform for this project. Configuration='Release' Platform='Any CPU'. You may be seeing this message because you are trying to build a project without a solution file, and have specified a non-default Configuration or Platform that doesn't exist for this project.

しかし、 ASP.NET のプロジェクトファイルには、 OutPutPath の設定が入っているんですよね。

  <!-- 前略 -->
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <!-- 後略 -->

それなのになぜこんな警告がでるんだ?と30分ほど時間を無駄にしました。。。 そりゃテンプレートだって間違うことはありますよね。。。 妄信してしまった私が悪い。

YAML 側を AnyCPU と 半角スペースなしにして実行すると、ちゃんと動くようになります。

Azure Pipelines の YAML から Azure Key Valut のシークレットにアクセスする

f:id:masatsuna:20200414150838p:plain

シークレットとは、秘匿しておきたいデータのことを言います。 例えば、本番データベースへの接続文字列などがこれに当たります。 Azure にはシークレットを管理するサービスとして、 Azure Key Vault があります。 今回はこれを Azure Pipelines の YAML から利用できるようにする手順を解説しようと思います。

Service Connection を追加する

[Project settings] から実施します。

f:id:masatsuna:20200410154647p:plain
Service Connection メニュー

[Azure Resource Manager] を選択して画面下部の [Next] を押下します。

f:id:masatsuna:20200410154858p:plain
Azure Resource Manager を選択

[Service Principal (automatic)] を選択します。

f:id:masatsuna:20200410155010p:plain
Service Principal を選択

接続先の Azure サブスクリプション、リソースグループをドロップダウンから選択します。

f:id:masatsuna:20200410161243p:plain
接続先を設定

操作途中でログイン画面が出てくるので、 Azure サブスクリプションに紐づいているユーザーでログインしましょう。

f:id:masatsuna:20200410155532p:plain
ログイン

ログイン後、引き続き任意の設定をいくつか行い、画面下部の [Save] ボタンを押下します。 これで Service Connection が貼られました。

続いてうまく設定ができたかどうか、 Azure Portal から確認してみます。 Azure Portal にログインして、先ほど設定したサブスクリプションディレクトリに移動します。 そして、 [Azure Active Directory] を検索して選択します。

f:id:masatsuna:20200410162113p:plain
Azure Active Directory の選択

続いて [アプリの登録] メニューを選択します。

f:id:masatsuna:20200410162552p:plain
アプリの登録を選択

右側ペインの上部にある [すべてのアプリケーション] タブを選択します。

f:id:masatsuna:20200410162709p:plain
すべてのアプリケーションを選択

そうすると、先ほど登録した Service Connection が参照できると思います。 [表示名] の列が「Azure DevOps の組織名 - Azure DevOpsのプロジェクト名 - Azure サブスクリプションの ID 」となっているレコードがそれになります。

これで Service Connection が作成できました。

Azure Key Vault を作成する

続いてシークレットを管理する Azure Key Vault を作成していきます。 Azure Portal で [キー コンテナー] または {Key Vault] で検索しましょう。 表示されたサービスの中から 「キー コンテナー 」を選択します。

f:id:masatsuna:20200413163101p:plain
Key Vault の検索

そのまま [追加] ボタンを押下し、 [基本] のタブ内でKey Vault のリソース名、配置するリソースグループ、リージョンを設定します。

f:id:masatsuna:20200413170750p:plain
Key Vault の基本設定

設定が完了したら [アクセスポリシー] のタブに移動します。 中段あたりにある [アクセスポリシーの追加] を押下します。

f:id:masatsuna:20200413171248p:plain
アクセスポリシーの追加

先ほど作成した Azure AD 上のアプリケーションから、この Key Vault に対するアクセス権を設定してきます。 今回は Key Vault のシークレットを利用できれば良いので、 [シークレットのアクセス許可] を開いて、 [取得] と [一覧] の権限を追加します。

f:id:masatsuna:20200413171648p:plain
シークレットの取得と一覧権限を設定

そして [プリンシパルの選択] を押下して、登録した Azure AD 上のアプリケーション名を検索して設定します。 「Azure DevOps の組織名 - Azure DevOpsのプロジェクト名 - Azure サブスクリプションの ID 」を検索して設定してください。

f:id:masatsuna:20200413171847p:plain
プリンシパルの選択

この設定が完了したら [作成] ボタンを押下します。 細かな設定については、要件に合わせて行ってください。

シークレットの登録

続いて作成した Key Vault に、シークレットを登録します。 [シークレット] のメニューを選択します。

f:id:masatsuna:20200413172603p:plain
[シークレット] メニューを選択

上部の [生成/インポート] ボタンを押下します。

f:id:masatsuna:20200413173033p:plain
[生成/インポート] を押下

シークレットはキー(名前)と値で構成します。 キー名、値を入力しましょう。 入力が完了したら [作成] を押下します。

f:id:masatsuna:20200413173218p:plain
キーと値の設定

これでシークレットが登録され、利用できるようになりました。

f:id:masatsuna:20200413173401p:plain
シークレット登録完了

Azure Pipelines に YAML を追加する

まずは YAML を保存するリポジトリを作成します。 手軽にやるなら、 Azure DevOps でプロジェクトのページに入り、 [Repos] → [Files] メニューを選択し、下部の [Initialize] ボタンを押下します。 今回は KeyValutTest0410 という名前のプロジェクトに、同名の Git リポジトリを作成しておきました。

続いて Azure Pipelines の YAML ファイルを作成していきます。 [Pipelines] → [Pipelines] メニューを選択し、[Create Pipeline] ボタンを押下します。

f:id:masatsuna:20200414093703p:plain
パイプラインの追加

YAML の保存先を選択します。 今回は作成した Azure Repos の Git リポジトリをそのまま使うようにします。

f:id:masatsuna:20200414093759p:plain
[Azure Repos Git] を選択

f:id:masatsuna:20200414093945p:plain
リポジトリを選択

新し YAML ファイルを作成するので、 [Starter pipeline] を選択します。

f:id:masatsuna:20200414094106p:plain
[Starter pipeline] を選択

これで YAML を作成するための画面になります。 ここに、 Azure Key Vault と接続するための設定を書いていきます。

f:id:masatsuna:20200414094224p:plain
YAML の作成画面

YAML から Azure Key Vault にアクセスする

YAML を編集していきましょう。 今回はトリガーなし(手動起動のみ)、 Microsoft-Hosted のビルドエージェントを利用するようにします。 テンプレートのコードを全部削除して、以下のように変更しておきます。

trigger: none

pool:
  vmImage: 'windows-latest'

steps:

続いて Azure Key Vault にアクセスする処理を追加していきます。 まずは画面右上にある [Show assistant] ボタンを押下しておきます。 これを利用すると、いきなり素の YAML を書かなくてもよくなるので便利です。

f:id:masatsuna:20200414110014p:plain
アシスタントの表示

Task の一覧が出てくるので、 [Azure Key Valut] のタスクを検索して選択します。

f:id:masatsuna:20200414110749p:plain
Key Vault のタスクを選択

[Azure subscription] のドロップダウンを開くと、最初に登録した Service Connection が表示されるので選択します。

f:id:masatsuna:20200414131214p:plain
Service Connection の選択

そして取得する Key Vault を [Key vault] のドロップダウンに設定します。 [Secrets filter] にはこのパイプライン内で使用するシークレットの名前をカンマ区切りで指定できます。 全部取得したい場合は「*」を入力しておきます。

f:id:masatsuna:20200414134316p:plain
取得する Key Vault の設定

そして左側の YAML 定義部分で、 steps: の次の行にカーソルを合わせて [Add] ボタンを押下します。 すると先ほど編集した結果が YAML として反映されます。

f:id:masatsuna:20200414140909p:plain
YAML に追加

ここまで実施することで、 YAML 内で Azure Key Vault の値を使用できるようになります。

YAML 内で Azure Key Vault に定義した値を取得する

Azure Key Vault には Secret1 という名前で Sample Value という値のシークレットを登録しておきました。 これを PowerShell のタスク内で取得して、値が設定されている確認してみます。

シークレットの値は、 YAML 内でシークレットの名前を変数にして取得できます。 今回の例では Secret1 という名前のシークレットを Azure Key Vault に登録しているので、 YAML 内では $(Secret1) と書くことでその値を取得できます。 ただし、普通の変数とは異なり、コンソールにその値を出力することは制限されています。 以下のようにして、設定した値(Sample Value)が取得できているか、コンソールにシークレットの値が流出しないか確認してみます。

trigger: none

pool:
  vmImage: 'windows-latest'

steps:
- task: AzureKeyVault@1
  inputs:
    azureSubscription: 'Connect to VmEnvironmentSampleRG'
    KeyVaultName: 'SampleSecrets'
    SecretsFilter: '*'
- task: PowerShell@2
  inputs:
    targetType: 'inline'
    script: |
      Write-Host '$(Secret1)'
      If ('$(Secret1)' -eq 'Sample Value') {
        Write-Host 'Match'
      } Else {
        Write-Host 'Unmatch'
      }

これを実行すると以下のようになります。

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

単純にシークレットの値をコンソールに出力するよう記載した箇所は、シークレットの値が *** に置き換えられていることがわかります。 しかし、その後に続く処理で、実際の値が Azure Key Vault に登録した Sample Value であったことが確認できました。

まとめ

今回は Azure Pipelines の YAML から Azure Key Vault にアクセスするまでの手順をかなり詳細に解説しました。 今回解説した方法と、以下の記事で解説した内容を YAML で書き直して組み合わせることで、 VM やコンテナー環境にリリースするアプリケーションの設定を Azure Pipelines から設定できるようになります。 セキュアな開発環境の構築が可能になるのではないでしょうか。

tsuna-can.hateblo.jp

tsuna-can.hateblo.jp

はてなブログで自動的にPing送信する方法(Azure Logic Apps で RSS フィードをトリガーとして使用する)

f:id:masatsuna:20200413232307p:plain

せっかくブログをやっているので、にほんブログ村に登録してみました。 にほんブログ村は、様々なブログサイトのリンク集のようなサイトで、ブロガーたちは日々そのランキングを競っているのだとか。

にほんブログ村のようなサイトに、自分のブログが更新されたことを通知する方法として、ブログサービス側の Ping 自動送信機能を使う方法が一般的です。

blogmura-help.muragon.com

しかし、残念ながら当ブログのプロバイダーであるはてなブログには、この機能がありません。 ということで、せっかくなら自分で作ってみようかなーというのが本記事の趣旨となります。 実は検索すればいろいろとタダでやる方法は出てくるのですが、ここでは Azure を活用してやってみようかと思います。

Ping 送信機能って何よ?

自分の管理しているブログが更新されたよ、という通知を HTTP リクエストでにほんブログ村に通知することを言います。 何の記事が更新されたかはにほんブログ村側で解析してくれるので、送信する側からは、各自に割り当てられた URL に対して HTTP リクエストを送るだけでよいようです。

何を使うか

今回やりたいことは、はてなブログ側の更新をトリガーにして、にほんブログ村に用意されている URL を叩ければよいわけです。 はてなブログの更新を検知するためには、 RSS フィードを購読すれば簡単にできそうです。 定期的に RSS フィードを巡回して、更新されていれば Ping の HTTP リクエストを送る、ということなので、 Azure Logic Apps を使ってこれを実現してみることにしました。

RSS フィードを取得する

はてなブログRSS フィードは、ブログの URL に 「/rss」をつけることで取得できます。 当サイトならこんな感じです。

https://tsuna-can.hateblo.jp/rss

Ping 送信先の URL を取得する

にほんブログ村Ping 先 URL はマイページから取得できます。 マイページのトップから [記事管理] → [記事反映/Ping送信] のメニューを選択します。

または、以下の URL にログインしてアクセスしてみてください。

https://mypage.blogmura.com/ping

そうすると、 [あなた専用のPing送信先URL] というところに送信先の URL が表示されているはずです。

Logic App を作る

続いてこれらをつなげるために、 Logic App を作っていきます。 Azure Portal で Logic App を作成し、RSS フィードが更新されたときに起動するようトリガーを追加します。

f:id:masatsuna:20200413004936p:plain
RSS の新着項目をトリガーに設定

そしてそのトリガーに、読み込む RSS フィードを設定していきましょう。

f:id:masatsuna:20200413005040p:plain
読み込む RSS フィードを設定

これで 3 分間隔で RSS フィードに更新がないかをポーリングしてくれるようになります。

続いて Ping を飛ばしましょう。 Ping と言っていますが、正確に言えば単純な HTTP リクエストを送信するだけなので、以下のように HTTP リクエストを送信するためのアクションを追加します。

f:id:masatsuna:20200413005316p:plain
HTTP リクエスト送信のアクションを選択

そして先ほど取得しておいた Ping 送信先 URL に対して、 HTTP GET でリクエストを送信するように設定します。

f:id:masatsuna:20200413005510p:plain
HTTP リクエストの送信アクション

ここまで完成したら、保存しておきましょう。

動作確認

ではブログに新しい記事を投稿して、動作確認をしてみます。 ブログ投降後、しばらく待つと作成した Logic App が起動します。

f:id:masatsuna:20200413005828p:plain
Logic App の起動確認

3 分ごとに確認するよう設定していますが、なぜか 30 分ごとに起動しているようです。 またなぜか 1 回起動するたびに、同時に 2 回呼び出されている様子も見て取れます。 この Logic App は従量課金のプランでホスティングしています。 その影響を受けている可能性がありそうですね(詳細は調べていません)。

にほんブログ村の方にも正しく変更が反映されました。

f:id:masatsuna:20200413010543p:plain
記事が反映された

まとめ

今回は Azure Logic Apps のお勉強もかねて、こんなことができるよ、という話を書いてみました。 実はこのあと、以下のように Logic App を拡張しています。

f:id:masatsuna:20200413010938p:plain
最終的に作成したもの

にほんブログ村への Ping 送信と同時に、 SendGrid を使ってメール送信しています。 Azure Logic Apps には Twitter 用のコネクターとかもあるので、もっと面白いことができるかもしれませんね。

Data Explorer を使って Cosmos DB にデータを登録/検索する方法

f:id:masatsuna:20200412225312p:plain

前回、 Cosmos DB Emulator を使って、ローカルマシン上で Cosmos DB を実行する方法について解説しました。 また記事の最後の方で、データベース、コンテナーを作成する手順を解説しました。

tsuna-can.hateblo.jp

今回はその続きということで、作成したデータベース、コンテナーにデータを登録/検索する方法を解説しようと思います。

Cosmos DB には、いくつかの API が提供されています。 もっとも一般的なのが SQL API です。 他には、MongoDB API 、 Cassandra API などがあります。

Cosmos DB Emulator は、何もオプションを指定しないで実行すると、 SQL API が使われるようになっています。 また前回紹介した Data Explorer は、 SQL API のみをサポートしています(2020/4/12 現在)。 今回は Data Explorer を使ってデータの登録、検索をやっていきますので、 SQL API を使います。

Data Explorer の起動

まずはスタートメニューから [Azure Cosmos DB Emulator] を起動しましょう。 起動後に表示される右下のアイコンから [Open Data Explorer] を選択します。

f:id:masatsuna:20200411171958p:plain
Data Explorer の起動

ブラウザーが起動して Cosmos DB Emulator 用の Data Explorer が表示されます。

データを登録する

続いて前回作成したデータベース、コンテナーにデータを登録していきましょう。 左側のメニューから [Explorer] を選択します。 前回作成したデータベース(CosmosDbSampleDatabase)、コンテナー(Books)を開いて、 [Items] を選択し、上部の [New Item] ボタンを押下します。

f:id:masatsuna:20200411174533p:plain
データの登録を開始

すると、右側の画面に登録するデータを書き込むエディターが表示されます。

f:id:masatsuna:20200411174613p:plain
エディターの起動

Cosmos DB は、リレーショナルデータベースのように、事前にテーブル定義を行って、データの形式を整えてから使うようなデータベースではありません。 登録するデータは JSON 形式で記載し、設定する属性は登録時に自由に定めることができます。 コンテナーはリレーショナルデータベースでいうところのテーブルと同じようなものですが、仕組みはまるで異なるものです。

今回作成しているコンテナーは書籍のコンテナーですので、書籍に関するデータを JSON 形式で記載しました。 ISBN コード、書籍名、価格、著者のリストを今回は属性として設けてみました。 JSON 形式で記載できるため、著者のリストのように、 1 つのレコード内に、著者のリストのような配列要素を持たせることもできます。 なお id 属性は、必須で定義しなければならない属性です。 リレーショナルデータベースでいうところの主キーを設定しましょう。 入力が完了したら、上部の [Save] ボタンを押下しましょう。

f:id:masatsuna:20200411174731p:plain
登録する JSON を記載

JSON のフォーマット誤りなど、入力値に誤りがあると、 [Save] ボタンが非活性状態になり押下できません。 たぶん一番ハマるのは、 JSON の Trailing Comma がフォーマットエラーとして取り扱われることです*1

正常に登録が完了すると、以下のようにデータが参照できるようになります。

f:id:masatsuna:20200411174819p:plain
登録完了

実際に登録されたデータを見ると、いくつか登録していないデータが登録されていることがわかります。 この半角アンダースコア始まりの属性は、 Cosmos DB の内部管理用データで、ユーザーが意識するものではありません。 それぞれが何を表しているかは、以下にまとまっていますので、興味のある方はどうぞ。

docs.microsoft.com

データを検索する

検索を試すのにあたって、検索対象のデータが 1 件だと面白くないので、 3 件のデータを登録しておきました。 なお著者のデータは、前述の通り配列データとしてあります。

id isbnCode name price authors
B00001 978-4-00-000000-0 素晴らしい本 2500 著者 太郎, 著者 花子
B00002 978-4-00-000001-0 そこそこの本 2000 著者 次郎
B00003 978-4-00-000002-0 普通の本 1800 著者 花子

単純なデータの抽出

では実際に検索を行っていきましょう。 データ登録時と同様に、左側のコンテナーを開いて [Items] を選択します。 この状態だと、登録した全データが表示されます。

この時表示しているデータは、上部に表示されている SQL によって検索したデータです。 初期状態ですと全権検索するクエリが表示されていると思います。 その右側にある [Edit Filter] ボタンを押下すると、 WHERE 句を追加することができます。

f:id:masatsuna:20200412154423p:plain
[Edit Filter] ボタンを押下

例えば価格が 2000 以上のデータを検索する場合は、以下のように WHERE 句を入力して、右側にある [Apply Filter] ボタンを押下します。

SELECT * FROM c JOIN a IN c.price >= 2000

f:id:masatsuna:20200412174408p:plain
価格が 2000 以上のデータを検索

ちゃんと価格が 2000 以上のデータが抽出できています。 この辺りはリレーショナルデータベースの SQL クエリと同じような感覚で使えると思います。

f:id:masatsuna:20200412174537p:plain
価格 2000 以上の検索結果

配列要素の中の値でデータを抽出する

続いてもう少し複雑な検索もしてみます。 著者の属性には、配列のデータを配置していました。 例えば著者の名前(著者 花子)で検索する場合は、以下のようなクエリを記載します。

SELECT * FROM c JOIN a IN c.authors WHERE a.name = "著者 花子"

配列にした属性は、通常のリレーショナルデータベースでいうところの別テーブル扱いであると考えましょう。 SQL も上記に示す通り、 JOIN を使うことがポイントです。 これを適用すると、以下の通り期待通りのデータを抽出できます。

f:id:masatsuna:20200412175655p:plain
JOIN 句を使った検索結果

まとめ

今回は Cosmos DB Emulator に付属する Data Explorer を用いて、データの登録と検索を行う方法についての基本を書きました。 対象が Azure 上の Cosmos DB であってもやり方はほとんど同じですので、いろいろ遊んでみてください。

*1:これ本気で何とかしてほしい。。。

ローカルマシンで Cosmos DB を実行する

f:id:masatsuna:20200409010157p:plain

Azure のサービスの 1 つに、 Cosmos DB というデータベースサービスがあります。 データベースと言っても、ドキュメント DB や Key-Value Store など、リレーショナルデータベースではないデータベースサービスです。 こいつの特徴を簡単に言うなら、地理冗長の構成を作るのがめちゃくちゃ簡単であり、高可用性を実現するのが容易、ということでしょうか。

ただ、お値段が非常に高額であるというデメリットもあります。 利用料は、 1 秒間にどれだけのデータを読み書きするかによって決まります。 2020 年になって、 1 つの Azure サブスクリプションに対して 1 つだけ、無償の Cosmos DB を作ることができるようになりましたが、あくまで検証レベルでしか使えません。 またサブスクリプションに対して 1 つだけ、という制限もなかなかに厳しい印象を受けます。

Cosmos DB のエミュレーターを使う

そんなちょっと手の出しにくい存在である Cosmos DB ですが、昔からローカルマシン上で Cosmos DB を実行できるエミュレーターが存在します。 これをローカルマシンにインストールしておけば、 Azure のリソースを使わずに、 ローカルマシン内で Cosmos DB の動作確認ができてしまいます。 ただし、あくまでエミュレーターですので、Azure 上の Cosmos DB とはいろいろな機能差があります。 一番大きいのは SQL APIしか使えないところです。 この辺の詳細情報は、以下を参照してください。

docs.microsoft.com

エミュレーターをインストールする

エミュレーター本体は以下からダウンロードできました。

https://aka.ms/cosmosdb-emulator

こいつをダウンロードして、ローカルマシンに 管理者権限でインストール しましょう。 いくつか途中でインストールする必要があるため、管理者権限で実行しないと怒られます。

エミュレーターを実行する

Cosmos DB のエミュレーターは、スタートメニューから起動できます。 インストールが終わると、スタートメニューにエミュレーターの起動アイコンが追加されます。 こいつを押下して実行しましょう。

f:id:masatsuna:20200409003247p:plain
Cosmos DB のエミュレーター

起動が成功すると、以下のように起動したことをあらわす通知が表示されます。

f:id:masatsuna:20200409003418p:plain
Cosmos DB エミュレーターの起動

そしてもうしばらく待つと、ブラウザが起動して、以下のようなエミュレーターの管理画面が開きます。

f:id:masatsuna:20200409003634p:plain
Cosmos DB エミュレーター

しばらく待っても管理画面が開かない場合は、画面右下にあるインジケーターの中から Cosmos DB エミュレーターのアイコンを右クリックして [Open Data Explorer] を押下します。

f:id:masatsuna:20200409003908p:plain
管理画面の起動

これでブラウザーが自動的に立ち上がってきます。

これで Cosmos DB をローカルマシン上で実行できるようになりました。

データベース、コンテナーの作成

ここまでで準備が整ったので、 Cosmos DB エミュレーターにデータベース、コンテナーを作成していきます。 初期画面は接続情報を表示する画面になっていると思います。 エミュレーターの左側にある [Explorer] メニューを押下すると、Cosmos DB エミュレーター内に保存しているデータを管理できます。

f:id:masatsuna:20200409004317p:plain
Explorer メニュー

まずはデータベース、コンテナーを作成します。 [New Container] のドロップダウンを開いて、 [New Container] を押下します。

f:id:masatsuna:20200409004535p:plain
New Container を押下

画面右側に、作成するデータベース、コンテナーの設定箇所が開くので、データベース名など適宜指定して [OK] ボタンを押下します。

f:id:masatsuna:20200409004945p:plain
データベース名、コンテナー名を入力

この例ではスループットに 400 RU を指定していますが、ちょっとした動作検証程度なら 400 RUで十分です。 またこの例では書籍のデータを登録することを想定しています。 Partition Key には、書籍の ISBN コードを使うものとして設定を行っておきます。

ここまで行うと、データベースとコンテナーの作成が完了します。

f:id:masatsuna:20200409005556p:plain
データベースとコンテナーの作成完了

まとめ

今回は Cosmos DB のエミュレーターをローカルマシンにインストールして、データベースとコンテナーを準備するところまでを解説しました。 次回Explorer の簡単な使い方を解説する予定です。

1つのソリューション内でC#とVBを使う

f:id:masatsuna:20200328164345p:plain

最近 C#VB を混在させる話をよく質問されたので回答しておきます。

C#VB はソリューション内に混在できる

結論から言うと、これは可能です。 VB のプロジェクトと C# のプロジェクトを用意し、プロジェクト参照を行うことで、 1 つのソリューション内で複数の言語を取り扱うことができます。

f:id:masatsuna:20200328163452p:plain
C#VB を混在させたソリューション

このような形で別の言語で作成したプロジェクトを、参照に追加することができるようになっています。 そうすると、以下のように C# のプロジェクトから VB のプロジェクトを呼び出すことが可能になります。

Public Module Math
    Public Function Add(ByVal val1 As Int32, ByVal val2 As Int32) As Int32
        Return val1 + val2
    End Function
End Module
using System;

namespace CSharpApp
{
    class Program
    {
        static void Main(string[] args)
        {
            int val = VBLib.Math.Add(10, 20);
            Console.WriteLine(val);
        }
    }
}

ただし、 1 つのプロジェクト内に複数の言語を混在させることはできません。 また今後のことを考えると、新しく作るソースコードC# で作っておくことを個人的にはおすすめします。

Visual Studio 2019 のタブ表示を切り替える

f:id:masatsuna:20200323235825p:plain

Visual Studio 2019の小ネタポストです。

Visual Studio を用いてそこそこ大きなプロジェクトを開発していると、知らず知らずのうちに大量のファイルを用いて開発を進めていくことになります。 何も設定しないでVisual Studioを使っていると、以下のようにファイルはブラウザのタブ表示のような形状で表示されます。

f:id:masatsuna:20200323234258p:plain
ファイルのタブ表示

これは割と厄介で、大量にファイルを開いていると、最初の方に開いたファイルがドロップダウンの中に隠されてしまい、各ファイルへのアクセスが悪くなります。

f:id:masatsuna:20200323234327p:plain
ファイルがドロップダウン内に隠れる

同じような名前のファイルをたくさん開いていると、タブのタイトルを見ても、どれがどのプロジェクトのファイルか判断することができません。 こういうのに嫌気がさしていたのですが、Visual Studioの設定を変更することで、こういった問題が解決できます。

環境

Visual Studio 2019 (16.5以降)

タブ表示の形式を変更する

実はこの表示形式、 Visual Studio の設定で変更することができます。 ファイル名の表示されているタブの一番右側にある歯車型のアイコンを押下してください。

f:id:masatsuna:20200323234605p:plain
タブの表示形式設定

ここから好みの形式のタブ表示形式を設定することができます。 十分に広いディスプレイをお使いの場合、 [タブを左に配置] または [タブを右に配置] がおすすめです。 これを選択すると、開いているファイルがドロップダウンの中に隠れてしまう現象を回避できます*1

開いているタブをプロジェクト単位でグルーピングする

Visual Studio 2019 16.5 で導入された新機能で、開いているファイルをプロジェクトごとにグルーピングして表示することができるようになっています。 この機能を有効にするには、先ほどの設定画面で [タブを左に配置] または [タブを右に配置] を選択します。 そのうえで、 [プロジェクト別にグループ化] を選択すると、プロジェクト単位に開いているファイルの一覧を表示できます。

f:id:masatsuna:20200323235223p:plain
プロジェクト別にグループ化

まとめ

この機能、私も今まで設定せずに使っており、 16.5 のリリースノートで設定変更できることを知ったのでした。。。 大量のファイルを開く人には便利な設定ですので、活用してみてください。

*1:縦幅に収まらないほどのファイルを開いた場合は、縦スクロールが表示されます。

Azure ReposリポジトリにGitHubリポジトリの最新の状態を反映する方法

f:id:masatsuna:20200227094745p:plain

前回GitHubリポジトリをAzure Reposにインポートする方法を解説しました。

tsuna-can.hateblo.jp

今回はその続きとして、インポート元のGitHubリポジトリに対して行われた修正を、Azure Reposのリポジトリに反映する方法について解説しようと思います。 なお今回はVisual Studioを使った方法を解説します。 コマンドラインでも同等のコマンドを発行することで、同じことができます。

環境

Gitリポジトリをクローンする

まずはGitHubからインポートしたAzure ReposのGitリポジトリをローカルにクローンしておきます。 Visual Studioを起動して [チームエクスプローラー] を立ち上げて、Azure ReposのGitリポジトリをクローンします。

リモートリポジトリを設定する

次に、Gitリポジトリの設定画面を開きます。 [チームエクスプローラー] の上部にあるドロップダウンから [設定] を開きます。 そして下部に表示される [リポジトリの設定] を選択します。

f:id:masatsuna:20200225174410p:plain
リポジトリの設定

リポジトリの設定画面の中の [リモート] のセクションを開きます。 初期状態ですと、 [origin] という名前のAzure Reposのリモートリポジトリが設定されていると思います。 [追加] を選択します。

f:id:masatsuna:20200225174619p:plain
リモートリポジトリの追加

[リモートの追加] ダイアログが出るので、そこにGitHubリポジトリを設定します。 名前はなんでも構わないのですが、わかりやすく [upstream] にしておきます。

f:id:masatsuna:20200225174717p:plain
GitHubリポジトリを設定

これでリモートリポジトリの設定ができました。

GitHubに入った変更を取得してローカルのmasterブランチに反映する

再度 [チームエクスプローラー] に戻り、上部のドロップダウンから [同期] を選択します。 次に [フェッチ] を選択して、先ほど設定した [upstream] をドロップダウンから選択します。 設定が終わったら [フェッチ] ボタンを押下します。

f:id:masatsuna:20200225174955p:plain
GitHubリポジトリをフェッチ

これで正常に動作していれば、GitHubリポジトリの情報がフェッチされます。 上部のドロップダウンから [ブランチ] を選択します。 正常にフェッチが完了していれば、 [remotes/upstream] にGitHubリポジトリの情報が見えるはずです。

f:id:masatsuna:20200225175112p:plain
GitHubのブランチが表示される

続いてGitHubリポジトリに対する変更をローカルのmasterブランチに取り込んでいきます。 引き続き [ブランチ] のウィンドウで [リベース] を選択します*1。 今回はローカルのmasterブランチに、GitHub上のmasterブランチを取り込んでいくので、以下のような設定になります。 設定が終わったらそのまま [リベース] ボタンを押下しましょう。

f:id:masatsuna:20200225175201p:plain
ローカルリポジトリをリベース

うまくマージができたら、Azure Reposに変更を反映します。 上部のドロップダウンから [同期] を選択します。 [出力方向のコミット] にGitHub上で行われたコミットが並ぶので、そのまま [プッシュ] を押下して反映させます。

f:id:masatsuna:20200226093102p:plain
変更をプッシュ

これでGitHubリポジトリの変更を、Azure Reposのリポジトリに取り込むことができました。

インポートしたAzure ReposからGitHubへのプルリクエストは作れない

前回の記事でも記載した通り、ここで解説した方法はGitHubやAzure Reposのフォークを使った方法ではありません。 そのため、インポートしたAzure Reposに対して行った修正を用いて、GitHubリポジトリに修正を反映させることはできません。 プルリクエストもGitHub、Azure Repos単独の機能なので、この境界を超えることはできないんですよね。 こういったことをやりたいのであれば、元となるリポジトリと同じサービス内にフォークしたリポジトリをたててあげる必要があります。

*1:こういったケースでは、git rebaseで取り込む方が都合が良いと思います。

GitHubのリポジトリをAzure Reposにインポートする

f:id:masatsuna:20200226102651p:plain

GitHubやAzure Reposにはフォークという機能があります。 フォークという機能はGitの機能ではなく、GitHubやAzure Repos単独の機能です。 なのでGitHubリポジトリをフォークしてAzure Reposに持ってくることは仕組み上できません。

しかし、Azure Reposには別のリポジトリをインポートする機能があります。 この機能を用いると、GitHubリポジトリをAzure Reposに持ってくることができます。 簡単にその手順を残しておきます。

GitHubリポジトリのURLを取得する

まずはAzure ReposにインポートしたいGitHubリポジトリを開いて、リポジトリのURLを取得します。 リポジトリのトップに入り、右上の「Clone or download」のドロップダウンを開くとURLが取得できます。

f:id:masatsuna:20200226100419p:plain
GitHubリポジトリのURLを取得

Azure Reposにインポートする

続いてAzure ReposでGitHubリポジトリをインポートしていきます。 Azure Reposを開き、上部にあるリポジトリの一覧のドロップダウンを開いて、「Import repository」を選択します。

f:id:masatsuna:20200225173351p:plain
インポート開始

インポートするリポジトリの設定画面で、先ほど取得しておいたGitHubリポジトリURLを入力します。 またインポート後のGitリポジトリの名前を「Name」に設定しておきます。 今回は公開リポジトリをインポートするので認証設定等は行っていません。 そのまま画面最下部にある「Import」ボタンを押下することで、インポートが実行されます。

f:id:masatsuna:20200226094222p:plain
リポジトリのインポート

インポートが無事に成功すると以下のような画面になります。

f:id:masatsuna:20200226094426p:plain
インポート成功

これでAzure ReposにGit Hubのリポジトリが複製されました。 あとは普通のAzure ReposのGitリポジトリと同じように、ブランチを作ったりして開発してください。

HttpClientを使ってHTTPリクエストの送信前後に任意の処理を差し込む方法

f:id:masatsuna:20200219004757p:plain

.NET Framework 4.5で追加されたHttpClient、非常に便利に使うことができていい感じですよね。 Web APIへのアクセスを簡単に実装できて素晴らしい。

docs.microsoft.com

そんなHttpClientですが、HTTPリクエストを送信する前後に、自由に処理を差し込むことができるようになっています。 本稿ではHttpClientがどのような仕組みで動作するかを簡単に触れて、HTTPリクエストの送信前後に任意の処理を差し込む方法について書いてみようと思います。

前提とする環境

HttpClientの仕組み

HttpClientクラス自身は、HTTPリクエストを送信する機能を持っていません。 実際にHTTPリクエストを送信する処理を行っているのは、HttpClientHandlerという名前のクラスです。

docs.microsoft.com

HttpClientHandlerの内部ではHttpWebRequestを使ってHTTPリクエストを送信しています。

docs.microsoft.com

さて、HttpClientを使ってHTTPリクエストを送信する際、簡単にやると以下のようなコードを書くことになります。

using System.Net.Http;
using System.Threading.Tasks;

public class Hoge
{
    private static HttpClient httpClient = new HttpClient();

    public Task<HttpResponseMessage> Get(string url)
    {
        return httpClient.GetAsync(url);
    }
}

実はHttpClientのコンストラクターには、先ほど説明したHttpClientHandlerのベースクラスであるHttpMessageHandlerのオブジェクトを受け取ることできるオーバーロードが存在します。 上記の例ではHttpClientのインスタンスを生成する際、引数なしコンストラクターを使用していますが、あえてこれを明示して実装するなら、以下のように記述できます。

private static HttpCleint httpClient = new HttpClient(new HttpClientHandler());

これで引数なしコンストラクターを呼び出したのと等価なコードになります。

処理を差し込む方法

さて、HttpClientの仕組みを理解したら、続いて処理を差し込む方法も見ておきましょう。

処理を差し込む場合、DelegatingHandlerを用いるのが非常に便利です。 DelegatingHandlerは別のHttpMessageHandlerクラスに処理を丸ごと移譲することができるようになっています。 移譲するHttpMessageHandlerのオブジェクトをInnerHandlerプロパティに設定することで処理を移譲する仕組みです。 すなわち、実際にHTTPリクエストを送信する処理は、あらかじめ準備されているHttpClientHandlerを使い、差し込みたい処理だけ自分で頑張ってDelegatingHandlerを継承したクラスに実装するのが定石です。

HTTPリクエストを送信する際、SendAsyncメソッドが呼び出されます。 ここに差し込みたい処理を実装します。 処理を差し込むポイントは3か所作ることができます。

  1. HTTPリクエスト送信前
  2. HTTPリクエスト送信直後(レスポンスがまだないタイミング)
  3. HTTPレスポンス受信後の処理

ここに、差し込みたい処理を好きなように実装しましょう。 コードで示すと以下のような形になります。

using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

public class CustomDelegatingHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // HTTP リクエスト送信前の処理

        var result = base.SendAsync(request, cancellationToken);

        // HTTP リクエスト送信後の処理

        var response = await result;

        // HTTP レスポンス受信後の処理

        return response;
    }
}

そして最後に、作成したDelegatingHandlerを継承したクラスを、HttpClientのコンストラクターで指定します。 またInnerHandlerプロパティにHttpClientHandlerのオブジェクトを設定し、HTTPリクエストを送信する処理は移譲するようにしておきます。

private static HttpClient httpClient = new HttpClient(
    new CustomDelegatingHandler
    {
        InnerHandler = new HttpClientHandler()
    });

1点だけ注意があります。 DelegatingHandlerのSendAsyncメソッドは、スレッドセーフに実装しなければなりません。 特にWebアプリケーションで使用する場合、複数のスレッドから同時にこのメソッドが呼び出されることになります。

まとめ

HttpClientを使って、HTTPリクエストの送信前後に任意の処理を差し込む方法について説明しました。 非常に簡単に実装できるので、是非使ってみてください。