WPFアプリケーションの公開作業をAzure Pipelinesを使って自動化する

前回の記事で行った作業を自動化できたらいいなと思っていたらAzure Pipelinesでいい感じにできたので、その時の記録です。Azureの他のサービスとの連携が簡単で、Azure Blob Storageにアップロードする作業も簡単に書くことができました。

準備

前回の記事の続きとして書くので、記事の中で作成したWPFアプリケーションが既にあることを前提とします。今回の作業では、前回の「公開」作業を行った時点で生成される「ClickOnceProfile.pubxml」を使います。Azure Blob Storageも前回と同じコンテナーを使う想定で書きます。

必要なもの

前回の記事に加えて、本記事では以下のもの利用します。

記事の構成

この記事は以下の9つの手順でまとめます。

  1. プロジェクトにランタイムを指定する
  2. Azure DevOpsでプロジェクトを作成する
  3. Azure Reposにコードをプッシュする
  4. Azure Pipelinesでパイプラインを作成し、 Gitリポジトリと紐づける
  5. Pipelineを編集する
  6. Pipelineを実行する
  7. バージョンアップしてGitリポジトリにプッシュする
  8. Pipelineが自動実行することを確認する
  9. アプリが自動更新するか試す

1. プロジェクトにランタイムを指定する

SimpleWPFApplication.csprojRuntimeIdentifiers タグを追加します。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    ...
    <RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
    ...
  </PropertyGroup>
</Project>

ちょっと説明

RuntimeIdentifiers タグを追加しておかないと、手順6でパイプラインを実行したときに以下のようなエラーが出てしまいます。

##[error]C:\Program Files\dotnet\sdk\5.0.402\Sdks\Microsoft.NET.Sdk\targets\Microsoft.PackageDependencyResolution.targets(241,5): Error NETSDK1047: Assets file 'D:\a\1\s\SimpleWPFApplication\obj\project.assets.json' doesn't have a target for 'net5.0-windows/win-x64'. Ensure that restore has run and that you have included 'net5.0-windows' in the TargetFrameworks for your project. You may also need to include 'win-x64' in your project's RuntimeIdentifiers.

このエラーを避けるため、エラーメッセージで勧められている通りランタイムを指定しておきます。

2. Azure DevOpsでプロジェクトを作成する

Azure DevOpsの自分の組織(Organization)のページを開き、 [+ New project] から新しいプロジェクトを作成します。
今回は [SimpleWPFApplication] という名前で作成しました。

3. Azure Reposにコードをプッシュする

プロジェクトの中にいくつかアイコンがありますが、その中の [Repos] を開きます。

クローンしたりプッシュするためのURLやコマンド例が載せられているので、手元の Git クライアントを使ってコードをプッシュします。手順は省略します。ちなみに .gitignore はGitHub公式のこちらが役に立ちます。注意点として、この .gitignore は今回使いたい pubxml ファイルが含まれているので pubxml をGit管理できるように変更する必要があります。

プッシュするとReposの画面は以下のようになります。

4. Azure Pipelinesでパイプラインを作成し、 Gitリポジトリと紐づける

プロジェクトの中から [Pipelines] を開き、 [Create Pipeline] から新しいパイプラインを作成します。接続先を選択する画面では、 [Azure Repos Git] から先程作成したリポジトリを選択します。ちなみに、候補にあるGitHubやBitbucketなどのGitホスティングサービスと連携することもできます。試しにGitHubとの連携をやってみましたがとても簡単にできました。以降の手順はそれらのサービスと連携した場合も同じように行えます。

5. Pipelineを編集する

この記事のメインです。パイプラインのテンプレートを選べるので [.NET Desktop] を選択します。

そうすると画面上で直接yamlが編集できるようになるので、以下のように編集します。yamlの編集が完了したら [Save] します。

# .NET Desktop
# Build and run tests for .NET Desktop or Windows classic desktop solutions.
# Add steps that publish symbols, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net

trigger:
- main

pr: none

pool:
  vmImage: 'windows-latest'

variables:
  solution: 'SimpleWPFApplication.sln'
  project: 'SimpleWPFApplication\SimpleWPFApplication.csproj'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'

steps:
- task: NuGetToolInstaller@1

- task: NuGetCommand@2
  inputs:
    restoreSolution: '$(solution)'

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: VSTest@2
  inputs:
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: VSBuild@1
  displayName: 'Publish'
  inputs:
    solution: '$(project)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'
    msbuildArgs: '-t:publish -p:PublishProfile=ClickOnceProfile.pubxml -p:PublishDir="bin\publish\\"'

- task: AzureFileCopy@3
  displayName: 'Upload setup.exe'
  inputs:
    SourcePath: 'SimpleWPFApplication\bin\publish\setup.exe'
    azureSubscription: '(サブスクリプション名)'
    Destination: 'AzureBlob'
    storage: '(ストレージアカウント名)'
    ContainerName: '$web'

- task: AzureFileCopy@3
  displayName: 'Upload SimpleWPFApplication.application'
  inputs:
    SourcePath: 'SimpleWPFApplication\bin\publish\SimpleWPFApplication.application'
    azureSubscription: '(サブスクリプション名)'
    Destination: 'AzureBlob'
    storage: '(ストレージアカウント名)'
    ContainerName: '$web'

- task: AzureFileCopy@3
  displayName: 'Upload Application Files'
  inputs:
    SourcePath: 'SimpleWPFApplication\bin\publish\Application Files'
    azureSubscription: '(サブスクリプション名)'
    Destination: 'AzureBlob'
    storage: '(ストレージアカウント名)'
    ContainerName: '$web'
    BlobPrefix: 'Application Files'

ちょっと説明

ブラウザの右側にある [Tasks] から使いたいタスクを選択し、フォーム入力すればタスクのyamlを自動生成してくれます。まずはこの機能を使ってyamlを自動生成し、必要であれば直接編集する、という流れが良いと思います。

pr: none はプルリクエストを作成した時にパイプラインを実行しないように指定します。

