😋

[.NET 6] AWS SSM Parameter StoreをGeneric Hostに登録して使う

2022/10/13に公開

概要

AWSでシステムを組む際に、パスワードなど秘匿したいデータを入れておく定番の1つ、Systems Manager Parameter Store です(以降 Parameter Storeと表記)。

ASP.NET Coreを筆頭にGeneric Hostで利用する際に、Parameter StoreをDI・Configurationと組み合わせて"きれいに"使う方法についてまとめます。

Azure Key Vaultを使う方法は公式ドキュメントがあります。Parameter Storeとの合わせ技は情報が乏しく苦労しています。

環境・利用パッケージ

Amazon.Extensions.Configuration.SystemsManager を使う

基本

すぐ上の再掲になりますが、Amazon.Extensions.Configuration.SystemsManager という拡張パッケージがAWS公式から出ています。本記事はこれの説明に終始します。
https://www.nuget.org/packages/Amazon.Extensions.Configuration.SystemsManager
https://github.com/aws/aws-dotnet-extensions-configuration

以下のような名前の2つの値がParameter Storeには保存されていると仮定します。中身はStringまたはSecureStringです。

  • /foo/bar/db_password
  • /foo/bar/slack_webhook_url

基本的な利用法はREADMEの通りです。AddSystemsManagerをします。

Parameter Storeのpath指定については、1個上の階層を指定するようにします。その下のパラメータを全部取得してきてくれます [1]

Program.cs
using Amazon.Extensions.Configuration.SystemsManager;
using Amazon.SimpleSystemsManagement.Model;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Configuration.AddSystemsManager("/foo/bar/");  // !!!

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.MapControllers();
app.Run();

取得できたかどうか見てみます。デバッガで見てもログに出しても何でも良いですが、以下はGET /から文字列で返してしまう雑な例です。得られた値を見ると、Parameter Storeのキーの最後のスラッシュ以降の部分が名前になっているのがわかります。

app.MapGet("/", () => 
    string.Join("\n", builder.Configuration.AsEnumerable().Select(kv => $"{kv.Key} => {kv.Value}")));
/*
windir => C:\Windows
VSSKUEDITION => Community
(中略)
db_password => hogehoge
slack_webhook_url => piyopiyo
*/

AWS認証設定

上記はAWSのcredentialsに関することを何もしていないので、特にローカル環境での開発では動かしにくいケースがあるかもしれません。

credentialsの面倒を見る方法はおそらく3通りあります。

1. 環境変数

環境変数を何らかの方法で事前に設定する、AWS利用者にはおなじみの方法です。

2. appsettings.jsonに書く

以下のような"AWS"セクションを作っておくと、自動で読み取ってくれます。他のAWSSDK.NET 各種でも同様に使える流儀です。https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/net-dg-config-netcore.html

appsettings.json
{
  "AWS": {
    "Profile": "development",
    "Region": "ap-northeast-1"
  }
}

3. C#コードで指定

運用上おすすめしませんが、AWSCredentialsオブジェクトを流し込むこともできるのでなんでもでき最後の手段です。

Program.cs
builder.Configuration.AddSystemsManager(config =>
{
    config.Path = "/foo/bar/";
    config.AwsOptions = new AWSOptions
    {
        Credentials = new InstanceProfileAWSCredentials(), 
        Profile = "development",
        Region = RegionEndpoint.APNortheast1
    };
});

モデルにバインド (POCOにマッピング)

以上でParameter Storeの値を個別に得ることはできます。

もしモデルにバインドする場合は、以下のようにすると良さそうでした。

Program.cs
builder.Configuration.AddSystemsManager(config =>
{
    config.Path = "/foo/bar/";
    config.Prefix = "SSM";
});

// DIに登録する場合
builder.Services.Configure<SsmParameters>(builder.Configuration.GetSection("SSM"));

// その場でマッピングした値が欲しい場合
var model = builder.Configuration.GetSection("SSM").Get<SsmParameters>();


public record SsmParameters
{
    [ConfigurationKeyName("db_password")]
    public string DbPassword { get; set; } = null!; 
    [ConfigurationKeyName("slack_webhook_url")]
    public string SlackWebhookUrl { get; set; } = null!;
}

Prefixを指定することで、Parameter Storeから入ってきた設定値名に一括でプレフィックスが付きます。今回の場合はSSM:db_passwordのようになります。:または__はセクションの区切りと見なされます(参考)。

Parameter Storeでの命名規則とC#での命名規則がずれる場合も、ConfigurationKeyNameAttributeによりマッピング可能です [2]

以上のようにConfigureで登録した値は、いつも通りのDIの感じで利用できます。

FooController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

[ApiController]
[Route("[controller]")]
public class FooController : ControllerBase
{
    public FooController(IOptions<SsmParameters> ssmParameters)
    {
        ...

Configureしなかった場合も、個別に値を取り出すことができます。これもいつも通りです。

FooController.cs
public class FooController : ControllerBase
{
    public FooController(IConfiguration configuration)
    {
        string dbPassword = configuration["SSM:db_password"];

他の設定値プロバイダを優先させる

ASP.NET Coreでは、環境変数やappsettings.jsonのプロバイダをデフォルトで読みに行くようになっており、今回の AddSystemsManager は最後に付け足した格好になっています。

(あまりないかもしれませんが、)もし環境変数等の優先度を後にする(環境変数などでParameter Storeの値を上書きする)場合は、恒例のAddXXX拡張メソッドではなく素の書き方をして先頭に挿入するのが楽です。

ただし素の書き方をする場合、AwsOptionsが省略不可です。デフォルトの挙動を期待したければ以下のようにします。

Program.cs
// 環境変数で上書きしたい
Environment.SetEnvironmentVariable("SSM__db_password", "dummydummy");

// 先頭にSSMのを挿入
builder.Host.ConfigureAppConfiguration((hostingContext, config) =>
{
    config.Sources.Insert(0, new SystemsManagerConfigurationSource
    {
        Path = "/foo/bar/",
        Prefix = "SSM",
        AwsOptions = AwsOptionsProvider.GetAwsOptions(config)
    });    
});

ほかにも、config.Sources.Clear(); してから自分で必要なものを必要な順でAddしていく方法等もありです。

活用シーンの例: 環境で分岐

以下はそこそこありそうなシーンです。

こんな感じになりそうです。ここまでの総まとめです。

プロバイダ両者で、設定値のキーは揃えるように注意します。Parameter Storeで /foo/bar/baz というキーならば、それに相当する環境変数では SSM__baz という名前にしておく、といった要領です。

Program.cs
if (builder.Environment.IsDevelopment())
{
    // ASP.NET Coreの場合はデフォルトでやってくれるので実際は指定不要
    // builder.Configuration.AddUserSecrets(...);
    // builder.Configuration.AddEnvironmentVariables();
}
else
{
    builder.Configuration.AddSystemsManager(config =>
    {
        config.Path = "/foo/bar/";
        config.Prefix = "SSM";
    });
}
builder.Services.Configure<SsmParameters>(builder.Configuration.GetSection("SSM"));

Secret Managerの参考: https://learn.microsoft.com/ja-jp/aspnet/core/security/app-secrets?view=aspnetcore-6.0&tabs=windows

脚注
  1. この挙動のため、付与する権限として ssm:GetParametersByPath を要します。 ↩︎

  2. System.Text.Jsonの[JsonPropertyName]は効きません ↩︎

  3. AWSのSecrets Managerのことではありません ↩︎

Discussion