なむゆだよ

なむゆなのだよ

Azureの資格試験「DP-100」を取得したので合格者の視点から語ってみる

f:id:nam_yu_sql:20210712072106j:plain この記事はcloud.config tech blogにもマルチポストします。

はじめに

マイクロソフト資格MCPにはRoleに合わせた様々なものがあるのですが、その中には機械学習系のRoleに関するものもあります。
以前から気になってちょくちょく勉強したりしていたのですが、最近ついに受験して合格してきたので、今回はそのDP-100という試験について共有したいと思います。

DP-100ってどんな試験?

公式サイトこちらです。
AzureのAI、機械学習系のサービスに関する資格試験の一つで、特にAzureのサービスを使って機械学習を用いたモデリングや分析を実際に行う方法に関する理解度を問われます。
機械学習の分析手法のことだけでなく、モデリングに使用するマシンの用意の方法から作成したモデルの運用まで、データサイエンスの業務のフロー全般が試験範囲になっています。
似た分野の試験はいくつかあります。例えば、AI-900という試験は人工知能機械学習の全般的なトピックを扱っています。特にAzure Bot ServiceやQnA Maker、Cognitive Serviceを用いた人工知能系のサービスに重点が置かれています。
他には、DP系試験の中にDP-203という試験があったりするのですが、こちらは機械学習そのものというよりは、機械学習に使用するためのデータの扱いが焦点となっています。
Azure SynapseやDatabricksを使用して様々な種類の大量のデータをどのように扱うかといった内容が試験範囲になっており、分析それ自体よりも機械学習で使用するためのデータの扱い方に興味がある方はこちらの方が向いているかもしれないです。
DP-100はそれらの中では機械学習の分析を行う方法の周りに焦点が置かれているのが特徴といえます。

DP-100ってどんな人向け?

Azureの機械学習系サービスに興味がある人、あるいはそれらを用いて業務を実際にこなしている人。
機械学習と言っても分析することだけでなく、マシンリソースの話から作ったモデルを運用するところまで含めて興味がある人向けの試験のように思います。
人工知能の分野も含めるとAI-900も候補に挙がりますが、チャットボットを作ったり画像内の物体検知したり云々というよりはデータを基にしてモデリングを行い、知見を抽出するデータサイエンスのトピックに興味がある人はDP-100が向いているように思います。
あるいは、次章のトピックを眺めてみて面白そうだったら受けてみるという決め方もいいかもしれません。

出題範囲のトピック色々

出題範囲は、サービスとしてみるとAzure Machine LearningサービスとDatabricks周りがメインになるのですが、機械学習系の業務のフローで見ると大きく分けて分析前の下準備の話、モデリングの話、モデリング後のデプロイや運用の話の3つくらいに分けられるように思います。
今回もその中で個人的におもしろそうだと思ったトピックをピックアップしていきます。
気になった部分がある方は、試験ページのLearnから、対応するトピックを探してみてください。

下準備

  • データの取得元の話。SQL Database、Databricks、Azure Storage等に配置したデータを分析対象にしたり。
  • データには欠損値や偏りがありがち。その解消方法とか。
  • 機械学習には分析のための高性能なマシンが必要。その設定方法とか。
  • 大量のデータを扱うのに使えるDatabricksというサービスのお話。

機械学習モデリング

デプロイ&運用

  • 作ったモデルはaksクラスターにWebアプリとしてデプロイして利用することができる。そのやり方とか。以前記事にも書きました。
  • いわゆるMLOpsの話。一度デプロイしたモデルを、時間の経過とともに更新して新しいデータに対しても精度を落とさないようにするには?
  • Application Insightsと連携してデプロイしたWebサービスからメトリックを出力し、運用状況を監視したり。

おわりに

今回は、最近試験を受けて合格したMicrosoft試験のDP-100について語ってみました。
この資格の試験範囲はAzureを使って実際にどういったやり方で機械学習を行うのかを学ぶのに一番適しているように思います。
興味が出たら、実際に試験を受けないでも、試験ページの下にある試験範囲に対応したLearnを見て興味のある分野について学んでみてください。

参考

Azure DevOpsでTerratest使ってTerraformスクリプトのテスト自動化をしましょーよ!の会

f:id:nam_yu_sql:20210518081229j:plain

この記事は後ほどcloud.config tech blogにもマルチポストします。

はじめに

最近はTerraformも回しています、なむゆです。
IaCを実現するツールであるTerraformですが、そのコードを編集した際にちゃんと思った通りに動くか確認する方法としては、そのコードを実際に動かして作成された環境を確認するのが一般的かと思います。
しかし、そうすると毎回terraform planしてterraform initし、環境が作成されて動作確認もできたら手動でその環境を削除しなければならず、かなり手間と時間がかかります。
ここを効率化しようとしたときに使えそうなツールをいろいろ探していた時に見つけた方法として、今回はTerratestを用いたAzure DevOpsパイプラインを作る方法を共有したいと思います。

Terratestとは

Terratestは、Gruntwork.ioが開発している、インフラストラクチャー向けのGo言語のテストライブラリです。
terra---という名前のとおり、terraformをメインでサポートしていますが、他にPackerやDocker、Kubernetesの構築のテストも行えるようです。
特に、terraformをサポートしているため、terraformがサポートしているGCPAWS、そしてAzureのインフラもテストすることができます。

