👻

AWS CDKを使ってAWS Lambdaへ配備したGo言語のプログラムが書かれたContainerの監視をNewRelicで行う

2022/02/23に公開

AWS CDK(以下 CDK と呼ぶ) を使って Container Image 形式(以下 Container 形式と呼ぶ)で配布した Go 言語で書かれている AWS Lambda(以下 Lambda と呼ぶ) の監視を New Relic で行うには、Monitoring AWS Lambda with Serverless monitoring に書いてあることを上から順番に読んで進めるとよい。ただ New Relic の文は汎用的に書かれているためか、試行錯誤や判断を要する部分が残っている。それらについて書く。

New Relic で行いたい以下の 2 つの機能は、セットアップの部分は重複しているものの別々に動作するため、それぞれのトピックに分けてとりあげる。

  • Lambda の処理を計測(処理回数、処理にかかった時間、並行処理数、エラー数など)し集計する
  • Lambda のログを集計する

Lambda の処理を計測(処理回数、処理にかかった時間、並行処理数、エラー数など)し集計する

Lambda のランタイムには go1.x ではなく provided.al2 を使う

Recommended AWS Lambda language runtimes

New Relic では AWS が Go 言語のために提供している go1.x のランタイムを使わず、AWS が汎用的な利用を想定して提供している providedprovided.al2 の利用をおすすめしている。理由は

We recommend building self-hosting Go lambda functions, because the latest AWS Lambda APIs are not supported in the AWS go1.x runtime.

と、go1.x ランタイムが最新の AWS Lambda API に対応していないためだ。

provided は無印の Amazon Linux、provided.al2 は Amazon Linux2 をベースの OS としたランタイムである。古い方の OS をあえて利用する理由がなかったので provided.al2 を採用した。

AWS アカウントと New Relic アカウントのリンクには newrelic-lambda コマンドを使う

Step 1: Link your AWS and New Relic accounts

AWS のリソース管理は、できるだけ少ないツールで完結させて、セットアップやアップグレードの見通しを良くしておきたい。今回だと既に CDK を利用したリソース管理を行っているので、できればツールを足さずにすませたい。そういう思いをもちながらも、 以下のような理由から AWS アカウントと New Relic アカウントのリンクには newrelic-lambda integrations install コマンドを使うことに決めた。

New Relic 自身がおすすめしていること。さらに AWS アカウント毎に一度実行するだけでよく、AWS のリソースに変更を加えたときに都度実行する必要がないこと。さらに CLI で作られたリソースは Cloud Formation のスタックで管理され、その名前は NewRelicLogIngestionNewRelicLicenseKeySecret など NewRelic から始まり、自身が作成したものと見分けが容易で、面倒な状態を引き起す心配が少なかったためだ。

AWS のプロファイルが default でなく、指定したものを使ってコマンドを実行する場合は、AWS regions and profilesにあるとおり、環境変数に設定し以下のように実行するとよい。

AWS_PROFILE=YOUR_AWS_PROFILE newrelic-lambda integrations install --nr-account-id YOUR_NR_ACCOUNT_ID --nr-api-key YOUR_NEW_RELIC_USER_KEY

New Relic によって提供されている Lambda Layer 相当の機能を Container Image を利用した Lambda へ適用する

Step 4: Instrument your own Lambda functions

New Relic が Lambda を計測する機能は Lambda Layer を用いて実現されている。Lambda Layer は

You can use layers only with Lambda functions deployed as a .zip file archive.

と書かれているように、Zip で配布している Lambda でしか使えない。私が利用している Lambda は Container 形式での配布なため Lambda Layer を使えない。newrelic-lambda CLI quickstart に書かれている nwerelic-lambda も Lambda Layer を前提としているので利用できない。

AWS 公式ブログの Working with Lambda layers and extensions in container images
には Container 形式での配布でも Lambda Layer の拡張を利用する手段が複数掲載されている。展開の方法はそれぞれ異なるが、どの方法でも Container Image の /opt に Lambda Layer の内容を展開している。

New Relic の Lambda Layer はどのように作られているか。Layer customization
にリンクの記載がある通り、GitHub のnewrelic/newrelic-lambda-layers のコードから作られている。README には extension/publish-layer.sh を実行すると公開されると書かれているので、そのファイルをみると EXTENSION_DIST_URL_X86_64=https://github.com/newrelic/newrelic-lambda-extension/releases/download/v2.1.0/newrelic-lambda-extension.x86_64.zip と書かれており /opt へ展開する元となる zip は newrelic/newrelic-lambda-extension から取得されているとわかる。これを私たちの Container Image の /opt へ展開すればよい。

Lambda へ展開する、 Go 言語に対応した Container Image の Dockerfile は

# You can find the latest container from https://gallery.ecr.aws/lambda/provided
ARG CONTAINER_NAME=public.ecr.aws/lambda/provided:al2.2022.02.01.09-x86_64