ClickOnce公開のためのコマンドは公式ページを参考にしました。yamlでは [VSBuild] となっていますが、おそらくMSBuildを表していて同じオプションで動きました。PublishProfileオプションを使えば、Visual Studioで操作したときに生成された [ClickOnceProfile.pubxml] を指定してオプションをすっきりさせることができます。しかし、公式ページにもある通りPublishDirオプションはここでも指定する必要があります。

[AzureFileCopy] というタスクでAzure Blob Storageにアップロードしています。[azureSubscription] には、自分のAzureサブスクリプションを指定してください。執筆時点では最新版であるバージョン4 [@4] では動作しませんでした。アップロードしているファイルやフォルダは基本的に前回の記事と同じです。ただしインストールするときに使えるWebページ Publish.html はMSBuildコマンドでは出力されないようで、今回は含めていません。インストール用のリンクが載ったページが必要であれば自分で作成する必要がありそうです。

6. Pipelineを実行する

画面右上の [Run] からパイプラインを実行します。うまくいけば下の図のように成功します。

7. バージョンアップしてGitリポジトリにプッシュする

最後に手順7から9で、コードを変更→mainブランチにプッシュ→パイプライン実行→アプリを更新の流れをやってみます。

ここはVisual Studioで作業します。前回の記事の「発行」機能を使えばリビジョンが自動で増加しましたが、今回はそういうわけにはいかないので ClickOnceProfile.pubxml を自分で編集する必要があります。バージョン番号はなんでもいいですが 1.0.1.0 としてみました。

<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <ApplicationRevision>0</ApplicationRevision>
    <ApplicationVersion>1.0.1.*</ApplicationVersion>
    ...
  </PropertyGroup>
</Project>

前回と同様に MainWindow.xaml を編集して画面にバージョン番号を表示するようにします。

<Window
    ...
    >
    <Grid>
        <TextBlock Text="Ver.1.0.1.0" />
    </Grid>
</Window>

これらの変更分をgit commitしてmainブランチにプッシュします。

8. Pipelineが自動実行することを確認する

Azure DevOpsのパイプラインのページを見にいくとパイプラインが実行しています。

9. アプリが自動更新するか試す

アプリを起動すると更新ダイアログが表示され、更新すると新しいバージョンになっていることが確認できます。

WPFアプリケーションをClickOnce発行してAzure Blob Storageに公開する

タイトル通りなのですが、開発したWindowsデスクトップアプリケーションをインターネットからインストールして使えるようにしたいということがあったので、その時の備忘録です。

必要なもの

この記事で紹介している方法を試すには以下のものが必要です。

記事の構成

この記事は以下の7つの手順でまとめます。

  1. Azure Blob Storageに静的Webサイト用コンテナーを作成する
  2. WPFアプリケーションを作成する
  3. WPFアプリケーションをClickOnce発行する
  4. アプリのファイル一式をAzure Blob Storageにアップロードする
  5. アプリをインストールする
  6. アプリをバージョンアップして再アップロードする
  7. アプリが自動更新するか試す

1. Azure Blob Storageに静的Webサイト用コンテナーを作成する

Azure Portalに移動して、ストレージアカウントを作成します。

作成し終わったら、左メニューから [静的なWebサイト] を選択します。

[有効] を選択して [保存] をクリックします。(裏で $web という名前のコンテナーが生成されます)

表示されるプライマリエンドポイントをコピーしておきます。(手順3で使います)

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

ここからはVisual Studioで作業します。新しいプロジェクトの作成で [WPFアプリケーション] を選択します。

プロジェクト名は [SimpleWPFApplication] とします。

ターゲットフレームワークは [.NET5.0] とします。

以下のようなファイルが自動生成されます。

MainWindow.xaml を少し書き換えてバージョン番号を表示するようにしておきます。

<Window
    ...
    >
    <Grid>
        <TextBlock Text="Ver.1.0.0.0" />
    </Grid>
</Window>

3. WPFアプリケーションをClickOnce発行する

Visual Studioのソリューションエクスプローラーでプロジェクトを右クリックし [発行] をクリックします。

[ClickOnce] を選択します。

発行場所はデフォルトの [bin\publish\] のままでいきます。

インストール場所は [Webサイトから] を選択し、手順1でコピーしたBlob Storageのプライマリエンドポイントを貼り付けます。

設定はデフォルトのままでいきます。ClickOnceには更新プログラムを確認して自動で更新する機能があるのでそれを使ってみます。

今回マニフェストの署名はなしでいきます。

構成はいろいろいじります。「配置モード」は [自己完結] 、「ターゲットランタイム」は [win-x64] を選択し、 [単一ファイルの作成] をチェックします。ここは全てのパターンを試してないですが、この構成でうまく動いたのでこれでいきます。

[完了] をクリックすると下の画像のような画面になるので、 [発行] をクリックします。

発行後、発行場所を見に行くといろいろなファイルが生成されています。

4. アプリのファイル一式をAzure Blob Storageにアップロードする

Azure Storage Explorerを使って作業します。以下のファイルとフォルダを$webコンテナーにアップロードします。Azure Storage Explorerを使えば簡単にドラッグアンドドロップでアップロードできます。

  • setup.exe
  • SimpleWPFApplication.application
  • Publish.html
  • Application Files フォルダ

アップロード後、Azure Portalでも確認できます。

5. アプリのインストール

