ROMANCE DAWN for the new world

Microsoft Azure を中心とした技術情報を書いています。

クラウドデベロッパーちゃんねるで Azure Kubernetes Service の話をしてきました

クラウドデベロッパーちゃんねるで、ちょまどさんと「Azure Kubernetes Service を活用したマイクロサービス開発のベストプラクティス」 というタイトルでお話してきました。

www.youtube.com

昨年の Advent Calendar で書いた記事の内容を解説している動画です。

gooner.hateblo.jp

当初は20分くらいでまとめる予定でしたが、いざ始めてみると30分くらい話してしまいました。ちょまどさんとの掛け合いというか相槌のタイミングは、オンラインだと難しいなと感じました。このあたりは慣れかもしれませんが、これからもオンラインの登壇は続くと思うので上手いやり方を探していきたいです。

なお、セッションで使ったスライドは、こちらで公開しています。

speakerdeck.com

ユーザー委任 SAS を使った AzCopy で Azure Blob Storage にファイルをアップロードする

Shared Access Signature(SAS)を使うと、Azure Storage へのアクセス許可を細かく制御できます。しかし、SAS が漏洩すると誰でも Azure Storage にアクセスできてしまう潜在的なリスクがあります。
ユーザー委任 SAS を使うことで、SAS が Azure AD で保護され、コードで SAS を管理する必要がなくなり、漏洩リスクを軽減できます。
docs.microsoft.com

ユースケース

売上分析プラットフォームにおいて、各店舗の POS システムから売上データを収集し、全店舗のデータをまとめた横断的な売上分析を行うシナリオを想定します。
Ingest Layer の Blob Storage に対して、各店舗から売上データ XML ファイルをセキュアにアップロードできるようにします。
gooner.hateblo.jp

RBAC のカスタムロールを作成する

組み込みの Storage Blob Data Contributor では必要以上の権限を与えてしまうので、SAS を生成して Blob Storage にファイルをアップロードのみできるカスタムロール azcopy-role を作成します。

{
  "Name": "azcopy-role",
  "IsCustom": true,
  "Description": "",
  "Actions": [
    "Microsoft.Storage/storageAccounts/blobServices/generateUserDelegationKey/action"
  ],
  "NotActions": [

  ],
  "DataActions": [
    "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/write",
    "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/add/action"
  ],
  "AssignableScopes": [
    "/subscriptions/<サブスクリプションID>/resourceGroups/<リソースグループ名>/providers/Microsoft.Storage/storageAccounts/<ストレージアカウント名>"
  ]
}

Azure Portal でも Storage Blob Data Contributor など組み込みの RBAC を Clone してカスタムロールを作れますが、ここでは Azure CLI を使います。

$ az role definition create --role-definition azcopy-role.json

カスタムロールにサービスプリンシパルを割り当てる

まずは、Azure Portal の Active Directory で App registrations からサービスプリンシパルを作成しておきます。今回は azcopy-user を作成しました。

$ az ad sp list --display-name "azcopy-user" --query "[].{appId:appId}"
[
  {
    "appId": "53c3cd6f-861e-44de-b15a-e1cf2cc5a332"
  }
]

RBAC には、リソース、リソース グループ、サブスクリプション、管理グループという 4 つのレベルのスコープがあります。必要最低限のアクセス権を付与したいので、ストレージのリソースに対してカスタムロールを割り当てます。

$ az role assignment create --assignee "53c3cd6f-861e-44de-b15a-e1cf2cc5a332" --role "azcopy-role" --scope "/subscriptions/<サブスクリプションID>/resourceGroups/<リソースグループ名>/providers/Microsoft.Storage/storageAccounts/<ストレージアカウント名>"

ユーザー委任 SAS を生成する

ここからは、各店舗側の POS システムで行う作業になります。あらかじめ、サービスプリンシパルのシークレットを作成しておきましょう。
Azure CLI を使って、サービスプリンシパルでログインします。

$ az login --service-principal --username 53c3cd6f-861e-44de-b15a-e1cf2cc5a332 --password <シークレット> --tenant <テナントID>

az storage container generate-sas コマンドを使って、ユーザー委任 SAS を生成します。指定されたIPアドレスから特定のコンテナへの書き込みのみが許可された SAS が返されます。

