👩‍🚀

[AWSSDK.NET] AssumeRoleで得る権限を自動更新する

2024/12/18に公開

概要

AWSのAssumeRoleにより一時的なCredentialsを得られますが、既定で1時間、最大でも12時間で期限が切れてしまいます。

AssumeRoleを使用して12時間を超えて動作するプログラムを実装する際は、ExpirationをこまめにチェックしながらなんとかしてCredentialsを再度取り直す、あまり美しくないお手製実装を書きがちでした。

RefreshingAWSCredentials を使うことで、すっきり実装することが可能です。
https://docs.dndocs.com/n/AWSSDK.Core/3.7.106.27/api/Amazon.Runtime.RefreshingAWSCredentials.html

RefreshingAWSCredentialsを継承して自分で書くこともできますが、AssumeRoleについては AssumeRoleAWSCredentials というズバリのクラスがあります。

AssumeRoleAWSCredentialsの存在を知らずに記事を書き終えたところで、知ってしまいました。供養のため両方のやり方を残しておきます。

参考

https://www.owenrumney.co.uk/implementing-refreshingawscredentials/

なお、Python (boto3) でも同様に備わっています。
https://www.owenrumney.co.uk/implementing-refreshingawscredentials-python/

環境

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="AWSSDK.S3" Version="3.7.410.6" />
    <PackageReference Include="AWSSDK.SecurityToken" Version="3.7.401.13" />
    <PackageReference Include="AWSSDK.Extensions.NETCore.Setup" Version="3.7.301" />
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
  </ItemGroup>
</Project>

AWSSDK.S3についてはサンプルコードの都合であり、必須ではありません。

RefreshingAWSCredentials を使う場合

RefreshingAWSCredentials を継承するクラス

RefreshingAWSCredentials クラスは名前空間Amazon.Runtime、NuGetパッケージはAWSSDK.Coreにあります。本記事の例ではAssumeRole(STS)と共に使用しますが、ほかの用途にも使えます。

RefreshingAWSCredentials はabstractなので、適宜継承して使います。最低限、GenerateNewCredentialsAsync メソッドをoverrideするようにします[1]。 初回または期限切れした後の更新時に呼ばれます。Credentialsを取得して、Expiration(期限)を添えて返すように書きます。

AssumeRoleRefreshingAWSCredentials.cs
using Amazon.Runtime;
using Amazon.SecurityToken;
using Amazon.SecurityToken.Model;

public class AssumeRoleRefreshingAWSCredentials(IAmazonSecurityTokenService stsClient, AssumeRoleRequest assumeRoleRequest) : RefreshingAWSCredentials
{
    protected override async Task<CredentialsRefreshState> GenerateNewCredentialsAsync()
    {
        var assumeRoleResponse = await stsClient.AssumeRoleAsync(assumeRoleRequest).ConfigureAwait(false);
        var immutableCredentials = new ImmutableCredentials(
            assumeRoleResponse.Credentials.AccessKeyId,
            assumeRoleResponse.Credentials.SecretAccessKey,
            assumeRoleResponse.Credentials.SessionToken);
        return new CredentialsRefreshState(immutableCredentials, assumeRoleResponse.Credentials.Expiration);
    }

    // デバッグ用: 無くて構わない
    public DateTime? Expiration => currentState?.Expiration;
}

このクラスを使い、実際にAssumeRoleによるCredentialsを得るときは、GetCredentialsAsync メソッドを使用します。[2]

using var stsClient = new AmazonSecurityTokenServiceClient();
var assumeRoleRequest = new AssumeRoleRequest();
using var assumeRole = new AssumeRoleRefreshingAWSCredentials(stsClient, assumeRoleRequest);
var cred = await assumeRole.GetCredentialsAsync();

利用例

Program.cs
using Amazon;
using Amazon.Runtime.CredentialManagement;
using Amazon.SecurityToken;
using Amazon.SecurityToken.Model;

var stsClient = new AmazonSecurityTokenServiceClient();
var assumeRoleRequest = new AssumeRoleRequest
{
    DurationSeconds = 900,  // 最小
    RoleSessionName = "Session1",
    RoleArn = "arn:aws:iam::123456789012:role/foo-role"
};
using var refreshingCredentials = new AssumeRoleRefreshingAWSCredentials(stsClient, assumeRoleRequest);

// 1秒ごとに権限の状態を表示する
while (true)
{
    var currentCredentials = await refreshingCredentials.GetCredentialsAsync().ConfigureAwait(false);
    Console.WriteLine("{0} | Expiration={1}, SecretKey={2}", DateTime.Now, refreshingCredentials.Expiration, currentCredentials.SecretKey);
    await Task.Delay(1000).ConfigureAwait(false);
}

