SREã®è åã§ãã
ã«ã³ã ã®ãµã¼ãã¹ã¯Webãµã¼ãã¹ã»ãããå¦çãªã©ãå«ãã¦åºæ¬çã«ã¯ECSä¸ã§åããã¦ããã®ã§ãããç°¡åãªãããå¦çã¯Lambda+EventBridge Schedulerã®çµã¿åããã§åãããã¨ãããã¾ãã
Lambdaã¯ECSã«æ¯ã¹ã¦Dockerã¤ã¡ã¼ã¸ã®ãã«ããECRã®æºåãä¸è¦ã§ä½æã®æéãå°ãªãã®ã§ãããterraformã§ãããã¤ã¾ã§å«ãã¦ç®¡çãããã¨ããã¨å°ãåé¡ãããã¾ããã
terraformã§ã®Lambdaã®ãããã¤ã®åé¡ç¹
ä¾ãã°ä»¥ä¸ã®ãããªæ§æã®Node.jsã®Lambdaããããã¤ããå ´å
/
âââ lambda.tf
âââ lambda
âââ app.js
âââ package-lock.json
âââ package.json
// app.js const util = require("util"); const gis = util.promisify(require("g-i-s")); exports.handler = async (event) => { const rs = await gis("nyan"); console.log(JSON.stringify(rs, null, 2)); };
null_resourceï¼ã¾ãã¯terraform-dataï¼ã¨archive_fileã使ã£ã¦ãterraformã§Lambdaã®ä½æã¨ãããã¤ãè¡ãã¾ãã
resource "null_resource" "npm_install" { triggers = { package_json = filebase64sha256("lambda/package.json") package_lock_json = filebase64sha256("lambda/package-lock.json") } provisioner "local-exec" { working_dir = "lambda" command = "npm install" } } data "archive_file" "nyan" { type = "zip" output_path = "app.zip" source_dir = "lambda" depends_on = [null_resource.npm_install] } resource "aws_lambda_function" "nyan" { function_name = "nyan" runtime = "nodejs20.x" role = "..." handler = "app.handler" filename = data.archive_file.nyan.output_path source_code_hash = data.archive_file.nyan.output_base64sha256 }
ããããã®æ¹æ³ã ã¨
- archive_fileããã¼ã¿ã½ã¼ã¹ã§ãããããterraformãå®è¡ãããã³ã«zipãã¡ã¤ã«ã使ããã *1
- ç¹ã«CIãAtlantis*2ã§terraformãå®è¡ããå ´åãæå³ããªãã¿ã¤ãã³ã°ã§Lambdaã®æ´æ°ãå®è¡ããã
- npm installãpip installãªã©zipãã¡ã¤ã«ä½æåã®å¦çã®å®ç¾©ãè¤éã«ãªã
ã¨ããåé¡ãããã¾ãã
terraform-provider-lambdazip
ããã§ããããã®åé¡ã解決ãterraformã ãã§Lambdaã®ç®¡çãè¡ããããã«ãããããterraformãããã¤ãã¼ãèªä½ãã¾ããã
- ãã¼ã¿ã½ã¼ã¹ã§ã¯ãªããªã½ã¼ã¹ãªã®ã§triggersã®å¤æ´ããªããã°zipãã¡ã¤ã«ä½æå¦çãèµ°ããªã
- before_createã§zipãã¡ã¤ã«ä½æåã®å¦çãæå®ã§ãã
lambdazipãããã¤ãã¼ã使ã£ã¦å ã»ã©ã®lambda.tfãæ¸ãç´ãã¨æ¬¡ã®ããã«ãªãã¾ãã
data "lambdazip_files_sha256" "triggers" { files = [ "lambda/app.js", "lambda/package.json", "lambda/package-lock.json", ] } resource "lambdazip_file" "nyan" { base_dir = "lambda" sources = ["**"] output = "lambda.zip" before_create = "npm i" triggers = data.lambdazip_files_sha256.triggers.map } resource "aws_lambda_function" "nyan" { function_name = "nyan" runtime = "nodejs20.x" role = "..." handler = "app.handler" filename = lambdazip_file.nyan.output source_code_hash = lambdazip_file.nyan.base64sha256 }
Go Lambda
社å ã®Lambdaã«ã¯PythonãJavaScriptã使ããããã¨ãããã¾ãããç§ãLambdaã使ããå ´åã¯æ £ãã¦ããGoã§å®è£ ãããã¨ãå¤ãã§ãã
- npmãpipãªã©ã©ã¤ãã©ãªã®å梱ã«ã¤ãã¦èããå¿ è¦ããªã
- æå ã®ç°å¢ã§ãCI/Atlantisç°å¢ã§ããããã¤ç¨ã®ãã¤ããªã®ã¯ãã¹ã³ã³ãã¤ã«ãã§ãã
- Go 1.21以éã§åç¾å¯è½ãªãã«ããã§ããããã«ãªã£ãã®ã§ã½ã¼ã¹ã³ã¼ãã®å¤æ´ã ããããªã¬ã«ãããã¤ã§ãã
- å ±æã©ã¤ãã©ãªã¸ã®ä¾åãé¿ãããã
ãªã©ãGo Lambdaã®è¯ãç¹ã§ãã
terraformã§ã®å®ç¾©ã¯
/
âââ lambda.tf
âââ lambda
âââ main.go
âââ go.mod
âââ go.sum
data "lambdazip_files_sha256" "triggers" { files = ["lambda/*.go", "lambda/go.mod", "lambda/go.sum"] } resource "lambdazip_file" "app" { base_dir = "lambda" sources = ["bootstrap"] output = "lambda.zip" before_create = "GOOS=linux GOARCH=amd64 go build -o bootstrap main.go" triggers = data.lambdazip_files_sha256.triggers.map } resource "aws_lambda_function" "app" { filename = lambdazip_file.app.output function_name = "my_func" role = aws_iam_role.lambda_app_role.arn handler = "my-handler" source_code_hash = lambdazip_file.app.base64sha256 runtime = "provided.al2023" }
ã®ããã«ãªãã¾ãã
以ä¸ãæ¥åã§ä½¿ç¨ãã¦ããGo Lambdaã®ä¸ä¾ã§ãã
ä¾: ãªã¶ã¼ããã¤ã³ã¹ã¿ã³ã¹ã®æéã®ã¡ããªã¯ã¹å
ã¤ã³ãã©ã³ã¹ã忏ããAWS RDSãOpenSearchã®ãªã¶ã¼ããã¤ã³ã¹ã¿ã³ã¹ãå©ç¨ãã¦ããã®ã§ãããAWS Cost Explorerãæä¾ãã¦ããæéåãã¢ã©ã¼ãã¯Eã¡ã¼ã«ã¸ã®éç¥ã®ã¿ã§ãã¾ã7æ¥åã»30æ¥åã»60æ¥åã¨æ±ºããããã¿ã¤ãã³ã°ã«ããéç¥ãéããã¨ãã§ãã¾ããã
ã«ã³ã ã®ã¤ã³ãã©ã®ã¢ã©ã¼ãã¯ã»ã¨ãã©ãDatadogã§ç®¡çããã¦ãããªã¶ã¼ããã¤ã³ã¹ã¿ã³ã¹ã®æéåãã¢ã©ã¼ãããªãã¹ãDatadogã«éç´ããããã¾ãéç¥ã®ã¿ã¤ãã³ã°ä»¥å¤ã«ãè¤æ°ã®ã¢ã«ã¦ã³ãã®ãªã¶ã¼ããã¤ã³ã¹ã¿ã³ã¹ã®æéãã©ã®ç¨åº¦è¿«ã£ã¦ããã®ãç°¡åã«ææ¡ããããã¨ãã£ãã¢ããã¼ã·ã§ã³ãããGo Lambdaã使ã£ã¦ãªã¶ã¼ããã¤ã³ã¹ã¿ã³ã¹ã®æéãDatadogã®ã¡ããªã¯ã¹ã«ãã¦ã¿ã¾ããã
main.go
Goã®å®è£ ã¯GetReservationUtilization APIãå¼ã³åºãã¦ãDatadogã«ã¡ããªã¯ã¹ãéãã ãã®åç´ãªãã®ã§ãã AWS Organizationsã®è¦ªã¢ã«ã¦ã³ãã§GetReservationUtilizationãå¼ã³åºãã¨ãåã¢ã«ã¦ã³ãã®RIã®æ å ±ãåå¾ãããã¨ãã§ãã¾ãã
package main import ( "context" "fmt" "log" "os" "time" "github.com/DataDog/datadog-api-client-go/v2/api/datadog" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/costexplorer" "github.com/aws/aws-sdk-go-v2/service/costexplorer/types" "github.com/aws/aws-sdk-go-v2/service/secretsmanager" ) var ( TARGET_SERVICES = []string{ "Amazon Relational Database Service", "Amazon OpenSearch Service", } DD_API_KEY_FROM = os.Getenv("DD_API_KEY_FROM") DD_APP_KEY_FROM = os.Getenv("DD_APP_KEY_FROM") ) const ( METRIC_NAME = "costexplor.reservation.days_to_expiry" ) func main() { lambda.Start(HandleRequest) } func HandleRequest(ctx context.Context, event any) error { now := time.Now() output, err := getReservationUtilization(ctx, now) if err != nil { return fmt.Errorf("failed to getReservationUtilization: %w", err) } if len(output.UtilizationsByTime) == 0 { log.Println("No data") return nil } utilizations := output.UtilizationsByTime[0] for _, g := range utilizations.Groups { endDateTime, err := time.Parse("2006-01-02T15:04:05.000Z", g.Attributes["endDateTime"]) if err != nil { return fmt.Errorf("failed to parse endDateTime: %w", err) } daysToExpiry := endDateTime.Sub(now).Hours() / 24 if daysToExpiry < -10 { // 10æ¥ä»¥ä¸çµã£ãéå»ã®Reservationã¯ç¡è¦ continue } tags := []string{ "account_name:" + g.Attributes["accountName"], "service:" + g.Attributes["service"], "lease_id:" + g.Attributes["leaseId"], } submitMetrics(ctx, now.Unix(), daysToExpiry, tags) } return nil } func getReservationUtilization(ctx context.Context, now time.Time) (*costexplorer.GetReservationUtilizationOutput, error) { cfg, err := config.LoadDefaultConfig(ctx) if err != nil { return nil, err } client := costexplorer.NewFromConfig(cfg) input := &costexplorer.GetReservationUtilizationInput{ TimePeriod: &types.DateInterval{ Start: aws.String(now.AddDate(0, 0, -90).Format("2006-01-02")), End: aws.String(now.Format("2006-01-02")), }, Filter: &types.Expression{ Dimensions: &types.DimensionValues{ Key: "SERVICE", Values: TARGET_SERVICES, }, }, GroupBy: []types.GroupDefinition{ { Type: "DIMENSION", Key: aws.String("SUBSCRIPTION_ID"), }, }, } return client.GetReservationUtilization(ctx, input) } func submitMetrics(ctx context.Context, ts int64, daysToExpiry float64, tags []string) error { ddApiKey, err := getSecretValue(ctx, DD_API_KEY_FROM) if err != nil { return err } ddAppKey, err := getSecretValue(ctx, DD_APP_KEY_FROM) if err != nil { return err } body := datadogV2.MetricPayload{ Series: []datadogV2.MetricSeries{ { Metric: METRIC_NAME, Type: datadogV2.METRICINTAKETYPE_GAUGE.Ptr(), Unit: datadog.PtrString("day"), Points: []datadogV2.MetricPoint{ { Timestamp: datadog.PtrInt64(ts), Value: datadog.PtrFloat64(daysToExpiry), }, }, Tags: tags, }, }, } configuration := datadog.NewConfiguration() apiClient := datadog.NewAPIClient(configuration) api := datadogV2.NewMetricsApi(apiClient) ctx = context.WithValue(ctx, datadog.ContextAPIKeys, map[string]datadog.APIKey{ "apiKeyAuth": {Key: ddApiKey}, "appKeyAuth": {Key: ddAppKey}, }) _, _, err = api.SubmitMetrics(ctx, body, *datadogV2.NewSubmitMetricsOptionalParameters()) if err != nil { return fmt.Errorf("Error when calling `MetricsApi.SubmitMetrics`: %w\n", err) } log.Printf("Put metric value=%.2f tags=%v ", daysToExpiry, tags) return nil } func getSecretValue(ctx context.Context, secretId string) (string, error) { cfg, err := config.LoadDefaultConfig(ctx) if err != nil { return "", err } input := &secretsmanager.GetSecretValueInput{ SecretId: aws.String(secretId), } client := secretsmanager.NewFromConfig(cfg) output, err := client.GetSecretValue(ctx, input) if err != nil { return "", err } return aws.ToString(output.SecretString), nil }
tfãã¡ã¤ã«
åè¿°ã®éãterraformã§Lambdaãå®ç¾©ããEventBridge Schedulerã§ä¸æéãã¨ã«ã¡ããªã¯ã¹ãéä¿¡ãã¾ãã
data "lambdazip_files_sha256" "dd_ce_reservation_days_to_expiry" { files = [ "./lambda/dd-ce-reservation-days-to-expiry/main.go", "./lambda/dd-ce-reservation-days-to-expiry/go.mod", "./lambda/dd-ce-reservation-days-to-expiry/go.sum", ] } resource "lambdazip_file" "dd_ce_reservation_days_to_expiry" { base_dir = "./lambda/dd-ce-reservation-days-to-expiry" sources = ["bootstrap"] output = "lambda.zip" before_create = "GOOS=linux GOARCH=amd64 go build -o bootstrap main.go" triggers = data.lambdazip_files_sha256.dd_ce_reservation_days_to_expiry.map } resource "aws_lambda_function" "dd_ce_reservation_days_to_expiry" { function_name = "dd-ce-reservation-days-to-expiry" runtime = "provided.al2023" role = aws_iam_role.lambda_dd_ce_reservation_days_to_expiry.arn handler = "bootstrap" filename = lambdazip_file.dd_ce_reservation_days_to_expiry.output source_code_hash = lambdazip_file.dd_ce_reservation_days_to_expiry.base64sha256 timeout = 300 environment { variables = { DD_API_KEY_FROM = aws_secretsmanager_secret.datadog_DD_API_KEY.name DD_APP_KEY_FROM = aws_secretsmanager_secret.datadog_DD_APP_KEY.name } } depends_on = [ aws_cloudwatch_log_group.lambda_dd_ce_reservation_days_to_expiry, ] } # (ç¥) resource "aws_scheduler_schedule" "dd_ce_reservation_days_to_expiry" { name = "dd-ce-reservation-days-to-expiry" schedule_expression = "rate(1 hour)" schedule_expression_timezone = "Asia/Tokyo" state = "ENABLED" flexible_time_window { mode = "OFF" } target { arn = aws_lambda_function.dd_ce_reservation_days_to_expiry.arn role_arn = aws_iam_role.dd_ce_reservation_days_to_expiry_schedule.arn } }
表示ä¾
Datadogã§ã¡ããªã¯ã¹ã表示ããã¨ãã©ã®ã¢ã«ã¦ã³ãã®ã©ã®RIãã©ã®ç¨åº¦æ®ã£ã¦ããã®ããä¸ç®ã§ãããã¾ãã

ã¾ã¨ã
terraformã§Go Lambdaããããã¤ã§ããã¨ãã¡ãã£ã¨ããå¦çããããåããã®ãã¨ã¦ã楽ã«ãªããã¤ã³ãã©ç°å¢ã®æ¹åãé²ã¿ã¾ãã ããã«Atlantisã¨ã®çµã¿åããã§ãPRä¸ã§Lambdaã®ãããã¤ãå¯è½ã«ãªããéçºä½é¨ãé常ã«è¯ãã§ãã
ä»å¾ãå¼ãç¶ãç°å¢ã®æ¹åã«åãã¦ããããã¨ããã§ãã
*1:ãªã½ã¼ã¹ã®archive_fileãããã®ã§ããdeprecatedã§ã
*2:RPä¸ã§terraformãå®è¡ã§ãããã¼ã«ã§ã https://www.runatlantis.io/