kkamegawa's weblog

Visual Studio,TFS,ALM,VSTS,DevOps関係のことについていろいろと書いていきます。Google Analyticsで解析を行っています

Logic AppsでAzure DevOpsの監査ログをCosmos DBへ保存する

はじめに

Azure DevOpsには監査機能が入っており、https://dev.azure.com/{orgnazation}/_settings/audit というURLでアクセスできます。ただし、この監査ログは90日分しか保持されておらず、それ以上の期間保存したい場合、別途APIアクセスでJSONやCSVに抜き出す必要があります。

docs.microsoft.com

毎日取得するのも面倒なので、LogicAppsを使って自動化してみましょう。

Token取得

f:id:kkamegawa:20190901092840p:plain

最初にPATを作成します。今回の場合、監査ログだけ取れればいいので、Read Audit Logの権限だけあれば問題ありません。

Cosmos DB

f:id:kkamegawa:20190901092847p:plain

次はCosmos DBのコンテナーを作ります。Cosmos DBの作り方については解説しません。ここではSQLを使ってサーバーを作っています。

PartitionKeyは何を対象にするかで変えてください。Cosmos DBの課金に結構影響します。今回の場合、ユーザー単位で監査するという仕様でactorUserIdをPartition Keyにしています。

操作単位で監査したいのであれば、actionIdになるでしょうか。ipAddressだとnullにあることがあるようなので、お勧めできません。

Logic Apps

全体フロー

f:id:kkamegawa:20190901092851p:plain

だいたいこんな感じで作りました。細かいステップを解説します。

トリガー

f:id:kkamegawa:20190901092855p:plain

タイマートリガーで毎日一度実行させます。UTCで0時0分(日本時間の朝9時)に一日一度実行します。

(9/3追記)
0:00としても、正確に0:00(UTC)に動くとは限らないようです。1秒前に実行されるとかおきましたので、少なくとも数分後に動かすようにしたほうがいいですね。

開始終了時刻

f:id:kkamegawa:20190901092800p:plain

REST APIに渡す始点、終点の日付部分を作ります。

formatDaterTime(addDays(utcNow(), -1), 'yyyy-MM-dd')

f:id:kkamegawa:20190901092804p:plain

開始時刻をstartTime,終了をendTimeとして作ります。

開始時刻

concat(outputs('日付作成'), 'T00:00:00Z')

終了時刻

concat(outputs('日付作成'), 'T23:59:59Z')

PAT 作成

f:id:kkamegawa:20190901092837p:plain

Azure DevOpsで作ったPATをbase64ToString()で変換します。

HTTP呼び出し

f:id:kkamegawa:20190901092809p:plain

Azure DevOps HTTP呼び出しというコネクターもあるのですが、こちらのほうがわかりやすいので、標準のHTTPコネクターを使います。

docs.microsoft.com

REST APIのドキュメント通りですが。

  • ヘッダー: Authentication : Basic Base64エンコードしたPAT
  • api-version: 5.1-preview.1
  • skipAggregation: false
  • startTime : outputs('startTime')
  • endTime : outputs('endTime')
  • 認証:なし

JSONの解析

f:id:kkamegawa:20190901092814p:plain

HTTPの出力結果を使うためのJSON Schemaを定義します。APIのページにあるサンプルレスポンスを使ってサンプルペイロードからJSON Schemaを作ればいいです。

(9/5追記)
と思っていたのですが、ipAddressがnullになることがある場合の失敗と、Azure DevOps自身が定期的に実行するジョブの監査ログでsummryがnullになることが多かったので、こんな感じにしました。

{
  "type": "object",
  "properties": {
    "continuationToken": {
      "type": "string"
    },
    "decoratedAuditLogEntries": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "actionId": {
            "type": "string"
          },
          "activityId": {
            "type": "string"
          },
          "actorCUID": {
            "type": "string"
          },
          "actorDisplayName": {
            "type": "string"
          },
          "actorImageUrl": {},
          "actorUserId": {
            "type": "string"
          },
          "area": {
            "type": "string"
          },
          "authenticationMechanism": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "categoryDisplayName": {
            "type": "string"
          },
          "correlationId": {
            "type": "string"
          },
          "data": {
            "type": "object"
          },
          "details": {
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "ipAddress": {},
          "projectId": {
            "type": "string"
          },
          "projectName": {},
          "scopeDisplayName": {
            "type": "string"
          },
          "scopeId": {
            "type": "string"
          },
          "scopeType": {
            "type": "string"
          },
          "timestamp": {
            "type": "string"
          },
          "userAgent": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "correlationId",
          "activityId",
          "actorCUID",
          "actorUserId",
          "authenticationMechanism",
          "timestamp",
          "scopeType",
          "scopeDisplayName",
          "scopeId",
          "projectId",
          "projectName",
          "ipAddress",
          "userAgent",
          "actionId",
          "data",
          "details",
          "area",
          "category",
          "categoryDisplayName",
          "actorDisplayName",
          "actorImageUrl"
        ]
      }
    },
    "hasMore": {
      "type": "boolean"
    }
  }
}

