tech.guitarrapc.cóm

Technical updates

Azure DevOps の Template を利用してビルドする

Azure DevOps Pipeline で何度も同じ処理をYAMLに書いていた場合、Templateを使うとまとめられて便利になります。

実際にビルドが多く重複した定義の多いプロジェクトに適用したところ、表向き300行 (template 含めると100行) 減らせて見通しは良くなりました。

# before
$ find . -type f | xargs cat | wc -l
788

# after
$ cat *.yml | wc -l
483

$ find . -type f | xargs cat | wc -l
628

今回は、Template について書いておきます。

目次

TL;DR

Template を使うとパラメーターだけ変えてほぼ同じ条件のビルド定義を簡単に複数作成できるのでおすすめ。 単一ビルドの場合はまとめ上げる単位によってはメリットが薄くなるので、逐次書いたほうがむしろわかりやすく選択しない選択が妥当なことが多い。(なんでもやればいいってものじゃない。)

サンプルリポジトリ

GitHub にサンプル構成を用意してあります。 リポジトリの内容に沿って説明します。

github.com

Azure DevOps Pipeline Build の構造

Azure DevOps Pipeline Build は、Stages/Jobs/Steps の3つを使って構造を分割しています。 構造を知っておくとTemplateの範囲が予想できるので、先にこれを簡単に説明しますがStages と Jobs は普段使っていると出てこないことが多いと思います。

AzureDevOps のWeb UI で自動生成される azure-pipelines.yml でも省略されていることからも察してください。(つまり読み飛ばしてok)

# Starter pipeline
# Start with a minimal pipeline that you can customize to build and deploy your code.
# Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml

trigger:
- master

pool:
  vmImage: 'ubuntu-latest'

steps:
- script: echo Hello, world!
  displayName: 'Run a one-line script'

- script: |
    echo Add other tasks to build, test, and deploy your project.
    echo See https://aka.ms/yaml
  displayName: 'Run a multi-line script'

構造に興味ある人はYAML定義のリンクに詳細があります。

docs.microsoft.com

Stages

パイプラインの最も大きな分割単位で、複数のJobs を持つことができます。"build this app", "run these tests", and "deploy to pre-production" はわかりやすい分割例です(まとめてやらないだろうと思いつつ)

Stage の構造は次のとおりですが、正直 Stages はまずかかないです。

stages:
- stage: string  # name of the stage, A-Z, a-z, 0-9, and underscore
  displayName: string  # friendly name to display in the UI
  dependsOn: string | [ string ]
  condition: string
  variables: { string: string } | [ variable | variableReference ] 
  jobs: [ job | templateReference]

もしYAMLでstages: を指定しなくても、暗黙的に1つのstageが割り当てられて実行されます。(これも書かない要因です)

stages:
- stage: A
  jobs:
  - job: A1
  - job: A2

- stage: B
  jobs:
  - job: B1
  - job: B2

Stage は、ポーズしたり各種チェックやStage間の依存関係を指定することもできます。

stages:
- stage: string
  dependsOn: string
  condition: string

これを利用して fan-in/fan-out も組めます。

stages:
- stage: Test

- stage: DeployUS1
  dependsOn: Test    # this stage runs after Test

- stage: DeployUS2
  dependsOn: Test    # this stage runs in parallel with DeployUS1, after Test

- stage: DeployEurope
  dependsOn:         # this stage runs after DeployUS1 and DeployUS2
  - DeployUS1
  - DeployUS2

Stage レベルで変数を持って、Job や Step で利用することもできます。

Jobs:

Jobs は Steps(実際にやる処理) の塊で中のStep が直列で実行されます。

Jobs の構造は次のとおりです。この Jobs もまず書かないです。

jobs:
- job: string  # name of the job, A-Z, a-z, 0-9, and underscore
  displayName: string  # friendly name to display in the UI
  dependsOn: string | [ string ]
  condition: string
  strategy:
    matrix: # matrix strategy, see below
    parallel: # parallel strategy, see below
    maxParallel: number # maximum number of agents to simultaneously run copies of this job on
  continueOnError: boolean  # 'true' if future jobs should run even if this job fails; defaults to 'false'
  pool: pool # see pool schema
  workspace:
    clean: outputs | resources | all # what to clean up before the job runs
  container: containerReference # container to run this job inside
  timeoutInMinutes: number # how long to run the job before automatically cancelling
  cancelTimeoutInMinutes: number # how much time to give 'run always even if cancelled tasks' before killing them
  variables: { string: string } | [ variable | variableReference ] 
  steps: [ script | bash | pwsh | powershell | checkout | task | templateReference ]
  services: { string: string | container } # container resources to run as a service container

もしYAMLでjobs: を指定しなくても、暗黙的に1つのjobが割り当てられて実行されます。(これも書かない要因です)

YAML に直接 container と書いて、Docker Hub のイメージを使うことがあると思います。 あるいは、strategy と書いて Matrix Build していることがあると思います。 これらはJobの機能を使っているということです。

pool:
  vmImage: 'ubuntu-16.04'

strategy:
  matrix:
    ubuntu14:
      containerImage: ubuntu:14.04
    ubuntu16:
      containerImage: ubuntu:16.04
    ubuntu18:
      containerImage: ubuntu:18.04