1秒ごとに、現在の権限のExpirationとSecretKeyを表示する(ちょっと危ない)コードです。DurationSecondsの最小値は900秒なので15分は我慢して待ちます。15分後、以下のようにExpirationが延びてSecretKeyが切り替わるのを観測できるはずです。

               (前略)
               2024/12/17 23:39:43 | Expiration=2024/12/17 23:39:48, SecretKey=dummyftXCaxQ9VsJYunEdVyY+AbGJH25LPKDUMMY
               2024/12/17 23:39:44 | Expiration=2024/12/17 23:39:48, SecretKey=dummyftXCaxQ9VsJYunEdVyY+AbGJH25LPKDUMMY
               2024/12/17 23:39:45 | Expiration=2024/12/17 23:39:48, SecretKey=dummyftXCaxQ9VsJYunEdVyY+AbGJH25LPKDUMMY
               2024/12/17 23:39:46 | Expiration=2024/12/17 23:39:48, SecretKey=dummyftXCaxQ9VsJYunEdVyY+AbGJH25LPKDUMMY
               2024/12/17 23:39:47 | Expiration=2024/12/17 23:39:48, SecretKey=dummyftXCaxQ9VsJYunEdVyY+AbGJH25LPKDUMMY
!ここで変わる!  2024/12/17 23:39:49 | Expiration=2024/12/17 23:54:19, SecretKey=y6ZktjBLkY+DUMMYDUMMYDUMMY+bfAnKR88crPYg
               2024/12/17 23:39:50 | Expiration=2024/12/17 23:54:19, SecretKey=y6ZktjBLkY+DUMMYDUMMYDUMMY+bfAnKR88crPYg
               2024/12/17 23:39:51 | Expiration=2024/12/17 23:54:19, SecretKey=y6ZktjBLkY+DUMMYDUMMYDUMMY+bfAnKR88crPYg
               2024/12/17 23:39:52 | Expiration=2024/12/17 23:54:19, SecretKey=y6ZktjBLkY+DUMMYDUMMYDUMMY+bfAnKR88crPYg
               2024/12/17 23:39:53 | Expiration=2024/12/17 23:54:19, SecretKey=y6ZktjBLkY+DUMMYDUMMYDUMMY+bfAnKR88crPYg

               (続く...)

AssumeRoleAWSCredentialsを使う場合

こちらは継承不要でいきなり使えます。と言うか、AssumeRoleAWSCredentialsRefreshingAWSCredentialsを継承しており、つまり前述の実装をやってくれている次第です。そう考えると以下に出てくる引数の意味も全て理解できると思います。

RefreshingAWSCredentialsを継承しているということで、使い方は前述の自前継承版とほとんど同様です。

// STSのために使う権限。InstanceProfileは一例。
var credentials = new InstanceProfileAWSCredentials();

using var assumeRoleCredentials = new AssumeRoleAWSCredentials(
    credentials, 
    roleArn: "arn:aws:iam::123456789012:role/foo-role",
    roleSessionName: "Session1",
    options: new AssumeRoleAWSCredentialsOptions
    {
        DurationSeconds = 900,
    });
// 既定で「15分」がセットされており、DurationSecondsとあわせて適宜設定すること
assumeRoleCredentials.PreemptExpiryTime = TimeSpan.FromSeconds(30);

RefreshingAWSCredentialsの機能として、PreemptExpiryTimeプロパティを指定することで、真のExpirationより少し早く更新するよう仕向けることができます。AssumeRoleAWSCredentialsの場合、PreemptExpiryTimeには既定で「15分」がセットされており、本記事のようにDurationSeconds=900にしていると残り時間は差し引きゼロとなり、つまり毎秒更新を要求してしまいます。実際はDurationをもっと長く取るでしょうから問題にはなりにくいですが、このあたりは注意します。

Generic HostでのAssumeRole活用

DIコンテナとAWSSDK.NETを共に利用するときは、AWSSDK.Extensions.NETCore.Setup の活用が非常におすすめです。
https://docs.aws.amazon.com/ja_jp/sdk-for-net/v3/developer-guide/net-dg-config-netcore.html

appsettings.json に以下のように書けば、AssumeRoleによるCredentialsを使ってくれるようになります。

appsettings.json
{
  "AWS": {
    "Region": "ap-northeast-1",
    "SessionName": "MySession",
    "SessionRoleArn": "arn:aws:iam::123456789012:role/foo-role"
  }
}
Program.cs
using System.Reflection;
using Amazon.Runtime;
using Amazon.Runtime.CredentialManagement;
using Amazon.S3;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateApplicationBuilder(args);
builder.Configuration.AddJsonFile("appsettings.json", optional: false);
builder.Services.AddAWSService<IAmazonS3>();

var app = builder.Build();
var s3Client = app.Services.GetRequiredService<IAmazonS3>();

// s3Clientが持つCredentialsの中身を無理やり見てみる
var credentials = typeof(AmazonS3Client)
    .GetProperty("Credentials", BindingFlags.Instance | BindingFlags.NonPublic)
    .GetValue(s3Client);
Console.WriteLine(credentials is AssumeRoleAWSCredentials);  // True

コード一番下の通り、クライアントはAssumeRoleAWSCredentialsを持ったインスタンスとなります。本記事の内容を踏まえれば期限切れの心配なく使えると理解でき、便利です。

STSのために使う権限の融通が利きにくい欠点はあるかもしれません。例えば「STSにはProfile名で解決した権限を使い、その他はAssumeRoleで賄いたい」というケースだと、appsettings.jsonのシンプルな記述ではおそらく難しそうで、デフォルトの"AWS"セクション以外にもう1つセクションを作るとか、C#コードにてAWSOptionsパラメタをうまく取りまわす等の対処になろうかと思います。

脚注
  1. Async無しのGenerateNewCredentialsでも構いません ↩︎

  2. 例によってAsync無し版もあります。 ↩︎

Discussion