ブラウザで「Publish.html」を見に行きます。(URLは https://ストレージアカウント名.z11.web.core.windows.net/Publish.html のようになります。)

[インストール] をクリックするとsetup.exeがダウンロードされます。setup.exeを起動すると確認ウィンドウが表示されるので発行元を確認して [実行] をクリックします。

[インストール] をクリックするとインストールが始まります。

インストールが完了するとアプリが起動します。

左下のスタートメニューから起動することができるようになりました。

6. アプリをバージョンアップして再アップロード

ここからは少し蛇足になりますが、新バージョン版をアップロードしてアプリが自動更新するところを手順6と7で記載します。まず、バージョンアップしたことが分かるように画面 MainWindow.xaml のバージョン番号を変えておきます。

<Window
    ...
    >
    <Grid>
        <TextBlock Text="Ver.1.0.0.1" />
    </Grid>
</Window>

手順3の再度で行った [発行] を再度クリックします。リビジョン(バージョン番号の一番右)が自動で増加するようになっているので、ここで発行したバージョンは「1.0.0.1」となります。

手順4で行ったアップロードをもう一度行います。

7. アプリの自動更新

手順5でインストールしたアプリを起動します。すると更新を促すウィンドウが出るので、そのまま [OK] をクリックすると更新されます。

更新後にアプリを開くと新しいバージョン(1.0.0.1)になっていることが確認できます。

ItemsControlを使ってWPFでGitHub風ヒートマップを描画する

WPFアプリで日々の実績を図で表現したいと思い、GitHubのContribution Graph(芝生)を参考に描画してみたのでその時の記録を残しておきます。外部データを参照し動的に描画できるように繰り返し描画に便利な ItemsControlを利用しました。xamlはWPFの素の機能だけで書いていますが、裏側はMVVMパターンで実装できるPrismというフレームワークを利用しています。

環境

使用しているプラットフォームやフレームワークは以下の通りです。

実装:xaml

メインテーマであるItemsControlはこんな感じです。

<ItemsControl ItemsSource="{Binding WeekHeatmaps}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Orientation="Horizontal" />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>

            <ItemsControl ItemsSource="{Binding HeatmapElements}">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <StackPanel Orientation="Vertical" />
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Grid
                            Width="15"
                            Height="15"
                            ToolTip="{Binding Comment}">
                            <Border
                                Margin="1"
                                Background="{Binding Color}"
                                CornerRadius="2" />
                        </Grid>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>

        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

ちょっと説明

ItemsControlを入れ子にしています。外側が芝生の列を表していて、実績データでいう1週間分に相当する部分です。横方向に連ねていくため、水平方向(Horizontal)に伸びるStackPanelを使います。内側は芝生の一つのマスを表していて、実績データでいう1日分に相当する部分です。縦方向に連ねていくため、垂直方向(Vertical)に伸びるStackPanelを使います。どちらのItemsControlも中身のカスタマイズにItemTemplateを利用しています。

実装:バインディングするデータ

バインディングしているデータWeekHeatmapsですが、 ViewModel内で以下のようなリストとして定義しています。

public class MainWindowViewModel : BindableBase
{
    ...

    public List<WeekHeatmap> WeekHeatmaps { get; }

    ...
}

リストの型として使っているWeekHeatmapクラスと関連クラスは以下の通りです。

public class WeekHeatmap
{
    public WeekHeatmap(List<HeatmapElement> heatmapElements)
    {
        if (heatmapElements.Count > 7) throw new ArgumentOutOfRangeException(nameof(heatmapElements), "ヒートマップの数が7を超えています。");
        HeatmapElements = heatmapElements;
    }

    public List<HeatmapElement> HeatmapElements { get; }
}
public class HeatmapElement
{
    public HeatmapElement(string color, string comment)
    {
        Color = color;
        Comment = comment;
    }

    public string Color { get; }
    public string Comment { get; }
}

ちょっと説明

芝生のマスの情報はHeatmapElementクラスに持たせています。芝生の色を表す文字列とマウスオーバーした時に表示されるTooltip用の文字列をプロパティとしています。芝生縦一列の一週間分の情報はWeekHeatmapクラスに持たせています。HeatmapElementのリストをプロパティとしています。機能には関係ないですが、一週間分という性質を補強するためリストの長さが7以上だった場合はエラーになるようにしています。

実行

サンプルのデータを代入し実行すると以下の画像ように芝生を描画できました。補足情報として赤文字でどの情報がどこを表しているか記しています。

実行画面。赤文字は後付け

参考

コード全体はGitHubにアップロードしています。リポジトリをcloneしてきたらVisualStudioでGitHubStyleHeatmap.slnを開いてF5実行すると上に記載した画像のウィンドウが表示されます。

まとめと感想

WPFでGitHubのContribution Graph(芝生)風のグラフを描画してみました。グラフを動的に生成するためにItemsControlを利用しました。StackPanelを入れ子にすることで芝生の行と列を表現しました。ItemsControlのItemTemplateを使ってマスの描画方法を指定しています。今回の取り組みでItemsControlの柔軟性を実感しました。特に入れ子にすることでリストの中身にもItemsControlが使えるのは表現の幅が広がっていいですね。また、バインディングするデータに自作クラスが使えるので工夫次第でいろんなリスト情報を見やすく描画するためにも使えると思います。発展として、WeekHeatmapクラスに「概要」プロパティを追加することで芝生の上部に情報を描画させることができるかもしれません。GitHubのように月を表示できそうです。

Unityで作ったゲームをWebGLビルドしてAzure Static Web Appsで公開する

Unityで作ったゲームを気軽に遊べるようにしたいと考えていたところ、Azureに静的Webサイトを簡単に無料でデプロイできるサービス(Static Web Apps)があったので使ってみました。

ちなみにサンプルのゲームにはUnity勉強でお世話になっている本「Unity2021 3D/2Dゲーム開発実践入門」のCHAPTER-3で作成するゲーム「Illumiball」を利用しています。

必要なもの

この記事の内容を試すためには以下の準備が必要です。

  • Unity HubでWebGL Build Supportモジュールをインストールしておく
  • Azureアカウント
  • GitHubアカウント

記事の構成

本記事は以下の2部構成で書いています。

  1. UnityでWebGLビルドする
  2. Azure Static Web Appsで公開する

1. UnityでWebGLビルドする

まずはUnityでの操作です。

圧縮を無効化する

デフォルトの設定では生成されるJavaScriptファイルが圧縮されているのですが、恐らくStatic Web Appsが対応していないことによりゲームが動作しないので、圧縮を無効化します。

  • [Edit] -> [Project Settings…] とクリックし、プロジェクト設定ダイアログを開く
  • [Player]メニューを選択し、WebGLのアイコンのタブを開く
  • [Publishing Settings] の [Compression Format] を [Disabled] にする

ビルドする

ビルドしてWebコンテンツを生成します。

  • [File] -> [Build Settings…] とクリックし、ビルド設定ダイアログを開く
  • [Platform] で [WebGL] を選択し、 [Switch Platform] をクリックする
  • プラットフォームの切り替えが完了したら、 [Build] をクリックする
  • フォルダ選択のダイアログが開くので、Webコンテンツを生成してほしいフォルダを指定して [フォルダーの選択] をクリックする

2. Azure Static Web Appsで公開する

先ほど生成したWebコンテンツを公開します。

GitHubにWebコンテンツをアップロードする

Static Web Apps は基本的にGitHubとの連携を前提としているのでまずはWebコンテンツをGitHubにアップロードします。

Static Web Appsを作成する

いよいよ最後の作業です。

  • Azureポータルから [Static Web Apps] を選択し、 [Create] をクリックする
  • 以下の画像を参考に情報を入力する。特筆する点は以下の通り
    • 個人の開発なら [Plan type] はFreeを選べる
    • [Azure Functions and staging details] は地理的に近そうな [East Asia] を選ぶ
    • [Deployment details] は [GitHub] にして、 [Sign in with GitHub] からGitHubアカウントを認証する
    • その後、先ほど作成したGitHubリポジトリを指定する
    • [Build Presets] は [Custom] にする
  • [Review + create] をクリックし、リソースを作成する
  • 少し待つとGitHub Actionsのワークフローが動き出し、WebコンテンツがSite Web Appsにデプロイされる

確認

Static Web Appsのリソースページから [Browse] をクリックし、ゲーム画面が表示されれば成功です。

ASP.NET Core APIでリストを一括更新したいときはAutoMapperが便利だった

APIで扱うデータの中にリストが含まれる場合に更新(PUT)でリストを一括で対応してほしいとき、EntityFrameworkの追跡機能とAutoMapperを使えば楽に実装できた、という話です。

例えば GET した時が以下のようなデータとします。

GET /api/TodoItems/1
{
    "id": 1,
    "name": "Task1",
    "isComplete": false,
    "todoSubItems": [
        {
            "id": 1,
            "name": "Sub task1",
            "isComplete": false,
            "todoItemId": 1
        },
        {
            "id": 2,
            "name": "Sub task2",
            "isComplete": false,
            "todoItemId": 1
        },
    ]
}

このような場合PUT の時は TodoSubItems に追加、変更、削除があってもそのまま上書きしてもらいたいということです。

PUT /api/TodoItems/1
{
    "id": 1,
    "name": "Task1",
    "isComplete": false,
    "todoSubItems": [
        {
            "id": 2,
            "name": "Sub task2 (updated)"
            "isComplete": false,
            "todoItemId": 1
        },
        {
            "id": 0,
            "name": "New task"
            "isComplete": false,
            "todoItemId": 1
        }
    ]
}

この例では TodoSubItem の1を削除、2を編集、一つを新たに追加しています。

補足: サブタスクでAPIのエンドポイント作ればいいんじゃない?

確かに TodoItems と TodoSubItems でエンドポイントを分ける設計もあります。ですが今回サブタスクはメインタスクに従属するもので、サブタスクの変更もメインタスクの更新時に行いたい、というユースケースを想定しています。例えば、伝票と伝票明細の関係、記事と記事タグなど、1対多の関係で1の属性の一つとして多(リスト)を持っている場合には実用的なパターンかと思っています。

準備

今回はデータベースにSQL Serverを利用します。今はDockerで簡単にSQL Serverを起動できます。

docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=<your_strong_password>" -p 1433:1433 --name todosubapi -d mcr.microsoft.com/mssql/server:2019-latest

たったこれだけ。いい時代になりました。

実装

ここから実装していきます。サンプルとして公式サイトのチュートリアルにあるToDoアプリを使います。このアプリにToDoタスクが複数のサブタスクを持てる機能を追加します。基本的にチュートリアル通りに進めて、サブタスク機能を実現するのに必要な最低限の実装を付け足す方針です。

Webプロジェクトの作成

公式チュートリアル通りです。詳細な作成方法は割愛します。プロジェクト名は「 TodoSubApi 」としました。

テスト

初期状態で正常に起動するか確認します。

  • Ctrl + F5 を押して実行する
  • Webブラウザが起動しSwaggerの画面が表示されることを確認する

モデルクラスの追加

基本公式チュートリアル通りですが、サブタスクを表す TodoSubItem を追加します。

  • ソリューションエクスプローラーを右クリックして [追加] -> [新しいフォルダー] を選択する
  • 「 Models 」という名前をつける
  • Models フォルダーを右クリックして [追加] -> [クラス] を選択する
  • クラスに「 TodoItem 」という名前をつける
  • 同様に Models 配下に「 TodoSubItem 」というクラスをつくる
  • TodoItem.cs と TodoSubItem.cs を以下のように作成する
namespace TodoSubApi.Models
{
    public class TodoItem
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public bool IsComplete { get; set; }
        public List<TodoSubItem> TodoSubItems { get; set; }
    }
}
namespace TodoSubApi.Models
{
    public class TodoSubItem
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public bool IsComplete { get; set; }
        public long TodoItemId { get; set; }
    }
}
ちょっと説明