container: $[ variables['containerImage'] ]

steps:
  - script: printenv

1つのビルドエージェントは1つのjob を実行できます。 そのため、並列にジョブを実行したい場合は、並列度の分だけエージェントが必要になります。

例えばこれは直列実行になります。

jobs:
- job: Debug
  steps:
  - script: echo hello from the Debug build
- job: Release
  dependsOn: Debug
  steps:
  - script: echo hello from the Release build

これは依存を互いに持たないので並列実行です。

jobs:
- job: Windows
  pool:
    vmImage: 'vs2017-win2016'
  steps:
  - script: echo hello from Windows
- job: macOS
  pool:
    vmImage: 'macOS-10.13'
  steps:
  - script: echo hello from macOS
- job: Linux
  pool:
    vmImage: 'ubuntu-16.04'
  steps:
  - script: echo hello from Linux

多くの場合は、matrixで自動生成された job を 最大並列度を指定したりすることが多いでしょう。

jobs:
- job: BuildPython
  strategy:
    maxParallel: 2
    matrix:
      Python35:
        PYTHON_VERSION: '3.5'
      Python36:
        PYTHON_VERSION: '3.6'
      Python37:
        PYTHON_VERSION: '3.7'

Job レベルで変数を持って、Step で利用することもできます。

Steps

steps では、一連の実際にやりたい処理をstepとして定義します。job の中に、1つのsteps があるはずです。(CircleCI や TravisCI、drone.ioなど他のCIサービスでやるときと同じですね。)

steps:
- script: echo This runs in the default shell on any machine
- bash: |
    echo This multiline script always runs in Bash.
    echo Even on Windows machines!
- task: ProvidedUsefulTask
    ParamA: "This multiline script always runs in PowerShell Core."
    ParamB: "Even on non-Windows machines!"

Azure DevOps は stepごとに1つのプロセスを起動して実行します。そのため、プロセス環境変数はstep間で共有されず、ファイルシステムやUser/Machineレベルの環境変数、Stages/Jobs/Steps ごとの変数で共有できます。

steps では、CIのために複数のステップを書くことになります。 中には1つのCIで10step 書くこともあるでしう。

steps:
- script: OpA
- script: OpB
- script: OpC
- script: OpD
- script: OpE
- script: OpF
- script: OpG
- script: OpH
- script: OpI
- script: OpJ

同じ処理を条件ごとに渡すパラメーターを変えて実行する

もし一度のトリガーで実行されたPipelineで、複数組み合わせのパラメーターで実行する場合 matrix を使うと思います。 matrixを使えば、自動的にmatrix定義した条件ごとにJobが生成され実行されるので、steps を複数書く必要がありません。

matrix:
linux:
  imageName: 'ubuntu-16.04'
mac:
  imageName: 'macos-10.13'
windows:
  imageName: 'vs2017-win2016'

pool:
  vmImage: $(imageName)

steps:
- task: NodeTool@0
  inputs:
    versionSpec: '8.x'

- script: |
    npm install
    npm test

- task: PublishTestResults@2
  inputs:
    testResultsFiles: '**/TEST-RESULTS.xml'
    testRunTitle: 'Test results for JavaScript'

- task: PublishCodeCoverageResults@1
  inputs: 
    codeCoverageTool: Cobertura
    summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/*coverage.xml'
    reportDirectory: '$(System.DefaultWorkingDirectory)/**/coverage'

- task: ArchiveFiles@2
  inputs:
    rootFolderOrFile: '$(System.DefaultWorkingDirectory)'
    includeRootFolder: false

- task: PublishBuildArtifacts@1

しかし、特定のパスやブランチをフックしてビルドするときなど、matrix が使えない条件で同じ処理でパラメーターだけ違うものを書くこともあります。

// azure-pipelines-debug.yml
steps:
- script: OpA
- script: OpB -c Debug
- script: OpC -c Debug
- script: OpD

// azure-pipelines-release.yml
steps:
- script: OpA
- script: OpB -c Release
- script: OpC -c Release
- script: OpD

こういった「渡すパラメーターだけが違って処理は同じ」時に便利なのが、Templates機能 です。

Templates

Templates は単純にいうと、処理を別YAMLに定義しておいて呼び出す機能です。呼び出すときにパラメーターを渡すことができるため、Templateでパラメーターを使って処理を書くことでパラメーターごとに同じ処理を呼び出すことができます。

YAML schema/Template Reference - Azure Pipelines | Microsoft Docs

CircleCI だと、Commands を外部YAMLに定義して利用するイメージに近いでしょう。

circleci.com

Templates は Stages/Jobs/Steps あとはVariables(変数) として持つことができます。一番使われやすい「JobとStepを取りまとめた」例でTemplateを見てみます。

docs.microsoft.com

まずはわかりやすい例で、npm の install/test だけを外に出してみましょう。 template を使わないときに次のように書いているイメージです。

# File: azure-pipelines.yml

jobs:
- job: macOS
  pool:
    vmImage: 'macOS-10.13'
  steps:
  - script: npm install
  - script: npm test

- job: Linux
  pool:
    vmImage: 'ubuntu-16.04'
  steps:
  - script: npm install
  - script: npm test