$ az storage container generate-sas --account-name <ストレージアカウント名> -n store-a --https-only --permissions c --expiry 2021-03-07T00:00:00Z --ip "176.134.171.0-176.134.171.255" --auth-mode login --as-user

AzCopy でファイルをアップロードする

先ほど生成した SAS を使って、AzCopy で Blob Storage に XML ファイルをアップロードします。
%date:~0,4%/%date:~5,2%/%date:~8,2% は、アップロードを実行した日付のパスにアップロードするための指定です。

$ azcopy cp "*.xml" "https://gooner0102.blob.core.windows.net/store-a/sales/%date:~0,4%/%date:~5,2%/%date:~8,2%?<SAS>"

Blob Storage を確認すると、XML ファイルをアップロードできたことがわかります。

f:id:TonyTonyKun:20210306120738p:plain

この SAS を使って Blob の一覧を取得しようとしても、403 エラーが返されます。

$ azcopy list "https://gooner0102.blob.core.windows.net/store-a/sales/%date:~0,4%/%date:~5,2%/%date:~8,2%?<SAS>"
RESPONSE Status: 403 This request is not authorized to perform this operation using this permission.

ユーザー委任 SAS を取り消す

この SAS が漏洩したことを想定し、az storage account revoke-delegation-keys コマンドを使って、ユーザー委任 SAS を取り消します。

$ az storage account revoke-delegation-keys --ids /subscriptions/<サブスクリプションID>/resourceGroups/<リソースグループ名>/providers/Microsoft.Storage/storageAccounts/<ストレージアカウント名>

AzCopy でファイルをアップロードしようとしても、403 エラーが返されます。

$ azcopy cp "*.xml" "https://gooner0102.blob.core.windows.net/store-a/sales/%date:~0,4%/%date:~5,2%/%date:~8,2%?<SAS>"
RESPONSE Status: 403 Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.

まとめ

ユーザー委任 SAS を使って AzCopy で Azure Blob Storage にファイルをアップロードしました。
SAS を使う場合、RBAC のカスタムロールで要件に応じた必要最低限のアクセス権を付与できるので、可能な限りユーザー委任 SAS を使うことを検討しましょう。

Azure Pipelines で Azure Web Apps for Containers のパイプラインを構築する

以前の記事で、Azure Pipelines を使って Azure Web Apps にデプロイする内容を記載しました。
gooner.hateblo.jp
今回は、Azure Web Apps for Containers 向けのパイプラインを構築します。Azure Web Apps との違いは少ないので、相違点のみを記載します。

パイプラインの全体設計

以前の記事と同様に、パイプラインは、自動デプロイされる開発環境向け(Commit Stage)と承認デプロイされる本番環境向け(Production Stage)を作ります。YAML を Build と Release を分けて、可変部をパラメータで切り替えできるテンプレートを作ることで再利用性を向上させます。

ビルド用パイプライン

ビルド用の build-pipelines.yml です。

parameters:
  imageName: ''
  dockerRegistryServiceConnection: ''
  imageRepository: ''
  dockerfilePath: ''
  tag: ''
  buildConfiguration: ''
  projects: ''
  testProjects: ''
  dotnetSdkVersion: ''

jobs:
- job: Build
  pool:
    vmImage: ${{parameters.imageName}}
  steps:
  - task: DotNetCoreCLI@2
    displayName: 'Install .NET Core SDK $(dotnetSdkVersion)'
    inputs:
      packageType: 'sdk'
      version: $(dotnetSdkVersion)
  - task: DotNetCoreCLI@2
    displayName: 'Build'
    inputs:
      command: 'build'
      projects: ${{parameters.projects}}
      arguments: '--configuration ${{parameters.buildConfiguration}}'
  - task: DotNetCoreCLI@2
    displayName: 'Test'
    inputs:
      command: 'test'
      projects: ${{parameters.testProjects}}
      arguments: '--configuration ${{parameters.buildConfiguration}}'
  - task: Docker@2
    displayName: 'Build and push an image to container registry'
    inputs:
      command: buildAndPush
      repository: ${{parameters.imageRepository}}
      dockerfile: ${{parameters.dockerfilePath}}
      containerRegistry: ${{parameters.dockerRegistryServiceConnection}}
      tags: |
        $(tag)