TodoItem クラスの TodoSubItems プロパティはコレクションナビゲーションプロパティと呼ばれるものです。TodoSubItem クラスの TodoItemId は外部キーです。(参照

データベースコンテキストの追加

公式チュートリアルではインメモリデータベースを採用していますが、今回はSQL Serverで試すので、NuGetパッケージ「 Microsoft.EntityFrameworkCore.SqlServer 」を追加します。その後以下のようにデータベースコンテキストを追加します。

  • Models フォルダーを右クリックして [追加] -> [クラス] を選択する
  • クラスに「 TodoContext 」という名前をつける
  • TodoContext.cs を以下のように作成する
using Microsoft.EntityFrameworkCore;

namespace TodoSubApi.Models
{
    public class TodoContext : DbContext
    {
        public TodoContext(DbContextOptions<TodoContext> options)
            :base(options)
        {
        }

        public DbSet<TodoItem> TodoItems { get; set; }
        public DbSet<TodoSubItem> TodoSubItems { get; set; }
    }
}

データベースコンテキストの登録

公式チュートリアルとは異なり、SQL Serverを使う形に Startup.cs を書き換えます。

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<TodoContext>(opt =>
        opt.UseSqlServer("Server=tcp:127.0.0.1,1433;Initial Catalog=todoapi;User ID=sa;Password=<your_strong_password>;Persist Security Info=False;"));

    ...
}

