はまったりひらめいたり…とか…

Angularや.NETやAzureやその他色々。

Azure Communication Services Emailを触ってみた話

はじめに

2022年5月25日時点の記事となります。

この時点でAzure Communication Services EmailはPreviewです。

記事中にでてくる画面キャプチャやコードは参照時期によっては変更されている可能性がありますのでご留意ください。

Azure Communication Services Email

Azure上でメール送信する仕組みは非推奨ということもあり

Azureでメールを取り扱いたいといった場合、Logic Appsを経由してOutlookで送るとか、外部サービスのSend Grid等を使用するかしかありませんでした。

Azure 上にメールサーバー/SMTP サーバーを構築する場合の注意事項 | Microsoft Docs

しかし、今回のBuildでAzure Communication Services Emailが発表されたことにより

外部サービスを使うことなく利用できることになりそうなので良きって感じですね!

早速見ていきたいと思います。

触ってみる

Communication Servicesを作成する

Communication Servicesを介してEmailが送信されるのでまずはCommunication Servicesの作成からです。

Data Locationは一応United Statesを選択。

Communication Services Emailを作成する

作成時の設定内容は特に難しいものはないので割愛

Domainの設定を行う

まずは雑にAzureのドメインを利用して作ってみます。

といってもClickしたら作られるだけなので特に設定作業ははさみません。簡単。

Communication ServicesとCommunication Services Emailを接続する

Communication Services側からEmailのDomainで先程作成したドメインと接続を行います。

接続もリソースを選択していくだけなので簡単ですね。

送信コードを組んでみる

今回はC#で組んでみようと思います。

NuGetのPrereleaseでAzure.Communication.Emailが提供されているのでそれを利用します。

接続文字列はCommunication Services側のKeysから取得します。

あとはこんな感じで送信。ほぼ公式のサンプル通りですがちょいちょい付け足しています、

using Azure.Communication.Email;
using Azure.Communication.Email.Models;

Console.WriteLine("Hello, World!");

var connectionString = @"Communication Servicesから取得した接続文字列";

var emailClient = new EmailClient(connectionString);

// EmailContentの文字列引数はメールタイトル
EmailContent emailContent = new EmailContent("Welcome to Azure Communication Services Email APIs.");

// 内容(Htmlが設定されたらHTML側が優先される?)
emailContent.PlainText = "This email meessage is sent from Azure Communication Services Email using .NET SDK.";
emailContent.Html = "<h1>HTMLのあれやこれや</h1>";

// 送信するアドレスの設定
List<EmailAddress> emailAddresses = new List<EmailAddress> { new EmailAddress("<送信先メールアドレス>") };
// EmailRecipientsのインスタンスは(To, Cc, Bcc)引数のようだ
EmailRecipients emailRecipients = new EmailRecipients(emailAddresses);

EmailMessage emailMessage = new EmailMessage("<送信元メールアドレス>", emailContent, emailRecipients);
SendEmailResult emailResult = emailClient.Send(emailMessage, CancellationToken.None);

// 送信結果の確認
var status = emailClient.GetSendStatus(emailResult.MessageId);

Console.WriteLine("おわり");

こんな感じで届きました。簡単ですね!

送信までは少し時間が掛かるようなので送信結果を抑えてから別の処理を行いたい場合は

emailClient.GetSendStatus(emailResult.MessageId)outForDeliveryになるまで待ちましょう。

ステータスはこんな感じみたいです。

添付ファイルはBase64変換したファイルをEmailAttachmentに設定してEmailMessageAttachmenstsのListに追加すれば良さそうです。

// 添付ファイル
string base64St = Convert.ToBase64String(File.ReadAllBytes(@"C:\temp\sample.txt"));
EmailAttachment attachment =  new EmailAttachment("さんぷる.txt", EmailAttachmentType.Txt, base64St);
emailMessage.Attachments.Add(attachment);

上記のコードで👇のように届きます。

MS Docsより詳細なサンプルも例のごとくGitHubに転がっているので何はともあれ見てみるのがいいと思います

気になること

まだベストプラクティスは出ていないですが、他のAzure SDKのClient同様RESTベースの通信していそうです。

ライブラリの実装:azure-sdk-for-net/EmailRestClient.cs at 7a5012587cee93bf34d23cc8caddb0dd915fba1c · Azure/azure-sdk-for-net · GitHub

SDKのプラクティス:C#.NET Guidelines: Implementation | Azure SDKs

なのでWeb Appsなどで構成する場合はSingleton構成にするのがベターなのかなーと思ってたりはしています。

送信元の情報変更

メールアドレスなど送信者の情報のメタデータを変更できます。

作成したメールドメインの設定画面から変更できるようです。

MailFromはDoNoyReplyから好きなものに変更できます

ユーザーがメールを開いたかなどのトラッキングも行えるようです。

設定を行ったあとメール送信すると下図のようになります。

名前やアドレスはもちろん、トラッキング用と思しきタグが生えているのも確認できます。

データの確認なんかはまだログだとできないんですかね。