Azure Web Apps との違いは、Docker task の Docker@2 を使って、Docker build と Azure Container Registry への Push を行っている部分のみです。docs.microsoft.com

リリース用パイプライン

リリース用の release-pipelines.yml です。

  
parameters:
  imageName: ''
  azureSubscription: ''
  webAppsName: ''
  containerRegistry: ''
  imageRepository: ''
  tag: ''
  environment: ''
  webAppsSlotName: 'production'

jobs:
- deployment: Deploy_Azure_WebApps
  displayName: 'Release'
  pool:
    vmImage: ${{parameters.imageName}}
  environment: ${{parameters.environment}}
  strategy:
    runOnce:
      deploy:
        steps:
        - task: AzureWebAppContainer@1
          displayName: 'Deploy to Azure Web Apps for Container'
          inputs:
            azureSubscription: ${{parameters.azureSubscription}}
            appName: ${{parameters.webAppsName}}
            containers: ${{parameters.containerRegistry}}/${{parameters.imageRepository}}:${{parameters.tag}}
            slotName: ${{parameters.webAppsSlotName}}

Azure Web Apps と違って、Azure Web App for Container task の AzureWebAppContainer@1 を使っています。
docs.microsoft.com
スワップは、Azure Web Apps 向けのパイプラインをそのまま使えます。

開発環境向けパイプライン

開発環境向けの azure-pipelines.yml です。

trigger:
- main

variables:
- group: variable-group-commit
- name: imageName
  value: 'ubuntu-latest'
- name: azureSubscription
  value: 'AzureSponsorships'
- name: dockerRegistryServiceConnection
  value: 'ACR'
- name: imageRepository
  value: 'item-service'
- name: dockerfilePath
  value: '$(Build.SourcesDirectory)/item-service/Dockerfile'
- name: tag
  value: '$(Build.BuildId)'
- name: containerRegistry
  value: 'gooner.azurecr.io'
- name: environment
  value: 'Commit-Stage'
- name: buildConfiguration
  value: 'Release'
- name: projects
  value: '**/item-service.csproj'
- name: testProjects
  value: '**/item-service.Tests.csproj'
- name: dotnetSdkVersion
  value: '5.0.x'

stages:
- stage: Build
  jobs:
  - template: pipelines/build-pipelines.yml
    parameters:
      imageName: $(imageName)
      dockerRegistryServiceConnection: $(dockerRegistryServiceConnection)
      imageRepository: $(imageRepository)
      dockerfilePath: $(dockerfilePath)
      tag: $(tag)
      buildConfiguration: $(buildConfiguration)
      projects: $(projects)
      testProjects: $(testProjects)
      dotnetSdkVersion: $(dotnetSdkVersion)

- stage: Release
  dependsOn:
  - Build
  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
  jobs:
  - template: pipelines/release-pipelines.yml
    parameters:
      imageName: $(imageName)
      azureSubscription: $(azureSubscription)
      webAppsName: $(webAppsName)
      containerRegistry: $(containerRegistry)
      imageRepository: $(imageRepository)
      tag: $(tag)
      environment: $(environment)

Azure Web Apps との違いは、ACR のリポジトリ名や Dockerfile の場所を指定している部分です。Docker Image のタグは、パイプラインの BuildId を使っています。ACR の Service Connection は、あらかじめ作成しておきましょう。
本番環境向けパイプラインは、リリース前の承認と Deployment Slot を使ったスワップが追加されるくらいで、大きな違いはありません。

まとめ

リポジトリへのマージがトリガーとなり、ビルドとリリースのパイプラインが実行され、Azure Web Apps for Containers へのデプロイまでが自動で実行されます。

f:id:TonyTonyKun:20210228121514p:plain

こちらから、パイプライン YAML の全体を確認できます。
github.com

余談

公式ドキュメントで Azure Web Apps for Containers の CI/CD を調べると、ACR Webhook の記事が見つかります。
docs.microsoft.com
この Webhook は Docker Image のタグが固定なので、はっきり言って使えません。どうやらコンテナを再起動するだけの Webhook みたいです。
latest タグで固定した CI/CD はやりたくないので、結局 Azure Pipelines YAML を書くことになります。これは便利そうだと思って試したときの残念な感じが辛かった・・・