AzureへのAzure Pipelinesを使ったアプリ(functions)とインフラ(Bicepメイン)のCICD / PullRequestをトリガーに独立環境を展開する

現在業務でAzureをプラットフォームにしたシステムの開発をしています。.netのコードがありそれがAzure functionsに展開されて動いている感じです。「何を作るか」「どうやって作るか」などかなりの部分を私がコントロール可能な状況にさせてもらってるので、色々とやりたいことが実現出来て素敵な感じです。

そんな中、アプリ開発はチームメンバーに任せて、私は主にインフラ周りのコードなどを書いてます。これまで個人でボッチ開発しかまともにしたことが無かったのでgitでbranchの扱いとか、commit履歴のコントロールとか色々と勉強になっております。

そして、開発基盤にAzure DevOpsを使っております。GitレポジトリもAzure Repos, パイプラインもAzure Pipelinesを使っています。

そこで、PullRequestの生成をトリガーに完全に独立した環境を構築してそこで事前確認ができる環境を作ってみました。レポジトリが1つなら簡単だったのですが、アプリケーションのレポジトリとインフラ展開のレポジトリを分割している状況であまり直接的な例が見つけられずかなり苦労してしまいました。多分丸4日くらいかかりました…。

次回以降簡単に実装できるように記録しておきたいと思います。

なお、私はまだまともなAzure Pipelines歴は3ヶ月くらいなので効率的じゃない部分多いかと思います。アドバイスありましたらコメントいただけると嬉しいです。

レポジトリ構成

  • AppRepo – .netのアプリケーションのコードが書かれている
  • InfraRepo – インフラ展開周りのコードが書かれている

できるようにしたこと

  1. アプリもインフラも更新があったら最新のコードでメインの環境が更新される
  2. アプリもインフラもPullRequestがあったらPullRequestに対応した独立環境が作成される。PullRequestに使われたBranchに更新があったらその更新も反映される。これによりmainブランチにマージする前にAzureの環境上で実際の動作を確認できる。

もうちょっと詳しいできるようにしたこと

  1. AppRepoのmainレポジトリに更新があったら自動でビルド&テスト。ビルド&テストがOKなら最新のインフラのコードとともにメイン環境に展開(更新)。
  2. InfraRepoのmainレポジトリに更新があったら自動で最新のアプリのビルドとともにメイン環境に展開(更新)。
  3. AppRepoにPullRequestがあったらPullRequestのbranchの最新のアプリのコードで自動でビルド&テスト。ビルド&テストがOKなら最新のインフラのコードとともにPullRequestのIDから作成した新規のリソースグループに最新のインフラのコードとともに展開(新規作成)。
  4. PullRequestのbranchに更新があったらその最新のコードで自動で自動でビルド&テスト。ビルド&テストがOKなら最新のインフラのコードとともにPullRequestのIDから作成したリソースグループの環境をインフラもアプリも更新(更新)。
  5. InfraRepoにPullRequestがあったらPullReqestのbranchの最新のインフラのコードでPullRequestのIDから作成した新規のリソースグループにmainブランチの最新のアプリのコードとともに展開(新規作成)。

実装アイデアおよび苦労した点

  • PipelineはAppRepoにパイプライン1つ、InfraRepoにパイプライン1つという形で実装しました。ほかにも良いやり方がある気がしましたが、色々と悩んだ末…。
  • Pipelineが起動された時、特にインフラ側のPipelineが起動された時にそれがどういうパターンで起動されたのかを判別させる部分で苦労しました。下記4パターンがあります。具体的な判別のさせ方はPipelineの中身のコードの方で解説します。
    • 1. AppRepoのPipelineが直接起動されたか、mainブランチの更新で起動されてビルドに成功した結果としてInfraRepoのパイプラインが起動された場合
    • 2. AppRepoのPipelineがPullRequestで起動された結果ビルドに成功し、その結果としてInfraRepoのパイプラインが起動された場合
    • 3. InfraRepoのパイプラインが直接あるいはmainブランチの更新によって起動された場合
    • 4. InfraRepoのパイプラインがPullRequestにより起動された場合
  • Azure PipelinesのPipeline間で情報を連携する良い方法がわからなくて苦労しました。結局ビルド結果やほかの方法での受け渡し方がわからなかったのでPullRequestのIDを書いたテキストも一緒にZipで固めてArtifactsとして発行し、インフラ展開側のPipelineでダウンロードして利用しました。(※PullRequestIDの受け渡しはもっと良い方法が明らかにある気がしています。)

具体的なやり方

AppRepoのPipeline