- job: Windows
  pool:
    vmImage: 'vs2017-win2016'
  steps:
  - script: npm install
  - script: npm test
  - script: sign              # Extra step on Windows only

step で同じことをしているbuild/test処理を1つのYAMLに書き出して テンプレート化します。仮にsteps/build.yml に置きます。

# File: steps/build.yml

steps:
- script: npm install
- script: npm test

あとは、先程のazure-pipelines.yml で template を使ってテンプートにしたbuild.ymlファイルを呼び出すだけです。

# File: azure-pipelines.yml

jobs:
- job: 
  displayName: macOS
  pool:
    vmImage: 'macOS-10.13'
  steps:
  - template: steps/build.yml # Template reference

- job
  displayName: Linux
  pool:
    vmImage: 'ubuntu-16.04'
  steps:
  - template: steps/build.yml # Template reference

- job: 
  displayName: Windows
  pool:
    vmImage: 'vs2017-win2016'
  steps:
  - template: steps/build.yml # Template reference
  - script: sign              # Extra step on Windows only

パラメーターを利用する

先程の処理で、Job の環境もほぼ同じに見えます。 Tempalte を呼び出すときにパラメーターを渡して共通化してみましょう。

パラメーターは、parameters で定義して、${{ parameters.YOUR_PARAMETER_KEY }} でテンプレート内部で参照します。

# File: templates/npm-with-params.yml

parameters:
  name: ''  # defaults for any parameters that aren't specified
  vmImage: ''

jobs:
- job: 
  displayName: ${{ parameters.name }}
  pool: 
    vmImage: ${{ parameters.vmImage }}
  steps:
  - script: npm install
  - script: npm test

実際にTemplate の利用時にパラメーターを渡すには、template: を指定したときにparameters: で定義したパラメーターキー:渡したい値とします。

# File: azure-pipelines.yml

jobs:
- template: templates/npm-with-params.yml  # Template reference
  parameters:
    name: Linux
    vmImage: 'ubuntu-16.04'

- template: templates/npm-with-params.yml  # Template reference
  parameters:
    name: macOS
    vmImage: 'macOS-10.13'

- template: templates/npm-with-params.yml  # Template reference
  parameters:
    name: Windows
    vmImage: 'vs2017-win2016'

Steps でTemplateを利用する

先程 Job sでみてみましたが、step で利用するときも同様です。

「追加テストを一部だけでやりたい」時に、Template利用時に true/false を渡して実行するか決めてみます。 初期値だけ設定しておかないといけないことに気をつけてください。

# File: templates/steps-with-params.yml

parameters:
  runExtendedTests: 'false'  # defaults for any parameters that aren't specified

steps:
- script: npm test
- ${{ if eq(parameters.runExtendedTests, 'true') }}:
  - script: npm test --extended

Template の参照時に、true を渡したときだけ実行されます。

# File: azure-pipelines.yml

steps:
- script: npm install

- template: templates/steps-with-params.yml  # Template reference
  parameters:
    runExtendedTests: 'true'

サンプル

ASP.NET Core でビルドするときに、dotnet と docker の両方を用意してみました。

github.com

steps/dotnetcore_publish.yml にdotnet でビルドするものを用意しておきます。

parameters:
  ProjectName: ''
  ExtraBuildArguments: ''
  BuildConfiguration: 'Debug'
  DotNetCoreInstall: '2.2.100'

steps:
- task: DotNetCoreInstaller@0
  inputs:
    packageType: 'sdk'
    version: '${{ parameters.DotNetCoreInstall }}'    
- task: DotNetCoreCLI@2
  displayName: 'dotnet restore'
  inputs:
    command: restore
    projects: '${{ parameters.ProjectName }}/**/*.csproj'
- task: DotNetCoreCLI@2
  displayName: 'dotnet publish'
  inputs:
    command: publish
    publishWebProjects: false
    projects: '${{ parameters.ProjectName }}/**/*.csproj'
    arguments: '-c ${{ parameters.BuildConfiguration }} -o $(Build.ArtifactStagingDirectory)'
    zipAfterPublish: false
    modifyOutputPath: false

あとは呼び出すだけです。気にするのはプロジェクト名のみなのは、チーム内のテンプレートとしては便利です。

trigger:
  batch: true
  branches:
    include:
      - '*'
jobs:
- job: build
  displayName: dotnet core (Debug)
  pool:
    name: Hosted Ubuntu 1604
  steps:
  - checkout: self
  - template: steps/dotnetcore_publish.yml
    parameters:
      ProjectName: 'WebApplication'
      BuildConfiguration: 'Debug'
  - task: PublishBuildArtifacts@1
    displayName: 'Publish Artifact: drop'

同様に、Azure Container Regisotry に docker build > push するときもかけます。docker タスクでのビルドがめちゃめちゃ使いにくいのでちょっと楽です。

parameters:
  Subscription: ''
  SubscriptionId: ''
  Registry: ''
  DockerImageName: ''
  DockerFile: ''
  ResourceGroup: ''
  ImageName: ''

steps:
- bash: 'docker build -t ${{ parameters.Registry }}.azurecr.io/${{ parameters.DockerImageName }}:$(Build.SourceVersion)_$(Build.BuildId) -t ${{ parameters.Registry }}.azurecr.io/${{ parameters.DockerImageName }}:latest -f ${{ parameters.DockerFile }} .'
  displayName: 'docker build'

