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

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

VisualStudioからAzure KeyVaultにアクセスするときにえらいハマった話

はじめに

今回の記事は下記の環境で検証を行っています。

記事を参照されるタイミングによっては画面構成や設定などが変わる可能性がありますのでご留意ください。

※以降Visual StudioはVSと記載しています

発生した問題

とあるアカウントを使用した場合、VSのConnected Serviceを使用してAzure Key Vaultにアクセスできない。

結論

16.7以降のVisualStudioからAzureに接続するための認証を行う場合、認証がうまく行かない場合があるようです。

Azureのリソースは認識できてるのに、肝心の接続のときはうまくいかない!

そんな場合はNuGetでAzure.Identityのバージョンを1.2.2以上に上げて、下記のいずれかを行うことでうまくいくようです。

  • launchSettings.jsonAZURE_TENANT_IDを指定する
  • new DefaultAzureCredential(new DefaultAzureCredentialOptions { VisualStudioTenantId = "<AzureのテナントID>" }))を指定する

どえらいハマってVSアンインストールとかして悔しい感じなので、以降、解決までの試行錯誤やらの過程の話です。

ConnectedServiceでAzure KeyVaultにアクセスできなくなった

VS16.7からVisualStudioのConnectedServiceを使用した際に構成される内容が変わりました。

開発時は基本的にユーザーシークレットを使用すると思うのですがちょっと使ってみたろと思ったのがすべての始まりでした。

ひとまず、Azure KeyVaultへの接続設定をVSから行ってみます。

f:id:TakasDev:20200905132659p:plain

f:id:TakasDev:20200829111829p:plain

構成されるライブラリだけで見ると👆な感じの変更内容のようです。

Azure.Identityを使用するような変更が行われたという感じですね。

さて、これで実行を行うとエラーとなり実行ができませんでした。

f:id:TakasDev:20200905132549p:plain

正常に実行できる環境もあり、いわゆるおま環事象といった感じです。

AzureADアプリケーションを調べる

MsalServiceException: AADSTS70002: The client does not exist or is not enabled for consumers. If you are the application developer, configure a new application through the App Registrations in the Azure Portal at https://go.microsoft.com/fwlink/?linkid=2083908.

エラー内容は上記のとおりです。指定されたAzureADアプリが存在しないようです。

なるほどー。というわけでAzureADアプリを作成し、KVのアクセスポリシーに割り当てます。

f:id:TakasDev:20200905133919p:plain

Program.csも下記のように作成したClientIdを使用するように書き換えました。

.ConfigureAppConfiguration((context, config) =>
{
    var keyVaultEndpoint = new Uri(Environment.GetEnvironmentVariable("VaultUri"));
        config.AddAzureKeyVault(
        keyVaultEndpoint,
        new DefaultAzureCredential(new DefaultAzureCredentialOptions { ManagedIdentityClientId = "<作成したADアプリID>" }));
})

しかし結果は変わりませんでした。

別の認証が通るか試してみる

Azure SDK: What’s new in the Azure Identity August 2020 General Availability Releaseという記事でライブラリについて詳細にかかれています。

f:id:TakasDev:20200905132859p:plain

ひとまずVisualStudioの認証で失敗したけど、他の認証で成功してたらおkなのでは?と短絡に考え

Azure CLIでログイン状態にしてリトライしてみましたが結果は同じでした。

ADアプリの設定の問題かな?と思ったので正常に動作する側のAzureADアプリを見てみようと考えました。

認証フローを追う

設定を見るにも認証に使用されているAzureADのClientIDを見ないと始まりません。

そこでどのような認証が行われているのか正常に動作する環境を動かしながらFiddlerで追っかけてみました。

色々アレな情報が出てくるのでキャプチャは控えますが

ここで使用されているであろうClientIDの特定はできましたが、正常に動作する環境上でもAzureADアプリは見つかりませんでした。

で、あれば問題の根本はアプリが存在しないということはないだろうな。ということで別の線を当たることにしました。

テナントを指定する

先程記載したブログの記事でテナントの指定の仕方を記載した項目があります

authenticate-to-a-specific-tenant

僕のアカウントはAD/AD B2C含め複数のテナントに所属しており、且つオーナーとなっているテナントも複数あるのでHomeテナントを誤認したのかもしれません。

まずは👆記事に記載されている内容で解決できるか確認してみます。

Azure.Identityの1.1.1のDefaultAzureCredentialOptionsVisualStudioTenantIdは存在しないので

Azure.Identityの1.2.2に上げる必要があります。

.ConfigureAppConfiguration((context, config) =>
{
    var keyVaultEndpoint = new Uri(Environment.GetEnvironmentVariable("VaultUri"));
    config.AddAzureKeyVault(
        keyVaultEndpoint,
    new DefaultAzureCredential(new DefaultAzureCredentialOptions { VisualStudioTenantId = "<Azure Tenant Id>" }));
})

これでやっと動作するようになりました。

その後に試したこと

DefaultAzureCredentialOptionsを指定せずに解決できないか試しました。

VisualStudioのAzureサービス認証のアカウント絞り込み