FROM ${CONTAINER_NAME} as newrelic-lambda-layer
# You can find the latest extention from https://github.com/newrelic/newrelic-lambda-layers/releases?q=%22New+Relic+Lambda+Extension%22&expanded=true
ENV EXTENTION_VERSION=v2.1.0
RUN mkdir -p /opt
RUN yum install -y unzip
RUN curl -OL https://github.com/newrelic/newrelic-lambda-extension/releases/download/${EXTENTION_VERSION}/newrelic-lambda-extension.x86_64.zip
RUN unzip newrelic-lambda-extension.x86_64.zip -d /opt

# Build go code
FROM ${CONTAINER_NAME} as build
# Install compiler
RUN yum install -y golang
RUN go env -w GOPROXY=direct
# Cache dependencies
ADD go.mod go.sum ./
RUN go mod download
# Build
ADD . .
RUN go build -o /main

# Copy artifacts to make a clean container image
FROM ${CONTAINER_NAME}
COPY --from=newrelic-lambda-layer /opt /opt
COPY --from=build /main /var/runtime/bootstrap

# CMD arguments is not be used on Go lambda.
# Though, provided:al2 image's entrypoint requires an argument as a handler name.
# So, we just pass empty string to work propely.
CMD [""]

となる。newrelic-lambda-layer というビルドステージで zip を解凍して /opt へ置き、最後のビルドステージで COPY --from=newrelic-lambda-layer /opt /opt という形式で newrelic-lambda-layer/opt をコピーするコードを追加している部分に注目してほしい。

newrelic-lambda コマンドによって作られた SecretManager を cdk から利用する

Continuous deployment

Lambda の情報を New Relic に送る際には New Relic のアカウント ID とライセンスキーが必要になる。newrelic-lambda integrations install を使ったセットアップをした場合、アカウント ID とライセンスキーは AWS Secrets Manager に NEW_RELIC_LICENSE_KEY という名前でリソースが作られ、その中に保持されている。

このリソースに保持されている情報を Lambda から利用するために、cdk に以下の記述を追加した。
cdk でのリソースの取得と、Lambda がそのリソースの値を読み取りるための権限の付与である。

// newrelic-lambda が AWS 上に作成した SecretManager リソースを取得する
const newrelicLicenseKeySecretManager = Secret.fromSecretNameV2(
  this,
  // id は cdk を書くプログラマが任意につけてよいもの
  "newrelic-license-key-secret-manager",
  // newrelic-lambda にて生成された名前なので固定値
  "NEW_RELIC_LICENSE_KEY"
);

// Conainer Image を使った Lambda リソースを作成する
const dockerImageFunction = new DockerImageFunction(...);

// SecretManager の値を読み取り可能とする
newrelicLicenseKeySecretManager.grantRead(dockerImageFunction);

Go 言語で NewRelic を利用する

最も網羅的に記述されていたのは Legacy manual instrumentation for Lambda monitoringの Go の項だった。

Install the agent: go get -u github.com/newrelic/go-agent/v3/newrelic.
Install the nrlambda integration go get -u github.com/newrelic/go-agent/v3/integrations/nrlambda.
In your Lambda code, import our components, create an application, and update how you start your Lambda. See our instrumentation examples:

にある通り、コマンドで

go get -u github.com/newrelic/go-agent/v3/newrelic
go get -u github.com/newrelic/go-agent/v3/integrations/nrlambda

して、ライブラリを取得してから main 関数を

func main() {
	app, err := newrelic.NewApplication(nrlambda.ConfigOption())
	if nil != err {
		fmt.Println("error creating app (invalid config):", err)
	}
	# lambda.Start(handler) から変更する
	nrlambda.Start(handler, app)
}

と書くと最小限(かつ十分)な設定の準備ができあがる。

The following environment variables are not required for Lambda monitoring to function but they are required if you want your Lambda functions to be included in distributed traces. To enable distributed tracing, set these environment variables in the AWS console:

NEW_RELIC_ACCOUNT_ID. Your account ID.
NEW_RELIC_TRUSTED_ACCOUNT_KEY. This is also your account ID. If your account is a child account, this is the account ID for the root/parent account.

にある環境変数 2 つは CDK で以下のように渡せる。newrelicAccountIdnewrelicTrustedAccountKey はコードの中に直接書いてもよいが、私は cdk コマンドを実行するときに cdk deploy --context newrelic-account-id=aaaa --context newrelic-trusted-account-key=bbbb という形で渡し、プログラムの中で tryGetContext を使って取得した。

const newrelicAccountId = this.node.tryGetContext("newrelic-account-id") || "";
const newrelicTrustedAccountKey =
  this.node.tryGetContext("newrelic-trusted-account-key") || "";

const dockerImageFunction = new DockerImageFunction(
  this,
  "docker-image-function",
  {
    // ...
    environment: {
      NEW_RELIC_ACCOUNT_ID: newrelicAccountId,
      NEW_RELIC_TRUSTED_ACCOUNT_KEY: newrelicTrustedAccountKey,
    },
  }
);

