0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Go] secretfetchを使って透過的にAWS SecretManagerから構造体にマッピングする

Posted at

はじめに

GoでAWS Secrets Managerを利用する際、AWS SDKをそのまま使うとボイラープレートなコードが増えがちになってしまう。シークレットの取得、JSONのパース、構造体へのマッピングなど、本来注力したいビジネスロジック以外を作るのに時間を取られてしまう。まぁとはいえ、そこまで大変なものでもないし、1回作ってしまえば使いまわしできるようなものではありつつも、なんかめんどうくさい。

何よりSecret Managerを強く意識するような構造になってしまう部分が少しイヤだ。例えば、ECSの場合は、Secret Managerから簡単に環境変数としてマッピングしてコンテナ内部に注入することが可能だが、Lambdaの場合はそうもいかない。未だに環境変数にSecret Managerから自動的にマッピングするような標準機能はない。だからといってLambda環境変数やコードとして平文で置いておくのはさすがにイヤだ。

そこで今回は、構造体のタグ定義だけでAWS Secrets Managerから値を自動注入できるライブラリ secretfetch)を紹介する。

あくまで個人的な話だがこちらのモジュールを使うことで非常に簡単に、そして便利にSecret Managerを使えるようになって大変便利に思っている。

現実問題、ローカル開発からSecret Managerはあまり使わないと思うので 「ローカル開発は .env、本番は Secrets Manager」 という運用もこちらのモジュールで簡単に実現できる。

secretfetch とは

secretfetch は、Goの構造体タグ(Struct Tags)を利用して、Secret Managerで管理している秘匿情報を構造体に安全かつ直接的にマッピングしてくれる。

AWS SDKの GetSecretValue をラップしており、以下の特徴がある。

  1. 宣言的な記述: どのフィールドがどのシークレットに対応するかをタグで記述できる。
  2. JSONパースの自動化: シークレットの中身がJSONの場合、特定のキーだけを抽出してマッピング可能。
  3. キャッシュ機能: APIコールをキャッシュし、パフォーマンス向上とコスト削減(APIスロットリング回避)に寄与する。

なぜ便利なのか

通常、AWS SDKを使ってシークレットを取得する場合、以下のような手順を踏む必要がある。

  1. AWS Session / Configの初期化
  2. Secrets Managerクライアントの作成
  3. GetSecretValue の呼び出し
  4. エラーハンドリング
  5. JSONのUnmarshal
  6. 目的のフィールドへの代入

使いたいSecretが増えるたびにこの処理を書くのはさすがに面倒。secretfetch を使えばめっちゃ簡単になる。管理も楽になる。

実践: タグだけで "AWS → env フォールバック"

secretfetch はタグに aws=...env=... を併記できるので、

  • 本番:Secrets Manager から取得
  • ローカル:.env(= 環境変数)へフォールバック

を、追加のマージ処理なしで実現できてしまう。めっちゃ便利。

1. 構造体の定義(構造体1つで完結)

例として「DB パスワードは Secrets Manager、本番以外は .env」を想定する。例えばDB接続ポートなんかはデフォルトがあるだろうからそういうのはfallbackタグによって簡単に代替させる。

package config

type Configuration struct {
    DBHost string `secret:"env=DB_HOST"`
    DBPort string `secret:"env=DB_PORT,fallback=5432"`
    DBUser string `secret:"env=DB_USER"`
    DBName string `secret:"env=DB_NAME"`

    // 本番: Secrets Manager
    // ローカル: env(.env)へフォールバック
    DBPasswd string `secret:"aws=aurora/credential/password,env=DB_PASSWORD"`
}

env=... は 別途dotenv を読んでおけばそのままそれを反映できる。fallback=... を書いておくと、env が未設定でもデフォルト値を反映できる。

  1. ロード処理(.env を読んで Fetch するだけ)
package config

import (
    "context"
    "fmt"
    "os"
    "time"

    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/crazywolf132/secretfetch"
    "github.com/joho/godotenv"
)