- task: Docker@0
  displayName: 'Push an image'
  inputs:
    azureSubscription: ${{ parameters.Subscription }}
    azureContainerRegistry: '{"loginServer":"${{ parameters.Registry }}.azurecr.io", "id" : "/subscriptions/${{ parameters.SubscriptionId }}/resourceGroups/${{ parameters.ResourceGroup }}/providers/Microsoft.ContainerRegistry/registries/${{ parameters.Registry }}"}'
    action: 'Push an image'
    imageName: '${{ parameters.ImageName }}'
    includeLatestTag: true

あとは呼び出すだけです。 実際のプロジェクトでは、Subscription や SubscriptionIdは埋めてしまっていいでしょう。

trigger:
  batch: true
  branches:
    include:
      - "*"
jobs:
- job: build
  displayName: web docker build (dev)
  pool:
    name: Hosted Ubuntu 1604
  steps:
  - checkout: self
  - template: steps/docker_push.yml
    parameters:
      Subscription: 'your_azure_subscription'
      SubscriptionId: '12345-67890-ABCDEF-GHIJKL-OPQRSTU'
      Registry: 'your_azure_container_registry_name'
      DockerImageName: 'webapplication'
      DockerFile: 'WebApplication/Dockerfile'
      ResourceGroup: 'your_azure_resource_group'
      ImageName: 'webapplication:$(Build.SourceVersion)_$(Build.BuildId)'

Refs

YAML schema - Azure Pipelines | Microsoft Docs

Stages in Azure Pipelines - Azure Pipelines | Microsoft Docs

Jobs in Azure Pipelines, Azure DevOps Sever, and TFS - Azure Pipelines | Microsoft Docs

Job and step templates - Azure Pipelines | Microsoft Docs

DELL PC のスペックに対するコストパフォーマンスを考える

相談されたので少し考えていたことのメモを書いておきます。

ネットがメイン、少し絵を書いたりするぐらいの大多数のユーザーに取っては、DELLのこのモデルはコストパフォーマンスはすごくいいと感じます。 残念ながら、わたしはUnity使ってHoloLens やVR開発も行い、ゲームもするのでGPUがないこのスペックは選ばないです。

目次

TL;DR

ライトユースなら、DELL のこのモデルはコスパ的にはかなりいいけど、128GB SSD は小さいので使い方でカバーが必要なのが唯一の不満点。

256GB SSD + 1TB HDD や 512GB SSD のモデルがあると現時点では嬉しそう。

用途

ネットサーフィンが主。時々絵を書く、リタッチする程度の利用。

相談されたPCスペック

このスペックで 64,240円。

【楽天市場】Dell プレミアム i5 8GB 1TB HDD Inspiron-3470 デスクトップ[新品]:DELL

Dell プレミアム i5 8GB 1TB HDD Inspiron-3470 デスクトップ[新品]

所感

DELL のPCスペックはお得でコスパよいです。すごい。BTOなどで必要なスペックに絞っても割高になるのでいいものを見つけられたと思います。気になるのは2点です。

ストレージ

ハードディスクの項目に、3.5” 1TB 7200 rpm ハードドライブとあり、HDD (Hard Disk Drive) になっており、これが最大の懸念です。

HDDの故障による保存していたデータの読み取りができなくなる可能性

HDD はモーター、つまり駆動するものが中にあるため、故障する可能性が高いです。 磁気保存している保存部分以外に駆動する場所があるHDDは、経年劣化、動作中の振動などで、駆動部分と読み取り部分(ヘッダといいます) の故障が起こったときも読み込めなくなります。また、磁気保存部分の不良、傷によっても読み込めなくなる可能性があります。 このため、SSD に比べてHDD は故障の確率はかなり高く、ずっと同じPCを使うならHDDを2-3年を目安に買い替えをしていく必要があります。(SSD でも似たようなものですが、故障の発生率がHDDはより高いです)

HDDは遅い

HDDは読み込み、書き込みともに遅くSSDのほうが圧倒的に良いです。 ディスクからのデータの読み込みは、PCの起動、ソフトの起動、ソフトの処理などあらゆる処理で利用されるため、ここが遅いと自分の作業の待ち時間が伸びます。 SSDにするだけで体感できるレベル (秒~Windows起動に至っては数十秒) 早くなります。

容量

容量512GB でいい説がありますが使い方によるので案程度に挙げておきます。 絵を書いたりしていると、1TB の容量はすぐに食ってしまう一方で、Google Drive など各種データサービスにデータを逃して置くほうが、HDDの故障問題からは逃げやすく安全です。また、Google Driveとかに保存するデータを逃して、PCでは作業するデータだけおいておくと1TBも容量はいらず 512GB程あれば十分になることも多いです。

グラフィックボード

ビデオコントローラが、インテル UHDグラフィックス630になっているのが注意です。 オンボードになっているのですが、Core i5-8400 のグラフィック性能がそこまで良くないので、フォトショなどをごりごりしないならいいのですが、ちょっとしたことをしようとすると重くなったりすることが多いので気をつけてください。

対策

