AWS SAMで定義されたLambda関数をNew Relicでモニタリングできるように設定してみました

2024.03.18

初めに

New Relicを利用したLambda関数のモニタリングする方法として標準のメトリクスを利用するのみではなくインテグレーションを利用することでより詳細な情報を取得することができます。

New Relicのインテグレーションの導入はいくつかあるようであればZipタイプの関数であればLambda Layerを直接Lambda側で指定する方法やnewrelic-lambdaコマンドを利用して導入する方法が手軽そうです。

自分の場合は普段SAMを利用していますのでこちらを利用する形でLambdaをモニタリングできるように設定してみます。

アカウントリンク

まずはNew Relic側でAWSアカウント(正確にはその中のLambda?)をリンクさせます。

こちらはアカウント全体の設定となるためSAMではなくコマンドを利用して個別に設定します。

ローカルからnewrelic-lambdaコマンドを実行しますがそのためにAPI Keyが必要なのでまずはこちらを発行します。

発行はNew Relicの画面左下の自身のユーザ名の部分のAPI Keyを開いて「Create a key」をクリックします。

API Keyにはいくつか種類がありますがUserで問題ないようです。

発行後はAPI Keyの一覧画面に戻りますので先ほど発行したキーの一番右の「...」部分を開いて「Copy Key」からAPI Keyを取得し控えておきます。

その後newrelic-lambdaを利用して初期設定を行います。

newrelic-lambdaをpipでインストールしnewrelic-lambda integrations installを実行します。

% pip install newrelic-lambda
...
% newrelic-lambda integrations install --nr-account-id {{your-new-relic-account-id}} --nr-api-key {{your-api-key}}
Validating New Relic credentials
Retrieving integration license key
Creating the AWS role for the New Relic AWS Lambda Integration
Waiting for stack creation to complete... ✔️ Done
✔️ Created role [NewRelicLambdaIntegrationRole_xxxxxxxx] in AWS account.
Linking New Relic account to AWS account
✔️ Cloud integrations account [New Relic AWS Integration - xxxxxxxx] was created in New Relic account [xxxxxxxx] with IAM role [arn:aws:iam::xxxxxxxx:role/NewRelicLambdaIntegrationRole_xxxxxxxx].
Enabling Lambda integration on the link between New Relic and AWS
✔️ Integration [id=xxxxxx, name=Lambda] has been enabled in Cloud integrations account [New Relic AWS Integration - xxxxxxxx] of New Relic account [xxxxxxxx].
Creating the managed secret for the New Relic License Key
Setting up NewRelicLicenseKeySecret stack in region: ap-northeast-1
Creating change set: NewRelicLicenseKeySecret-CREATE-xxxxxx
Waiting for change set creation to complete, this may take a minute... Waiting for change set to finish execution. This may take a minute... ✔️ Done
Creating newrelic-log-ingestion Lambda function in AWS account
Setting up CloudFormation Stack NewRelicLogIngestion in region: ap-northeast-1
Fetching new CloudFormation template url
Creating change set: NewRelicLogIngestion-CREATE-xxxxxx
Waiting for change set creation to complete, this may take a minute... Waiting for change set to finish execution. This may take a minute... ✔️ Done
✨ Install Complete ✨

完了すると3つのスタックが作成されます。ざっと見連携用?のLambda関数やIAMロール、New RelicのAPI Key含まれるシークレットが作成されるようです。

なお最初API Key発行の際にUserではなくINGEST - LICENSEで発行してしまったのですがこの場合はコマンド実行時にエラーとなりました。

% newrelic-lambda integrations install --nr-account-id xxxxx --nr-api-key xxxxx
Validating New Relic credentials
Retrieving integration license key
Usage: newrelic-lambda integrations install [OPTIONS]
Try 'newrelic-lambda integrations install --help' for help.

Error: Invalid value for New Relic Account ID: Could not retrieve license key from New Relic. Check that your New Relic Account ID is valid and try again.

設定が完了すると「Infrastructure > AWS」の部分で連携されていることが確認できます。

また「Serverless Functions」を見るとアカウント上のLambda関数の一覧の一部が確認できます。

SAM追加設定

ベーステンプレート

以前以下の記事で利用したテンプレートをします。
本体のHelloWorldFunctionとAuthorizer用のAuthFunctionがあるので両方に対して追加します。

テンプレートを抜粋します。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
  Function:
    Timeout: 60
    MemorySize: 128