Lambda のログを集計する

Step 2 (optional): Set up AWS CloudWatch log collection

if you used the newrelic-lambda CLI, that automatically installs an AWS CloudWatch logs collector by default.

と書いてあるので、ログ集計の処理は不要かと思ったが、実際には必要だった。"AWS CloudWatch logs collector" は自動的にインストールされるとは書いてあるし、実際にも newrelic-log-ingestion という Lambda 関数が作られていたが、設定は自分たちで行うということなのかもしれない。

まず newrelic-log-ingestion Lambda 関数の環境変数の LOGGING_ENABLEDtrue になっているか確認し、なっていなければ変える。記憶が定かではないが、手元で作ったときにはなっていなかったので変えたような気がする。各環境変数の設定値は Install and configure the Cloudwatch logs Lambda function に記載がある。

次に Create a Lambda trigger にある通り、nwrelic-log-ingestion Lambda 関数のトリガーへ、送りたい Lambda の CloudWatch ログを指定する。この文は AWS コンソールから操作した場合の方法が書かれているので、これを CDK で行う方法に読み替える。

// newrelic-lambda が AWS 上に作成した AWS Lambda リソースを取得する
// 関数の名前は `newrelic-log-ingestion` と決まっているので、arn はそこから作成可能
// ARN のフォーマットは https://docs.aws.amazon.com/quicksight/latest/APIReference/qs-arn-format.html を参照
const newrelicLogIngestionArn = `arn:aws:lambda:${this.region}:${this.account}:function:newrelic-log-ingestion`;
const newrelicLogIngestion = Function.fromFunctionArn(
  this,
  "newrelic-log-ingestion",
  newrelicLogIngestionArn
);

// Conainer Image を使った Lambda リソースを作成する
const dockerImageFunction = new DockerImageFunction(...);

// newrelic-log-ingestion Lambda 関数へ、dockerImageFunction のログを送る
// "docker-image-function-logfilter" という id は cdk を書くプログラマが任意につけてよいもの
dockerImageFunction.logGroup.addSubscriptionFilter("docker-image-function-logfilter", {
  destination: new LambdaDestination(newrelicLogIngestion),
  // もし送るログをフィルタリングしたい場合はここのパターンを変更する
  filterPattern: FilterPattern.allEvents(),
});

まとめ

AWS CDK を使って Container Image 形式で配布した Go 言語で書かれている AWS Lambda の監視を New Relic で行うときにつまづく点について書いた。

CDK は最終的に以下のような形になる。

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

    const newrelicAccountId =
      this.node.tryGetContext("newrelic-account-id") || "";
    const newrelicTrustedAccountKey =
      this.node.tryGetContext("newrelic-trusted-account-key") || "";

    // newrelic-lambda が AWS 上に作成した SecretManager リソースを取得する
    const newrelicLicenseKeySecretManager = Secret.fromSecretNameV2(
      this,
      // id は cdk を書くプログラマが任意につけてよいもの
      "newrelic-license-key-secret-manager",
      // newrelic-lambda にて生成された名前なので固定値
      "NEW_RELIC_LICENSE_KEY"
    );

    // newrelic-lambda が AWS 上に作成した AWS Lambda リソースを取得する
    // 関数の名前は `newrelic-log-ingestion` と決まっているので、arn はそこから作成可能
    // ARN のフォーマットは https://docs.aws.amazon.com/quicksight/latest/APIReference/qs-arn-format.html を参照
    const newrelicLogIngestionArn = `arn:aws:lambda:${this.region}:${this.account}:function:newrelic-log-ingestion`;
    const newrelicLogIngestion = Function.fromFunctionArn(
      this,
      "newrelic-log-ingestion",
      newrelicLogIngestionArn
    );

    // Conainer Image を使った Lambda リソースを作成する
    const dockerImageFunction = new DockerImageFunction(
      this,
      "docker-image-function",
      {
        // ...
        environment: {
          NEW_RELIC_ACCOUNT_ID: newrelicAccountId,
          NEW_RELIC_TRUSTED_ACCOUNT_KEY: newrelicTrustedAccountKey,
        },
      }
    );

    // SecretManager の値を読み取り可能とする
    newrelicLicenseKeySecretManager.grantRead(dockerImageFunction);

    // newrelic-log-ingestion Lambda 関数へ、dockerImageFunction のログを送る
    // "docker-image-function-logfilter" という id は cdk を書くプログラマが任意につけてよいもの
    dockerImageFunction.logGroup.addSubscriptionFilter(
      "docker-image-function-logfilter",
      {
        destination: new LambdaDestination(newrelicLogIngestion),
        // もし送るログをフィルタリングしたい場合はここのパターンを変更する
        filterPattern: FilterPattern.allEvents(),
      }
    );
  }
}
GitHubで編集を提案

Discussion