おすすめ順で。

  1. DELL のものを購入して、別途SSD 512GB 前後を買って載せ替え (+7000円程度で買えますが、DELLの保証が切れる可能性があるので注意がいります)
  2. DELL のものを購入してそのまま使う。(HDD が遅い以外はいいと思います)
  3. DELL の128GB SSD モデルを購入してそのままつかう。 (128GB SSD は小さいことが多いのが懸念です)
  4. BTOで2つ探しておきましたが、DELLが安すぎて2の案が一番コスパ◎

DELL の128GB SSD モデル

SSD 128GB が追加されて、68,830円。

コスパいいです。*1 起動ドライブがSSD っぽい、のでHDDによるトロイ挙動は改善されます。 ただし、SSD 128GB は、経験から Windows 10 を入れておくには小さいのが懸念です。(256GBはほしい)

【楽天市場】Dell プレミアム i5 8GB 1TBHDD+128SSD Inspiron-3470 デスクトップ[新品]:DELL

Dell プレミアム i5 8GB 1TBHDD+128SSD Inspiron-3470 デスクトップ[新品]

別のモデルで、128GB SSD が 128GB M.2 になったものが78,010円。 さすがにコスパが悪くなっています。 M.2の入れ替え面倒なことも合わせると256GBはほしいところ。

【楽天市場】Dell プレミアム i5 8GB 1TBHDD+128SSD inspiron-3670デスクトップ[新品]:DELL

Dell プレミアム i5 8GB 1TBHDD+128SSD inspiron-3670デスクトップ[新品] M.2

BTO で同じようなスペックを組んで見る

84,940円と+20000円弱になってしまった。

小型PCです。スリムより小さいので置き場は困らないと思います。 SSD500GBあるので、Google Driveなりを使う前提でHDDも不要にしています。 Ryzenのこのモデルはグラフィック性能はi5の上です。

DELLにあるWifiとカードリーダーがないので注意です。SSDに変えて小型PCにしたもののDELLが安すぎる。

Radiant SPX2700A300A|ゲーミングPC|BTOパソコン|BTO パソコン(PC)の【@Sycom】(サイコム)

Radiant SPX2700A300A

82,320円 と小型PCより2000円安い程度。

スリムPCです。DELLより2cm幅が広いです。 SSD500GBあるので、Google Driveなりを使う前提でHDDも不要にしています。 Ryzenのこのモデルはグラフィック性能はi5の上です。

DELLにあるWifiがないので注意です。小型PCより安いものの、DELLが安すぎる。

Radiant SBX2650B450AG|省スペースPC|BTOパソコン|BTO パソコン(PC)の【@Sycom】(サイコム)

Radiant SBX2650B450AG

蛇足

私は、構成を変えたがるのでDELLは選ばないです。 逆にいうと、構成を変えないならDELLはめちゃめちゃ割安でいいと思います。(ただでさえ安くてWifiもついてくるのはすごい)

私は小型PCがおすすめかつ、SSDより M.2 にします。 職業柄、ストレージは速さとコストのバランスから選んでいる側面もあるので万人が対象ではありません。(HDD->SSDほど SSD->M.2は体感できる速度向上がないので、割高感があるのは確かです。それでもM.2一択ですが)

もし、ほしいPCがグラフィックほどほどでいいなら、今ならIntel Core i シリースではなく Ryzen を選ぶほうがコスパは抜群に良いと思います。

保証切れるのがいや、HDD -> SDD への載せかえがいや(わからない) 場合は、DELLのでいいと思います。

私が今開発環境を組むならこうなるので、全然方向が違う。

gist.github.com

*1:SSD 128GBを自分で買って、とか考えると若干割高感がありますが、時間や手間を買うと思うと逆に安いです。

AWS Secret Manager を使ってASP.NET Core のシークレット情報を扱う

.NET Core で AWS において機微情報を扱うときに、AWS Secret Manager や System Manager の Parameter Store が候補に上がります。

ここでは、Secret Manager を使った ASP.NET Core での組み込みについて書いておきます。

目次

TL;DR

コード、並びにappsettings.json などgit commit から機微情報を排除し、CIで差し込むのではなく、実行環境に応じて安全に取得するように組むことでより安全なアプリケーションからのアクセスを実現できます。 AWS Secretを使うことで、許可された環境からでないと守る必要があるデータへのアクセスができないように制御します。

コードのサンプルは、GitHub にあげておきます。

github.com

AWS Secret の選択

AWS で機微情報を扱う方法としては、AWS System Manager の Parameter Store と、AWS Secret Manager の選択があります。

AWS Secret Managerは 料金以外の心配が少なく、スケーラブルな環境で一斉にデプロイしても問題が起こりにくいメリットがあります。 一方で、シークレット一件当たりの料金が$0.4/monthであるため、ユーザー一人一人の情報を扱う/毎度一意なテンポラリの情報を扱うといったシークレットの件数の増加で恐ろしいほどコストがかかります。

AWS Secret はシークレットの件数が増えると料金が跳ね上がる

aws.amazon.com

Secret Manager ではアプリごとに単一になる情報を取り扱うほうがコスト的にはいいでしょう。

アプリケーションの設定などはコストが抑えられて使いやすい

System Manager の Parameter Store は、同時アクセスの規定が明確でなく Rate Limit に到達する可能性はありますが、無料でできるためアクセス頻度の少ないパラメーターを保持するのにはとても優秀です。