マイグレーションとテーブル作成

この作業は公式チュートリアルにはありません。今回はSQL Serverを使うので事前にSQL Serverにテーブルを作成しておきます。

  • Visual Studioの [パッケージマネージャーコンソール] を開く
  • 以下のコマンドを実行し、マイグレーションを作成する
PM> Add-Migration InitialCreate
Build started...
Build succeeded.
To undo this action, use Remove-Migration.
  • プロジェクト配下に Migrations フォルダーがあることを確認する
  • 以下のコマンドを実行し、データベースを更新する
PM> Update-Database
Build started...
Build succeeded.
Done.

コントローラーのスキャフォールディング

公式チュートリアル通りです。詳細な方法は割愛しますがスクリーンショットだけ載せておきます。

PostTodoItem 作成メソッドの更新

公式チュートリアル通りです。

GetTodoItems メソッドとGetTodoItem メソッドの更新

これは公式チュートリアルにはありません。GET してきたときに TodoSubItems も取得できるように処理を追加します。

  • TodoItemsController.cs を以下のように編集する
[HttpGet]
public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
{
    return await _context.TodoItems.Include(x => x.TodoSubItems).ToListAsync();
}

...
[HttpGet("{id}")]
public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
{
    var todoItem = await _context.TodoItems.Include(x => x.TodoSubItems).FirstOrDefaultAsync(x => x.Id == id);

    if (todoItem == null)
    {
        return NotFound();
    }

    return todoItem;
}
ちょっと説明

データコンテキストからデータを取得する処理に Include メソッドを追加して TodoSubItems を読み込んでいます。

PutTodoItem メソッドの更新

この記事のメインです。データ更新時に TodoSubItems の中身を自由に変えても適切に更新してもらうためAutoMapperを使う方法に変えます。

  • NuGetパッケージ「 AutoMapper 」を追加する
  • まず TodoItemsController.cs のコンストラクタにAutomapperの定義と設定を書く
private readonly IMapper _mapper;

...

public TodoItemsController(TodoContext context)
{
    _context = context;

    var config = new MapperConfiguration(cfg =>
    {
        cfg.CreateMap<TodoItem, TodoItem>();
        cfg.CreateMap<TodoSubItem, TodoSubItem>();
    });
    _mapper = config.CreateMapper();
}
  • PutTodoItem メソッドを書き換える
[HttpPut("{id}")]
public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
{
    if (id != todoItem.Id)
    {
        return BadRequest();
    }

    // コメントアウトする
    //_context.Entry(todoItem).State = EntityState.Modified;

    try
    {
        var found = _context.TodoItems.Include(t => t.TodoSubItems).FirstOrDefault(x => x.Id == id);

        _mapper.Map(todoItem, found);

        _context.TodoItems.Update(found);

        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!TodoItemExists(id))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return NoContent();
}
ちょっと説明

UPDATE処理のメインは try 句の中です。一度データベースから該当するIDのデータを持ってきて、更新したいデータをAutoMapperで上書きします。Entity Frameworkの追跡機能が変更箇所を判別してくれるのでいい感じにデータ更新してくれます。正直この辺の仕組みは完全には理解できていないですが、とりあえず想定していた動きをしてくれます。

Postman を使ってテスト

最後にAPIが想定通り動作するか Postmanを使って確かめます。

  • Ctrl + F5 で起動する
  • 作成 (POST)。Postmanで以下のクエリを送信する
    • ※:ポート番号は環境によって異なります。
POST: https://localhost:44366/api/TodoItems
{
    "id": 0,
    "name": "Task1",
    "isComplete": false,
    "todoSubItems": [
        {
            "id": 0,
            "name": "Sub task1",
            "isComplete": false,
            "todoItemId": 0
        },
        {
            "id": 0,
            "name": "Sub task2",
            "isComplete": false,
            "todoItemId": 0
        }
    ]
}
  • レスポンス例
{
    "id": 1,
    "name": "Task1",
    "isComplete": false,
    "todoSubItems": [
        {
            "id": 1,
            "name": "Sub task1",
            "isComplete": false,
            "todoItemId": 1
        },
        {
            "id": 2,
            "name": "Sub task2",
            "isComplete": false,
            "todoItemId": 1
        }
    ]
}
  • 取得 (GET)。Postmanで以下のクエリを送信する
GET: https://localhost:44366/api/TodoItems/1
  • レスポンス例
{
    "id": 1,
    "name": "Task1",
    "isComplete": false,
    "todoSubItems": [
        {
            "id": 1,
            "name": "Sub task1",
            "isComplete": false,
            "todoItemId": 1
        },
        {
            "id": 2,
            "name": "Sub task2",
            "isComplete": false,
            "todoItemId": 1
        }
    ]
}
  • 更新 (PUT)。Postmanで以下のクエリを送信する
    • ※:サブタスク1を削除、サブタスク2を変更、新しいサブタスクの一つ追加した、という想定です。
PUT: https://localhost:44366/api/TodoItems/1
{
    "id": 1,
    "name": "Task1",
    "isComplete": false,
    "todoSubItems": [
        {
            "id": 2,
            "name": "Sub task2 (updated)",
            "isComplete": false,
            "todoItemId": 1
        },
        {
            "id": 0,
            "name": "New task",
            "isComplete": false,
            "todoItemId": 1
        }
    ]
}
  • 再取得 (GET)。Postmanで以下のクエリを送信する
GET: https://localhost:44366/api/TodoItems/1
  • レスポンス例
{
    "id": 1,
    "name": "Task1",
    "isComplete": false,
    "todoSubItems": [
        {
            "id": 2,
            "name": "Sub task2 (updated)",
            "isComplete": false,
            "todoItemId": 1
        },
        {
            "id": 3,
            "name": "New task",
            "isComplete": false,
            "todoItemId": 1
        }
    ]
}

サブタスクの削除、変更、追加が全て反映されているのが分かります。

まとめと感想