name: $(Date:yyyyMMdd).$(Rev:r)

trigger:
  batch: true
  branches:
    include:
    - main

  paths:
    exclude:
      - README.md

variables:
  - name: vmImage
    value: "windows-latest"

steps:
  - task: DotNetCoreCLI@2
    displayName: Build and Restore
    inputs:
      command: "build"
      projects: '**/*.csproj'
      arguments: --output $(System.DefaultWorkingDirectory)/publish_output --configuration Release

  - task: DotNetCoreCLI@2
    displayName: Test
    inputs:
      command: 'test'
      projects: '**/*Test.csproj'
      arguments: '--configuration Release'      

  # PullRequestIDも保存しておく
  - script: echo "$(system.pullRequest.pullRequestId)" > $(System.DefaultWorkingDirectory)/publish_output/pullrequestID.txt
    condition: eq(variables['Build.Reason'], 'PullRequest')

  - task: ArchiveFiles@2
    displayName: "Archive Files"
    inputs:
      rootFolderOrFile: "$(System.DefaultWorkingDirectory)/publish_output"
      includeRootFolder: false
      archiveType: zip
      archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
      replaceExistingArchive: true

  # 発行
  - publish: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
    displayName: Create function app artifact
    artifact: drop

ほとんど素直にビルド、テストしたうえでArtifactを発行しているだけですが下記の部分で「PullRequestで起動された場合にはPullRequestのIDをpullrequestID.txtというテキストファイルに書いて一緒に発行されるようにしています。

# PullRequestIDも保存しておく
  - script: echo "$(system.pullRequest.pullRequestId)" > $(System.DefaultWorkingDirectory)/publish_output/pullrequestID.txt
    condition: eq(variables['Build.Reason'], 'PullRequest')

variables[‘Build.Reason’] にはパイプラインが起動された理由が入ってきます。PullRequestの場合には「PullRequest」と入ってますのでそれを見て判別しています。

$(system.pullRequest.pullRequestId)にはpullRequestのIDが入ってます。

この辺り$()でpipelineの変数を参照するパターンとvariables[”]で参照するパターンが混ざっていてこれだけでも難しい感じがします…。慣れれば簡単になるのかな…。

InfraRepoのPipeline

name: $(Date:yyyyMMdd).$(Rev:r)

trigger:
  - main
pr:
  - none

resources:
  pipelines:
  - pipeline: AppRepo
    source: AppRepo
    trigger: true

variables:
  - name: vmImage
    value: "windows-latest"
  - name: isTriggeredByResourceTrigger
    value: $[eq(variables['Build.Reason'], 'ResourceTrigger')]

stages:
  - stage: Build_and_Publish
    jobs:
      - job: "Use_Artifacts"
        condition: eq(variables.isTriggeredByResourceTrigger, 'True')
        pool:
          vmImage: ${{variables.vmImage}}
        steps:
          - template: "Pipelines/Build_and_Publish/useartifacts.yaml"

      - job: "Build_latest"
        condition: ne(variables.isTriggeredByResourceTrigger, 'True')
        pool:
          vmImage: ${{variables.vmImage}}
        steps:
          - template: "Pipelines/Build_and_Publish/buildlatest.yaml"
            parameters:
              projectFiles: "**/*.csproj"

      - job: "Publish_BicepTemplates"
        pool:
          vmImage: ${{variables.vmImage}}
        steps:
          - publish: $(Build.Repository.LocalPath)/Resources
            displayName: Create Bicep template artifacts
            artifact: deploy      

  - stage: Deploy
    dependsOn: ["Build_and_Publish"]
    jobs:
      - job: "Deploy"
        pool:
          vmImage: ${{variables.vmImage}}
        steps:
          - template: "Pipelines/Deploy/downloadartifacts.yaml"
          - template: "Pipelines/Deploy/setvariables.yaml"
          - template: "Pipelines/Deploy/deploy.yaml"
            parameters:
              buildNumber: $(Build.BuildNumber)

インフラ側ではまず下記の記載でmainブランチの更新ではパイプラインが起動するようにしつつ、プルリクエストではパイプラインが起動しないように明示的に書いています。プルリクエスト時の起動は別の機構を使います。

trigger:
  - main
pr:
  - none

Build_and_Publishステージではどの経路でもアプリケーションのビルド結果とインフラのコードが発行されるようにしています。

(mainブランチ更新トリガーにせよPullRequestトリガーにせよ)AppRepo側でビルドが完了してから起動された場合にはvariables[‘Build.Reason’] に「ResourceTrigger」と入っています。この場合AppRepoで生成されたものをArtifactから受け取ってそれを使います。そうでない場合にはAppRepoのmainブランチの最新のコードを取ってきて自分でビルドしています。