今回は、アプリケーションの起動時に1回読み込まれるRDSの接続情報をAWS Secret Manager に保持させて読み込んでみましょう。

AWS SecretManager の用意

AWS Secret Manager を使うため、Secret Manager とそこにいれるデータを用意しておきます。 SecretManager の名前を test として、JSON で取得することを想定します。

{
  "ConnectionStrings": {
    "DATABASE": "YOUR_AWESOME_CONNECTION_STRINGS"
  }
}

AWS Secret Manager にデータをいれる

AWS Secret Manager とデータをterraform やAWS Console でも aws cli でサクッと作ります。

gist.github.com

自動更新や自動Expire も可能ですが、DBの接続先としてはあまりないので今回は静的に組み立てておきます。

これでtest が登録されます。

Secret Store での登録例

呼び出し元がSecret Managerにアクセスできるようにする

作ったAWS SecretManager を呼び出すときの権限を委譲するため、IAM RoleにこのAWS SecretManager の 読み取り権限をつけておきましょう。

ここでは、AWS が提供している "arn:aws:iam::aws:policy/SecretsManagerReadWrite" で代用します。

gist.github.com

あとはIAM Role につければAWS 側の環境は準備は完了です。(アプリケーションはSecret ARN を知る必要がありません。)

.NET Core で AWS Secret Manager の呼び出しを行う

AWS SecretManager をコンフィグの置き場としてみなすため、nuget パッケージで公開されているASSDK.SecretsManager を用います。 .NET Coreでも同じSDKでokです。

NuGet Gallery | AWSSDK.SecretsManager 3.3.100.13

生で使うときの各種言語のコードは、Secret Manager にシークレットを作成したときに下に出ています。

gist.github.com

適切なIAM Roleがある状態で実行するとSecretManager に格納した情報が secret に格納されたことがわかります。

しかし、このコードはIAM Role前提で認証が渡されることを想定されているため、ローカルでプロファイルを使って実行しようとするとうまく動きません。 ローカルでプロファイルを使って動かすようにしてみましょう。

gist.github.com

違いは単純です。 実行時に Profile から認証を取得するように CredentialProfileStoreChain を使って、AmazonSecretsManagerClient にこの認証を渡しているだけです。 なお、Profile を使って認証する場合は AWSSDK.SecurityToken nuget パッケージを追加してください、このパッケージがないと認証トークンのハンドルができません。

これでAWS Secret Manager を .NET Core から取得する方法は把握できました。

ASP.NET Core や Generic Host で AWS Secret を取り扱う

動かすだけなら動きましたが、このままのコードでは生すぎて使いにくさがあります。 実際にアプリに組み込む場合は、ASP.NET Core や Generic Host へ追加することになるので、HostBuilder からのチェーンでIConfiguration に突っ込みたいところです。

ASP.NET Core なら WebHostBuilder からのチェーンだとうれしいです。

WebHost.CreateDefaultBuilder(args)
    .AddAwsSecrets()
    .UseStartup<Startup>();

Generic Host なら HostBuilder からのチェーンでしょう。 わかりやすい例としてのコード例ならMicroBatchFramework で次のように.AddAwsSecrets() が出来れば嬉しいと感じます。

BatchHost.CreateDefaultBuilder()
    .AddAwsSecrets()
    .RunBatchEngineAsync<CredentialSample>(args);

ではこのようなコードを書けるように組んでみましょう。

ASP.NET Core で Secret Store から値を取得する

ASP.NET Core でサンプルプロジェクトを開始します。 わかりやすいようにView に取得結果を表示するので、MVCで行きましょう。

初期状態は次のように Program.cs が書かれています。

gist.github.com

ここに AWS Secret をConfigとして読み込むのですが、自分で書かずともKralizek.Extensions.Configuration.AWSSecretsManager パッケージがある程度いい感じになっているのでこれを使います。

www.nuget.org

これで、ConfigureAppConfigurationを使って次のようにシークレットを呼び出せるようになります。

gist.github.com

もしプロファイルを使いたければ、先程の例のように Profile を AWSCredential に使えばいいでしょう。 もちろんその場合は、AWSSDK.SecurityToken パッケージを追加します。

gist.github.com

既存のIndex ページに仮表示しましょう。 新規にIndexViewModel を用意して、既存のView となるIndex.cshtml に埋め込み、IndexController から IConfiguration経由でSecretManager から取得したデータをViewModel に埋めます。

gist.github.com

SecretManager の値はSecretStoreの名前:JSONキー で指定する必要があるので、Controller でIConfigurationから GetValue するときに注意がいります。

これでデバッグ実行すると、意図したとおりに取得して表示されたことがわかります。

SecretManager からデータを取得した結果

使いやすく修正する

さて、一見良いようですが実際に利用するときにはあまり使い勝手がありません。

  • このままだとAWS SecretManager に登録してあるすべての値を読んでしまいます
  • SecretManager のキー名をアプリが知る必要がある

そこで、コンフィグに指定した特定キーのSecret Storeのみ読み込むことと、Secret Storeの名前をConfigのキーで指定せずに済むように修正をいれます。

