CDK で AWS Parameters and Secrets Lambda Extension を使ってみる

CDK で AWS Parameters and Secrets Lambda Extension を使ってパラメータを取得する Lambda を作成する。

環境

  • aws-cdk: 2.59.0
  • @aws-cdk/aws-lambda-go-alpha: 2.59.0-alpha.0
  • aws-cdk-lib: 2.59.0

概要

以前『Lambda (Go) + EventBridge (Cron) + Systems Manager Parameter Store + CDK』という記事を書いたが、この実装では Lambda が呼び出されるたびに AWS Systems Manager Parameter Store から値の呼び出しを行っていた。

もともと cron 実行的な Lambda であって大量に呼び出されるものではないので別にいいのだが、大量に呼び出される可能性がある Lambda の場合は取得した値をキャッシュしてやるほうがいい。

キャッシュするための機能として去年の秋に生まれたのが AWS Parameters and Secrets Lambda Extension である。
今回は前回実装したコードを書き換え、値をキャッシュするようにする。

ディレクトリ構成は前回と同じで完成品はここ

Lambda のソースコード

app/main.go

前回からの変更点をハイライトしている。

 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
package main

import (
	"database/sql"
	"encoding/json"
	"github.com/aws/aws-lambda-go/lambda"
	_ "github.com/go-sql-driver/mysql"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"time"

	"gitlab.com/k1350/sololog_gql/sololog_back/app/repository/jwk"
)

const (
	dsnParameterName = "/sololog/DSN"
)

type Parameter struct {
	ARN              string
	DataType         string
	LastModifiedDate string
	Name             string
	Selector         *string
	SourceResult     *string
	Type             string
	Value            string
	Version          int
}

type ParameterResponse struct {
	Parameter      Parameter
	ResultMetaData map[string]interface{}
}

func fetchDSN() (string, error) {
	token := os.Getenv("AWS_SESSION_TOKEN")
	req, err := http.NewRequest(
		http.MethodGet,
		"http://localhost:2773/systemsmanager/parameters/get?name="+url.QueryEscape(dsnParameterName)+"&withDecryption=true",
		nil,
	)
	if err != nil {
		return "", err
	}
	req.Header.Set("X-Aws-Parameters-Secrets-Token", token)

	client := new(http.Client)
	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}
	var result ParameterResponse
	err = json.Unmarshal(body, &result)
	if err != nil {
		return "", err
	}
	return result.Parameter.Value, nil
}

func rotateKey() error {
	dsn, err := fetchDSN()
	if err != nil {
		return err
	}

	db, err := sql.Open("mysql", dsn)
	if err != nil {
		return err
	}
	defer db.Close()
	if err = db.Ping(); err != nil {
		return err
	}

	jwkr := jwk.NewJWKRepository(db)

	now := time.Now().UTC()
	// 新規鍵作成
	err = jwkr.Create(now)
	if err != nil {
		return err
	}

	// 古い鍵削除
	return jwkr.DeleteOldKey(now)
}

func main() {
	lambda.Start(rotateKey)
}

パラメータストアに予め “/sololog/DSN” という名前で MySQL への接続情報を保存していることを前提としている。

AWS Parameters and Secrets Lambda Extension を使用すると、localhost の 2773 ポート(ポート番号は変更できる)にアクセスすることによってパラメータを取得できるようになる。
AWS_SESSION_TOKEN という環境変数は勝手にセットされているので、GET リクエスト時に X-Aws-Parameters-Secrets-Token というヘッダに設定する必要がある。

詰まった点としては、最初このページを見て

To retrieve a secret, for secretId, use the ARN or name of the secret.
GET: /secretsmanager/get?secretId=secretId

と書いてあったので secretId にパラメータ名を入れたら取得できなかった。

こっちのページには

For example, in Python, your GET URL might look something like the following example.
parameter_url = ('http://localhost:' + port + '/systemsmanager/parameters/get/?name=' + ssm_parameter_path)

と書いてあり、これに従って URL を変更したら取得できた。

CDK

cdk/lib/app-stack.ts

前回からの変更点をハイライトしている。

 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
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import { GoFunction } from "@aws-cdk/aws-lambda-go-alpha";
import { Rule, Schedule } from "aws-cdk-lib/aws-events";
import { LambdaFunction } from "aws-cdk-lib/aws-events-targets";
import { PolicyStatement } from "aws-cdk-lib/aws-iam";
import { LayerVersion } from "aws-cdk-lib/aws-lambda";

const AWS_PARAMETERS_AND_SECRETS_LAMBDA_EXTENSION_LAYER =
  "arn:aws:lambda:ap-northeast-1:133490724326:layer:AWS-Parameters-and-Secrets-Lambda-Extension:2";
const DSN = "/sololog/DSN";

export class AppStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const lambdaPolicyStatement = new PolicyStatement({
      sid: "GetParameterStore",
      actions: ["ssm:GetParameter"],
      resources: [`arn:aws:ssm:${this.region}:${this.account}:parameter${DSN}`],
    });

    const lambda = new GoFunction(this, "handler", {
      entry: "../app",
      layers: [
        LayerVersion.fromLayerVersionArn(
          this,
          `AWSParametersAndSecretsLambdaExtensionLayer`,
          AWS_PARAMETERS_AND_SECRETS_LAMBDA_EXTENSION_LAYER
        ),
      ],
    });
    lambda.addToRolePolicy(lambdaPolicyStatement);

    new Rule(this, "SolologLambdaScheduleRule", {
      schedule: Schedule.cron({ minute: "0", hour: "16", day: "1" }),
      targets: [new LambdaFunction(lambda, { retryAttempts: 3 })],
    });
  }
}

AWS Parameters and Secrets Lambda Extension は Layer として追加することになるが、ARN はこのページに書いてある。

また Lambda からパラメータストアを読み取る必要があるので読み取り権限は付与する必要がある。
前回使っていた ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMReadOnlyAccess") では駄目そうだったので、PolicyStatement を作成した。

また前回からの改善点として、GoFunction で Lambda を作成してから必要な Policy だけ追加するようにしている。

参考

以上