はじめに
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 をラップしており、以下の特徴がある。
- 宣言的な記述: どのフィールドがどのシークレットに対応するかをタグで記述できる。
- JSONパースの自動化: シークレットの中身がJSONの場合、特定のキーだけを抽出してマッピング可能。
-
キャッシュ機能: APIコールをキャッシュし、パフォーマンス向上とコスト削減(APIスロットリング回避)に寄与する。
- まぁここは Parameters and Secrets Lambda Extension で何とかなる部分ではある
なぜ便利なのか
通常、AWS SDKを使ってシークレットを取得する場合、以下のような手順を踏む必要がある。
- AWS Session / Configの初期化
- Secrets Managerクライアントの作成
-
GetSecretValueの呼び出し - エラーハンドリング
- JSONのUnmarshal
- 目的のフィールドへの代入
使いたい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 が未設定でもデフォルト値を反映できる。
- ロード処理(.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で使っていたりするが、それ以外の初期化処理をスッキリさせたい場合に非常に有効な選択肢であると思う。
参考資料
- Doc
-
GitHubリポジトリ: binbandit/secretfetch
- 以前は crazywolf132さんがホストしていたのだがリネーム?別アカウントに移管?したっぽい
- 私が見かけて使い始めるキッカケとなったredditのスレッド