APIのデータの中にリスト(配列)を設けた場合の実装方法をまとめました。リスト自体をAPIのエンドポイントにして一つずつCRUDさせる設計もありますが、リストが完全に親の要素に従属している場合は今回のような設計のほうが簡単に利用できるので、ここで書いた内容が役立つ場面は意外とあると思います。この記事では書きませんでしたが、実際のAPIのモデルクラスとDBに記録するデータモデルクラスに同一のものを使う必要はありません。今回のサンプルだとサブタスクの外部キー TodoItemId はAPIに含める必要がない項目ですし、サブタスクのIDも本質的には不要です。そのため実際にはモデルクラスとデータモデルクラスを分けて定義します。その場合はクラス間のデータ変換方法が課題になりますが、やはりAutoMapperが便利なのでこの記事の方法と同じようなAutoMapperを使った実装で課題に対応できます。

ASP.NET Core SignalR でルーム付きチャットアプリを作ってみた

最近SignalRが気になったので公式チュートリアルやってみました。
分かりやすくていいんだけどリアルタイムアプリにありがちなチャットルーム毎に通信を分ける機能がなかったので自分で実装してみました。
余計な機能はつけずに、最低限ルーム内通信が成り立つ程度のシンプルな実装にしています。
尚、ルーム内通信を実現するのにSignalRのグループ機能を利用しています。
実装済みのコードはGitHubに上げてます。

バージョン

  •  Visual Studio 2019
  • .NET 5.0

プロジェクト作成

基本公式チュートリアル 通りです。

  • Visual Studioで新しいプロジェクトを作成する
  • 「ASP.NET Core Webアプリ」を選択する
    • ※同じVS2019でもインストール環境によっては名称が少し違うことがあります。
  • プロジェクト名は「SignalRGroupChat」とした

とりあえず実行

プロジェクトが作成出来たら、まずは初期状態で動くことを確認するため「F5」キーを押して実行します。

SignalRクライアントライブラリを追加する

公式チュートリアル 通りです。

  • ソリューションエクスプローラーで、プロジェクトを右クリックし、[追加] -> [クライアント側のライブラリ]を選択する
  • [クライアント側のライブラリを追加します]ダイアログで以下の通り選択する
    • プロバイダー: unpkg
    • ライブラリ:@microsoft/signalr@latest
    • [特定のファイルの選択]を選択する
    • dist/browser フォルダを展開し、 signalr.jssignalr.min.js を選択する
    • [ターゲットロケーション]に wwwroot/js/signalr/ と入力する
    • [インストール]をクリック

SignalRハブを作成する

基本は公式チュートリアル 通りだけど、ハブに実装するコードについてはグループ機能に関する処理に書き換えます。

  • プロジェクト直下に Hubs フォルダを作成する
  • Hubs フォルダ内に ChatHub.cs を作成し、以下のように編集する
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace SignalRGroupChat.Hubs
{
    public class ChatHub : Hub
    {
        public async Task SendMessageToGroup(string group, string user, string message)
        {
            await Clients.Group(group).SendAsync("ReceiveMessage", user, message);
        }

        public async Task AddToGroup(string groupName)
        {
            await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
        }
    }
}
ちょっと説明
  • SendMessageToGroup 関数について
    • チュートリアルでは全クライアントに送信するが、今回は指定したグループに送信する(参考
  • AddToGroup 関数について
    • クライアントをグループに追加する処理(参考

SignalRを構成する

公式チュートリアル 通りです。

  • Startup.cs を以下のように変更する
using SignalRGroupChat.Hubs;
...
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddSignalR();
}
...
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
        endpoints.MapHub<ChatHub>("/chatHub");
    });
}

SignalRクライアントコードを追加する

ここは公式チュートリアルとは少し異なります。
まず、トップページはグループ名を入力してもらう画面にします。
そして、チャットルームページを新たに作成してチャット画面を実装します。

  • Pages\Index.cshtml を以下のように変更する
@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

<div class="container">
    <div class="row">
        <div class="col-2">Room name</div>
        <div class="col-4"><input type="text" id="roomName" /></div>
    </div>
    <div class="row"> </div>
    <div class="row">
        <div class="col-6">
            <input type="button" id="moveToRoom" value="Move to room" />
        </div>
    </div>
</div>
<script src="~/js/index.js"></script>
  • wwwroot\js\index.js を作成し、以下のように編集する