VisualStudioの認証で使用しているアカウントに紐づくテナントを絞り込めば同じ状態になるかな?

と思い試してみましたが結果は変わりませんでした。

f:id:TakasDev:20200905142122p:plain

lauch.settings.jsonAZURE_TENANT_IDを指定する

この方法はうまくいきました。実行ソース側をいじるのが嫌であればこちらを指定するほうが良い感じですね。

おわりに

認証まわりで沼ると解決まで時間がかかるので

ローカルでの開発時はユーザーシークレットがド安定ですね😏

msal.js v2.0.1 をAngularで使用する

はじめに

今回の記事で使用するコードは下記の構成で実装しています。

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

  • @angular/cli:10.0.5
  • @azure/msal-browser:2.0.1

msal.js v2.0.0がリリース

先月の末頃(2020年7月21日)にmsal.jsのv2.0.0 がリリースされました

(色々試している間にv2.0.1がリリースされちゃいましたが…

PKCEの対応が行われ、よりかんたんにセキュアに使用することができるようになります。

詳細なお話は公式のドキュメントや、今月の.NETラボを見ていただくとして…

ひとまず、僕がメインで使用しているAngularでの使用方法を確認していこうと思います。

@azure/msal-angular

msal.jsには派生ライブラリとして、Angular対応された@azure/msal-angularというライブラリが存在します。

ServiceやInterceptorが提供され、Angularで使用するのに便利なライブラリとなっています。

f:id:TakasDev:20200809223243p:plain

👆microsoft-authentication-library-for-jsより抜粋

上図の通り、@azure/msal-angularもv2.0がPKCE対応されるはずなのですが、まだリリースされていないようです。

Roadmapにも特に予定は明記されていないようですが

何はともあれ使用したいので、プレーンな@azure/msal-browserをAngularで使用してみましょう。

@azure/msal-browserをAngularで使用する。

基本的な使い方は@azure/msal-browserのドキュメントの通りで問題ありません。

ライブラリのインポート

npm install @azure/msal-browser

msalアプリケーションインスタンスの生成

msalのv1からmsalの処理のエントリークラスがUserAgentApplicationからPublicClientApplicationに変更されています。

インスタンスの生成にADアプリなどの設定のConfigurationを与えるあたりの使い方は変わらないようです。

Configurationの構成自体も大きく変わっていないようで、clientIdauthorityなどしか項目を使っていない場合は設定記述部分は変更する必要はありません。

参考 - Configuration

handleRedirectCallback

loginRedirectなどを行った場合に引っ掛けるhandleRedirectCallbackは大きく変更されています。

Promiseで実行されるようになっておりそれに合わせた変更が必要です。

const res = await this.clientApp.handleRedirectPromise().catch(err => {
    console.log({err});
});

自分は検証でエラーをトラップしたかったので雑に👆の実装にしていますが、resがvoid | AuthenticationResultとなるのでthenを使うのもありです。

handleRedirectPromiseの内部処理実行前にloginRedirect処理などが走るとエラーが発生するので

handleRedirectPromiseの処理終了を待機して、処理終了後にログイン処理を行うのがベターです。

login

loginを行う処理のインターフェースは変わっていません。loginRedirectloginPopupでログインを実行します。

Redirectは前述するCallbackで、PopupはPromiseで結果を取得します。

ユーザー情報の取得

msal.jsのv1ではユーザー情報を取得するインタフェースがClientApp上に用意されていましたが、v2でなくなりました。

msal.jsのv2では、ログインやトークン取得時の結果としてアカウント情報が含まれた状態で返却されるので

そこからデータを取得することになります。

リダイレクトの場合は下記のようにhandleRedirectPromiseで取得できたレスポンスから抽出することになります。

const res = await this.clientApp.handleRedirectPromise().catch(err => {
    console.log({err});
});
this.account = (res as AuthenticationResult).account;

PopupはloginPopupの返却値です。

アクセストークンの取得

acquireTokenSilentで必要な引数にアカウント情報のパラメータが追加されています。

const res = await this.clientApp.acquireTokenSilent({ scopes: this.scopes, account: this.account });
return res.accessToken;

ログイン時などにアカウント情報は取得し、プライベートな変数なりに格納しておいたほうが良さそうです。

ここまでの実装はAngular特有というものではなく、普通の@azure/msal-browserの使用の仕方ですね。

本家のmsalServiceよろしく、一つのServiceクラスに👆までの処理を実装しておくと使いやすいと思います。

Interceptorの作成

Angularのサービスらしく。ということでInterceptorを作成し、WebAPIへの通信時に自動的にヘッダにトークンを付与させます。

ここは@azure/msal-angularのInterceptorをちょっとだけ参考にします。

[
  { endPoint: string, scopes: string[] }
]

上記の構造の設定を使用して、アクセスするエンドポイントによって

使用するスコープを切り替える機能を実装していきます。

(👇本家と比べるとかなり雑ですが…!

@Injectable()
export class MsalInterceptor implements HttpInterceptor {

    constructor(
        private auth: MsalService
    ) {}

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        const target =  environment.endPointTargetScope.filter(f => req.url.includes(f.endPoint));
        let scopes = [];
        if (target.length > 0) {
             scopes = target[0].scopes;
        }
        if (!scopes || scopes === []) {
            return next.handle(req);
        }
        return from(
            this.auth.acquireTokenSilent(scopes)
                .then((token: string) => {
                    const authHeader = `Bearer ${token}`;
                    return req.clone({
                        setHeaders: {
                            Authorization: authHeader,
                        }
                    });
                })
        )
        .pipe(
            mergeMap(nextReq => next.handle(nextReq))
        );

    }
}

ひとまずこれで、ログイン👉アクセストークン取得👉WebAPI通信の必要最低限な動きは確認できるようになります。

まとめ

Angularでのmsal.js v2.0.xの使用の仕方を見てきました。

@azure/msal-angularのv2がリリースされる可能性はありますが

@azure/msal-browser準拠のインターフェースや使い方になりそうな気はしているので

これを使用して、変わりそうな箇所を抑えておくのも良いかもしれません。

今回検証で使用したソースコードは下記にのごった煮レポジトリ格納しています。

github.com

AngularのRouterでuseHashをtrueにしているときにmsalで`Error: Cannot match any routes. URL Segment: 'access_token'`が出るときの対処

はじめに

2020年7月時点の記事となります。

msal.jsのv2が出たのですが、書きかけだった記事の供養となります。。ご留意ください。

Angular・msal.js・@azure/msal-angularは下記のバージョンとなっています。

  • Angular: 10.0.1
  • msal.js: 1.3.2
  • @azure/msal-angular: 1.0.0

検証を行うバージョンによっては動作が異なることがありますのでご留意ください。

何が起きているか

タイトルのとおりですが、AngularのRouterModuleを設定するときにuseHashtrueにすると

Angularのルーティングはhttp://localhost:4200/#/<path>のような#付きのパスとなります。

AzureADでimplicit flowでトークンが返却される場合、リダイレクトURLに#access_token=~~のようにトークン情報が付与されて返却されているようです。

参考-Implicit grant (暗黙的な付与)

  1. fragmentでトークンが送られる
  2. Angularのルーティングで#付きのパス=遷移と誤認し遷移を実行
  3. 実際のパスは当然存在しないのでエラー

この流れが原因のようです。

queryで返却されるようになれば良かろうかとも思うのですが

msal.jsのConfigをさらっと眺めた感じだと、responce_modeをいじる設定はないように見えるので別の方法を模索してみます。

参考-msal・Configuration

どのように解消するか

routerのeventをsubscribeするのも良いですがちょっと手間なのでシンプルな解決方法が望ましいですね。

解決方法1:BootstrapModuleの変更

window.parent かどうか判定し、IFrame内部であればRouterを持たないダミーのモジュールを流し込めば良いはずです。

MSAL.js を使用してトークンを自動的に取得、更新するときにページのリロードを回避する」が参考になるかと思います。

別の問題の解消方法ですが、これに似た方法で対応が可能となります。

ただ、上記のサイトに上げた方法だと、useHash=trueにしたRouterModuleをImportすることになるので、問題となっていた現象を解決できません。

そこで、window.parentによる分岐をAppModule下ではなく

もう少し上のレイヤーで実行し、BootstrapするModuleをRouterを持たないダミーのModuleに変更する。といった対応を行います。

処理の分岐を下記のようにmain.tsで実行します。

if (window !== window.parent && !window.opener) {
  // IFrame内で実行されるダミーモジュール
  platformBrowserDynamic().bootstrapModule(RedirectDummyModule)
    .catch(err => console.error(err));
} else {
  // 通常のモジュール
  platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.error(err));
}