TerratestをAzure Pipelineで実行出来たら何がうれしいの?

そんなインフラのテストライブラリであるTerraformですが、CI(継続的インテグレーション)の仕組みとしてAzure Pipelineに組み込めるとさらに強力になります。
Azureの環境構築にTerraformを使っていた場合、そのTerraformスクリプトGithub等で管理するかと思うのですが、更新したときにはそのコードをマージしても動作することを保証するために動作確認を取るかと思います。
その際にTerratestを使ったパイプラインを用意することで、例えばGithubにPRが作成されたタイミングでTerratestによるテストを自動的に実行し、動作確認を取って結果が正常かどうかを確認することができます。
手動で動作確認を取ろうとすると環境作成の手順や作成したリソースの確認の手間がかかったり作業した箇所以外の影響に気付かずデグレが起きたりする可能性がありますが、テストとして動作確認の自動化の仕組みを作っておくことでこれらを解決できます。

実際にTerratestのテストパイプラインを作ってみる

ここではTerratestのサンプルスクリプトを使いながら、Azure DevOpsのパイプラインとしてAzureにリソースをデプロイするTerraformスクリプトのテストを行う例を示したいと思います。

スクリプトを用意

今回使用するTerraformとテスト用のgolangスクリプトはTerratestのExamplesのページにあるAzureの例のものになります。
4つあるので、これらをAzure DevOpsのReposなり、権限のあるGithubリポジトリなりに作成しておきます。
気を付ける必要があるのはこれらを配置するディレクトリ構成です。
terraform_azure_example_test.goからの相対パスの記述の都合上以下のような構成で配置するようにしてください。

├─examples  
│  └─azure  
│      └─terraform-azure-example  
│              main.tf  
│              outputs.tf  
│              README.md  
│              variables.tf  
│  
└─test  
    └─azure  
            terraform_azure_example_test.go  

それに加えて、パイプライン用の以下のyamlスクリプトリポジトリ内の適当な場所に作成しておきます。
テスト用のgoスクリプトがあるディレクトリのパスだけ実際のディレクトリに合わせて変更しましょう。

pr: main  
trigger: none  
  
variables:  
  - group: AZURE_CREDENTIALS  
  
steps:  
  - pwsh: |  
      az login --service-principal -u $(ARM_CLIENT_ID) -p $(ARM_CLIENT_SECRET) --tenant $(ARM_TENANT_ID)  
      az account set --subscription $(ARM_SUBSCRIPTION_ID)  
      cd $(Build.SourcesDirectory)/[ここにはソースのルートディレクトリからテスト用のgoスクリプトがあるディレクトリまでのパスが入ります]  
      go mod init "terratest-ex"  
      go get -v -u github.com/gruntwork-io/terratest  
      go test -v -timeout 30m  
    env:  
      ARM_CLIENT_ID: $(ARM_CLIENT_ID)  
      ARM_CLIENT_SECRET: $(ARM_CLIENT_SECRET)  
      ARM_SUBSCRIPTION_ID: $(ARM_SUBSCRIPTION_ID)  
      ARM_TENANT_ID: $(ARM_TENANT_ID)  

今回使用するスクリプトとしては上記のもので全部になります。
次はこれらのスクリプトを動かすための下準備をしていきます。

Azureのサービスプリンシパルを用意

この作業には、お手持ちのAzure サブスクリプションに対して権限を割り当てられたサービスプリンシパルが必要になります。
所有者権限があれば十分なので、サービスプリンシパルを作成し、サブスクリプションに対して権限を割り当てておきます。
具体的な方法についてはこちらのドキュメントを参考にすると分かりやすいかと思います。
この時、いくつかのパラメータをメモしておくようにしてください。
パイプライン実行時にterraformの実行対象のサブスクリプションへのログイン処理に必要になるためです。
必要なものとしては、「割り当てた対象のサブスクリプションのID」「作成したサービスプリンシパルのクライアントID」「サービスプリンシパルを作成したActiveDirectoryのテナントID」、「作成したサービスプリンスパルにログインするためのシークレット」の4つになります。
サービスプリンシパルのシークレットの作成については、上記のドキュメント内の「新しいアプリケーション シークレットを作成する」の項にやり方が書かれています。

Variable Groupを用意

パイプラインで使用するVariable Groupを用意します。
Azure DevOps内のPipelines -> Library -> 「+ Variable Group」のボタンから、VariableGroupを新規作成します。
Azureのサービスプリンシパルを用意する際に取得した4つのパラメータをそれぞれの名前で追加していきます。

ARM_CLIENT_ID: 作成したサービスプリンシパルのクライアントID  
ARM_CLIENT_SECRET: 作成したサービスプリンスパルにログインするためのシークレット  
ARM_SUBSCRIPTION_ID: 割り当てた対象のサブスクリプションのID  
ARM_TENANT_ID: サービスプリンシパルを作成したActiveDirectoryのテナントID  

これら4つの変数を追加出来たら、Variable group nameに「AZURE_CREDENTIALS」と名前を付けて保存します。

パイプラインを用意