document.getElementById("moveToRoom").addEventListener("click", function (event) {
    var roomName = document.getElementById("roomName").value;
    window.location.href = "Room/" + roomName;
});
  • Pages\Room.cshtml を作成し、以下のように編集する
    • VSを使っている場合はソリューションエクスプローラーで Pages フォルダを右クリックして[追加] -> [新しい項目] -> [Razorページ – 空]を選択すると良い(C#コードビハインドを自動生成してくれる)
@page "{roomname?}"
@model RoomModel
@{
    ViewData["Title"] = "Room";
}

<h1>@ViewData["Title"] : @Model.RoomName</h1>
<input type="hidden" id="roomName" value="@Model.RoomName" />
<div class="container">
    <div class="row"> </div>
    <div class="row">
        <div class="col-2">User</div>
        <div class="col-4"><input type="text" id="userInput" /></div>
    </div>
    <div class="row">
        <div class="col-2">Message</div>
        <div class="col-4"><input type="text" id="messageInput" /></div>
    </div>
    <div class="row"> </div>
    <div class="row">
        <div class="col-6">
            <input type="button" id="sendButton" value="Send Message" />
        </div>
    </div>
</div>
<div class="row">
    <div class="col-12">
        <hr />
    </div>
</div>
<div class="row">
    <div class="col-6">
        <ul id="messagesList"></ul>
    </div>
</div>
<script src="~/js/signalr/dist/browser/signalr.js"></script>
<script src="~/js/chat.js"></script>
  • Pages\Room.cshtml.cs を以下のように変更する
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace SignalRGroupChat.Pages
{
    public class RoomModel : PageModel
    {
        public string RoomName;

        public void OnGet(string roomName)
        {
            RoomName = roomName;
        }
    }
}
ちょっと説明

ここにはURLの末尾をルーム名として設定する処理を書いています。

  • wwwroot\js\chat.js を作成し、以下のように編集する
"use strict";

var connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build();

document.getElementById("sendButton").disabled = true;

connection.on("ReceiveMessage", function (user, message) {
    var li = document.createElement("li");
    document.getElementById("messagesList").appendChild(li);
    li.textContent = `${user} says ${message}`;
});

connection.start().then(function () {
    document.getElementById("sendButton").disabled = false;

    var roomName = document.getElementById("roomName").value;
    connection.invoke("AddToGroup", roomName).catch(function (err) {
        return console.error(err.toString());
    });

}).catch(function (err) {
    return console.error(err.toString());
});

document.getElementById("sendButton").addEventListener("click", function (event) {
    var roomName = document.getElementById("roomName").value;
    var user = document.getElementById("userInput").value;
    var message = document.getElementById("messageInput").value;
    connection.invoke("SendMessageToGroup", roomName, user, message).catch(function (err) {
        return console.error(err.toString());
    });
    event.preventDefault();
});
ちょっと説明
  • chat.js は公式チュートリアルのコードを参考にしています。
  • 公式チュートリアルに追加したのは以下の2点です。
    • SignalR接続開始時にグループに参加する処理を追加しています。
    • 送信ボタンを押したときにグループを指定してチャットメッセージ送信するようにしています。

アプリを実行する

  • 「F5」キーを押して実行します。
  • ブラウザが起動します。
  • トップページでルーム名を入力するとルーム専用のチャットページに遷移します。
  • 下の画像のようにルームに分かれてメッセージをリアルタイムで送受信できます。
    • 上2つのブラウザウィンドウはRoom1に、下2つはRoom2に参加している、という想定です。
チャットアプリ実行画面
チャットアプリ実行画面

まとめと感想

公式チュートリアルのSignalRサンプルアプリにルーム別メッセージ送受信機能を追加しました。そのためにSignalRのグループの仕組みを使って実装しました。
実装して感じたのはSignalRは高水準ライブラリだということ。クライアントとサーバで情報を送信しあうというより、お互いの関数を実行させる、というイメージでプログラミングできました。
ネットワーク上の流れを意識せずにプログラムの関数レベルでプログラミングできるのは処理内容に集中できるので使いやすいですね。

AutoMapperで詰め替えるDTOクラスにはinitアクセサーが使えそう(C#)

プログラム内のオブジェクトを外部に転送するためにDTO (Data Transfer Object) を使うことがありますが、C#であればAutoMapperという便利なライブラリがあります。

これまでAutoMapperでDTOに詰め替えする際、DTOクラスはプロパティは①getアクセサーのみでコンストラクタで代入するのが一般的でした。もしくは簡単に②setアクセサーを設定する方法もありました。

それぞれの方法の特徴は以下の通りです。

①getアクセサーのみでコンストラクタで代入

  • DTO生成後に意図せず値を書き換えられない(良いこと)
  • コンストラクタを作るのが面倒

②setアクセサーを設定する

  • DTO生成後に値を書き換えられてしまう
  • コンストラクタを作らなくていいので楽

それぞれ良いところと悪いところがありましたが、C#9.0 の新機能に initアクセサーが登場したことでこの2つのいいとこどりができるようになりました。特徴をまとめておきます。

③initアクセサーを使う

  • DTO生成後に値を書き換えられない(良いこと)
  • コンストラクタが不要

参考のため各方法のサンプルコードを載せておきます。

サンプルコード

①getアクセサーのみでコンストラクタで代入

using AutoMapper;
using System;

namespace AutoMapperTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var config = new MapperConfiguration(cfg => cfg.CreateMap<Item, ItemDto>());
            var mapper = config.CreateMapper();

            var item = new Item(1, "商品1", 100);
            ItemDto itemDto = mapper.Map<ItemDto>(item);

            Console.WriteLine("Item:");
            Console.WriteLine($"Id: {item.Id}, Name: {item.Name}, Price: {item.Price}");

            Console.WriteLine("ItemDto:");
            Console.WriteLine($"Id: {itemDto.Id}, Name: {itemDto.Name}, Price: {itemDto.Price}");

        }
    }

    class Item
    {
        public Item(int id, string name, int price)
        {
            Id = id;
            Name = name;
            Price = price;
        }

        public int Id { get; }
        public string Name { get; private set; }
        public int Price { get; private set; }
    }

    class ItemDto
    {
        public ItemDto(Item item) : this(item.Id, item.Name, item.Price)
        {
        }

        public ItemDto(int id, string name, int price)
        {
            Id = id;
            Name = name;
            Price = price;
        }

        public int Id { get; }
        public string Name { get; }
        public int Price { get; }
    }
}

②setアクセサーを設定する

using AutoMapper;
using System;

namespace AutoMapperTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var config = new MapperConfiguration(cfg => cfg.CreateMap<Item, ItemDto>());
            var mapper = config.CreateMapper();

            var item = new Item(1, "商品1", 100);
            ItemDto itemDto = mapper.Map<ItemDto>(item);

            Console.WriteLine("Item:");
            Console.WriteLine($"Id: {item.Id}, Name: {item.Name}, Price: {item.Price}");

            Console.WriteLine("ItemDto:");
            Console.WriteLine($"Id: {itemDto.Id}, Name: {itemDto.Name}, Price: {itemDto.Price}");

        }
    }

    class Item
    {
        public Item(int id, string name, int price)
        {
            Id = id;
            Name = name;
            Price = price;
        }

        public int Id { get; }
        public string Name { get; private set; }
        public int Price { get; private set; }
    }

    class ItemDto
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Price { get; set; }
    }
}

③initアクセサーを使う

using AutoMapper;
using System;

namespace AutoMapperTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var config = new MapperConfiguration(cfg => cfg.CreateMap<Item, ItemDto>());
            var mapper = config.CreateMapper();

            var item = new Item(1, "商品1", 100);
            ItemDto itemDto = mapper.Map<ItemDto>(item);

            Console.WriteLine("Item:");
            Console.WriteLine($"Id: {item.Id}, Name: {item.Name}, Price: {item.Price}");

            Console.WriteLine("ItemDto:");
            Console.WriteLine($"Id: {itemDto.Id}, Name: {itemDto.Name}, Price: {itemDto.Price}");

        }
    }

    class Item
    {
        public Item(int id, string name, int price)
        {
            Id = id;
            Name = name;
            Price = price;
        }

        public int Id { get; }
        public string Name { get; private set; }
        public int Price { get; private set; }
    }

    class ItemDto
    {
        public int Id { get; init; }
        public string Name { get; init; }
        public int Price { get; init; }
    }
}