appsettings.json や appsettings.Development.json でシークレット名を指定できるようにマッピングクラスを用意し、これに対応したappsettings.json のセクションを作ります。*1

gist.github.com

あとは、このフィルタを効かせつつSecretManagerを読み込むようにAddSecretsManagerに軽くラップをかけた AwsSecretsConfigurationBuilderExtensions を用意します。

gist.github.com

これで、Program.cs では次のようにかけるようになりました。

gist.github.com

ローカル開発でProfileを使いたい場合は、次のようにかけます。

gist.github.com

HomeController でも、Secret Store の名前を知ることなく、JSONキーでほしいデータが取れるようになっています。

gist.github.com

実行してみると思ったとおりのデータが取れました、バッチリですね。

修正後、指定したキーのSecretStoreが取得できている

Secret Store から必要なシークレットのみ取得するフィルタ

実装を見てみましょう。

public static IWebHostBuilder AddAwsSecrets(this IWebHostBuilder hostBuilder, string prefix, string region, string profile) のシグネチャはプロファイル経由での読み込みようなので無視してok です。

実際にアプリから利用するのは、public static IWebHostBuilder AddAwsSecrets(this IWebHostBuilder hostBuilder, string region) シグネチャです。 AWS Secret Manager はregion 依存なので、適当にリージョンを合わせてください。 引数やAWS_REGION などの環境変数から取得するようにするのもいいでしょう。

今回、appsettings.json で必要なキーを指定しているので、Secret Manager に問い合わせる前にフィルタしている方をマッピングしています。

            // build partially
            var partialConfig = configurationBuilder.Build();
            var settings = new AwsSecretsManagerSettings();
            partialConfig.GetSection(nameof(AwsSecretsManagerSettings)).Bind(settings);

            // Filter which secret to load
            var allowedPrefixes = settings.SecretGroups
                .Select(x => $"{prefix}{x}")
                .ToArray();

あとは、Func である SecretFilter で対象のSecret Store があるか検査して読み込むだけです。

            configurationBuilder.AddSecretsManager(region: endpoint, credentials: credentials, configurator: opts =>
            {
                opts.SecretFilter = entry => HasPrefix(allowedPrefixes, entry);
                opts.KeyGenerator = (entry, key) => GenerateKey(allowedPrefixes, key);
            });

// 省略

        private static bool HasPrefix(IEnumerable<string> allowedPrefixes, SecretListEntry entry)
            => allowedPrefixes.Any(prefix => entry.Name.StartsWith(prefix));

SecretStore名を除く

これは単純ですね。

            configurationBuilder.AddSecretsManager(region: endpoint, credentials: credentials, configurator: opts =>
            {
                opts.SecretFilter = entry => HasPrefix(allowedPrefixes, entry);
                opts.KeyGenerator = (entry, key) => GenerateKey(allowedPrefixes, key);
            });

// 省略

        private static string GenerateKey(IEnumerable<string> prefixes, string secretValue)
        {
            // don't use '/' in your environment or secretgroup name.
            var prefix = prefixes.First(secretValue.StartsWith);

            // Strip the prefix
            var s = secretValue.Substring(prefix.Length + 1);
            return s;
        }

留意点

AWS Secret Store は、JSON や Environment Variables、引数のIConfiguration 処理後に読んでいるため、もし同じキーのコンフィグをAWS Secret Store から読んだ場合上書きされます。

まとめ

あくまで薄いラッパーなのでご自身の使いやすいように調整できるはずです。

例えばローカル開発向けに、「既存のConnectionStrings がもし定義されていたらSecret Store はみない」、とかも簡単ですね。

コードからシークレットを抜く、かと言って環境変数にいれるのではなく いわゆるSecret Store / KeyValt から取得するのは、やっておいて損はないのでさくっとどうぞ。

Generic Host も IWebHostBuilder が IHostBuilder になるだけでほぼ一緒です。

Ref

docs.aws.amazon.com

他言語

www.sambaiz.net

AWSSecretsManagerConfigurationExtensions のコード例

github.com

参考

Secure secrets storage for ASP.NET Core with AWS Secrets Manager (Part 1)

Secure secrets storage for ASP.NET Core with AWS Secrets Manager (Part 2)

*1:private class なのは処理の中で外に公開する必要がないからです

非ASP.NET Coreなプロジェクトで UserSecretsを使うためのVisual Studio 拡張を作りました

非ASP.NET Core、特にGeneric Host で開発するなら必要になるVS拡張を公開していたのを忘れていました。*1

今回は、GenericHost など非ASP.NET Coreなプロジェクトでも、ASP.NET Core なプロジェクトにある Manage UserSecrets (ユーザーシークレットの管理) と同様の手助けをするVisual Studio 拡張 Open UserSecrets を作った話です。

marketplace.visualstudio.com

目次

TL;DR

Open UserSecrets拡張をいれることで、UserSecrets を開くためのメニューが 非ASP.NET Core プロジェクトでも表示されます。

Open UserSecrets

appsettings.Development.json にはCD先の開発環境を設定しておいて、ローカル開発環境はUserSecrets で実行時にコンフィグ上書きするのが楽になるのでぜひどうぞ。

marketplace.visualstudio.com

身近そうな例として、MicroBatchFramework の Config を利用した例は、この Open UserSecrets を使うとローカルテストが圧倒的に楽になります。

