Abstract

C#でAWS Secrets Managerをローカルエミュレータ(LocalStack)上で使ってみました。

1. Introduction

1.1. Preparation

  1. Install the package

    1
    
    $ dotnet add package AWSSDK.SecretsManager
    
  2. Launch LocalStack (AWS Secrets Manager)

    docker-compose.yaml

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    services:
      localstack:
        image: localstack/localstack
        container_name: "${LOCALSTACK_DOCKER_NAME:-localstack-main}"
        ports:
          - "127.0.0.1:4566:4566" # LocalStack Gateway
        environment:
          # LocalStack configuration: https://docs.localstack.cloud/references/configuration/
          - SERVICES=secretsmanager # Write the AWS services you want to use, separated by commas
          - DEBUG=${DEBUG:-0}
        volumes:
          - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
          - "/var/run/docker.sock:/var/run/docker.sock"
    
    1
    
    $ docker compose up -d
    

1.2. Create a Secret

1
$ awslocal secretsmanager create-secret --name my-secret --description "LocalStack Secret" --secret-string file://secrets.json

secrets.json

1
2
3
4
{
    "username": "admin",
    "password": "password"
}

2. C# Source Code

Program.cs

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
using Amazon.SecretsManager;
using Amazon.SecretsManager.Model;
using Amazon.Runtime;

/// <remarks>
/// The program initializes a connection to the LocalStack instance running on `localhost:XXXX`.
/// Make sure that LocalStack is running and that the secret is created before executing the application.
/// </remarks>
class Program
{
    private static readonly string _localstackEndpoint = "http://localhost:4566"; // LocalStackのエンドポイント

    static async Task Main(string[] args)
    {
        // 引数のチェックとシークレット名の取得
        if (args.Length < 1)
        {
            Console.WriteLine("Error: Missing required argument. Please provide the secret name to retrieve.");
            return;
        }
        var secretName = args[0];

        // クライアント作成
        var config = new AmazonSecretsManagerConfig
        {
            ServiceURL = _localstackEndpoint,
        };

        // ダミー認証情報(LocalStackは実際の認証を行いませんが、形式的には必要です)
        // IAMロールでAWS Secrets Managerへのアクセス権限が付与されていれば、クレデンシャルは不要
        var credentials = new BasicAWSCredentials("dummy-access-key", "dummy-secret-key");
        var client = new AmazonSecretsManagerClient(credentials, config);

        // Secretの取得処理
        try
        {
            var response = await GetSecretAsync(client, secretName);
            if (response is not null)
            {
                var secret = DecodeString(response);
                if (!string.IsNullOrEmpty(secret))
                {
                    Console.WriteLine($"The decoded secret value is: {secret}.");
                }
                else
                {
                    Console.WriteLine("No secret value was returned.");
                }
            }

        }
        catch (AmazonSecretsManagerException ex)
        {
            Console.WriteLine($"AWS SecretsManager error: {ex.Message}");
            Console.WriteLine($"Error code: {ex.ErrorCode}, HTTP status: {ex.StatusCode}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"An unexpected error occurred: {ex.Message}");
            Console.WriteLine(ex.StackTrace);
        }
        finally
        {
            if (client is IDisposable disposable)
            {
                disposable.Dispose();
            }
        }
    }

    /// <summary>
    /// Asynchronously retrieves the value of a secret from AWS Secrets Manager.
    /// </summary>
    /// <param name="client">The <see cref="IAmazonSecretsManager"/> client used to communicate with the Secrets Manager service.</param>
    /// <param name="secretName">The name or ARN of the secret to retrieve from Secrets Manager.</param>
    /// <returns>A <see cref="Task"/> that represents the asynchronous operation, containing a <see cref="GetSecretValueResponse"/> that holds the secret value.</returns>
    /// <exception cref="AmazonSecretsManagerException">Thrown when the request to Secrets Manager fails.</exception>
    /// <exception cref="Exception">Thrown when an unexpected error occurs during the retrieval of the secret.</exception>
    static async Task<GetSecretValueResponse> GetSecretAsync(IAmazonSecretsManager client, string secretName)
    {
        var request = new GetSecretValueRequest
        {
            SecretId = secretName
        };

        var response = await client.GetSecretValueAsync(request);
        return response;
    }

    /// <summary>
    /// Decodes the secret returned by the call to GetSecretValueAsync and
    /// returns it to the calling program.
    /// </summary>
    /// <param name="response">A GetSecretValueResponse object containing
    /// the requested secret value returned by GetSecretValueAsync.</param>
    /// <returns>A string representing the decoded secret value.</returns>
    public static string DecodeString(GetSecretValueResponse response)
    {
        // Decrypts secret using the associated AWS Key Management Service
        // Customer Master Key (CMK.) Depending on whether the secret is a
        // string or binary value, one of these fields will be populated.
        if (response.SecretString is not null)
        {
            var secret = response.SecretString;
            return secret;
        }
        else if (response.SecretBinary is not null)
        {
            var memoryStream = response.SecretBinary;
            StreamReader reader = new StreamReader(memoryStream);
            string decodedBinarySecret = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(reader.ReadToEnd()));
            return decodedBinarySecret;
        }
        else
        {
            return string.Empty;
        }
    }
}

3. Execution Result

1
2
3
4
5
❯ dotnet run my-secret
The decoded secret value is: {
    "username": "admin",
    "password": "password"
}

4. Reference