解決方法2:リダイレクト先の変更

そもそもAngularを使用していないプレーンなダミーサイトにリダイレクト先を変更しちゃおうという手です。

ADAL Wiki - FAQ - Q1. My app is re-loading every time adal renews a token.」が参考になります。

これはadalの記事ですがmsal、@azure/msal-angularでも同様の実装が可能となります。

リダイレクト先のHTMLとして、下記のようなHTMLを用意しておきます。

リダイレクト時点でmsalのUserAgentApplicationインスタンスが生成されていないとエラーとなるので

msal.jsの読み込みと、UserAgentApplicationのインスタンス生成を行う必要があります。

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>Frame Redirect</title>
    <script type="text/javascript" src="https://alcdn.msftauth.net/lib/1.3.2/js/msal.min.js" integrity="sha384-4cH4i4aLXsdIJJz5DF27S+GxZXRSqBORS4NN7kc7NyZBBIugxFyt6hQt5FiR/DuZ" crossorigin="anonymous"></script>
</head>
<body>
    dummy
    <script>
        var msalConfig = {
            clientId: "<Azure AD App Client ID>"
        };
        var authContext = new Msal.UserAgentApplication(msalConfig);
        authContext.handleWindowCallback((err, res) => {
            console.log({ err, res });
        });

        if (window === window.parent) { window.location.replace(location.origin + location.hash); }
    </script>
</body>
</html>

仮に、このHTMLがredirect-page.htmlとすると