レコード判定

f:id:kkamegawa:20190901092818p:plain

組織で使うときはたぶんないとは思いますが、日付によっては監査ログがない場合もあります。

empty(body('JSON_の解析'))

これでレコードがある場合のみCosmos DBへの登録処理を呼び出します。

Cosmos DBへの登録

f:id:kkamegawa:20190901092824p:plain

For each制御を使って、レコード単位に登録します。JSONのdecoratedAuditLogEntriesがログエントリーなので、この配列の数だけループさせます。

Logic AppsでデータベースIDやコレクションIDが参照できない場合、Cosmos DBのファイアウォールでAzureデータセンター内からのアクセスを受け入れるを設定する必要があります。

docs.microsoft.com

ドキュメントは現在のアイテム、Partition Keyはユーザー単位で作ることにするので、actorUserIdを指定します。一番はまったのですが、Partition Keyには明示的にダブルクォーテーションを付けないと、Invalid Partition Keyといわれてエラーになります。

Cosmos DBで作ったPartition Keyと一致させればあとはCosmos DBが勝手に入れてくれます。

f:id:kkamegawa:20190901092832p:plain

実行すると、こんな感じでCosmos DBに格納されます。あとは適度に検索してください。

自動削除

監査ログなので、不要になったら消すということもあるかと思います。コンテナーに明示的にTTLを設定すればいいと思いますが、単位が秒なので、一年なら31536000ですかね。

docs.microsoft.com

まとめ

結構苦労しましたが、Logic Apps使ってノンコーディングで監査ログのエクスポートができるようになりました。より高度な監査としては、特定のログが出たらアラートメールを送るようにAzure Functionsをかませてみるとかも面白いと思います。

Azure DevOps CLIでWork Itemの一覧をざっくり処理する

知り合いから、「Azure BoardsのWork Itemをざっくり見たい。ブラウザー以外で」という相談をたまに受けます。

なぜブラウザー以外かといえば、ブラウザーはたいていテストに使ってるからなんですね。やむを得ない事情で閉じないといけないとか、常時起動できるかといえばそうでもない。

Azure CLIの拡張機能でAzure DevOpsが提供されています。ネットワークの問題がなければ(GitHub Releaseからのダウンロードなので、止めている組織もあります)、これで入ります。

az extension add --name azure-devops

az devops loginでPATを使ったログインになります。

Azure Boardsの特定のIDを中身を取りたい場合こうします。

az boards work-item show --id xxx

これでWork Itemの結果が取得できますが、REST APIと同じJSONで返却されます。概要だけ見たいときはtable形式で。

az boards work-item show --id xxx --output table

ID    Type    Title                         Assigned To           State
----  ------  ----------------------------  --------------------  -------
614   Task    ワークスペースのところを書く     hoo@exsample.com      Active

これではいちいちID覚えてないといけないので、クエリで自分(@Me)にアサインされているタスクを検索します。

az boards query --wiql
  "SELECT [System.Id], [System.Title] FROM WorkItems WHERE [System.AssignedTo] = @Me AND [System.State] = 'Active'"

WIQLの構文はこちらを見てください。

docs.microsoft.com

JSON形式で返ってくるので、--output tableでもうちょっと見やすくなります。

ID    Title
----  ----------------------------------------------
23    サブメニューまでログイン
92    TFS本サンプルのテスト
107   TFSUGの華麗なデモ

これでIDとTitleが取れるので、az board show --id 23 --open で対象のアイテムがブラウザーで開かれます。プレビューですが、az interactiveでインタラクティブなコマンドが使えます。ヒストリーも強力なので便利そうです。

az interactiveを使う場合、プレビューですが、Windows Terminalsを使うほうがいいでしょう。cmd.exeよりもコンソール出力の応答がいいようです。

Packerで独自のAzure DevOpsビルドエージェントを作る

Microsoftが提供するMicrosoft hostedエージェントは便利なのですが、性能(2コア)やディスクが足りない(上限10GB)、などの理由で独自エージェントを立てたいという理由はよくあります。

OSに一からVSなどを入れるのは少々めんどくさいです。そんなときのためにMicrosoftからMicrosoft Hosted agentと同じエージェントを作るための仕組みが提供されています。

github.com

PowerShell DSCではなく、Packerで作ります。LinuxやmacOSもサポートしないといけないからでしょう。

GitHubのリリースのページから作りたいイメージをダウンロードします…といっても、同じバージョン(M150等)であれば、どれを選んでも中身は一緒です。レポジトリをgit cloneして固めてるだけなので。