他のサービスよろしくACS???みたいな感じで生えているかなーと少し期待していたのですが。ここもしばらく待っておけば出てくるかもしれません。

(EventLog追っかける元気はなかった…😫

価格

Public Preview段階ですが価格情報ももう出ています。

Email pricing - An Azure Communication Services concept document | Microsoft Docs

電子メール送信と転送されたデータで価格が決定するようです。

価格の例が出ているのでわかりやすいですね。

まとめ

Azure Communication Service Emailでメールの送信を行うことが確認できました。

色々とやりにくかった、Azureでのメール送信が楽になるかもしれませんね。

SendGridなどの外部サービスとくらべて何ができて何ができないかは気になるところではありますが、Azureの中で一つの選択肢ができたのはとてもわくわくすることだと思います。

Emailを使用するような機能を実装したい場合は積極的に検証していきたいですね(まだPreviewですが…

余談ですが、会社でSend Gridを使っているのですが、それと同じようなノリでSDKを使うことができました。

ロジックをうまく分離できていたらメール送信層だけすげ替えればあっさりと移行できるかもしれないのも良いなと思いました。

かんたんなdevcontainerのコンテナ構成作り方メモ

はじめに

この記事は2022年3月時点の情報で作成されています。

参照時期によっては掲載のコードなど動作しない可能性がありますのでその点ご留意ください。

とても便利なdevcontainer...だが…

Visual Studio Codeのdevcontainer(Visual Studio Code Remote - Containers)は非常に便利です。

が、世間一般で公開されているコンテナだけだと、ちょっと自分の開発用途に合わないってことが多いです。

なので基本的に自分でコンテナイメージを作ることになると思うのですが、これがまぁ大変です。

常日頃Dockerfileを弄るような開発をしているわけではないですし、一回作ったらあとはそれ使い続けるみたいなこともするのでなかなか食指が伸びない。

(.NETの開発なんかだと構築比較的楽だから余計に伸びない…)

なので、都度都度思い出せるように楽に作るためのメモ書きを残しておこうと思います。

devcontainerの作り方自体は記事が多く転がっていると思うので開発環境のコンテナづくりのメモになります。

devcontainerのDockerfileやdocker-composeファイルを楽に作る

そもそも自分の開発用途に合うものがないか探してみる

vscode-dev-containersのリポジトリに大量のサンプルが転がっています。

そもそもここの構成で自分の開発用途にあっているものがないか探してみるというのが良さそうです。

vscode-dev-containers/containers at main · microsoft/vscode-dev-containers · GitHub

.NETの開発からJava、Denoまで様々なものが転がっています。

ひょっとしたらここ覗くだけで解決する可能性のほうが高いかもしれません。

コンテナの作りかたを見る

たとえばdotnetAzure Functions(Java)を組み合わせたいなーとなった場合があるかもしれません。

その場合はそれぞれの.devcontainerの中にあるDockerfilebase.Dockerfileの中を見てみます。

Azure Functions(Java)の場合はDockerFile内にFROM mcr.microsoft.com/azure-functions/java:4-java11-core-toolsしているのですが

そもそもこいつどうやって作ってるのよ?って情報はコメントで書いてあるURLのhttps://github.com/Azure/azure-functions-docker/blob/dev/host/4/bullseye/amd64/java/java11/java11-core-tools.Dockerfileから参照することができます。

dotnetの場合はbase.Dockerfileですね。

このような感じで比較的シンプルな構成から開発環境を構築するための構成を、どんどんたどっていくこともやりやすいです。

なのでまずはこのリポジトリにあるものをベースに始めていくというのが良さそうだと思っていますし

開発用のコンテナを作るための初学者の勉強にも最適だと思います。

作ってみたもの

CsharpStudy/LearnDevContainer at master · Takas0522/CsharpStudy · GitHub

Angular + ASP.NET Core + Azure Functions(Java) + azurite な完全に俺得にしかならないような環境…

ただ、ここで書いている流れでやれば、こういうニッチな環境であってもコンテナ初学者でも構成しやすいだろうと思いました。

YARPを利用して実装したWeb APIをAzure ADで保護してOn-Behalf-Ofフローでトークンを再取得し通信する

はじめに

この記事は2021年11月に記述された内容です。

参照時期によっては記載している実装内容と異なっている可能性がありますのでその点ご留意ください。

記事中の実装内容はYarp.ReverseProxyの1.0.0をベースに実装されています。

YARP

Visual Studio 2022 Launch 記念 C# Tokyo イベントの発表中にYARPがリリースされたことが触れられていました。

触ってみたところ、欲しかったものだったので公式Docsに記載されている内容以外で少し検証したことを軽くまとめておこうと思います。

そもそもYARPとは

Yet Another Reverse ProxyでYARPらしいです。

名前の通りリバースプロキシで、ASP.NET Core上でプロキシルーティングや処理動作を簡易に使用したりカスタマイズしたりできるライブラリのようです。

具体的には下記のようにappsetting.json内に記述することでhttp://example.com/proxyone/proxyapiにアクセスするとhttp://other.example.com/proxyapiにアクセスしデータが取得されます。

{
...
  "ReverseProxy": {
    "Routes": {
      "proxy-one-route": {
        "ClusterId": "proxy-one-api",
        "Match": {
          "Path": "/proxyone/{*any}"
        },
        "Transforms": [
          { "PathRemovePrefix": "/proxyone" }
        ]
      }
    },
    "Clusters": {
      "proxy-one-api": {
        "Destinations": {
          "destination1": {
            "Address": "http://other.example.com/"
          }
        }
      }
    }
  }
...
}

どのような事ができるのか、またどのように実装すれば良いのかは公式のドキュメントYARPのGitHubのリポジトリsamplesサンプルソースが多数あります。非常に参考になりました。

検証「YARPで実装したWeb APIをAzure ADで保護してOn-Behalf-Ofフローでトークンを再取得する」

なにをしたのか

YARPの変換処理でOn-Behalf-Ofフローができるか確かめました

なぜそのようなことをしたのか

比較的社内アプリのようなものを作ることが多いため、Web APIMicrosoft.Identity.Webを使用し、Azure ADで保護することが多いです。

そのため、YARPで実装したWeb API(以降YARP APIと呼称します)もAzure ADで保護を行いたいと考えました。

また、YARP APIから接続するWeb APIも別のAzure ADアプリケーションで保護されたWeb APIMicrosoft Graphである可能性があるため

YARP APIの中でアクセストークンを適切に付与し直したいと欲求がありました。

AADアプリで保護したWeb APIから別のAADアプリで保護したWeb APIへのアクセスにOn-Behalf-Ofフローを使うことはしょっちゅうあるため、今回の検証を行いました。

実装内容

まずはYARP APIの保護

Microsoft.Identity.Webを使用して、Startup.csで設定するおなじみの方法で保護できました。

On-Behalf-OfフローでITokenAcquisitionを使用するためにEnableTokenAcquisitionToCallDownstreamApiを使用したりするのもおなじみの方法です。

public void ConfigureServices(IServiceCollection services)
{
  services.AddMicrosoftIdentityWebApiAuthentication(Configuration).EnableTokenAcquisitionToCallDownstreamApi().AddInMemoryTokenCaches();
}

参考:damienbod/AspNetCoreYarp

これで、YARP APIにアクセスするさいにアクセストークンがない場合401が返却されるようになりました。

特定のパスのときにOn-Behalf-Ofフローする

公式のサンプルReverseProxy.Auth.Sampleを参考にしました。公式ドキュメントではTransformと呼ばれている処理を実装します。

まずはルーティング時にTransformするよう設定

appsetting.jsonは下記のように設定しました。

{
...
  "ReverseProxy": {
    "Routes": {
      "proxy-one-route": {
        "ClusterId": "proxy-one-api",
        "AuthorizationPolicy": "default",
        "Match": {
          "Path": "/proxy-one-api/{*any}"
        },
        "Transforms": [
          { "PathRemovePrefix": "/proxy-one-api" },
          { "OnBeHalfOfProxy": "xxxxx" }
        ]
      }
    },
    "Clusters": {
      "proxy-one-api": {
        "Destinations": {
          "destination1": {
            "Address": "https://localhost:44352/api/"
          }
        }
      }
    }
...
}

proxy-one-apiのパスが来たときにproxy-one-apiのパスをリムーブ、今回実装したOnBeHalfOfProxyの変換を実行の後、https://localhost:44352/api/にアクセスされます。

OnBeHalfOfProxyの変換処理において、取得先のスコープ情報がほしいので、マスクしていますがValue値にはスコープ情報が記述されます。

Startup.csでカスタムTransformを使用するよう設定

OnBeHalfOfProxyStartup.csで設定されます。

public void ConfigureServices(IServiceCollection services)
{
...
  var proxyBuilder = services.AddReverseProxy().AddTransformFactory<ProxyAuthFactory>();
  proxyBuilder.LoadFromConfig(Configuration.GetSection("ReverseProxy"));
...
}

AddTransformFactoryで今回実装したオリジナルのTransformの実装であるProxyAuthFactoryを追加しています。

カスタムTransformの実装

ProxyAuthFactoryは下記のような実装です。実装している内容は記載しているコメントのとおりです

我ながらだいぶ雑味が強いので参考程度に…。

public class ProxyAuthFactory : ITransformFactory
{

    public bool Build(TransformBuilderContext context, IReadOnlyDictionary<string, string> transformValues)
    {
        // appsetting.jsonで設定するTransformの識別子"OnBeHalfOfProxy"
        if (transformValues.TryGetValue("OnBeHalfOfProxy", out var scope))
        {
            if (string.IsNullOrEmpty(scope))
            {
                throw new ArgumentException("A non-empty OnBeHalfOfProxy value is required");
            }
            context.AddRequestTransform(async transformContext => {
                // Transform時に実行される処理

                // Microsoft.Identity.WebのITokenAcquisitionを使用してアクセストークンを取得し直す。
               // 普通に使用する場合はDIを使用するが、タイミング的にコンストラクタで読み込めないためここでインスタンスを取得する
                var tokenAcquisition = transformContext.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
               // いつものOn-Behalf-Ofフローのアクセストークン取得処理
                var res = await tokenAcquisition.GetAccessTokenForUserAsync(new List<string> { scope });
               // Authorizationヘッダにアクセストークンを付与(Transform)
                transformContext.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", res);
            });
            return true;
        }
        return false;
    }

    // appsetting.jsonでこのTransformを使用する設定があった場合行われるチェック処理のようだ
    public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary<string, string> transformValues)
    {
        if (transformValues.TryGetValue("OnBeHalfOfProxy", out var value))
        {
            if (string.IsNullOrEmpty(value))
            {
               // アクセストークンの再取得にスコープ情報がほしいので、Valueを必須にした。
                context.Errors.Add(new ArgumentException("A non-empty OnBeHalfOfProxy value is required"));
            }

            return true; // Matched
        }
        return false;
    }
}

これでproxy-one-apiにアクセスした際に、指定したスコープでアクセストークンが再取得されるようになりました。

例えば、{ "OnBeHalfOfProxy": "user.read" }としたときはuser.readスコープでアクセストークンが再取得されるようになるので

「このパスのときはこのスコープでアクセストークン再取得して」といった設定が非常に簡単にできるようになっています。

APIMなんかで同じことをしたときは、XML上にC#スクリプトでゴニョゴニョする感じだったので、それと比べるとこちらのほうが個人的には好みです。

さいごに

というわけで、軽くYARPを触ってみたメモ書きでした。

APIMはお高すぎる&機能がFatすぎる的な理由で、独自に色々実装したりしていましたが、これを使用することで色々きれいに運用できそうな気がします。

今回検証に使用したソースコードは下記に格納されています。On-Behalf-Ofはもちろんですが

GET~DELETEとか、Formデータ送った場合とか、IFormFile受け取れるのかとか、DELETEでbody突っ込んでみたりとか色々実験したりしています。

github.com

Azure.IdentityのTokenCredentialの認証全部試す(その1)

はじめに

この記事は2021年10月時点の記事になります。

参照時期によっては記載している実装内容が動作しない可能性がありますので

その点ご留意いただければと思います。

Azure.IdentityのTokenCredential

Azure Key Vaultのシークレット値などを使用するときに大活躍してくれるAzure.Identityですが

認証のルールなどがいまいちわかりにくい状態になっています。

DefautCredentialを使うとAzure CLIの認証状態から引っ張ってきたりと、実装側の意図しない動作になることもよくあります。

Key Vaultのシークレットを取得する際に使用するSecretClientなどのインスタンス生成時に引数として提供するTokenCredentialのDocsを見ると認証は色々なものを使用できるようです。

どれがどの認証なんだろうということや、DefaultAzureCredentialの内部で行われているフローが原因でドハマりすることも多いので

とりあえず全部試してどのような認証ができるのか確認してみようと思います。

AuthorizationCodeCredential

AADアプリケーションを利用した認証で取得した承認コードを利用するものです。

承認コードの取得はこちらが参考になると思います。

認証は別のビジネスフローで終了してて別途シークレット取得したいときとかに使用する感じと思います。

ただこれはPKCEに対応していない古い認証方式で取得した認証コードでしか使用できないようです(参考)。

なので、使用範囲は限定的、かつあまり使うという選択肢は取らないほうがよいものという印象です。

下記のように使用します。

var cred = new AuthorizationCodeCredential(<TenantId>, <ClientId>, <ClientSecret>, <AuthCode(0.AVY...)>, new AuthorizationCodeCredentialOptions { RedirectUri = new Uri(@<RedirectUri>) });
var client = new SecretClient(new Uri(<KeyVaultUrl>), cred);
var d = client.GetSecret("<SecretName>");

必要なパラメータから分かる通り、内部でアクセストークンを取得するフローが実行されるので

Azure ADアプリケーションでリダイレクトURLの設定や、シークレットの生成、Azure Key Vaultへのアクセス許可が必要です。

また前述の通りv1エンドポイントを使用する必要があるため、プラットフォームはSingle Page ApplicationではなくWebを選択します。

AzureCliCredential

DefaultAzureCredentialの図中にあるAzure CLIを利用した認証です。

f:id:TakasDev:20211031122508p:plain

Azure CLIを使用している人しかいない開発現場であれば積極的に使ってもいいかと思いますが

個人的にはあまり外部ツールに依存する作りにするのは好きじゃないので僕は積極的に使わないと思います。

下記のように使用します。

var cred = new AzureCliCredential();
var client = new SecretClient(new Uri(<KeyVaultUrl>), cred);
var d = client.GetSecret("<SecretName>");

テナントをいくつも持っている人は下記のようにテナントを指定したほうが良いかもしれません。

var cred = new AzureCliCredential(new AzureCliCredentialOptions { TenantId = TenantId });
var client = new SecretClient(new Uri(<KeyVaultUrl>), cred);
var d = client.GetSecret("<SecretName>");

az loginしているユーザーがKey Vaultにアクセスすることになるので、アクセスポリシーにユーザーを追加しといてあげましょう。

AzurePowerShellCredential

DefaultAzureCredentialの図中にあるAzure PowerShellを利用した認証です。

Azure CLIと同じような感じなので感想は割愛します。

下記のように使用します。

var cred = new AzurePowerShellCredential();
// テナントをいくつも持っている人は下記のようにテナントを指定したほうが良いかもしれません。
// var cred = new AzurePowerShellCredential(new AzurePowerShellCredentialOptions { TenantId = TenantId });
var client = new SecretClient(new Uri(<KeyVaultUrl>), cred);
var d = client.GetSecret("<SecretName>");

ChainedTokenCredential

TokenCredentialの配列を食わせることができます。

f:id:TakasDev:20211031122508p:plain

👆のような構成を自前で作り動作させることができます。

与えられた配列の先頭から検証が行われるようです(参考)

下記のように使用します。

var f = new AzurePowerShellCredential(new AzurePowerShellCredentialOptions { TenantId = TenantId });
var s = new AzureCliCredential(new AzureCliCredentialOptions { TenantId = TenantId });
var t = new ClientCertificateCredential(TenantId, ClientId, CertPath);
var list = new List<TokenCredential> { f, s, t };

var cred = new ChainedTokenCredential(list.ToArray());
var client = new SecretClient(new Uri(<KeyVaultUrl>), cred);
var d = client.GetSecret("<SecretName>");

PowerShell -> Azure CLI -> ClientCertificateの順番でトークン取得が検証/実行されます。

個人的に実装するなら、Env->Managed Identity->Intaractiveな順番が良いかと思ってます。

Azure上に展開するときはEnvironment/ManagedIdentityを参照し、開発時はユーザーがログインすることでシークレットにアクセス可能になります。

上述の通り外部ツールに依存する構成が個人的に好きではないというだけの理由です。

最終的にIntractive見るって書いてあるんですけど、なーんかあんまりいい印象がないので自前で途中に変なのが入らないようにしちゃうのが良いと思ってます。

ClientCertificateCredential

クライアント証明書による認証です。Azure ADアプリケーションに証明書を登録することで使用します。

証明書ストアにある証明書でもローカルのpfxファイルを指定する方法でも使用可能です。

この認証では裏で生成されるトークン内にユーザー情報が付与されません。

そのため、Azure Key VaultのアクセスポリシーにはユーザーではなくAzure ADアプリケーションを設定しておく必要があります。

下記のように使用します。

var cred = new ClientCertificateCredential(<TenantId>, <ClientId>, <CertPath>);
var client = new SecretClient(new Uri(<KeyVaultUrl>), cred);
var d = client.GetSecret("<SecretName>");

個人的にはあまり使わないと思います。

ClientSecretCredential

Azure ADアプリケーションのシークレット認証です。

AuthorizationCodeCredentialと同じくシークレットを使用しますが

ClientCertificateCredentialと同じくユーザー情報を伴わない認証となります。

下記のように使用します。

var cred = new ClientSecretCredential(<TenantId>, <ClientId>, <ClientSecret>);
var client = new SecretClient(new Uri(<KeyVaultUrl>), cred);
var d = client.GetSecret("<SecretName>");

EnvironmentやManaged IdentityがあるためAzureのリソース上で利用する場合はあまり使うことはないと思います。

Azureのリソース外で展開する場合で、ユーザーの認証を伴わない場合に利用することになるかと思います。

DefaultAzureCredential

f:id:TakasDev:20211031122508p:plain

これです。

サンプルなどで頻繁に登場しますし割愛します。

DeviceCodeCredential

Intuneなどで管理されているデバイスで使用できるDeviceCodeフローです。

僕の個人環境はIntune管理されているデバイスはないので割愛します。

EnvironmentCredential

環境変数に設定された値を参照して認証が行われます。

引数として値を指定するClientSecretCredentialUsernamePasswordCredentialと違うのは環境変数内の値を参照する点ですね。

どのような値を設定するかはこちらを参照してください。

下記のように使用します。

var cred = new EnvironmentCredential();
var client = new SecretClient(new Uri(<KeyVaultUrl>), cred);
var d = client.GetSecret("<SecretName>");

Azureのリソースの場合は基本Managed Instanceを利用することになると思います。

Azureのリソース外などで使用する場合に使用する感じですね。

内部で行われているのはClientSecretCredentialUsernamePasswordCredentialと同じなので、設定としてもそれらと同じことをしてあげればOKです。

InteractiveBrowserCredential

名前の通りInteractiveに認証が行われます。ブラウザでユーザーID/パスワード入力画面が立ち上がります。

デスクトップアプリケーションから実行する場合はプラットフォームMobile and Desktop Applicationsで認証が通るようにしておく必要があります。

下記のように使用します(コンソールアプリケーションで使用するための実装なのでその点ご留意ください)。

var cred = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { ClientId = ClientId, TenantId = TenantId, RedirectUri= new Uri(@"http://localhost/") });
var client = new SecretClient(new Uri(<KeyVaultUrl>), cred);
var d = client.GetSecret("<SecretName>");

ローカルからアクセスする場合はこれが一番都合が良いかなと思います。

ManagedIdentityCredential

Azureのリソースに展開する場合はこれがド安定だと思います。

生成されたManaged IdentityをAzure Key Vaultのアクセスポリシーに設定してあげましょう。

下記のように使用します。

var cred = new ManagedIdentityCredential();
var client = new SecretClient(new Uri(<KeyVaultUrl>), cred);
var d = client.GetSecret("<SecretName>");

ただ当然ながらローカルだと使えないので、開発時はChainedTokenCredentialを使って他の認証方法と組み合わせて使うことになると思います。

以下次回

思ったよりボリュームが多かったので残ったものは次回にしようと思います。

ただ、ここまで見てわかったとおり、多くはAzure ADアプリケーションを使用した認証フローで構成されていることがわかります。

認証周りのDocsの内容を抑えておけば、どのような認証ができるのか、どのような実装/設定になるのかといった点が想像しやすくなると思います。

今回実装したもののベースは下記リポジトリに格納しています ※色々自分で検証しやすくしているので、記事中のコードと少し構成が違います

github.com

Azure DevOpsのパイプラインで `version` という名前の変数を作るとNuGetパッケージ生成で面倒くさいことになった話

はじめに

2021年10月時点の情報です。

参照時期によっては動作が変わっている可能性がありますのでご注意ください。

なにをしようとしていたか

.NETのライブラリプロジェクトをビルドしてNuGetパッケージをビルドしようとしています。

ライブラリのバージョンはPrefix/SuffixをCIコマンド上で指定して外からバージョン情報を変更できるようにしたいと考えています。

そこで、Powershellで取得した値をversion変数に格納し、その値をmsbuildタスクのmsbuildArgumentsで使おうとしていました。

しかし、このバージョン指定が全く意図したとおりに動きません。

下記は検証を踏まえて作成した再現可能な必要最低限のymlとcsprojファイルの構成なります。

  • csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
    <AssemblyVersion>1.2.3.4</AssemblyVersion>
    <FileVersion>1.2.3.4</FileVersion>
    <VersionPrefix>1.2.3</VersionPrefix>
  </PropertyGroup>
</Project>
  • CIのyml
trigger:
  branches:
    include:
    - master

variables:
  vmImageOfBuild: windows-latest
  version: '1.2.34' #👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈👈

stages:
  - stage: pre_build
    displayName: PreBuild
    jobs:
      - job: build
        pool:
          vmImage: $(vmImageOfBuild)
        steps:
        - task: NuGetCommand@2
          inputs:
            command: 'restore'
            feedsToUse: 'config'
            includeNuGetOrg: true
            restoreSolution: 'SampleLibrary/*.sln'
        - task: MSBuild@1
          displayName: 'Build PreProd'
          inputs:
            solution: '**/SampleLibrary.csproj'
            configuration: Release

何が起きるか

csprojで指定したバージョンでNuGetパッケージが生成されてほしいところですが

CIのyamlで作成したversion変数の1.2.34でNuGetパッケージがリリースされます。タスクでプロパティなどに指定はしていません。まじか。

f:id:TakasDev:20211017155054p:plain

色々試してみる

VersionPrefixはどうなるのか

csprojの<version></version>でバージョン指定するとそこで指定されたバージョンでNuGetパッケージが発行されます。

ってことは、同じようにVersionPrefixを指定したら変わるのでは?ということで試してみました。

variables:
  vmImageOfBuild: windows-latest
  # version: '1.2.34'
  versionprefix: '3.2.1'

これはcsprojで設定したバージョンで生成してくれました。

f:id:TakasDev:20211017160007p:plain

versionprefix / versionPrefix / VersionPrefix など試してみましたが変わらなかったので、影響があるのはversion変数だけのようです。

msbuildタスクだけなのか

おそらく一番良く使うdotnetタスクでも同様の現象は発生するのでしょうか?

csprojの構成はそのままに、ymlを下記のように変更してみました。

trigger:
  branches:
    include:
    - master

variables:
  vmImageOfBuild: windows-latest
  version: '1.2.34-dev.0'

stages:
  - stage: pre_build
    displayName: PreBuild
    jobs:
      - job: build
        pool:
          vmImage: $(vmImageOfBuild)
        steps:
        - task: DotNetCoreCLI@2
          inputs:
            command: 'restore'
            projects: '**/*.csproj'
        - task: DotNetCoreCLI@2
          inputs:
            command: 'build'
            projects: '**/*.csproj'

結果、パッケージのバージョンは書き換わりました。タスクは関係なく書き換わりそうです。

f:id:TakasDev:20211017161706p:plain

csprojでバージョン指定しなければどうなるだろうか

csprojのXML内からバージョン指定部分をごっそり消して下記のような構成にしてみました。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
  </PropertyGroup>

</Project>

f:id:TakasDev:20211017162004p:plain

この状態でも書き換わったので、csprojの構成内容に関わらずversionを指定するとライブラリのバージョンが書き換わるようです。

まとめ

事前定義済み変数でもなさそうですし 、そもそもタスクでプロパティ指定していないのにVariable指定で勝手に書き換わるというのもどうにもバグ臭いです。

まぁPrefix/Sufixを外部から指定する方法面倒くさいっちゃ面倒くさいので楽にできるのはありがたいのですが、さすがにこれは…

フィードバックは投げますが、事前定義済み変数など触りの部分しか調べていないので、これが仕様とわかる何かがあれば教えていただきたいです🙇‍♂️

と、いうわけで現状version変数を使用する場合は注意しましょう。というか使わないほうが無難かと思います。

他の変数でも同様の現象が発生する可能性も(めちゃ低いとは思いますが)ありますので、どうしても意図と違う動作から抜け出せないときは変数の名前を変更してみると、なにかヒントが得られるかもしれませんね。

パイプライン結果、検証で使用したソースコードなどは下記から確認可能です。

dev.azure.com

まんがリマインダーWebサービス(α)を公開してみました

Webサービスでまんがリマインダーを公開してみました。

manrem.devtakas.jp

f:id:TakasDev:20210815114038p:plain

どんなサービスなの?

キーワードを登録しておくと近日中に発売される/された漫画の一覧が表示されます。以上。

ブラウザのlocalstorage内にキーワード情報とか格納しているのでサイトへの登録とか必要ありません。

(ユーザー登録が必要ないサービスを自分が欲しかったので作ったというのもある)

あとはキーワードのブラウザ間の連携機能だったり

今調整中で一部の人(自分)しか使えませんがGoogleカレンダーへの連携機能だったりがあります。

私の欲しいもの且つ実験場的な要素が強いので、予告なく機能が追加されたり改善されたり変更されたりしますのでご了承ください。

構成の話

f:id:TakasDev:20210822160956p:plain

上図な感じの構成です。

  • メインはAzureStaticWebAppsを使用
  • WebAPIはAzure Functions
    • Bring Your Owns Functionsしてみたかったのでそのような構成
  • フロントはAngularで構成。バックエンド側はすべてC#で構成
  • ストレージは今の所localStorageとSQL Databaseがメイン
  • 漫画の発売情報元はRakuten WebAPI
    • WebAPIの仕様上長時間のバッチ実行になりそうだったのでDurable Functionsで対応
    • Amazonはなんか面倒くさかった(アフィとか色々めんどかったので気軽に使える方を使用)
    • 画像データも合わせて取得して半年くらいキャッシュ効くように設定した上でBLOBに格納

StaticWebApps使ってちょっとサイト組んでみたい。。っていうのと

最近自粛生活で漫画買い忘れが頻発したのでちょうどええし作ってみるか。ってのが経緯です。

割と勢い駆動開発をしてきたので、Azureの構成等々改善する余地はまだまだありそうな感じです(すでにLoadが重い)。

ソースコードの構成の話

f:id:TakasDev:20210822161237p:plain

上図のような構成です。

  • 一つのリポジトリでフロントからDBまで管理
    • モノレポ構成ってやつですね
  • リソースはすべてGitHubActionsでデプロイ

GitHub上で公開しています。気になる方は見てみていただければと。

まさかり歓迎しています。

GitHub - Takas0522/ComiCal

今後の話

やりたいことはGitHubのIssueに書いています。

ほぼ自分用のサービスなので実験場にもなりそうな気はしていますが

なにか追加してほしい機能とかあればIssueかDiscussionに書いていただければと思います。

AngularからGoogle Calendar APIでイベントを登録する

はじめに

この記事のソースコードは下記のバージョンで構成されています。

  • Angular: v12.1.3

参照されるタイミングによっては動作しない可能性がありますのでご留意ください。

やりたいこと

Front(Angular)のみで、任意のユーザーにサインインさせ、サインインしたユーザーのカレンダーに、Google Calendar APIを使用してイベント登録を行いたい。

想像以上にやり方が複雑怪奇な感じになったのでメモ書き程度に残しておきます。

GCPGoogle Calendar APIを使用するための準備

Google Calendar APIを有効にする

GCPのプロジェクト画面からAPIを有効にしていきます。

f:id:TakasDev:20210731212435p:plain

認証の設定

f:id:TakasDev:20210731212726p:plain

「認証情報を作成」から「OAuthクライアントID」を選択します

設定対象 設定内容
アプリケーションの種類 ウェブアプリケーション
名前 任意
承認済みのJavaScript生成元 http://localhost:4200
承認済みのリダイレクトURL http://localhost:4200

OAuth同意画面(スコープ)の設定

f:id:TakasDev:20210731213219p:plain

「OAuth同意画面」の設定の入力項目はほぼ任意な内容です。

「スコープを追加または削除」から必要なスコープを追加します。

必要なスコープはドキュメントを参考にします。今回はカレンダーにイベントを追加したいという目的です。

必要な処理は下記の通りなので、それぞれの処理で必要なスコープを確認しましょう。

  1. カレンダーのリストの取得
  2. 1.で取得したいずれかのカレンダーにイベントを登録

上記のドキュメントから必要なスコープは下記の通りでした

Angular側の実装

index.htmlの変更

GCPAPIをどうこうするのに使用するライブラリなんかはnpmなどで配布されていないように見受けられました。

ひとまずここは公式のドキュメント通りにスクリプトファイルをindex.htmlで読み込みます。

<!doctype html>
<html lang="jp">
<head>
  <script defer src="https://apis.google.com/js/api.js"></script> <!--追加-->
  <meta charset="utf-8">
  <title>LearnGoogleOAuth</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root></app-root>
</body>
</html>

各種@typesをインストール

とはいえTypeScriptな型の世界で開発を行いたいので、型定義をインストールします。

npm install --save-dev @types/gapi @types/gapi.auth2 @types/gapi.calendar

今回使用するGAPIのベース、認証、カレンダーAPIの型定義です。

型定義を使用したいファイル内で定義を読み込んでおきましょう

/// <reference types="gapi"/>
/// <reference types="gapi.auth2"/>
/// <reference types="gapi.calendar"/>

GAPIのLoad処理

GAPIのAuthやCalendarといったインスタンスgapiloadinitで生成されるようです。

それぞれのインスタンスをLoadする処理を実装します。

export class AppSerivce {
  ...
  private authClient!: gapi.auth2.GoogleAuth;

  clientLoad(): void {
    // gapi.authインスタンスを使用できるようにロードする(scopeはOAuthの同意画面で設定したScopeを指定)
    gapi.load('client:auth2', () => {
      this.authClient = gapi.auth2.init({
        client_id: environment.gapiClientId,
        fetch_basic_profile: true,
        scope: 'openid https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/calendar.events ',
      });
      // gapi.client.calendarを使用できるようにロードする
      gapi.client.init({
        discoveryDocs: ['https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest']
      })
    });
  }
  ...
}

サインイン

上記でgapi.authインスタンスの準備ができている場合は下記のコマンドでサインインが実行できます。

  async signIn(): Promise<void> {
    const res = await this.authClient.signIn();
  }

f:id:TakasDev:20210731221133p:plain

カレンダーリストの取得

上記でgapi.client.calendarインスタンスの準備ができている場合は下記のコマンドでカレンダーリストの取得ができます。

  getCalendarList() {
    // gapi.client.calendarインスタンスが生成されていると使用できる
    const req = gapi.client.calendar.calendarList.list()
    req.execute((res) => {
      // resにカレンダー情報が返却される
    })
  }

イベントの登録

上記でgapi.client.calendarインスタンスの準備ができている場合は下記のコマンドでイベントの登録が行えます。

イベント登録時に使用できるパラメータはドキュメントを参考にしてください。

  registerEvents(calendarId: string): void {
    const event: gapi.client.calendar.EventInput = {
      summary: 'Test',
      start: {
        dateTime: '2021-07-31T00:00:00.000Z'
      },
      end: {
        dateTime: '2021-07-31T00:00:00.000Z'
      }
    };
    const req = gapi.client.calendar.events.insert({
      calendarId,
      resource: event
    });
    req.execute((res) => {
      // 結果がresに返却される
      console.log(res)
    });
  }
}