これにて下準備は完了したので、パイプラインを用意します。
パイプラインの作成方法については過去記事に例があるのでそちらを参考にしてみてください。

実行

最後に作成したパイプラインを実行します。
このパイプラインは6分程度で完了するかと思います。
処理の途中でAzureを開くと、リソースが作成されていることが確認できるかと思います。
そして、テストが終わるとそのテストのために作成されたリソースが順次削除されていき、最終的にはテストの実行前の状態に戻ることが確認できるかと思います。

おわりに

今回はAzure DevOpsでTerratestを用いたTerraformのコードのテストを実行する方法を共有しました。
これによってTerratestでのTerraformスクリプトのテストをAzure Pipeline上の自動化されたタスクとして行えるようになります。
Terraformコードが本当に動くか確認するために実際にterraformを実行して目視で動作確認するスクリプトて、テストに書いてあることはより確実にチェックでき、変更による他の部分の予期しない影響も感知することができます。
また、今回は行いませんでしたが、terraformスクリプトをapplyするのではなくplanをした際の結果を用いてテストを行うこともでき、そちらの方法だと1回のテストにかかる時間が少ない分より頻繁にテストを実行することができます。

Terraformコードの動作確認の効率化をしたい際に検討してみるのもいいかと思います。

参考

Azure Pipeline のパイプラインのトリガーの設定値はちゃんと明示的に書くんよ~

f:id:nam_yu_sql:20210425080324j:plain この記事は cloud.config tech blog にもマルチポストしています。

tech-blog.cloud-config.jp

はじめに

相変わらずパイプラインを回しています、なむゆです。
今回は、Azure Pipeline でパイプライン作成時に遭遇したパイプラインの実行条件のネタで一席打ちます。

パイプライン yaml のトリガーの話

Azure Pipeline とは Azure DevOps に含まれるサービスの一つで、CI/CD のためのパイプラインを作成し、実行できます。
Github 等のバージョン管理システムと連携することができ、設定さえしておけば Github 上のリポジトリにコードを push するだけで反射的にパイプラインを実行し、アプリケーションをビルドしたり(Continuous な Integration)、ビルドしたアプリを各種実行用の環境にデプロイしたり(Continuous な Delivery)することができる、CI/CD の概念を実現するためのサービスです。
Azure Pipeline を用いて CI/CD のためのパイプラインを実行する際にはそのパイプラインで行うことを指定するパイプライン yaml というものを書くのですが、その例や中身のざっくりした意味合いについては、こちらの記事で一つの例を解説しています。
今回は、この中では trigger: none とか pr:noneとなっている部分の話です。

これらの行は何を設定しているかというと、そのパイプラインが実行される条件です。
CI トリガー、PR トリガーと呼ばれています。
パイプラインを実行するためのトリガーにはほかにもいくつか種類があるのですが、今回はこれらの 2 つのトリガーで起きることを解説するため、他の種類のトリガーの解説については割愛します。

CI トリガー

まず、trigger: で始まるセクションは、CI トリガーというものを設定します。
これは、githubリポジトリ等の特定のブランチにコードが push された際、それをトリガーにしてパイプラインを回す設定です。
例えば ↓ のような書き方をします。