アクセストークン取得時などにリダイレクトURLとしてこのURLを指定します。

    // 👇msal-angularのMsalService.acquireTokenSilent()
    // 👇msal.jsのUserAgentApplication.acquireTokenSilent()
    const res = await this.msalService.acquireTokenSilent({
      scopes: [ 'スコープ'],
      redirectUri: 'http://localhost:4200/redirect-page.html' // 👈こんな感じ
    });

デプロイする時も、今回作成したredirect-page.htmlが必要になるのででangular.jsonでデプロイ対象に指定しておきましょう。

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
   ....
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            ...
            "assets": [
              ...
              "src/redirect-page.html" 👈追加
            ],
            ...

まとめ

そもそも別問題の対応のために、解決方法1や2を行っていたので今更見つけた感じになります。

無限リダイレクト問題とかにぶつかって、解決方法1,2とかに行き当たる方は多いと思うので知らん間に対応している。といったことが多そうです。

useHash:trueで且つ、そこらの対応がされていないとかのすこしニッチな状況のものになりますね。

msalのv2でこの記事が無意味なものに変わってくれるといいなーと若干期待しています。

今回検証で使用したリポジトリは下記となります。

解決方法1

解決方法2

Microsoft Graphのpresenceについて

修正

2020/7/19 : presenceのβ公開時期を2020年6月から2019年12月に修正しました。

(@karamem0さんご指摘ありがとうございました!)

2020/8/2:presenceのSubscriptionのドキュメントが公開されたので反映しました。

はじめに

2020年7月時点のMicrosoft Graphを使用した記事となります。

また記事で扱うpresenceは2020年7月段階でβ状態なので

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

presence

2019年12月に取得WebAPIがβ公開され2020年7月に変更通知がサポートされました

presenceとは具体的になんなのでしょうか。

めちゃくちゃかんたんに説明すると👇コレです。

f:id:TakasDev:20200719131308p:plain

presenceの取得

Graphの変更履歴の7月からのリンクは404なのですがドキュメントは確認できます。

presenceのデータを取得するのは下記のWebAPIで取得します。

※今はβしか公開されていないのでhttps://graph.microsoft.com/beta/エンドポイントからしかアクセスできません。

GET /me/presence
GET /users/{id}/presence
GET /communications/presences

GET /communications/presencesと記載されていますが素直に使用しても複数人取得できず404になりました。

ユーザーid指定が必須のようなので、/communications/presences/{id}が正しいと思います。

複数人のpresenceの取得はgetPresencesByUserIdです。

POSTで取得したいユーザーのidをBodyに指定するので、ちょっと違和感がありますね。

POST communications/getPresencesByUserId
{
    "ids": ["<UserId>", "<UserId>"]
}

presenceで取得するプロパティについて

プレゼンスのリソースの種類

取得するプロパティは少なく、idactivityavailabilityの3種だけです。

f:id:TakasDev:20200719134234p:plain

👆のドキュメントはめったくそややこしいですが、動作させて確認したところこれで正しいようです。

