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
に設定してEmailMessage
のAttachmensts
の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ベースの通信していそうです。
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まで様々なものが転がっています。
ひょっとしたらここ覗くだけで解決する可能性のほうが高いかもしれません。
コンテナの作りかたを見る
たとえばdotnetとAzure Functions(Java)を組み合わせたいなーとなった場合があるかもしれません。
その場合はそれぞれの.devcontainer
の中にあるDockerfile
やbase.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 APIはMicrosoft.Identity.Web
を使用し、Azure ADで保護することが多いです。
そのため、YARPで実装したWeb API(以降YARP APIと呼称します)もAzure ADで保護を行いたいと考えました。
また、YARP APIから接続するWeb APIも別のAzure ADアプリケーションで保護されたWeb APIやMicrosoft 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(); }
これで、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を使用するよう設定
OnBeHalfOfProxy
はStartup.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突っ込んでみたりとか色々実験したりしています。
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を利用した認証です。
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
の配列を食わせることができます。
👆のような構成を自前で作り動作させることができます。
与えられた配列の先頭から検証が行われるようです(参考)
下記のように使用します。
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
これです。
サンプルなどで頻繁に登場しますし割愛します。
DeviceCodeCredential
Intuneなどで管理されているデバイスで使用できるDeviceCodeフローです。
僕の個人環境はIntune管理されているデバイスはないので割愛します。
EnvironmentCredential
環境変数に設定された値を参照して認証が行われます。
引数として値を指定するClientSecretCredential
やUsernamePasswordCredential
と違うのは環境変数内の値を参照する点ですね。
どのような値を設定するかはこちらを参照してください。
下記のように使用します。
var cred = new EnvironmentCredential(); var client = new SecretClient(new Uri(<KeyVaultUrl>), cred); var d = client.GetSecret("<SecretName>");
Azureのリソースの場合は基本Managed Instanceを利用することになると思います。
Azureのリソース外などで使用する場合に使用する感じですね。
内部で行われているのはClientSecretCredential
やUsernamePasswordCredential
と同じなので、設定としてもそれらと同じことをしてあげれば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の内容を抑えておけば、どのような認証ができるのか、どのような実装/設定になるのかといった点が想像しやすくなると思います。
今回実装したもののベースは下記リポジトリに格納しています ※色々自分で検証しやすくしているので、記事中のコードと少し構成が違います
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パッケージがリリースされます。タスクでプロパティなどに指定はしていません。まじか。
色々試してみる
VersionPrefixはどうなるのか
csprojの<version></version>
でバージョン指定するとそこで指定されたバージョンでNuGetパッケージが発行されます。
ってことは、同じようにVersionPrefix
を指定したら変わるのでは?ということで試してみました。
variables: vmImageOfBuild: windows-latest # version: '1.2.34' versionprefix: '3.2.1'
これはcsprojで設定したバージョンで生成してくれました。
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'
結果、パッケージのバージョンは書き換わりました。タスクは関係なく書き換わりそうです。
csprojでバージョン指定しなければどうなるだろうか
csprojのXML内からバージョン指定部分をごっそり消して下記のような構成にしてみました。
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> <GeneratePackageOnBuild>true</GeneratePackageOnBuild> </PropertyGroup> </Project>
この状態でも書き換わったので、csprojの構成内容に関わらずversion
を指定するとライブラリのバージョンが書き換わるようです。
まとめ
事前定義済み変数でもなさそうですし 、そもそもタスクでプロパティ指定していないのにVariable指定で勝手に書き換わるというのもどうにもバグ臭いです。
まぁPrefix/Sufixを外部から指定する方法面倒くさいっちゃ面倒くさいので楽にできるのはありがたいのですが、さすがにこれは…
フィードバックは投げますが、事前定義済み変数など触りの部分しか調べていないので、これが仕様とわかる何かがあれば教えていただきたいです🙇♂️
と、いうわけで現状version
変数を使用する場合は注意しましょう。というか使わないほうが無難かと思います。
他の変数でも同様の現象が発生する可能性も(めちゃ低いとは思いますが)ありますので、どうしても意図と違う動作から抜け出せないときは変数の名前を変更してみると、なにかヒントが得られるかもしれませんね。
パイプライン結果、検証で使用したソースコードなどは下記から確認可能です。
まんがリマインダーWebサービス(α)を公開してみました
Webサービスでまんがリマインダーを公開してみました。
どんなサービスなの?
キーワードを登録しておくと近日中に発売される/された漫画の一覧が表示されます。以上。
ブラウザのlocalstorage内にキーワード情報とか格納しているのでサイトへの登録とか必要ありません。
(ユーザー登録が必要ないサービスを自分が欲しかったので作ったというのもある)
あとはキーワードのブラウザ間の連携機能だったり
今調整中で一部の人(自分)しか使えませんがGoogleカレンダーへの連携機能だったりがあります。
私の欲しいもの且つ実験場的な要素が強いので、予告なく機能が追加されたり改善されたり変更されたりしますのでご了承ください。
構成の話
上図な感じの構成です。
- メインはAzureStaticWebAppsを使用
- WebAPIはAzure Functions
- Bring Your Owns Functionsしてみたかったのでそのような構成
- フロントはAngularで構成。バックエンド側はすべてC#で構成
- ストレージは今の所localStorageとSQL Databaseがメイン
- 漫画の発売情報元はRakuten WebAPI
- WebAPIの仕様上長時間のバッチ実行になりそうだったのでDurable Functionsで対応
- Amazonはなんか面倒くさかった(アフィとか色々めんどかったので気軽に使える方を使用)
- 画像データも合わせて取得して半年くらいキャッシュ効くように設定した上でBLOBに格納
StaticWebApps使ってちょっとサイト組んでみたい。。っていうのと
最近自粛生活で漫画買い忘れが頻発したのでちょうどええし作ってみるか。ってのが経緯です。
割と勢い駆動開発をしてきたので、Azureの構成等々改善する余地はまだまだありそうな感じです(すでにLoadが重い)。
ソースコードの構成の話
上図のような構成です。
- 一つのリポジトリでフロントからDBまで管理
- モノレポ構成ってやつですね
- リソースはすべてGitHubActionsでデプロイ
GitHub上で公開しています。気になる方は見てみていただければと。
まさかり歓迎しています。
今後の話
やりたいことはGitHubのIssueに書いています。
ほぼ自分用のサービスなので実験場にもなりそうな気はしていますが
なにか追加してほしい機能とかあればIssueかDiscussionに書いていただければと思います。
AngularからGoogle Calendar APIでイベントを登録する
はじめに
この記事のソースコードは下記のバージョンで構成されています。
- Angular: v12.1.3
参照されるタイミングによっては動作しない可能性がありますのでご留意ください。
やりたいこと
Front(Angular)のみで、任意のユーザーにサインインさせ、サインインしたユーザーのカレンダーに、Google Calendar APIを使用してイベント登録を行いたい。
想像以上にやり方が複雑怪奇な感じになったのでメモ書き程度に残しておきます。
GCPでGoogle Calendar APIを使用するための準備
Google Calendar APIを有効にする
認証の設定
「認証情報を作成」から「OAuthクライアントID」を選択します
設定対象 | 設定内容 |
---|---|
アプリケーションの種類 | ウェブアプリケーション |
名前 | 任意 |
承認済みのJavaScript生成元 | http://localhost:4200 |
承認済みのリダイレクトURL | http://localhost:4200 |
OAuth同意画面(スコープ)の設定
「OAuth同意画面」の設定の入力項目はほぼ任意な内容です。
「スコープを追加または削除」から必要なスコープを追加します。
必要なスコープはドキュメントを参考にします。今回はカレンダーにイベントを追加したいという目的です。
必要な処理は下記の通りなので、それぞれの処理で必要なスコープを確認しましょう。
- カレンダーのリストの取得
- 1.で取得したいずれかのカレンダーにイベントを登録
上記のドキュメントから必要なスコープは下記の通りでした
Angular側の実装
index.htmlの変更
GCPのAPIをどうこうするのに使用するライブラリなんかは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といったインスタンスはgapi
のload
やinit
で生成されるようです。
それぞれのインスタンスを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(); }
カレンダーリストの取得
上記で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) }); } }
結果、下図のように登録を行えるようになりました。
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の実行は同じような実装でできそう?という印象です。
今回検証で実装した全体は下記のリポジトリに格納しました。