結果、下図のように登録を行えるようになりました。

f:id:TakasDev:20210731223051p:plain

gapi.cllient.calendarインスタンスを生成しないやりかた

gapi.client.calendarを介さずに、clientからリクエスト直接叩くことでもイベント登録は可能です。

  registerEvents(calendarId: string): void {
    const event: gapi.client.calendar.EventInput = {
      summary: 'Test',
      start: {
        dateTime: '2021-07-31T00:00:00.000Z'
      },
      end: {
        dateTime: '2021-07-31T00:00:00.000Z'
      }
    };
    const req = gapi.client.request({
      path: `/calendar/v3/calendars/${calendarId}/events`,
      method: 'POST',
      body: event
    });
    req.execute((res) => {
      // 結果がresに返却される
      console.log(res)
    });
  }
}

APIに対応するインスタンスの生成ができなかった場合や、インスタンス内にインターフェースがなかった場合なんかはこっちの方法になりますね。

サクッと検証したい場合もこっちのほうが楽だったりするかもしれません。

さいごに

APIに対応するインスタンスのLoadなど変わった実装が必要なので最初は面食らいました。

見たのはカレンダーだけですが他の@typesとか見るに、Googleが提供するOAuthを伴うWebAPIの実行は同じような実装でできそう?という印象です。

今回検証で実装した全体は下記のリポジトリに格納しました。

github.com