さらにインフラのコードもArtifactとして発行しています。インフラのコードはPullRequestの場合には勝手にそのコードが触れる状態になっているのでPullRequestの管理は自分ではやらなくて大丈夫です。

#downloadartifacts.yaml

steps:
  - download: current
    artifact: drop
  - download: current
    artifact: deploy
  - task: ExtractFiles@1
    inputs:
      archiveFilePatterns: '$(Pipeline.Workspace)/drop/*.zip'
      destinationFolder: $(Pipeline.Workspace)/pullRequestId
  - task: PowerShell@2
    displayName: "get appPullrequestID"
    inputs:
      targetType: 'inline'
      script: |
        $appPullrequestID = ""
        If(Test-Path "$(Pipeline.Workspace)/pullRequestId/pullrequestID.txt") {
          $appPullrequestID = Get-Content "$(Pipeline.Workspace)/pullRequestId/pullrequestID.txt"
          Write-Host "##vso[task.setvariable variable=appPullRequestID;]$appPullrequestID"
        } else {
          Write-Host "There is no pullrequestID.txt."
          Write-Host "##vso[task.setvariable variable=appPullRequestID;]"
        }

downloadartifacts.yamlではダウンロードしてます。そして、pullrequestID.txtがあればその内容を変数にセットしています。後の処理ではこのpullrequestID変数がセットされているかどうかで分岐させています。

下記はsetvariables.yamlの一部です。

$appPullrequestID = "$(appPullrequestID)"
$buildReason = "$(Build.Reason)"
$uniqueString = "" # PullRequestの時にリソース名が重複しないようにするための文字
        if($appPullrequestID -ne "") {
            Write-Host "appPullrequestID is null"
            $deployNumber = $appPullrequestID
            $resourceGroupName = "pr-$appPullrequestID"
            $deploymode = "PullRequest"
            $uniqueString = "p"
        } elseif($buildReason -eq "PullRequest") {
            Write-Host "buildReason is PullRequest"
            $deployNumber = "$(System.PullRequest.PullRequestId)"
            $resourceGroupName = "pr-$(System.PullRequest.PullRequestId)"
            $deploymode = "PullRequest"
            $uniqueString = "p"
        } else {
            Write-Host "This is not PullRequest"
            $deployNumber = "001"
            $resourceGroupName = "rg-${serviceName}-${environmentName}-${deployNumber}"
            $deploymode = "Normal"
        }

$appPullrequestIDに何か値が入っていれば、AppRepoのPullRequestからトリガーされてきていることがわかります。

$appPullrequestIDに値が入っておらず、$buildReasonがPullRequestになっていればInfraRepoに対してのPullRequestで起動されたことがわかります。

どちらでもなければPullRequestトリガーではないので、メインの環境を対象とすればよいことがわかります。

それぞれのケースで必要な変数の値をセットしてあげてあとは普通に展開してあげればほぼ大丈夫です。

私の扱っているケースではリソース名の最後にその番号を付けています。xxxxxx001という感じです。同じ種類のリソースが複数あるとxxxxxx001, xxxxxx002, xxxxxx003と連番をふっていく感じになっています。

リソース名の重複を防ぐために$uniqueStringという変数を作ってありリソース名で002, 003など複数ある場合には重複をさせないようにしています。

        setvariable "keyvaultName001" "kv-${serviceName}-${environmentName}-${deployNumber}"
        setvariable "keyvaultName002" "kv-${serviceName}-${environmentName}-${deployNumber2}${uniqueString}"

pullrequestの時にとっても変な名前になっちゃうので、Bicepのuniquestring()を使った方がよかったかなとちょっと思ってますが、PullRequestの時には全リソースが変な名前になっていても、重複さえしなければよいのでOKということにしています。

PullRequestの時にPipelineを起動する方法

PullRequestの時にPipelineを起動するのはYamlに書くのではなくレポジトリのmainブランチのValidationポリシーを用いています

問題

一応うまく動いていますが、AppRepoとInfraRepoでPullRequestIDが重複すると名前が衝突してリソース生成に失敗します。回避は簡単ではありますがリソースの名前の文字長の問題で対処が難しく(もともとのネーミングルールの問題)、重複する可能性はかなり低いので気にしないことにしています。

とりあえず現状はこんな感じにしています。一応意図した動きはしていますが、力技だなぁと思っております…。

コメントを残す