Resources:
  Api:
    Type: AWS::Serverless::Api
    Properties:
      StageName: dev
      MethodSettings:
        - ResourcePath: /
          HttpMethod: GET
      Auth:
        DefaultAuthorizer: LambdaAuthorizer
        Authorizers:
          LambdaAuthorizer:
            FunctionArn: !GetAtt AuthFunction.Arn
            FunctionPayloadType: TOKEN
            Identity:
              Headers:
              - Authorization
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.9
      Architectures:
        - arm64
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get
            RestApiId: !Ref Api
  AuthFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: auth/
      Handler: app.lambda_handler
      Runtime: python3.9
      Architectures:
        - arm64

追加設定

New Relicに関する設定はGlobalsセクションに書いておくと全てのFunctionリソースに一括で適用されるので、テンプレート内の関数を複数監視する場合はこちらに記載してしまうのが良さそうです。

Pythonのバージョンや記載位置の変更等はありますがハイライト部分が今回の主な追加要素となります。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Parameters:
  # New RelicのアカウントID
  NewRelicAccountId:
    Type: String
  # 導入したいNew RelicのLambda LayerのARN
  NewRelicLayerArn:
    Type: String
  # 先のCFnでNEW_RELIC_LICENSE_KEYというシークレットが生成されているのでそのARN
  NewRelicSecretsArn:
    Type: String
Globals:
  Function:
    Timeout: 60
    MemorySize: 128
    Architectures:
      - arm64
    Runtime: python3.12
    Handler: newrelic_lambda_wrapper.handler
    Environment:
      Variables:
        NEW_RELIC_LAMBDA_HANDLER: app.lambda_handler
        NEW_RELIC_ACCOUNT_ID: !Ref NewRelicAccountId
        NEW_RELIC_EXTENSION_SEND_FUNCTION_LOGS: true
    Layers:
      - !Ref NewRelicLayerArn
    # これはできない
    # Policies:
    #   - Version: "2012-10-17"
    #     Statement:
    #       - Effect: Allow
    #         Action: secretsmanager:GetSecretValue
    #         Resource: !Ref NewRelicSecretsArn

Resources:
  Api:
    Type: AWS::Serverless::Api
    Properties:
      StageName: dev
      MethodSettings:
        - ResourcePath: /
          HttpMethod: GET
      Auth:
        DefaultAuthorizer: LambdaAuthorizer
        Authorizers:
          LambdaAuthorizer:
            FunctionArn: !GetAtt AuthFunction.Arn
            FunctionPayloadType: TOKEN
            Identity:
              Headers:
              - Authorization
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get
            RestApiId: !Ref Api
      Policies:
        - Version: "2012-10-17"
          Statement:
            - Effect: Allow
              Action: secretsmanager:GetSecretValue
              Resource: !Ref NewRelicSecretsArn
  AuthFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: auth/
      Policies:
        - Version: "2012-10-17"
          Statement:
            - Effect: Allow
              Action: secretsmanager:GetSecretValue
              Resource: !Ref NewRelicSecretsArn

上記のテンプレートができれば通常通りのデプロイでOKです。

なおPoliciesもGlobalsセクションに指定できれば新しい関数追加時に開発者が意識せずとも自動的にに必要な権限やレイヤーが追加されて良さそうでしたが、残念ながらSAMの仕様として権限の最小化を優先し意図しない権限が当てられないようにGlobalsセクションにPoliciesは指定できないように制限されていました。

Lambda LayerによるNew Relicの連携に利用するAPI Keyは特に指定がなければNEW_RELIC_LICENSE_KEYという名称のシークレットを自動で読み込みこみますが、このシークレットは先にnewrelic-lambdaを実行した時に合わせて作成されているので読み込み権限だけ与えればOKです。

余談ですがうまくログが飛ばずデバッグしていた時にCloudTrailを眺めていたところParameter Store->Secrets Managerの順に取得しようとする処理が走っていたので、ドキュメント上はSecrets Managerを利用するような記載がありますが適切に設置すればそちらでも良さそうです。

    "userAgent": "aws-sdk-go/1.44.288 (go1.20.14; linux; arm64)",
    "errorCode": "AccessDenied",
    "errorMessage": "User: arn:aws:sts::xxxx:assumed-role/sam-app-lambda-auth-AuthFunctionRole-xxxx/sam-app-lambda-auth-AuthFunction-xxxxx is not authorized to perform: ssm:GetParameter on resource: arn:aws:ssm:ap-northeast-1:xxxxx:parameter/NEW_RELIC_LICENSE_KEY because no identity-based policy allows the ssm:GetParameter action",