今回はVS2019 on Windows Server 2019を選びます。

  • 展開したファイルのazure-pipelines-image-generation-vs2019-150.1\images\winフォルダを開きます。
  • vs2019-Server2019-Azure.json というファイルがテンプレートファイルです。このファイルから一か所変更します。
  "install_password": null

となっています。ここもほかの値と同様に外だしの変数として取得するようにします。

  "install_password":  "{{env `ARM_PASSWORD`}}"

次にPackerで使用するclient_id, client_secret, subscription_id, tenant_id, object_idを取得します。とはいっても難しいことはなくて、Azureのドキュメントで解説されているので、ここにあるPowerShellスクリプトを動かすだけです。

docs.microsoft.com

Visual Studio 2017を入れてしまっている人は、アプリと機能から"Microsoft Azure PowerShell - April 2018"を削除して、AZモジュールを使ってください。ドキュメントは今Azモジュール前提で書かれています。

  • Connect-AzAccount
  • リソースグループ作成
  • 資格情報の作成

ここまでは上のドキュメントにも載っています。次、このスクリプトを実行するためにはリソースグループと同じリージョンにストレージアカウントが必要です。PackerそのものはManaged Diskをサポートしているようですが、このAzure DevOpsエージェントを作るjsonはストレージアカウント前提です。

この辺の値が取れたら、必要な値を別のjsonに書いて、packerを実行します。

{
   "client_id": "ないしょ",
   "client_secret": "ないしょ",
   "tenant_id": "ないしょ",
   "subscription_id": "ないしょ",
   "object_id": "ないしょ",
   "location": "Japan East",
   "resource_group": "リソースグループ名",
   "storage_account": "ストレージアカウント名",
   "install_password": "パスワード"
}
packer  build -var-file=作ったjson vs2019-Server2019-Azure.json

これでひたすら待ちます。私はVSのインストールが無応答状態になって、結局諦めたんですが、たぶんこのjsonをそのまま使ったら少なくとも6-7時間はかかると思います。サーバーなどの起動しっぱなしマシンでやるのがいいと思います。

このテンプレートから必要ない古いコンポーネントを削除するとか、逆に必要なDockerイメージをキャッシュしておくとか、カスタマイズはいろいろできると思います。

一度ゴールデンイメージができたら、あとはsysprep済みのVMイメージがストレージアカウントに入っているので、VMとして起動するとか、もしくはダウンロードしてローカルのHyper-Vにマウントしてしまうとか、いろいろ方法はあると思います。

TFSUGのSlack参加ワークフローを支えるFlow

はじめに

tfsug.hatenablog.jp

要望があって、TFSUGのSlackを用意しました。無料版なので、一定期間で読めなくなりますが、よければ参加してください。

参加は無制限ではなく、申請方式にしています。Formsで氏名とメールアドレスをもらったら手動で招待しています。だいたい2営業日程度で招待処理しています。

使ったもの

  • Microsoft Forms
  • OneDrive for Business
  • Microsoft Flow(Office 365)
  • Slack

Excelを用意

f:id:kkamegawa:20190120152412p:plain

まず、格納するためのExcelをSharePoint Onlineに用意します。Logic Appsではなく、Flowを使っているのは頻度が高くないことと、Logic AppsだとOffice 365とAzureが同一Azure ADで提供されていないとアクセスできないためです。

f:id:kkamegawa:20190120152410p:plain

Excelではテーブルを作成しないと Flowで認識できないので、必須です。ヘッダ部分の列を選択して、テーブルとして作成します。

f:id:kkamegawa:20190120152448p:plain

テーブルが作成できました。これで準備完了です。

Forms

f:id:kkamegawa:20190120152416p:plain

こんな感じのFormを用意します。Excelと併用しているのはFormsの回答を閲覧できるのは同一Office 365組織内の人だけだからです。

Flow

こんな感じでFlowを作りました。

f:id:kkamegawa:20190120152419p:plain

アンケートが作成されたとき、アンケートに対して、一行Excelに作成します。

f:id:kkamegawa:20190120152423p:plain

最後に私と@changeworldさんに「作成された」ということをメールで通知します。この時のメールにはExcelのURLのみ書かれており、氏名やメールアドレスは含んでいません。やろうと思えばできますが、めんどくさいのと、メールにかかれると嫌だろうということもあります。

アンケート回答は1人で複数作れるからか、Apply to eachで回答ごとに作ることになるんですね。

最後にSlackの管理者用チャンネルに「リクエストがあった」というメッセージをポストします。これはどちらでもいいのですが、作業完了のチェックもメッセージにつけられるので、便利ですね。一応Excel内にもあるんですが。

終わりに

せっかくあるものなので、Flowでつくってみましたが、試行錯誤するものの、コーディングレスでこういうことができるので、結構便利です。

今はTwitterに流しているAzure DevOps関係の情報も自動的にSlackに流すようにしていますので、よければ参加してみてください。