func LoadConfig(ctx context.Context) (Configuration, error) {
    // ローカル開発: dotenv があれば読む(env=... のソースになる)
    if _, err := os.Stat("./.env"); err == nil {
        _ = godotenv.Load()
    }

    // AWS Config のロード(IAM Role / env / shared config などに従う)
    awsCfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("ap-northeast-1"))
    if err != nil {
        return Configuration{}, fmt.Errorf("failed to load aws config: %w", err)
    }

    opts := &secretfetch.Options{
        AWS:           &awsCfg,
        CacheDuration: 15 * time.Minute,
        SecureCache:   true, // README の Secure Memory Handling を参照
    }

    var conf Configuration
    if err := secretfetch.Fetch(ctx, &conf, opts); err != nil {
        return Configuration{}, fmt.Errorf("secretfetch failed: %w", err)
    }

    return conf, nil
}

これで 「ローカルは .env、本番は Secrets Manager」 がシンプルに実現できる。

補足: 段階的に導入したいとき:env ベース設定に secret を上書きする

上述の “タグだけで完結” がいちばんスッキリだと思うが、既存コードの都合で次のようなケースもあリエルと思う。

  • 既に LoadConfig() に相当するコードが大きく存在していて、すぐにタグ方式へ寄せられない
  • secret を取得したあとに加工して埋めたい(複数 secret を合成する等)
  • 諸々の事情はまずは “secret 部分だけ” secretfetch に寄せたい

その場合は、Secrets Manager から取ってきたい値だけ別 struct にして、最後に必要なフィールドだけ上書きするのが現実的かもしれない。

package config

type SecretConfiguration struct {
    DBPasswd string `secret:"aws=aurora/credential/password"`
}

type Configuration struct {
    DBHost   string
    DBName   string
    DBPort   string
    DBUser   string
    DBPasswd string
}

package config

import (
    "context"
    "fmt"
    "time"

    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/crazywolf132/secretfetch"
)

func LoadConfigByAWS(ctx context.Context) (Configuration, error) {
    // 1) まず env/.env をベースとして読み込む(既存実装を流用)
    conf := LoadConfigFromEnv() // 既存の env ロード関数を想定

    // 2) secretfetch で埋めたい値だけ取得
    sconf := &SecretConfiguration{}

    awsCfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("ap-northeast-1"))
    if err != nil {
        return Configuration{}, fmt.Errorf("failed to load aws config: %w", err)
    }

    opts := &secretfetch.Options{
        AWS:           &awsCfg,
        CacheDuration: 15 * time.Minute,
        SecureCache:   true,
    }

    if err := secretfetch.Fetch(ctx, sconf, opts); err != nil {
        return Configuration{}, fmt.Errorf("secretfetch failed: %w", err)
    }

    // 3) 必要なものだけ上書き(順序を明示)
    conf.DBPasswd = sconf.DBPasswd
    return conf, nil
}

“段階導入”の観点ではこのパターンも現実的のはず。

メリット

タグによる管理の明確化

SecretConfiguration 構造体を見るだけで、どのフィールドがAWS上のどのパスに対応しているかが一目瞭然になる。コードとインフラ定義の乖離を防ぎやすい。

強力なキャッシュ戦略

CacheDuration を設定することで、アプリケーションからの過剰なAPIコールを防げる。AWS Secrets Managerの料金はAPIコール数にも依存するため、コスト削減に直結する。また、SecureCache: true により、メモリ上での扱いにも配慮されている点も非常に嬉しい。

柔軟な構成

上記コード例のように、「基本は環境変数、重要なパスワードのみAWS」というハイブリッド構成が容易に組める。ローカル開発時はモックを使わずとも、単に .env にダミー値を入れて LoadConfig() だけを使えば良いため開発体験性も良い。

まとめ

AWS Secrets Managerは何かしらの秘匿情報を持つ現代的なアプリケーション構築に必要不可欠だが、どうしても扱いは少々冗長になりがちだったりする。secretfetch を導入することで、Goらしい構造体ベースのアプローチでシンプルかつ宣言的にシークレットを管理できる。特に単にSecret Managerから反映させるというだけでなく、環境変数からの反映やフォールバック処理も簡単に書けてしまうのが良い。
私の場合は主にLambdaで使っていたりするが、それ以外の初期化処理をスッキリさせたい場合に非常に有効な選択肢であると思う。

参考資料

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?