tech.guitarrapc.cóm

Technical updates

PowerShell でsudo っぽいものを内蔵した関数を作る

時々思い出したようにPowerShell の記事を書いてみます。

スクリプトでよくあるのが、sudo で実行時に権限があるスクリプトの許可をしたいというケースです。

Windows は組み込みsudo がないので面倒でしたが、現状なら scoop で sudo をインストールするといいと思います。

scoop.sh

scoop install sudo

これで sudo ./your_script.ps1 とできるので特権が必要なときに、必要な権限を渡すことができます。

さて今回の記事は、Windows において実行中のスクリプトや関数が特権が必要な場合に、sudo を使わずにUACダイアログを出して昇格したPowerShellで同関数を実行し直してほしいというケースです。

通常の特権昇格フロー + Windows Diffender操作のため利用には注意してください。

この2つが自動化できるのは運用上は非常に便利ですが、誤った利用は技術を良くない方向に追い込みます。 チーム内での潤滑な運用での利用に留めることを推奨します。

目次

TL;DR

悪用禁止だけど、チーム内で使うには便利です。 特に、PowerShell のExecution Policy やダブルクリック問題はだいたいこれで解決するのが定番です。(chocolatey や scoop もこの手法)

サンプル

今回は、Windows Defender によってdotnet のビルドが遅いので除外して対応しようという記事を使ってやってみます。

baba-s.hatenablog.com

サンプルスクリプトを置いておきます。

PowerShellUtil/WindowsDefender at master · guitarrapc/PowerShellUtil · GitHub

Windows PowerShell で次のコマンドを実行すると、Windows Defender のスキャン除外パスに指定したパス + Visual Studio のパスが入り、除外プロセスにVisual Studio や dotnet、msbuildが入ります。

実行前に十分に気をつけてください

iex  (new-object net.webclient).downloadstring('https://raw.githubusercontent.com/guitarrapc/PowerShellUtil/master/WindowsDefender/remote_exec.ps1')

Windows PowerShell を特権のない状態で起動して実行すると、昇格するか聞かれます。

f:id:guitarrapc_tech:20190508140824p:plain
UAC昇格が聞かれる

y を押した時だけ、UACダイアログがでて関数がそこで再実行されます。

f:id:guitarrapc_tech:20190508140916p:plain
特権に昇格されたPowerShellで関数が再実行される

f:id:guitarrapc_tech:20190508140943p:plain
実行結果

仕組み

処理の本体は、Add-DefenderExclusionForDevEnv です。

PowerShellUtil/Add-DefenderExclusionForDevEnv.ps1 at master · guitarrapc/PowerShellUtil · GitHub

キーはここです。

gist.github.com

自分が特権で実行されているかは、これで検出できます。

$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
if (!$currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {

自分の関数名を、自動変数 $MyInvocation から取ります。

$me = $MyInvocation.MyCommand

自分の関数の定義を、Get-Command から取ります。

$myDefinition = (Get-Command $me).Definition

これで関数文字列が生成できます。

$myfunction = "function $me { $myDefinition }"

新規プロセスでPowerShell.exe を実行するときに、実行するコマンド文字列を組み立てます。 今のパスに移動するようにしています。

$cd = (Get-Location).Path
$commands = "Set-Location $cd; $myfunction; Write-Host 'Running $me'; $me; Pause"

さて、生成したコマンド文字列は関数の改行が含まれており、このままでは PowerShell.exe の -Command 引数に渡せません。 そこで、バイナリにしてBase64文字列を -EncodedCommand にわたすことで解釈させます。

このあたりは、PowerShell を使ったワーム攻撃でもよく使われる手法です。

$bytes = [System.Text.Encoding]::Unicode.GetBytes($commands)
$encode = [Convert]::ToBase64String($bytes)
$argumentList = "-NoProfile","-EncodedCommand", $encode

あとは、PowerShell を 特権で起動するため、-Verb RunAs を指定して先程の引数を食わせます。 PowerShell.exeの実行時に、-Wait をつけることで、起動したPowerShell.exe が自動で閉じません。

$p = Start-Process -Verb RunAs powershell.exe -ArgumentList $argumentList -Wait -PassThru

おわりに

最近のほげもげみてると、こういう記事書くのは心配です。 ただ、標準で用意されている方法を用途を限定して使うことまで阻害されるのは望ましくないものです。

攻撃にすでに利用もされている方法でもあるので、UAC は自分でプロセスを起動させるほうがパブリックにはいいです。(Chocolatey や Scoop がそう)

どうか正しく使われますように。

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ごとに一つのプロセスを起動して実行します。そのため、プロセス環境変数は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処理を一つの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

f:id:guitarrapc_tech:20190505121950p:plain
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

f:id:guitarrapc_tech:20190505123803p:plain
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

f:id:guitarrapc_tech:20190505123716p:plain
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】(サイコム)

f:id:guitarrapc_tech:20190505124333p:plain
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】(サイコム)

f:id:guitarrapc_tech:20190505124508p:plain
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であるため、ユーザー一人一人の情報を扱う/毎度一意なテンポラリの情報を扱うといったシークレットの件数の増加で恐ろしいほどコストがかかります。

f:id:guitarrapc_tech:20190502050502p:plain
AWS Secret はシークレットの件数が増えると料金が跳ね上がる

aws.amazon.com

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

f:id:guitarrapc_tech:20190502050614p:plain
アプリケーションの設定などはコストが抑えられて使いやすい

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

今回は、アプリケーションの起動時に一回読み込まれる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 が登録されます。

f:id:guitarrapc_tech:20190502050657p:plain
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 するときに注意がいります。

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

f:id:guitarrapc_tech:20190502051327p:plain
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

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

f:id:guitarrapc_tech:20190502051702p:plain
修正後、指定したキーの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

https://docs.aws.amazon.com/ja_jp/sdk-for-net/v3/developer-guide/net-dg-config-creds.html#creds-assigndocs.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 プロジェクトでも表示されます。

f:id:guitarrapc_tech:20190501024218p:plain
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 (ユーザーシークレットの管理) がコンテキストメニューに表示されます。

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

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

f:id:guitarrapc_tech:20190501025347p:plain
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 は、上記フローを一つのコマンドで実施し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 を検索してください。

f:id:guitarrapc_tech:20190501031131p:plain
Visual Studio拡張でOpen UserSecretsを検索

f:id:guitarrapc_tech:20190501031221p:plain
ダウンロードしたらVSを再起動します

f:id:guitarrapc_tech:20190501031303p:plain
拡張ダイアログがでるのでインストールします

f:id:guitarrapc_tech:20190501031326p:plain
インストール完了

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

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

github.com

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

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

f:id:guitarrapc_tech:20190501031805p:plain
Open UserSecrets を選ぶ

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

f:id:guitarrapc_tech:20190501031819p:plain
自動的に secrets.json がVisual Studio で開く

まとめ

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

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

なんならこの拡張あげるのではよ....

github.com

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