trigger:  
  branches:  
    include:  
    - '*'  
    exclude:  
    - feature/*  

この例の意味合いとしては、「基本的にどのブランチにコードが push されてもこのパイプラインを実行するよ~(*は全ブランチを示す)、でも feature/下のブランチにはコードが push されてもパイプラインを回さないよ」というものになります。
include:の下にあるブランチ名の配列がそのブランチにコードが push された際にパイプラインを実行する条件で、excludeの下にあるブランチ名の配列は include の条件で判別した後でやっぱりパイプラインを実行しない場合を指定する条件になります。

簡略な書き方としては、コードの push でトリガーする対象のブランチだけを指定するものもあります。

trigger:  
- main  
- releases/*  

上記の例では、main ブランチと release ブランチについてはコードが push されればパイプラインを実行しますが、他のブランチであれば実行しなくなります。

PR トリガー

次は、pr:で始まるセクションについてです。
こちらは PR トリガーというものを設定しています。
PR トリガーは、Github で PR が作成された際にパイプラインを実行するトリガーです。
例えば PR 作成時に単体テストを実行するなどといったことをするために使用します。

こちらは ↓ のような書き方をします。

pr:  
  branches:  
    include:  
    - main  
    - releases/*  
    exclude:  
    - releases/old*  

意味合いとしては、PR 作成時にパイプラインが実行されるということ以外 CI トリガーのものと基本的に同じです。
こちらにも、CI トリガーと同じように簡略化した書き方があります。

pr:  
- main  
- releases/*  

パイプラインのトリガーを設定しないとトリガーの対象が全てのブランチになる

Azure Pipeline のパイプライン yaml ではパイプラインの振る舞いを定義する様々な値を設定することができます。
その中のたいていのものは必要なければ設定を書かなければ何も起きず、パイプラインへの影響もありません。
しかし、その中で一部、何かしら値を設定しておかないと思っていたものと違う動作をする値があり、その例が CI トリガーと PR トリガーになります。

基本的にどの値も yaml の中に明示的に記述しなければ暗黙的にデフォルトの値が設定されるのですが、それが CI トリガーと PR トリガーでは以下のようになります。

trigger:  
  branches:  
    include:  
    - '*'  
pr:  
  branches:  
    include:  
    - '*'  

つまり、デフォルトではどのブランチにコードが push されても、どのブランチにマージしようとする pr が作成されてもパイプラインが実行されることになります。

これだと、例えば何かしら常に手動でのみ回したいパイプラインがあった時、CI トリガーと PR トリガーが必要ないやと思って何も書かないでいると、どこかしらのブランチにコードが push され、PR が作られるたびに勝手にパイプラインが実行されるようになります。
例えば手動でしか回したくないような重くて長時間かかるような処理をパイプラインとして実行していた場合、コードの push のたびにパイプラインが実行され、Organization で確保していた Pipeline 用の Agent の枠を圧迫するようになってしまいます。
そういった事態は避けたいですよね。
なので、CI トリガーと PR トリガーは、使用しない場合は明示的に書いてやる必要があります。

trigger: none  
pr: none  

このように書いておくと、コードの push や PR の作成をトリガーとしてパイプラインが実行されなくなるので、手動でのみ実行できるパイプラインになります。

おわりに

今回の記事では Azure Pipeline のトリガー設定は使用しない場合は明示的に使用しないことを記述しないと常に使用する状態になってしまうことを共有しました。
たぶんこの記事を読んでいる方は手動で回したりタイマーでトリガーするパイプラインが時々勝手に回っていることに悩んでいるのかなと思うので、その方向けの対処法を説明しました。
同じような問題を抱えている方の助けになれば幸いです。

参考

【小ネタ】PowershellでHashTableにHashTableを足す

f:id:nam_yu_sql:20210328183448j:plain

はじめに

最近Azure Pipelineとその縁でPowershellと仲良しこよしになっているなむゆです。
HashTableを扱うにあたって、HashTableに一つ一つのkeyvalueを追加する方法はどこでも解説されているのですが、HashTableにHashTableを足し合わせる方法についてはそこそこ調べても出てこなかったのでメモ書きです。

HashTableにHashTableを足し合わせる

$aと$bはどちらもHashTable型ですが、HashTableは以下のように + 演算子を使うことでHashTable同士を足し合わせることができます。

$a = @{  
    a="name a"  
    b="name b"  
}  
$b =@{  
    c="name c"  
    d="desperately"  
}  
  
$c= $a+$b  
  
# 出力  
# Name                           Value  
# ----                           -----  
# d                              desperately  
# a                              name a  
# c                              name c  
# b                              name b  

ただし、キーの値が被っているとエラーとなります。
以下の例だと、c のキーが重複するため、エラーとなります。

$a = @{  
    a="name a"  
    b="name b"  
    c="name c"  
}  
$b =@{  
    c="name c"  
    d="desperately"  
}  
  
$c= $a+$b  
  
# 出力  
# 項目は既に追加されています。辞書のキー: 'c'  追加されるキー:'c'  
# 発生場所 行:12 文字:1  
# + $c= $a+$b  
# + ~~~~~~~~~  
#     + CategoryInfo          : OperationStopped: (:) [], ArgumentException  
#     + FullyQualifiedErrorId : System.ArgumentException  

おわりに

HashTableにHashTableを足し合わせる方法についてはそこそこ調べても出てこなかった・・・と思って今調べたら「ハッシュテーブル同士 加算」あたりで色々出てきました。
ドキュメントにも記述にありますね。
「足し合わせる」よりも「加算」の方が言葉として正確でヒットしやすかったのかもしれません。
Powershellの「~について知りたいことのすべて」シリーズにはいつもお世話になっています。

参考

Azureパイプライン実行の流れをまとめてみる

f:id:nam_yu_sql:20210322063550j:plain この記事はcloud.config tech blogにもマルチポストしています。

tech-blog.cloud-config.jp

はじめに

パイプライン、回してますか?
今回もAzure Pipelineのネタで一席打ちます。
Azure Pipelineを回すとき、普段はなんとなしに「あー回ってんなーうわ止まったなんで」と眺めているのですが、Expressionの話を調べているとExpressionはコンパイルされるタイミングやタスクの開始時に評価されていることが分かり、「あーパイプラインも後ろでは順を追って回ってるんだなー」等考えるようになりました。
なので今回は、Azure Pipelineの実行の流れを調べたので、それを独断と偏見で重要そうなところをある程度かいつまんでまとめてみたいと思います。

Azure Pipelineのパイプライン実行の流れ

パイプライン実行の流れは大きく分けて「実行の最初に行われること」「stage単位で行われること」「job単位で行われること」「step単位で行われること」に分けられます。
stage,job,stepはYAML schema referenceで解説されているとおりの処理の単位で、stage、job、stepの順に細かくなっていきます。
しかし、これらの単位ごとの役割は単純に処理を括るだけのものではないのです。
順に見ていきましょう。

まず最初に

Azure DevOpsのパイプラインの実行APIを叩いたり、あるいはAzure DevOps上からパイプラインを実行したりした際、stepやjob等の手順に従って処理を始める前に、いくつかの前処理が走ります。
まず最初に、templateとtemplate expressionを処理します。
Templateとは大雑把に言うとパイプラインの一部を別のyamlファイルとして切り出すことで複数のパイプラインで使いまわせるようにする便利な仕組みなのですが、詳しくはまた今度解説したいと思います。
template expressionについては以前の記事で解説したとおりで、別名Compile time expressionと呼ばれ、つまるところパイプラインが回り始めるこのタイミングでyamlの文字列から実際の値に置き換わります。
それらが終わってから、一つ目のstageの処理に入ります。

stage単位での処理

stageは一番最初のものから順番に処理されていきます。
それぞれのstageの処理が始まるタイミングで、そのstageで使用するリソースが使える状態か、また、それらを利用する権限があるかをチェックします。
例えば、特定のazure リソースに接続するためのservice connectionで本当に対象のリソースにアクセスできるかをチェックします。
stageが始まるタイミングでこの処理が走るので、リソース名や権限関連の情報をvariableとして指定したい場合は、stageよりも上の層で指定する必要があります。そしてそうなると、runtime parameterで指定するか、あるいはpipeline層のvariable出しかこれらは設定できないことになります。
何か処理を行ってその結果で特定の場面で使用する認証情報を変えたい場合もあるかもしれませんが、その場合はstageを区切ってstage dapendenciesの仕組みを使って前のstageから認証情報を渡してやる必要があります。

一つのstageが終わると、前のstageの処理が成功していれば次のstageに移り、またはじめに戻って処理を始めます。
デフォルトでは前のstageが失敗していると残りのstageは実行されず、パイプライン全体として失敗とみなされます。

job単位での処理

jobは、agent単位で実行される処理の単位です。
一つのstageの中に複数のjobがあった場合、それらはそれぞれ別のagentで並列に実行されます。
stage内では、それぞれのjobのdependson等の値から、最初に実行できるjobを選び、それから実行していきます。
行うjobが決まると、まずはそのjobで使用するagentを決めます。yamlの中では pool: の部分を参照し、どの種類のagentを使用するか決めます。
agentはそれぞれの処理を物理的に行うもので、実体はself-hostedにしろMicrosoft-hostedにしろVMです。
ユーザーが構成したVMを使う場合をself-hostedと呼んだり、microsoftが作成したテンプレ構成のものを使う場合はMicrosoft-hostedと呼んだりする形です。
agentについては以前の記事で触れているのでそちらも見てみてください。
使用できるagentがあればそれを使用して処理を始めますが、無ければ使えるagentが出てくるまでこの状態で待ちになります。
使用するagentが確保できれば、そのagentにjobで行う処理の内容を送信し、それぞれのtaskで指定された処理を始めます。

そのjobが終わったら、他のjobのdependsOnやconditionから次に行うjobを決定し、実行します。
なお、job毎にagentを確保するという点ですが、これは別のjobに移るごとにそのagentのVMの中身がリセットされるということでもあります。
なので、生成したファイルに依存する処理は、できる限り同じjobに含めるようにしましょう。
あるいは、blob storageなどの共通で使える外部リソースを利用してjob間でファイルを受け渡す必要があります。

step単位での処理

stepはjobとは違い、上から順に処理されていきます。
ここまでくるとyamlに記述された個別のtaskを実行するのみで、事前準備として特筆することはあまりありません。
ちなみに、Azure Pipelineのタスクはこちらで纏められているとおり多種多様なのですが、個々のagentで行われる実体としてはpowershellコマンドかNode.jsの形になるようです。これは豆ですね。
また、jobの間ではマシン内に保存したファイルは引き継がれないという話をしましたが、task間では同じマシンを連続で使うためファイルが保持されます。

一つのstepが終わると、その結果を記録してから次のstepを開始します。
しかし、jobの中で複数のstepを順番に実行しているときに一つでも失敗した場合、そのjobの中のそれ以降のstepはデフォルトでは実行されなくなります。
conditionでalways()を指定しているstepは例外的に実行されるので、処理が途中で一度転んでも実行したいような処理(例えば変更のクリーンアップとか)は最後のstepとしてalways()の設定をして実行するといいです。

おわりに

今回はAzure Pipelineのパイプライン実行の流れについて纏めて共有しました。
yamlを書いて回せば回る」くらいの認識でもAzure Pipelineを運用することはできますが、その後ろで起きていることを知ることでその仕組みについてもう少し理解を深めることができるかと思います。
Azure Pipelineを極めて最強のDevOpsエンジニアを目指しましょう。

参考

Azure Identity SDKのDefaultAzureCredentialでローカルデバッグ時に詰まった話

f:id:nam_yu_sql:20210318072751j:plain この記事はcloud.config tech blogにもマルチポストしています。

tech-blog.cloud-config.jp

はじめに

Azure DevOpsとバックエンドを反復横跳びしているなむゆです。
最近はAzureのサービスとの絡みも増えて、インプットが増えている気がします。嬉しいですね。
今回はそんなAzureのサービスと絡む.NETアプリ開発の中で一つ問題が起きて七転八倒したお話を共有します。

Azure Identity SDKとは

Azure Identity SDKとは、Azure Active Directoryを用いたトークン認証をサポートするSDKです。
他のAzure SDKを用いてAzureリソースにアクセスしようとする時の認証に使います。
このSDKに含まれるものの中でも特に優秀なのが「DefaultAzureCredentials」というクラスで、これはこのクラスのインスタンスを渡しておくだけでAzure環境上であればそのアプリがデプロイされたリソースのマネージドIDを用いて認証を行ってくれます。
さらに、ローカル環境でデバッグする際はその環境のVisual StudioVisual Studio Code、あるいはコマンドコンソールでaz loginした際の認証情報を使って認証を行ってくれるので、Azure環境でしか動かないということもありません。
オンプレ環境であっても環境変数を見てくれるので死角なしです。
どうしてそうなっているかというと、このクラスの動作としてはその環境の認証情報がある場所を順に探索し、見つかればそれを使うというロジックになっているためです。
具体的な順番はDefaultAzureCredentialに書いてありますが、例えば一番最初は環境変数を確認しに行くのですがそこに必要な認証情報がなければ次はマネージドIDを探しに行きます。
それでも見つからなければ次は使用しているVisual Studioの認証情報を・・・といったように順に探しに行き、どこかで認証情報が見つかればそれを使う形になっています。
さらに、認証情報をappsettings等の環境情報として管理しなくてよくなるというメリットもあります。
ローカルではvisual studioやazure cliでログインしていれば、Azure上ではデプロイ先のリソースのマネージドIDを設定しておけばいいので、環境ごとに環境情報ファイルを書き換えて保存する手間が省けます。
今回も、それを用いて実装しようとしていました。

どんなエラーが起きたか

まずはローカルでデバッグだというわけで認証情報取得順の中ではAzure CLIの認証情報を使いたいため、別コンソールでaz loginしました。
.NETアプリケーションからAzure Keyvaultの接続する際、認証情報の取得をAzure Identity SDKのDefaultAzureCredentialクラスを使用して行おうとしました。
サンプルのコードはこちらで、大体そのサンプルコードの通りに実装してデバッグしてみました。
するとkeyvaultアクセスを行っている行で例外が発生しました。
以下がその例外のメッセージです。

Service request failed.  
Status: 401 (Unauthorized)  
  
Content:  
{"error":{"code":"Unauthorized","message":"AKV10032: Invalid issuer. Expected one of (keyvaultの属するテナントIDがいくつか入る), found (Visual StudioでログインしているユーザーのテナントID))"}}  
  
Headers:  
Cache-Control: no-cache  
Pragma: no-cache  
WWW-Authenticate: REDACTED  
x-ms-keyvault-region: (どこかのリージョン)  
x-ms-client-request-id: (clientIdが入っている)  
x-ms-request-id: REDACTED  
x-ms-keyvault-service-version: 1.2.205.0  
x-ms-keyvault-network-info: conn_type=Ipv4;addr=(IPアドレス);act_addr_fam=InterNetwork;  
X-Powered-By: REDACTED  
Strict-Transport-Security: REDACTED  
X-Content-Type-Options: REDACTED  
Date: (リクエストした時間)  
Content-Length: 345  
Content-Type: application/json; charset=utf-8  
Expires: -1  
  

エラーを見る限りkeyvaultにアクセスできる認証情報ではない認証情報が使われていました。
Azure CLIでログインした際の認証情報でもありません。
Visual Studio Enterprise関連の認証情報です。<- 大ヒント

原因

本当は話を引っ張ろうとも思ったのですが無理です。
前述のDefaultAzureCredential()のロジックとしてAzure CLIで取得した認証情報より先にVisual Studioでログインした情報を使用して認証しようとしていました。
この認証情報では目標のkeyvaultにはアクセスできないため、認証できないとしてエラーになっていました。

解決方法

「じゃあ途中の使いたくない認証情報を探しに行くのをスキップすればいいじゃん」という発想で「DefaultAzureCredential Exclude VisualStudio」等で調べたところ・・・ありました
DefaultAzureCredentialクラスをインスタンス化する際にOptionsとして ExcludeVisualStudioCredential = true を渡しておくことで、認証情報を探すフローからvisual studioのものを外してくれます。
他に、TokenCacheというらしいものもフローに含まれているらしい(ドキュメントに記載なし)ので、これも除外するようにした以下のコードにしたところAzure CLIの認証情報を使ってリソースにアクセスできました。

var client = new SecretClient(new Uri("KeyVaultのUrl"), new DefaultAzureCredential(new DefaultAzureCredentialOptions  
            {  
                ExcludeVisualStudioCredential = true,  
                ExcludeVisualStudioCodeCredential = true,  
                ExcludeSharedTokenCacheCredential = true  
            }  
        )  
    );  

おわりに

今回はAzureリソースにアクセスする際の認証情報の取得周りで少し詰まったことを共有しました。
同じ部分で詰まっている方の助けになれば幸いです~

参考

ややこssssssっし~なAzure PipelineのExpressionsのお話

f:id:nam_yu_sql:20210313234023j:plain この記事はcloud.config tech blogにもマルチポストしています。

tech-blog.cloud-config.jp

はじめに

世間ではいろんな人がトレーナーになって馬を走らせているようですがなむゆは相変わらずパイプラインを走らせています。あるいは回しています。
パイプラインを実行することの言い回しって「走る」と「回す」の二つを同じくらいよく聞くのですがこれを読んでいる人はパイプラインは走らせているのでしょうか、回しているのでしょうか気になる今日この頃です。

さて、今回はAzure Pipelineでパイプラインのyamlを書いていてしばらく混乱していたことがあるので整理がてら一席打ちたいと思います。
内容としてはAzure Pipelineのパイプライン定義yamlでよく書く$[]や${{}}、$()といったもの、いわゆるExpressionというものについてです。

Expressions

Expressionは直訳すると「式」になります。
C#ラムダ式(lambda-expression)とか言いますよね。
特殊な記法の式を書くことで、関数を簡単に書いたりなんやかんやするアレです。
で、Azure Pipelineにおいても特殊な記法をする「式」を用いて様々な操作を行うことがあります。
使い方のよくやる例としては以前の記事で共有したvariableやparameterの値の取得があります。
他にもラムダ式のような関数を定義する式も作れたりするのですが、個別の記法はExpressionsのドキュメントを参照してください。
さて、問題はというと、これらの記法は主に3つあり、どれも共通の機能の一つとしてパイプラインで設定した変数を扱うことができるのですが、それぞれ取得できる値や使いどころが微妙に違っていることです。
似たようなものなのでどれか一つの書き方で書こうとしていたら特定の場面では使えなかったりします。
なので、それぞれのExpressionの特徴を把握して使いどころを間違えないようになりましょう。

まず初めにAzure Pipelineで使用できる変数について復習

以前の記事でも取り上げたお話ですが、今回の話で重要なトピックなので最初に軽く解説していきます。
Expressionの使い分けの一つに「どのExpressionではどの変数が呼べるか」という点があるためです。

Azure Pipelineの機能として設定して使用できる変数はよく使うもので3つほどあります。
一つ目はパラメータです。これはランタイムパラメータとも呼ばれ、パイプラインの実行ごとに手動で指定してやるパラメータです。
パイプラインyamlでパラメータを設定しているとAzure Pipelineでパイプラインを実行する際にブランチ名のドロップダウンの下に入力欄が出てくるのでそこで指定します。

二つ目はvariableとして定義するstaticな変数です。これはパイプライン固有の固定値を設定するために用います。
root、stage、jobのレベルのそれぞれのスコープで指定することができ、変数名が被った場合はよりスコープが狭い方が優先されます。

三つ目は、variable groupとして設定する変数です。これはAzure DevopsのLibraryから設定する値で、Libraryからいつでも設定し直せます。じゃあparameterでいいようにも思えますが、parameter程実行ごとに変更するものではない値の設定や、また、variable groupにはsecret属性を持たせることもできるため、parameterとはまた違った目的で利用します。

それぞれの書き方は以下の通りです。
上から順にランタイムパラメータ、 group:の形で書かれているのはvariable groupの名前、 -namevalue に分かれているのがstaticのvariableです。

parameters:  
  - name: "sampleParam"  
    type: string  
    displayName: "SampleParameter"  
variables:  
  - group: SAMPLE_VARIABLE_GROUP  
  - name: SAMPLE_VARIABLE_VALUE  
    value: sample variable value  

${{}}について

${{}} の形で記述されるこのExpressionはTemplate expression、またはCompile time expressionといいます。
msdnExpressionsのページではCompile time expressionと呼ばれていますが、Define variablesのページではTemplate expressionと呼ばれています。実際はどちらも同じものです。
Compile timeと呼ばれるとおりAzure pipelineが回り始める下準備としてパイプラインyamlから実際の実行プランが作られる段階で、そのExpressionで計算された値(変数の呼び出しをするExpressionだったらその変数の値とか)と置き換えられます。
yamlから実行プランにコンパイルされる際に実際の値置き変えられるので、指定したスコープより内側であればyaml上のどこにでも書くことができます。
取得できる変数は、先程説明した3つの変数のうち固定値のvariableとruntime parameterです。
variable groupの値は取得できません。残念。
書き方としては以下のようになります。

${{ parameters.変数名 }} # runtime parameterから値を取りたい場合  
${{ varibales.変数名 }} # 固定値のvariableから値を取りたい場合  

$[]について

$[] の形で記述されるこのExpressionはRuntime expressionといいます。
こちらはCompile time expressionとは対照的に変数の値の評価のタイミング、つまりExpressionから実際の値に置き換えられるタイミングは処理の実行時になっています。
つまり、variableの値が以前のtask等で書き替えられた場合、そのExpressionが書かれている処理が行われるタイミングのvariableの値が使用されます。
別のタスクでvariableの値を書き換える例はこちらにあります。
このExpressionは使い方に癖があります。
Runtime expressionを使う際は、キーバリューペアの右側全体をカバーしなければならないのです!!!
キーバリューペアの右側全体をカバー・・・?意味が分からないですね。
これはvariableの解説ページのRuntime expression syntaxに書かれていることで、原文では

The runtime expression must take up the entire right side of a key-value pair.

とあります。
これが意味することとしては、例えばvariableの値としてruntime expressionを指定するときに以下のように書きますが・・・

  - name: SAMPLE_VARIABLE_VALUE  
    value: $[variables.sampleval]  

この時、 value: $[variables.sampleval]: を挟んだ右側全体をカバーしなければならないという意味です。なので、例えば以下のような書き方はできません。

  - name: SAMPLE_VARIABLE_VALUE  
    value: sampleval is $[variables.sampleval]  

そのexpression以外のものを : の右側に書くとエラーになってしまいます。
他の、例えばCompile time expressionは以下のような書き方ができます。

  - name: SAMPLE_VARIABLE_VALUE  
    value: sampleval is ${{ variables.sampleval }}  

後述のMacro expressionでも・・・

  - name: SAMPLE_VARIABLE_VALUE  
    value: sampleval is $(sampleval)  

というように書けます。
使えるタイミングとしてはvariableとconditionのキーバリューペアになるそうなのですがvariableの値の指定に使うのはちょっと不便ですね。
ちなみにconditionのキーバリューとはstageやjobに対して設定できる値で、この値がtrueの場合のみそのstageやjobを実行するといった動作を指定できます。
これを使用して、「前の処理が成功した場合のみこの処理を実行」というようなロジックを組み込むことができます。
実際にcondition内でどのような判定ロジックを組み込むかはSpecify conditionsのドキュメントを参照です。
今回は変数の取得周りに話を限って解説しています。この辺りは一旦話を広げ始めると無限に広がってしまうのですよ・・・
なお、取得できる変数は、先程説明した3つの変数のうち固定値のvariableとvariabe groupで指定されているvariableです。
runtime parameterの値は取得できません。Runtime Expressionなのにruntime parameterの値を取得できないとはこれいかに。
書き方としては以下のようになります。

$[varibales.変数名] # 固定値のvariable、variable groupのvariable 共通  

$()について

$() の形で記述されるこのExpressionはMacro expressionといいます。
Macro expressionはパイプラインの中で、それぞれのタスクが実行される前に評価されて実際の値に置き換えられます。
こちらもCompile time expressionと同じく、適切なスコープ内でなら基本的にどこでも使えるのですが、一部Runtime Expressionと同じような制限を受けるところがあります。
それは、キーバリューペアの中では : の右側にしか書けないということです。yamlコンパイルされ、プランとしては出来上がった後で評価されるため、プランの構造自体は操作できない・・・というような意味合いかと個人的には考えています。
ただ、前述のとおりRuntime Expressionとは違って : の右側であれば別の文字列の中に混ぜて記述することも可能です。
なお、取得できる変数は、先程説明した3つの変数のうちRuntime expressionと同じく固定値のvariableとvariabe groupで指定されているvariableです。
runtime parameterの値は取得できません。その場合はCompile time expressionを使いましょう。
書き方としては以下のようになります。

$(変数名) # 固定値のvariable、variable groupのvariable 共通  

変数名を直接書ける分シンプルですね。

まとめ

表にしてまとめると以下のようになります。

Expression名 書き方 variablegroupの値が取れるか variableの固定値が取れるか parameterの値が取れるか 評価タイミング 使用可能箇所 注意点
template expression ${{ parameters.パラメータ名 }}
または${{ variables.変数名 }}
x o o コンパイル どこでも(スコープが適切であれば) 別名: Compile time expression
runtime expression $[variables.変数名] o o x 実行時 variableとconditionの定義箇所 定義の右側全体をカバーする必要あり
macro expression $(変数名) o o x タスク実行前 タスクの定義内 定義の右側にのみ書ける

また、これらのExpressionの評価順はcompile time expression -> macro expression -> runtime expression となっています。
評価タイミングがやってくる前にvariableの値に変更があった場合、評価のタイミングが早い別のexpressionとは違った値が取得される可能性がありますので、ユースケースによっては色々利用できます。

おわりに

今回はAzure pipelineのExpressionについて、変数の取得方法の話と絡めて共有しました。
Expressionについてはよく使うユースケースとしてはパイプラインの変数の取得なのですが、実は本来それだけのためのものでないという話や、評価タイミングについてはそもそもパイプラインyamlからどのようにしてパイプライン実行されるかという話と絡んでいたりするという話もあったりして、切り口によって無限に話が膨らんでいってしまうので説明はすごく難しいです。
今回のこの記事も何度も「ややこssssssっし!!」と呟きながら纏めていました。
少なくともよく気になる観点については纏められたかなと思うので、少しでもお役に立てれば幸いです。

参考

  • Devine Variables
    Variableとそれを使うExpressionの解説はこちら。
    ここでは${{}}のことはTemplate expressionと呼ばれています。
  • Expressions
    Expressionそのものについての解説はこちら。
    ただし、こちらではmacro expressionについての解説はありません。なんで。
  • Specify conditions
    Runtime expressionの使いどころであるconditionの解説はこちら。
  • Add & use variable groups
    Variable Groupに関する解説はこちら。
    Azure Devops上でvariable groupを編集、追加する方法はCreate a variable groupの章のClassicタブにあります。
  • Runtime parameters
    Runtime parameterについての解説はこちら。
  • YAML schema reference
    Azure Pipelineの処理の内容を定義するパイプラインyamlの全体的な書き方はこちら。
    特に、stage、job、stepといった階層の話はvariableの指定、使用できるスコープの話と関連するので、variableについて調べるなら併せて読むと理解が深まるかと思います。