アーカイブ

Archive for 2019年8月

設定ファイル、環境変数、AWSSecretsManagerに定義された設定値を透過的に扱う

2019年8月26日 コメントを残す

ASP.NET Coreでは 環境変数と設定ファイルの値を両方から取得し、IWebHost構築時にConfigurationに定義した優先度で設定値をマージして利用する。

Kralizerk.Extensions.Configuration.AWSSecretsManager ( https://github.com/Kralizek/AWSSecretsManagerConfigurationExtensions )を利用すると、上記に加えAWS SecretsManagerのエントリも環境変数やJSONファイルと同列に扱うことができ、設定値をセキュアに運用することができる。

方針と設定情報の構築

下記の設定では appSettings.jsonappSettings.Development.json(ASPNETCORE_ENVIRONMENTに依存)⇒環境変数 ⇒SecretsManagerの順に設定値を読み込み、後に定義された値を優先して利用する。SecretsManagerは環境変数SECRETSMANAGER_PREFIXDev/App1のように定義すると、Dev/App1/Secrets1Dev/App1/Secrets1といったキーのシークレット情報を取得できるようになる。

AWSが提供するSDKを利用する場合、ServiceCollection構築時にAddDefaultAWSOptionsを指定するとAWS:Regionの設定を考慮してRegionをライブラリに設定してくれるのだが、Kralizerk.Extensions.Configuration.AWSSecretsManagerでは読み込んでくれなかったので、環境変数AWS__Regionから明示的にRegionを取得している。
また、ローカル開発時に、無駄にSecretsManagerから値をとらないように、前述した環境変数SECRETSMANAGER_PREFIXが定義されていない場合は、SecretsManagerから値を取得しないといった小細工を入れている。

        public static IWebHost CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration((hostingContext, config) =>
                    {
                        var env = hostingContext.HostingEnvironment.EnvironmentName;
                        config.SetBasePath(Directory.GetCurrentDirectory());
                        config.AddJsonFile("appSettings.json");
                        config.AddJsonFile($"appSettings.{env}.json", optional: true);

                        config.AddEnvironmentVariables();

                        var region = Environment.GetEnvironmentVariable("AWS__Region");
                        if (string.IsNullOrEmpty(region))
                        {
                            region = "ap-northeast-1";
                        }
                        var secretsManagerPrefix = Environment.GetEnvironmentVariable("SECRETSMANAGER_PREFIX");
                        if (!string.IsNullOrEmpty(secretsManagerPrefix))
                        {
                            config.AddSecretsManager(
                                region: RegionEndpoint.GetBySystemName(region),
                                configurator: cfg =>
                                {
                                    cfg.KeyGenerator = (entry, key) =>
                                    {
                                        var customKey = key.Substring(entry.Name.Length).Trim(':').Trim();
                                        return string.IsNullOrEmpty(customKey) ? key : customKey;
                                    };
                                    cfg.SecretFilter = entry => entry.Name.StartsWith(secretsManagerPrefix);
                                });
                        }
                    })
                .UseStartup()
                .Build();

値の設定例

例えば、それぞれ下記のように定義されていた場合
appSettings.json

{
  "Section1": {
    "Value1": "Value1",
    "Value2": "Value2",
    "Value3": "Value3",
    "Value4": "Value4",
    "Value5": "Value5"
  }
}

appSettings.Development.json

{
  "Section1": {
    "Value2": "from appSettings.Development.json",
  }
}

環境変数.sh

export Section1__Value3=fromEnvironment

SecretsManager Dev/App1/Secrets1

{
  "Section1": {
    "Value4": "from Dev/App1/Secrets1"
  }
}

SecretsManager Dev/App1/Secrets2

{
  "Section1": {
    "Value5": "from Dev/App1/Secrets2"
  }
}

実行結果

下記の結果で出力される値は次のようになる。

    public partial class App
    {
        public IConfiguration Configuration { get; }

        public App(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public void Run()
        {
            Console.WriteLine(Configuration.GetValue("Section1:Value1");
            Console.WriteLine(Configuration.GetValue("Section1:Value2");
            Console.WriteLine(Configuration.GetValue("Section1:Value3");
            Console.WriteLine(Configuration.GetValue("Section1:Value4");
            Console.WriteLine(Configuration.GetValue("Section1:Value5");
        }
    }

実行結果

Value1
from appSettings.Development.json
fromEnvironment
from Dev/App1/Secrets1
from Dev/App1/Secrets2

AWS で必要となるポリシー

Kralizerk.Extensions.Configuration.AWSSecretsManagerはSecretFilterで与えられたキーをSecretsManagerから検索して列挙するため、AWSのポリシーとしてsecretsmanager:ListSecretsとGetSecretValueの二つが必要になる。
これらのポリシーが無いロールで実行しようとした場合、下記のような例外メッセージが表示される。

Unhandled Exception: Amazon.SecretsManager.AmazonSecretsManagerException: User: arn:aws:sts::571419068914:assumed-role/EcsLinuxClusterInstance-Stg-EcsInstanceRole-16ODQN6YR1TPC/i-0159b6dc3deb1e536 is not authorized to perform: secretsmanager:ListSecrets —> Amazon.Runtime.Internal.HttpErrorResponseException: Exception of type ‘Amazon.Runtime.Internal.HttpErrorResponseException’ was thrown.

カテゴリー:未分類

EntityFramework Coreのマイグレーションのロールバック

2019年8月22日 コメントを残す

実行環境へのデプロイが失敗した場合、すぐさま解決できるかどうかわからないので、できればいったんリリース前の状態に戻したいのだけれど、データベースのマイグレーションがかかわってくるとだいぶ混乱する。手順としてちゃんとまとめてなかったので個人的にまとめる。

例にする状況

例えば、下記の例はC#側で管理されているマイグレーションがすべて実施された状態のC#ソリューションの状態とデータベースの状態で、最後に実施されたマイグレーションは初期登録(20190822050256_initial)になっている。
1.初期状態-db
1.初期状態

この状態でプログラム側では下記の順にデータベースのマイグレーションが追加されたとする。
– EntityBの追加
– EntityAに対するAddressカラムの追加
– EntityCの追加
2.EntityBの追加とEntityAに対する修正、EntityCを行い、修正スクリプトを追加した

で、例えば手で(Alter Tableなどで)EntityAにAddressカラムが追加されていたとしたら、当然のことながらマイグレーション実行時にAddressカラムがすでに存在するので、次のようなエラーが発生してマイグレーションが失敗する。

エラーメッセージ

この時点のデータベースの状態は下記のようになっている。
EntityBの追加は成功しているけれど、EntityAの修正とEntityCの追加が失敗しているので、履歴としては20190822050515_add_entitybが最新
4.失敗後のデータベースの状態

データベースのダウングレード

マイグレーションのダウングレード手順

マイグレーションをダウングレードする場合、次の手順で作業を行う。
– 現在のマイグレーションの状況を確認する
– マイグレーションの復旧時点を決める
– マイグレーションのダウングレードを行う
– 以前動作していたアプリを再デプロイする

マイグレーションの状況を確認と復旧時点の決定

マイグレーションはデータベース上の __EFMigrationHistory テーブルで管理されている。
マイグレーションに失敗した場合、このテーブルと、C#側で管理しているマイグレーションの履歴を確認しどこまでマイグレーションが実施されたかを確認する。今回は前回正しく動いていたのが20190822050256_initialの状態なので、ここを目指してダウングレードを行う。

コマンドはこんな感じ、あくまで実行されたマイグレーションのDownに定義された操作を実施するだけなので、手動で追加されたカラムはそのままになっている。
dotnet ef database update 20190822050256_initial -p ./Data -s ./WebApplication6

コメント 2019-08-22 142200

dotnet ef コマンドの詳細は下記のページを参照
Entity Framework Core ツールリファレンス-.NET CLI
https://docs.microsoft.com/ja-jp/ef/core/miscellaneous/cli/dotnet

で、後は以前動いていたアプリケーションを動かせばその日の対応は完了。

その後の対応

もちろんこの例の場合は、マイグレーションで管理されているテーブルに対して手動でメンテナンスしたことがそもそもの間違いなので、対象カラムをドロップ後マイグレーションを流しなおすのが正しい。もちろんデータの喪失を防ぐために一時的に添付テーブルにデータを退避後、マイグレーションが成功した後にパッチを当てるなどの対応が必要になる。

ただ、複数人で開発中にEntityをデータコンテキストに追加したままマイグレーションを追加忘れたままPushしてしまい、他の人のマイグレーションに紛れてしまってdevelopマージ後におかしくなる。みたいなケースがあるので、その場合は、誤ったマイグレーションを削除してマイグレーションを追加しなおす必要がる。
その場合は、dotnet migrations removeコマンドで不要なマイグレーションを削除したのち、正しいマイグレーションを追加することになる。

解決していない問題

基本的にはマイグレーションのエラーを起こさないのが一番なので、CI/CDで本番環境にデプロイする前にSTG環境のようなものを作り、マイグレーションの直前に本番データベースの状態をスナップショットからSTG環境を作成しマイグレーションのテストをするのが望ましい気がする。データボリュームとかで難しいかもしれないけれど、、、

実際にデータベースのロールバック時にいつ時点のマイグレーションに戻せばいいのかとか、リリースでてんぱっているときに調べるのはだいぶつらい気がする。回避の案としては、__EFMigrationHistory に対するデータベーストリガーを作っておいて、前回のデプロイ時にどのマイグレーションが走ったのかを管理するとかしておけば、まだどうにかなる気がする。__EFMigrationHistoryテーブルに更新日とか入ってくれないかなー。

カテゴリー:未分類