github.com

むしろ、これなしでUserSecrets 使うのつらすぎませんか?

UserSecrets とは

ASP.NET Core を開発していて、開発環境とローカル開発でコンフィグを分けたい時に使うのが UserSecrets です。

docs.microsoft.com

UserSecrets を使うことで、appsettings.jsonappsettings.Development.json 以外に secrets.json%APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json に保持できるようになります。

ASP.NET Core なプロジェクトではUserSecrets が空気のように利用できる

ASP.NET Core ではこれを管理するためのVisual Studio のメニュー拡張が自動的に追加され、Visual Studio 2017/2019 で プロジェクト名を右クリックすると Manage UserSecrets (ユーザーシークレットの管理) がコンテキストメニューに表示されます。

ASP.NET Core なプロジェクトを右クリックすると UserSecrets を管理するためのメニューが追加されている

これを選択するだけで、自動的に secrets.json が開かれます。

secrets.json をVSで編集できるようになる

非ASP.NET Core なプロジェクトでUserSecretsを利用する難しさ

UserSecretsは何も ASP.NET Core なプロジェクト専用の機能ではありません。

.NET Framework でも .NET Coreでも Microsoft.Extensions.Configuration.UserSecrets nuget をプロジェクトに追加することで利用できるようになります。

www.nuget.org

ただしNuGetをいれても素のVisual Studio のままでは、次の方法で管理することになります。

Enable secret storage

dotnet user-secrets を使った管理

  1. Enable secret storageに従い、csproj のPropertyGroup の中に、UserSecretsId 要素でGUID を指定して保存
  2. .csproj のあるパスで dotnet user-secrets set KEY VALUE コマンドで %APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json を作成
  3. 以降もdotnet user-secrets で管理

手で管理

  1. Enable secret storageに従い、csproj のPropertyGroup の中に、UserSecretsId 要素でGUID を指定して保存
  2. %APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json を作成
  3. secrets.json を開き編集

ASP.NET Core とはずいぶんと異なるフローであることに気づきます。 ASP.NET Coreが提供する Manage UserSecrets は、上記フローを1つのコマンドで実施しVisual Studio で secrets.json も開いてくれます。

Visual Studio Code で開発しているなら、dotnet user-secrets でもいいのですが、Visual Studio でプロジェクトに関わるファイルを別のエディタをいちいち開くのは辛いでしょう。

Open UserSecrets の紹介

Open UserSecrets を作った動機はまさにこの難しさをASP.NET Core のプロジェクトと同等まで簡単にすることです。

Visual Studio 2019 (VS2017 も同様です)を Open UserSecrest を使う流れを見てみましょう。とはいってもふつうのVisual Studio 拡張と同じです。

Visual Studio > Extensions で Online から Open UserSecrets を検索してください。

Visual Studio拡張でOpen UserSecretsを検索

ダウンロードしたらVSを再起動します

拡張ダイアログがでるのでインストールします

インストール完了

では Console App で利用してみましょう。

サンプルプロジェクトをリポジトリに用意してあります。

github.com

適当な .NET Core Console プロジェクトに、Microsoft.Extensions.Configuration.UserSecrets Nuget を入れます。(これで UserSecrets が利用できるようになります)

あとはプロジェクトを右クリックして Open UserSecrets を選ぶだけです。

Open UserSecrets を選ぶ

Manage UserSecrets と同様に secrets.json が Visual Studio で開くので好きに編集してください。

自動的に secrets.json がVisual Studio で開く

まとめ

みなさんが幸せになりますように。

ASP.NET Core チームでも .NET Core チームでもいいから早く Visual Studio 拡張かなにか用意してほしいです..... (Issue なのか?

github.com

*1:公開して3カ月がたっていました。スターで気づいた

CircleCI CLI を Windows でも利用する

circleci の cliを使うとconfigやorbs などローカルでの操作がはかどります。

この CircleCI Local CLI、以前は公式ページに macOS と Linux の記述しかありませんでしたがWindows の導入方法も示されるようになりました。

circleci.com

少し見てみましょう。

目次

TL;DR

  • Windows ローカルでも配置して利用できます
  • WSL 経由で利用できます

使いやすい方法どれでもいいでしょう。 workspace 使ってると circleci cli でローカル実行できないのでしょぼん。

circleci cli

CLI は、Golang でできています。この希望にあふれる感じを裏切らず、Windows でも利用ができます。Golang えらい。

circleci-cli の言語

Windows で CLI を配置する

公式ページには Chocolatey での導入が載っています。パスも更新も choco で管理されるので便利。

choco install circleci-cli -y

公式にはないものの Scoop でも導入できます。パスも更新も scoop で管理されるので便利。

scoop install circleci-cli

バイナリを直接持ってきたいなら、GitHub Release ページにバイナリが落ちています。バイナリならcircleci update による更新で管理もいいでしょう。

github.com

circleci cli を Windows で利用する

WSL

Linux インストールができるので、当然 Ubuntu 18.04 / 20.04 をWSLでいれれば利用できます。

VS Code などで開いておいて、ターミナルペインで bash から circleci でもいいですし、wsl circleci で呼び出してもいいでしょう。

VSCode で各種方法で circleci cli を起動する