CentOS8にGitLab Runnerをインストールする

自前のサーバで構築したGitLabでCI/CDを動かしたい場合はGitLab Runnerをインストールする必要がある。自分のGitLabサーバにGitLab Runnerをインストールする機会があったのでここに備忘録として残しておく。

前提

  • 自前のGitLabを既に構築しておりブラウザからrootアカウントでログインできる
  • GitLab RunnerサーバにSSHログインできてroot権限で作業できる
    • 今回はGitLabサーバとGitLab Runnerサーバを同一サーバに構築した
  • SELinuxを無効化している

バージョン

  • OS: CentOS8
  • GitLab: 13.2.3-ee
  • GitLab Runner: 13.2.2

準備

事前に以下の手順で設定情報をメモしておく。この情報はRunnerの設定時に利用する。

  1. GitLabにrootアカウントでログイン
  2. 「Admin Area」にアクセス
  3. Overview -> Runners ページにアクセス
  4. 「Set up a shared Runner manually」のURLとトークンをメモしておく

作業開始

ここからインストール、設定、起動の作業をする。GitLab RunnerサーバにSSHログインしrootアカウントになっておく。

Docker

今回はExecutor(CI/CDのjobを実行する実行環境)にDockerを採用するため、先にDockerをインストールし起動しておく。

インストール

curl -O https://download.docker.com/linux/centos/7/x86_64/stable/Packages/containerd.io-1.2.13-3.2.el7.x86_64.rpm
dnf install -y containerd.io-1.2.13-3.2.el7.x86_64.rpm
dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
dnf install -y docker-ce docker-ce-cli
systemctl start docker
systemctl enable docker

NAPTを設定

CentOS8のDockerではコンテナが名前解決のためホストと同じDNSサーバに問い合わせし、Firewallを起動している場合はIP到達性がなくなるためネットワークのエラーが発生してしまう。こちらの記事に載せられていた対策を実施する。

firewall-cmd --add-masquerade --permanent
firewall-cmd --reload

GitLab Runner

インストール

公式サイトを参考にした。

curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh | bash
dnf -y install gitlab-runner

設定

公式サイトを参考にした。以下のコマンドを実行して対話形式で情報を入力していく。

gitlab-runner register
Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com/):
https://xxxx.xxxx.xxxx/(ここに先ほどメモしたURLを貼り付ける)
Please enter the gitlab-ci token for this runner:
xxxxxxxxxxxxxx(ここに先ほどメモしたトークンを貼り付ける)
Please enter the gitlab-ci description for this runner:
[GitLab]: my-runner(この名前がRunner一覧ページに表示される)
Please enter the gitlab-ci tags for this runner (comma separated):
(指定しなくてもよい)
Please enter the executor: parallels, shell, ssh, kubernetes, custom, docker, docker-ssh, virtualbox, docker+machine, docker-ssh+machine:
docker(Executorはdockerにする)
Please enter the default Docker image (e.g. ruby:2.6):
alpine:latest(デフォルトにしたいDockerイメージを指定する)

確認

以下のコマンドでサービスが起動しているか確認できる。

systemctl status gitlab-runner

GitLabのWebページにrootアカウントでAdmin Areaにアクセスし、Runner一覧で今設定したのが登録されていることを確認する。

CI/CDを試してみる

簡単なPipelineを書いてRunnerを動かしてみる。

リポジトリを作成

GitLabのページからテスト用リポジトリを作成する。下の図では「runner-test」というリポジトリを作成している。

.gitlab-ci.ymlを作成

ブラウザでファイル作成、編集する便利機能があるのでそれを使ってみる。下の図の「New file」をクリックする。


「Select a template type」から「.gitlab-ci.yml」を選択


「Apply a template」から「Bash」を選択


「Commit changes」をクリックして、 .gitlab-ci.yml ファイルをリポジトリに追加する。

Pipelineの実行を確認

左メニューから「CI/CD」をクリックし、先ほどのcommitでパイプラインが実行されていることを確認する。(「Status」が「running」になっている)


しばらく待って「Status」が「passed」になればOK

AzureでCentOS8の仮想マシンを構築する

AzureでCentOS8の仮想マシンを構築する方法

仮想マシンを構築

まずはAzureのポータルにアクセスする。

トップページ左上の三本線をクリック

「Virtual Machines」をクリック

「+追加」をクリック

以下の情報を入力または選択し、「確認および作成」をクリック

  • 仮想マシン名:testVM(なんでもいい)
  • 地域:(Asia Pacific) 東日本
  • イメージ:CentOS-based 8.1
  • サイズ:Standard_B1ls(なんでもいいが検証なので一番安いやつ)
  • 認証の種類:パスワード
  • パスワード、パスワードの確認:任意のパスワード

確認ページが表示されるので問題なければ「作成」をクリック

デプロイが完了するまで待つ

デプロイが完了したら「リソースに移動」をクリック

仮想マシンにSSHログイン

「パブリックIPアドレス」をコピー

何かしらのSSHクライアントでログイン(下の図はPowerShell)

ssh AzureUser@(Public IP address)

ログインできた。

SSHできるIPアドレスを制限

このままだとどこからでもSSHログインできてしまうので、IPアドレスで制限する方法を載せておく。

仮想マシンのページから「ネットワーク」をクリックし、名前が「SSH」の行をクリック

以下の情報を入力し「保存」をクリック

  • ソース:IP Addresses
  • ソースIPアドレス/CIDR範囲:自分が使っているグローバルIPアドレス

リソースを削除

検証用途の場合、リソースを削除することを忘れずに。

仮想マシンを作ると他にもリソースが勝手に作られるのでリソースグループごと消すのが安心。

トップページ左上の三本線をクリックし、「リソースグループ」をクリック

今回作成したリソースグループ(今回は「testVM_group」)をクリック

「リソースグループの削除」をクリック

削除確認があるのでテキストボックスにリソースグループ名を入力して「削除」をクリック

これで今回作成した仮想マシンに関連するリソースがすべて削除される。