少し話が飛びましたが実際にデプロイされたLambda関数を確認すると指定したレイヤーが追加されています。

またnewrelic-lambda functions listコマンドを実行することでもアカウント内のLambda関数でインテグレーションの適用状態を確認できます。

% newrelic-lambda functions list
Function Name                                                    Runtime     Installed
---------------------------------------------------------------  ----------  -----------
dmarc-rua-report-searcher-RuaConverterFunction-xxxxxxxxxxxx      python3.12  No
sam-app-lambda-auth-AuthFunction-xxxxxxxxxxxx                    python3.12  Yes
...
sam-app-lambda-auth-HelloWorldFunction-xxxxxxxxxxxx              python3.12  Yes
...
newrelic-log-ingestion-xxxxxxxxxxxx                              python3.11  No

実行

curlを利用してAPI Gateway経由で何度かLambda関数を実行してみます。

% curl https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello -H "Authorization: Succes"
{"Message":"User is not authorized to access this resource with an explicit deny"}%                                                    
% curl https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello -H "Authorization: Success"
{"message": "hello world"}%

MetricsやDistributed Tracingを見てみると情報が取れていることを確認できます。
(本当はもう少し中身があればDistributed Tracingをもう少し詳しく見ても良かったのですが実行したLambda関数内容があまりにも無さすぎてみても...という感じでした)

ログについては時間を置いても来ない...と思って別途print文を埋め込んで再度実行したところ出力が確認できました。
どうやらLambdaがシステム的に出力するようなSTART RequestId:xxxxxのような部分のログは出力はとらないようでアプリ側で出力しているログのみを取るようです。

今回はLambda関数の環境変数にNEW_RELIC_EXTENSION_SEND_FUNCTION_LOGS=tureを設定することによりCloudWatch Logsを利用せずLambda上のNew Relicのインテグレーション側で収集された情報を送信するような仕組みを利用しましたが、その仕組みの関係上アプリレイヤーではないシステムログ的な部分は取れないのかもしれません(別途CloudWatch Logsのデータを読む形にすることで対応可能?)。

https://docs.newrelic.com/jp/docs/serverless-function-monitoring/aws-lambda-monitoring/enable-lambda-monitoring/instrument-example/
例題をテストしていると、テレメトリーがすぐに送信されないことがあることに気づくかもしれません。AWS Lambdaのライフサイクルでは、エージェントとLambda Extensionの実行に一定の制約があります。さらに、貴重なプラットフォームの遠隔測定は、呼び出しが完了した後にのみ利用できます。New Relic Extension は、一定期間テレメトリをバッファリングし、次の呼び出し時(またはシャットダウン時)に一括して New Relic に配信することで、全体のパフォーマンスとタイムリーなテレメトリ配信の必要性とのバランスを取っています。

また仕組み的に即時性なくは内容で反映まで多少ラグがあるようです。

ログがNew Relicに到達した後にCloudWatch Logs側を見ていると実行から6分ほど時間を置いた後にログを送信しているような処理が走ってるのでこのタイミングかもしれません。

終わりに

AWS SAMで構築されたLambda関数に良い感じにNew Relicによるモニタリングを導入してみました。

SAMで構築されている環境の場合はGlobalsセクションでほとんどの設定をまとめて定義することができるため、うまく活用すれば将来的なバージョンの更新(ランタイム・レイヤーいずれも)があった場合も特定の関数のみ更新漏れといったことも発生しづらいです。 (少し回り道をすればできそうな気もしますが)良くも悪くもセキュリティ確保のためにSAMの仕組み上ポリシーの一括追加ができず完全に一括で管理できないのがやや痛いところではあります。

別のサードパーティ製の関数が含まれており一部関数を例外としたい場合はIgnoreGLobalsポリシーを利用することで適用除外も可能ですので選択肢としてみてください。

補足: もう少し意味のある分散トレーシングを見る

今回実行された関数は単純に値比較出力のみで分散トレーシング(Distributed Trasing)はほとんど意味のないようなものとなってしまったのであまり詳細に書いていませんが、外部呼び出しのあるような関数を見るとこのような感じになります(私が個人的に情報収集用にデイリーで流しているLambda関数です)。

呼び出しが外部サービスのためシンプルなつながりではありますが外部サービスの呼び出しにかかっている時間周りが別途ログ埋め込みなしで取れるので良さそうです。
(自前の関数で呼び出しあっていればもっと色々見れそうではありますが)