(user's availabilityがavailibityじゃないんだ…的な)

Presenceの状態については、Teams でのユーザーのプレゼンスで確認できます。

availabilityとactivityの対応は下図の通りの認識です。

f:id:TakasDev:20200719134843p:plain

presenceの変更通知

2020年7月に変更通知に対応されました。

CreateSubscriptionにはリソースの明記はありませんが、OverViewによると/communications/presencesのようです。

presencesの取得から考えると単一ユーザー指定の/communications/presences/{id}の指定になるようです。

IDなしやカンマ区切りの複数指定は400になりました。

また、Subscriptionの作成では有効期限を指定する必要があります。

ドキュメントに記載の通り、1時間のようです。

サブスクリプションの作成は他のリソースのときと同様、下記のようなJSONをPOSTすることで作成できます。

{
      "changeType": "updated",
      "notificationUrl":  "<エンドポイントURL>",
      "resource": "communications/presences/<user id>",
      "expirationDateTime":  "<有効期限>"
}

複数指定する場合は$filterを使用して下記のように指定するようです。

参考-Doc-RequestExample

"resource": "communications/presences$filter=id in ('<userid>', '<userid>'...)",

他のリソースのSubscriptionを作成する時とそこまで大きく変わるところはありませんね。

変更通知で通知される内容

Subscriptionを作成して内容を確認した結果、下記のようなデータが取得できました。

{
    "value": [
        {
            "subscriptionId": "cc87f8f1-45aa-4b33-9dd3-660bbaf3ed57",
            "clientState": null,
            "changeType": "updated",
            "resource": "communications/presences('<userid>')",
            "subscriptionExpirationDateTime": "2020-07-18T23:03:50.4305891-07:00",
            "resourceData": {
                "@odata.type": "#Microsoft.Graph.presence",
                "@odata.id": "communications/presences('<userid>')",
                "id": null,
                "activity": "InACall",
                "availability": "Busy"
            },
            "tenantId": "<tenantid>"
        }
    ]
}

activityavailabilityも取得できているので、TeamsやEventのように別途WebAPIでリソースデータの取得などをする必要はなさそうです。

おわりに

β公開中のpresenceの取得と変更通知の使い方を見ていきました。

第 1 回 Japan M365 Dev User Group 勉強会ではこれをネタにLTする予定です。

よろしくおねがいします。

Angular Elementsで作成したWeb Componentsの設定情報をプレーンなJSから変更する

はじめに

この記事内のアプリケーションは下記バージョンで構成しています。

  • Angular CLI: 10.0.1
  • Angular: 10.0.2

この記事を参照されるタイミングによっては

サンプルコードが動作しなかったりする可能性がありますのでご留意くださいませ。

そもそもなにをしたいのか

Microsoft GraphはGraph ToolkitというWeb Componentsを公開していまして

オリジナルでそれを作ってみたいなー。というのが発端です。

今回の記事はその前哨戦。

Web Componentsを作ってみてハマった箇所の備忘録。といった感じですね。

Angular Elements

Angularを使用している場合は、Angularで作成したComponentをWebComponentsでパッケージングしてくれる

Angular Elementsというものが存在します。Angularで諸々なれている自分はこれを使うのが一番手っ取り早そうです。

基本的な使い方などは、👆のドキュメントを参考にすれば良いと思うので割愛します。

最終的に下記のようにAngularを使用していないようなプレーンなHTMLで

作成したWebComponentsのセレクタを指定してAngularで作成したComponentを表示させることが出来ます

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="../../dist/custom-element/styles.css">
    <script src="../../dist/custom-element/custom-element.js"></script>
</head>
<body>

    <h1>Web Component (Costum Elements)</h1>
    <scl-custom-button></scl-custom-button>
    <scl-sample-form-group></scl-sample-form-group>
    <scl-router-page></scl-router-page>
</body>
</html>

👇

f:id:TakasDev:20200705111959p:plain

情報を設定する🤔

さて、Micorosoft Graphにアクセスするということは、ADのアプリケーション情報などが必要になります。

設定情報をWeb Components作成時点でビルドインしても良いのですが、それでは使い回しがしにくいです。

具体的には下図のような構成にしたいところですね。

f:id:TakasDev:20200705113551p:plain

ビルドされたソースはどうなっている?

ビルドされたパッケージ内のServiceクラスに値を直接外部から突っ込めるでしょうか?

下記のTSがビルドされたソースを整形して見てみます。

export class LibSettingsComponent implements OnInit {
...
 constructor(
    private service: LibStateService
  ) { }
...
  set setting(setting: { state: string, userId: string, userName: string }) {
    console.log('emit');
    this.service.setting(setting);
    console.log(this.service.state);
    this.dummyStSet = 'emit';
  }
}
        Oy), By = ((Iy = function() {
            function e(t) {
                _classCallCheck(this, e),
                this.service = t,
                this.dummySt = "dd"
            }
            return _createClass(e, [{
                key: "ngOnInit",
                value: function() {}
            }, {
            ...
            }, {
                key: "setting",
                set: function(e) {
                    console.log("emit"),
                    this.service.setting(e),
                    console.log(this.service.state),
                    this.dummyStSet = "emit"
                }
            }]),

this.serviceはfunctionの引数のtで更にそこの呼び出し元の…?

いずれにせよServiceのインスタンスに直接アクションをするのは多大な労力を伴いそうです。

設定用のComponentを用意する

Componentからのアクセスは簡易に行えます。なので設定用のComponentを作成する。というアプローチをとってみます。

中身が空の設定用のComponentを用意します。

設定用のComponentは@InputでSetterのみを提供し、Setterの実装内で設定情報Serviceを書き換えます。

具体的なソースコードは下記のとおりです。

色々視認やデバッグしやすいようにHTMLテンプレートにものを突っ込んでますが空で問題ないと思います。

import { Component, OnInit, Input } from '@angular/core';
import { LibStateService } from '../../services/lib-state.service';

@Component({
  selector: 'scl-lib-settings',
  template: '<div>{{dummySt}}</div>'
})
export class LibSettingsComponent {

  dummySt = 'bf';

  constructor(
    private service: LibStateService
  ) { }

  @Input()
  set setting(setting: { state: string, userId: string, userName: string }) {
    // ここで設定情報をServiceに反映する
    this.service.setting(setting);
    this.dummySt = 'emit';
  }
}

これでプレーンなHTMLのタグ上などで、settingプロパティに値を突っ込むことで設定情報が反映されるようになりました。

設定用のコンポーネントタグを書かなくて良いようにする

さて、設定を外から与えることができるようになりましたが

何も情報を出力しない設定用のコンポーネントをHTML上に書くのはなんともダサ味があります。

なので、スクリプトでも設定情報の反映ができるようにしたいところです。

そこで、

  1. 設定用のコンポーネントタグをcreateElementして
  2. 設定を行い
  3. 設定用のコンポーネントタグを削除する

こんな感じの設定スクリプトをAngular Elementsから提供することにします。

設定スクリプトは下記のようなものです。

class LibSettings {

    setSettings(data) {
        const settingDom = document.createElement('scl-lib-settings');
        document.body.appendChild(settingDom);
        settingDom.dummySt = 'hoge';
        settingDom.setting = data;
        document.body.removeChild(settingDom);
    }
}

作成した設定スクリプトをWebComponentsをビルドする際に一緒に吐き出したいので、angular.jsonをいじります。

    "custom-element": { // Angular Elementsのプロジェクト名
      ...
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            ...
            "scripts": [
               // 👇を追加
              "projects/custom-element/src/static-scripts/lib-settings.js"
            ]

これで、WebComponentsをパッケージングしたときに、同一ディレクトリに設定スクリプトも吐かれるようになりました。

f:id:TakasDev:20200705121442p:plain

使ってみる

設定スクリプトで設定が反映されたサービスのプロパティをアラート表示するボタンComponentを作成しました。

このボタンを押下することで設定情報の反映の確認を行ってみます。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="../../dist/custom-element/styles.css">
    <script src="../../dist/custom-element/custom-element.js"></script>
</head>
<body>

    <h1>Web Component (Costum Elements)</h1>
    <scl-custom-button></scl-custom-button>
    <scl-sample-form-group></scl-sample-form-group>
    <scl-router-page></scl-router-page>
    <script src="../../dist/custom-element/scripts.js"></script>
    <script type="text/javascript">
        // 設定スクリプトを使用して設定情報の反映
        const cl = new LibSettings();
        const setting = { state: 'HTMLから設定したよ!', userId: 'user', userName: 'hogehoge' }
        cl.setSettings(setting);
        //
    </script>
</body>
</html>

上記の状態で、アラートに「HTMLから設定したよ!」と表示されればOKです。

f:id:TakasDev:20200705122727g:plain

設定できました。

これで設定情報を外部から与えることができそうです。

おわりに

長い前哨戦が終わったので、次回はようやく目的だったオリジナルのGraph Toolkitの作成に入ってみます。

手を付けれていないのでいつになるかわかりませんが

今回検証を行ったソースコードは下記となります。

github.com

色々検証で試行錯誤したので少々とっちらかっているのはご容赦くださいませ…

Microsoft MVPを初受賞しました

タイトルの通りMicrosoft MVPを初受賞しました。

カテゴリはOffice Developmentとなります。

登壇の機会を多くくださった.NETラボの皆様

セッションを聞いてくださった皆様

フィードバックをくださった皆様

個人の活動であるにも関わらず会社でぼくの活動をサポートしてくださった皆様

僕の活動にあらゆる形で関わってくださった皆様にお礼申し上げます。

本当にありがとうございました。

これからも楽しんで技術を追いかけ続けようと思います。

ただ看板を頂いた以上はそれに恥じない動きを心がけたいと思います。

感想

初受賞ということもあり、Twitterで感想おさまらんなー。というわけで感想をポストしています。

Officeの名を冠するカテゴリで受賞させていただいたのですが

僕のブログは「Office」で検索しても1件も引っ掛からないレベルでOfficeに触れていません。

f:id:TakasDev:20200703202650p:plain

(1件くらいあるやろと思ってたので「マジか」と自分でも思いました。。。)

  • よく言えばMicrosoft Graph特化で
  • OfficeというよりOfficeのデータに熱中していて
  • Webや.NETといったフロント方面から執拗にOfficeのデータにアプローチを行っている

そんな様が審査時に「変な人」的に印象にのこったかもしれないなーとか思ったりしています。

今後の話

受賞したからには継続して受賞していきたいと思いますし

Azureが好きで、Angularが好きで、.NET Coreが好きで、Microsoft Graphが好き。といった状況もまだまだ続くと思います。

そんな自分なので、Microsoft Graphはもちろんですが、Microsoft Graph以外も含め、多くの情報発信ができれば良いな。と考えていますので

今後ともよろしくおねがいいたします🙇

蛇足

受賞メールが届いた日は興奮したのか一睡もできず、かなり久々に完徹して出勤する事になりました。

結果、お昼すぎに睡魔に勝てず途中リタイア。

自分の老いにも確かな手応えを感じてしまったので、体調にも気をつけつつ頑張ろうと思います。。

AzureDevOpsのCIで.NET CoreとAngularのアプリケーションをバージョンアップする

はじめに

この記事内で使用する各フレームは下記バージョンで構成しています。

  • Angular: 9.1.8
  • .NET Core: 3.1

また、AzureDevOpsは記事作成時点のキャプチャやコマンドが掲載されています。

この記事を参照されるタイミングによっては

動作しなかったり画面が違うといった可能性がありますのでご留意くださいませ。

モチベーション

アプリケーションのバージョンアップ作業って地味で忘れがちです。

そういった作業は自動で行ってしまいたい!!

と、いうわけでAzure DevOpsのCIフローの中でバージョンアップ作業ができないか試してみました。

僕が主に使用しているのは.NET CoreとAngularのアプリケーションですので

その二つのアプリケーションでバージョンアップを実施するCIを組んでみようと思います。

.NETアプリケーション

.NETのアプリケーションのバージョンアップをCIで実現しようと思います。

.NETアプリケーションのバージョン番号はメジャー.マイナー.ビルド番号.リビジョンで表現されますが

ビルド番号、あるいはリビジョンに*指定で、ビルド時に自動的にバージョンアップをしてくれます。

なので普通に運用する場合は何も考えなくてもバージョンアップはしてくれるのですが

バージョン付番を独自のルールで運用したり、付番したバージョンをデータストアに格納したりとかしたい場合があります。

なので、今回は独自ルールでバージョンを付番してアプリケーションを発行するCIを構築してみようと思います。

バージョンアップルール

今回は下記のルールでバージョンアップしようと思います

  • 2020年6月2日からの経過日数をバージョン番号とする
  • ビルドを行った日の00:00からの経過分数をリビジョンとする

.NETアプリケーションのビルドコマンドでバージョン番号を指定できるよう準備

MsBuld.exeを使用してビルドする際のコマンドでカスタムプロパティを指定できます。

参考:MSBuild Properties1

そこで、ビルドコマンドのArgumentsでバージョンを変更できるようにします。

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

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <UseWPF>true</UseWPF>
    <PublishSingleFile>true</PublishSingleFile>
    <RuntimeIdentifier>win-x86</RuntimeIdentifier>
    <!-- バージョン番号のベースの経過日数を受け取る -->
    <BuildNumber Condition=" '$(BuildNumber)' == '' ">0</BuildNumber>
    <!-- リビジョンのベースの経過分数を受け取る -->
    <Revision Condition=" '$(Revision)' == '' ">0</Revision>
    <!-- バージョンの指定-->
    <AssemblyVersion>0.0.$(BuildNumber).$(Revision)</AssemblyVersion>
    <VersionPrefix>0.0.$(BuildNumber).$(Revision)</VersionPrefix>
  </PropertyGroup>

</Project>

これで、MsBuild.exe <プロジェクト> /p:BuildNumber=<適当な数字> /p:Revision=<適当な数字>でバージョンをコマンド上で指定できるようになりました。

MsBuild.exe <プロジェクト> /p:BuildNumber=1 /p:Revision=2した結果は下図のとおりです

f:id:TakasDev:20200613115738p:plain

CIの設定

CIで使用する変数の設定

CIの複数のタスクでバージョン情報を使い回すので、その情報を格納する変数を指定します。

経過日数を格納する$daysと経過分数を格納する$dayintervalを用意しています。

variables:
  days: 0
  dayinterval: 0

Power Shell

まずはPowerShellでコマンドで指定するバージョン番号とリビジョンを生成しています。

参考:PowerShellタスク

現在日付をゴニョゴニョして、$days$dayinterval 変数に格納します。

  • 2020年6月2日からの経過日数をバージョン番号とする
  • ビルドを行った日の00:00からの経過分数をリビジョンとする

ってことをやっています。

- task: PowerShell@2
  displayName: 'Calc Build Version'
  inputs:
      targetType: inline
      script: |
        $baseDate = [datetime]"06/02/2020"
        $currentDate = $(Get-Date)
        $interval = NEW-TIMESPAN -Start $baseDate -End $currentDate
        $days = $interval.Days
        Write-Host "##vso[task.setvariable variable=days]$days"
        echo $days
        $today = Get-Date -f d
        $startdate = Get-Date $today
        $todayinterval = NEW-TIMESPAN -Start $startdate -End $currentDate
        $dayinterval = [Math]::Truncate($todayinterval.TotalMinutes)
        Write-Host "##vso[task.setvariable variable=dayinterval]$dayinterval"
        echo $dayinterval

Write-Host "##vso[task.setvariable variable=<変数名>]<PowerShell内の変数(突っ込みたいValue)>"で👆で作成した変数に突っ込めます。

このPowershellでは、👆で指定したdaysdayintervalに値を突っ込んでいますね。

バージョンの情報を管理しているサービスなんかがある場合は

WebAPIを作成して、Invoke-WebRequestコマンドを使用して引っ張ってきたり登録したりすればいいかなと思います。

MsBuild

MsBuildで👆で指定したバージョンを指定してビルドを行うコマンドを作成していきます。

参考:MsBuildタスク

- task: MSBuild@1
  inputs:
    solution: '**/<プロジェクト名>/**/*.csproj'
    configuration: Release
    msbuildArguments: /t:Publish /p:BuildNumber=$(days) /p:Revision=$(dayinterval)

これはそこまで複雑なことはしていないですね。

PowerShellで設定した変数をMsBuildで使用するカスタムプロパティに指定しています。

実行結果

PowerShellタスクで指定するバージョン情報を出力しています。

f:id:TakasDev:20200613134513p:plain

Publishされたファイルのバージョンが、Powershellで生成した情報で構成されていることが確認できました。

Angularアプリケーション

普通にWebアプリケーションを作成するだけであれば特に意識する必要はないと思うのですが

ライブラリを作成してPublicなりPrivateなnpmレジストリにPublishしたい場合、バージョンアップは必要な作業です。

基本はnpm versionコマンドでバージョンアップしていけば良いかなと思っています。

バージョンアップルール

今回は下記のルールでバージョンアップしようと思います

  • masterブランチのCIはnpm version patchでパッチバージョンを上げる
  • developブランチのCIのときはプレリリースでdevバージョンとしてリリースする

CIの設定

ちょっとCIの順番と連動していないのですが、行う作業の関連など考慮して順不同で説明しています。

Power Shell

npmコマンドをPowerShellで実行します。

このPowershellコマンドでは下記のことを実行します

  • npm versionの実行
    • ライブラリのpackage.jsonのバージョンを上げたいのでライブラリのディレクトリで行う
  • バージョン変更をリモートリポジトリにPushする
    • そうしないと次回以降も同一のバージョンが生成されてしまうためですね

参考: MS Doc - Run Git commands in a script

参考: MS Doc - Build Azure Repos Git or TFS Git repositories

参考: Stack Overflow - How to increase a version of an npm package using Azure Devops pipeline

先にyamlを掲載すると👇な感じになります。

- task: PowerShell@2
  displayName: 'Version Up & Commit'
  inputs:
    targetType: inline
    script: |
      git config user.name "learn-angular-library-ci Build Service (devtakas-public)"
      git config user.email "dummy@example.com"
      $BranchName = "$(Build.SourceBranch)" -replace "refs/heads/"
      git checkout $BranchName --quiet
      cd projects\sample-lib
      npm version $env:VERSIONCOMMAND --preid=dev -m "$env:VERSIONCOMMENT" --force --silent
      git add .
      git commit -m "$env:VERSIONCOMMENT"
      git push --quiet

Script内1~2行目

git config user.name "learn-angular-library-ci Build Service (devtakas-public)"
git config user.email "dummy@example.com"

gitにコミットするためにユーザーの情報などを設定しています。

ここは好きな値で問題ないです。

Script内3~4行目

$BranchName = "$(Build.SourceBranch)" -replace "refs/heads/"
git checkout $BranchName --quiet

Azure DevOpsのCI実行ログを見ればわかるのですが

ソースコードの操作が行われているブランチは厳密にはCIコマンドで指定したブランチと異なります。

f:id:TakasDev:20200614140458p:plain

バージョン変更を行ったコミットをPushしたいブランチはCIで指定するブランチなので

環境変数に格納されている作業ブランチをベースに作業ブランチのチェックアウトを行う必要があります。

Script内最後の行まで

cd projects\sample-lib
npm version $env:VERSIONCOMMAND --preid=dev -m "$env:VERSIONCOMMENT" --force --silent
git add .
git commit -m "$env:VERSIONCOMMENT"
git push --quiet

作業ディレクトリをライブラリのpackage.jsonがあるところまで移動して諸々作業します。

$env:VERSIONCOMMAND はAzure DevOpsのCI上で設定する変数です。

f:id:TakasDev:20200614141100p:plain

masterのCIではpatch、developのCIではprereleaseを指定しています。

AzureDevOpsのCIでは、rootディレクトリ以外ではnpm versionしてもCommitが発生しなかったので

別途 git add .git commitしています。

警告などでるとAzure DevOpsのCIでエラーを吐いてしまうので

適宜--quiet--silentでエラーにならないように回避しています。

参考: Stack Overflow - How to generate NPM release candidate version

参考: はらへり日記 - npm scriptsでエラーログを表示させたくない話

認証の設定

参考: MS Doc - Run Git commands in a script

AzureDevOpsで管理されているリポジトリに対してPushを行いたいので

ソースを取得する段階で認証情報を設定しちゃいます。

下記で設定可能です。

steps:
- checkout: self
  persistCredentials: true

Trigger

例えばdevelopブランチの変更があった場合、developブランチにCommit/Pushが行われます。

つまり、なにも考慮しないと無限ループになります(なった)。

自分は、単純に下記の通り自動変更される対象のファイルをexclude設定にしました。

trigger:
  branches:
    include:
    - develop
  paths:
    exclude:
    - projects/sample-lib/package.json

ここは人により設定が変わりそうなところですね。

ユーザーの設定

CIには実行するユーザーが割り振られています。

なのでそのユーザーがソースコードに対してCommitしたりPushできたりするように設定する必要があります。

参考: MS Doc - Run Git commands in a script

設定場所は下図から確認できます。

f:id:TakasDev:20200614142736p:plain

Usersの<プロジェクト名> Build Serviceという名前に基本なっていると思います。

設定は下記のとおりですね。

f:id:TakasDev:20200614143016p:plain

  • Contribute / Read
    • 必須
  • Create Branch / Create Tag
    • 必要があれば
    • リリース時にタグ付けしたいとか、自動コミットじゃなくてブランチとPR作りたいとかの場合かな?と思います
  • Bypass policies when pushing
    • ブランチポリシーで保護しているブランチに対してPushしたい時に必要です。

実行結果

今回はDevOps上のArtifactsにライブラリをPublishしました。

  • developブランチのCIの場合

f:id:TakasDev:20200614144429p:plain

preleaseで出力されていることが確認できます。

  • masterブランチのCIの場合

f:id:TakasDev:20200614145048p:plain

patchで出力されていることが確認できます。

おわりに

今回使用したリポジトリは下記の通りです。

Angularの方はブランチポリシー下の動作検証のためAzure DevOpsでソース管理もしています。

.NET Core

Angular

供養

長く苦しい戦いの記録

f:id:TakasDev:20200614150251p:plain