New RelicによるAWSの監視をTerraformで自動化する(第1回: メトリクスのモニタリング)

# はじめに
AWSの監視は、Amazon CloudWatchシリーズで機能が一通り揃っているのでそちらを使うのがセオリーではあるものの、New RelicやDatadogといったオブザーバビリティによる監視に特化したSaaSを使うことも多くなってきている(AWSの標準機能でもオブザーバビリティに対応しているものはある)。

本記事では、New Relicの監視をTerraformを使ってAWSと一貫して構築を自動化する方法を構築している。

それぞれのリソース等のより詳細な情報は、以下の公式ドキュメント類を確認するのが良いが、本記事が、New Relic＋AWSを構築する初めの一歩の一助になればと思う。

https://docs.newrelic.com/jp/docs/infrastructure-as-code/terraform/terraform-intro/

https://registry.terraform.io/providers/newrelic/newrelic/latest/docs

なお、本記事で設定できる内容は、New Relicの無料利用でできる範囲に閉じている。
実際にプロダクション環境での実運用レベルで扱うには課金は必須になるが、使用したことなくて、どんなものかを試すときに参考にしていただきたい。

# Terraform Providerの設定
さて、まずはTerraformでNew Relicを動作させる環境から構築する。

Providerを以下のように設定して```terraform init```で取得できるようにしよう。

account_idには、New Relicのユーザ登録後、ログイン後のURLの以下の部分で参照できる7桁の値を設定する。

![キャプチャ1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/3e6e2090-c1fd-9e0f-4199-e929fd1ab39e.png)

api_keiには、ログイン後の左下のメニューで「API Keys」を選択し、

![キャプチャ2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/46416db5-3e60-98c0-8542-44f5784fa420.png)

次の画面で表示されるAPI Keyのうち```Type: INGEST-LISENCE```のValueを使おう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/a2b3a969-542f-29cc-6735-4945dc566baf.png)

```HCL
terraform {
  required_providers {
    newrelic = {
      source = "newrelic/newrelic"
    }
  }
}

provider "newrelic" {
  account_id = var.newrelic_account_id
  api_key    = var.newrelic_api_key
  region     = "US"
}
```

# AWSからメトリクスをNew Relicに送る
さて、まずは監視の第一歩は、AWSからNew Relicに情報を送る部分からだ。
New Relicでは、PUSH型とPULL型の2種類があるが、今回はよりディレイの少ないPUSH型の方法で自動化する。

PUSH型とPULL型のより詳細な差異については、以下のドキュメントを参照していただきたい。
※PUSH型の方がどの比較軸も優れてるよ、と言っているようだ。

https://docs.newrelic.com/jp/docs/infrastructure/amazon-integrations/connect/cloudwatch-metric-streams/aws-metric-stream/

# New Relic側の設定
以下のようにNew Relic側でのメトリクスを受ける設定を行う。

```HCL
resource "newrelic_cloud_aws_link_account" "example" {
  depends_on = [aws_iam_role_policy.metric_stream]

  arn                    = aws_iam_role.newrelic.arn
  metric_collection_mode = "PUSH"
  name                   = "example"
}
```

また、AWS側にもNew Relic用のIAMロールを作成してリソースにアクセス可能な状態にしておく。
えっ、アカウントID書いちゃってよいの？と思われるかもしれないが、これはNew Relicが運用されているAWSアカウントであるため問題ない。以下の公式のサイトにも書かれている設定値だ。

https://docs.newrelic.com/jp/docs/infrastructure/amazon-integrations/connect/connect-aws-new-relic-infrastructure-monitoring/


```HCL
resource "aws_iam_role" "newrelic" {
  name               = local.iam_newrelic_role_name
  assume_role_policy = data.aws_iam_policy_document.newrelic_assume.json
}

data "aws_iam_policy_document" "newrelic_assume" {
  statement {
    actions = ["sts:AssumeRole"]
    effect  = "Allow"

    principals {
      type        = "AWS"
      identifiers = ["754728514883"]
    }

    condition {
      test     = "StringEquals"
      values   = [var.newrelic_account_id]
      variable = "sts:ExternalID"
    }
  }
}

resource "aws_iam_role_policy_attachment" "newrelic_readonly" {
  policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
  role       = aws_iam_role.newrelic.id
}
```

# AWS側の設定
AWS
AWS側では、Amazon ClooudWatchのメトリクスストリームの機能を使う。
```output_format```は、```"opentelemetry0.7"```固定で問題ない。

最終的にストリームの値はAmazon Data Firehoseを通してNew Relicに送信するので、今回作るAmazon Data Firehoseのリソースに対するアクセス権をAmazon ClooudWatchのメトリクスストリームに渡しておく。

```HCL
resource "aws_cloudwatch_metric_stream" "newrelic" {
  name          = local.cloudwatch_metric_stream_name
  role_arn      = aws_iam_role.metric_stream.arn
  firehose_arn  = aws_kinesis_firehose_delivery_stream.newrelic.arn
  output_format = "opentelemetry0.7"
}

resource "aws_iam_role" "metric_stream" {
  name               = local.iam_metric_stream_role_name
  assume_role_policy = data.aws_iam_policy_document.metric_stream_assume.json
}

data "aws_iam_policy_document" "metric_stream_assume" {
  statement {
    effect = "Allow"

    actions = [
      "sts:AssumeRole",
    ]

    principals {
      type = "Service"
      identifiers = [
        "streams.metrics.cloudwatch.amazonaws.com",
      ]
    }
  }
}

resource "aws_iam_role_policy" "metric_stream" {
  name   = local.iam_metric_stream_policy_name
  role   = aws_iam_role.metric_stream.id
  policy = data.aws_iam_policy_document.metric_stream_custom.json
}

data "aws_iam_policy_document" "metric_stream_custom" {
  statement {
    effect = "Allow"

    actions = [
      "firehose:PutRecord",
      "firehose:PutRecordBatch",
    ]

    resources = [
      "${aws_kinesis_firehose_delivery_stream.newrelic.arn}",
    ]
  }
}
```

前述の通り、メトリクスストリームは、Amazon Data Firehoseを使ってNew Relic指定のエンドポイントに送信する。
以下のように設定しておこう。

```HCL
resource "aws_kinesis_firehose_delivery_stream" "newrelic" {
  name        = local.firehose_stream_name
  destination = "http_endpoint"

  http_endpoint_configuration {
    url                = "https://aws-api.newrelic.com/cloudwatch-metrics/v1"
    name               = "New Relic"
    access_key         = newrelic_api_access_key.example.key
    buffering_size     = 1
    buffering_interval = 60
    role_arn           = aws_iam_role.firehose.arn
    s3_backup_mode     = "FailedDataOnly"

    request_configuration {
      content_encoding = "GZIP"
    }

    s3_configuration {
      role_arn           = aws_iam_role.firehose.arn
      bucket_arn         = aws_s3_bucket.firehose_failed.arn
      buffering_size     = 1
      buffering_interval = 60
      compression_format = "GZIP"
    }

    cloudwatch_logging_options {
      enabled         = true
      log_group_name  = aws_cloudwatch_log_group.firehose.name
      log_stream_name = aws_cloudwatch_log_stream.firehose.name
    }
  }
}
```

ログ用のAmazon S3とAmazon CloudWatch Logsは以下のように設定し、Amazon Data Firehoseにアクセス権限を渡す。

```HCL
resource "aws_s3_bucket" "firehose_failed" {
  bucket = local.s3_bucket_name
}

resource "aws_s3_bucket_ownership_controls" "firehose_failed" {
  bucket = aws_s3_bucket.firehose_failed.id

  rule {
    object_ownership = "BucketOwnerEnforced"
  }
}

resource "aws_s3_bucket_public_access_block" "firehose_failed" {
  bucket = aws_s3_bucket.firehose_failed.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_cloudwatch_log_group" "firehose" {
  name = "/aws/kinesisfirehose/${local.firehose_stream_name}"
}

resource "aws_cloudwatch_log_stream" "firehose" {
  log_group_name = aws_cloudwatch_log_group.firehose.name
  name           = "DestinationDelivery"
}
```

Amazon Data Firehoseが作ったリソースそれぞれに書き込めるように以下のようにIAMロールを設定する。

```HCL
resource "aws_iam_role" "firehose" {
  name               = local.iam_firehose_role_name
  assume_role_policy = data.aws_iam_policy_document.firehose_assume.json
}

data "aws_iam_policy_document" "firehose_assume" {
  statement {
    effect = "Allow"

    actions = [
      "sts:AssumeRole",
    ]

    principals {
      type = "Service"
      identifiers = [
        "firehose.amazonaws.com",
      ]
    }
  }
}

resource "aws_iam_role_policy" "firehose" {
  name   = local.iam_firehose_policy_name
  role   = aws_iam_role.firehose.id
  policy = data.aws_iam_policy_document.firehose_custom.json
}

data "aws_iam_policy_document" "firehose_custom" {
  statement {
    effect = "Allow"

    actions = [
      "logs:PutLogEvents",
    ]

    resources = [
      "${aws_cloudwatch_log_group.firehose.arn}:log-stream:*",
    ]
  }

  statement {
    effect = "Allow"

    actions = [
      "s3:GetObject",
      "s3:GetObjectVersion",
      "s3:GetBucketVersioning",
      "s3:PutObject",
    ]

    resources = [
      "${aws_s3_bucket.firehose_failed.arn}",
      "${aws_s3_bucket.firehose_failed.arn}/*"
    ]
  }
}
```

これで準備は完了だ。```terraform apply```してから、実際にメトリクスをモニタリングしてみよう。

# いざ、動かす！
New Relicのコンソールの左のメニュー画面から```All Capabilities```⇒次の画面で```AWS```を検索して、表示された```Amazon Web Services```のパネルをクリックしてみよう。

![キャプチャ5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/6e00fdb4-198f-9e8a-01c6-5ecd5e84e382.png)

以下のように、自身のAWSのリソースの情報が表示されれば成功だ。

![キャプチャ6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/a917ac97-20b4-afb5-de01-70fe5dcfa537.png)

## 実際に動いているメトリクスを見る
ここまでの確認では面白くないので、次回のトピック(監視アラートを設定する)に繋げるために実際に動いているメトリクスを確認しよう。

今回、サンプルとして以下のように一定割合でエラーを返すLambdaを作成した。
インラインでスクリプトを書くのはやや反則だが、おためしなのでご容赦を。

```HCL
data "archive_file" "lambda_example" {
  type        = "zip"
  output_path = "../output/lambda_example.zip"

  source {
    filename = "lambda_example.py"
    content  = <<EOF
import json
import random

def lambda_handler(event, context):
  random_value = random.random()

  if random_value < 0.5:
    return {
      'statusCode': '200',
      'body': json.dumps({'message': 'OK.'}),
    }
  else:
    return {
      'statusCode': '500',
      'body': json.dumps({'message': 'NG.'}),
    }
EOF
  }
}

resource "aws_lambda_function" "example" {
  depends_on = [
    aws_cloudwatch_log_group.lambda,
  ]

  function_name    = local.lambda_function_name
  filename         = data.archive_file.lambda_example.output_path
  role             = aws_iam_role.lambda.arn
  handler          = "lambda_example.lambda_handler"
  source_code_hash = data.archive_file.lambda_example.output_base64sha256
  runtime          = "python3.10"

  memory_size = 128
  timeout     = 60

  tags = {
    newrelic_integration = true
  }
}

resource "aws_lambda_function_url" "example" {
  function_name      = aws_lambda_function.example1.function_name
  authorization_type = "NONE"
}

resource "aws_cloudwatch_log_group" "lambda" {
  name              = "/aws/lambda/${local.lambda_function_name}"
  retention_in_days = 3
}

resource "aws_iam_role" "lambda" {
  name               = local.iam_lambda_role_name
  assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
}

data "aws_iam_policy_document" "lambda_assume" {
  statement {
    effect = "Allow"

    actions = [
      "sts:AssumeRole",
    ]

    principals {
      type = "Service"
      identifiers = [
        "lambda.amazonaws.com",
      ]
    }
  }
}

resource "aws_iam_role_policy" "lambda" {
  name   = local.iam_lambda_policy_name
  role   = aws_iam_role.lambda.id
  policy = data.aws_iam_policy_document.lambda_custom.json
}

data "aws_iam_policy_document" "lambda_custom" {
  statement {
    effect = "Allow"

    actions = [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents",
    ]

    resources = [
      aws_cloudwatch_log_group.lambda.arn,
      "${aws_cloudwatch_log_group.lambda.arn}:log-stream:*",
    ]
  }
}

output "lambda_function_url" {
  value = aws_lambda_function_url.example.function_url
}
```

これを```terraform apply```して、以下のコマンドを数分流して、実際にメトリクスがどう変化するかを確認してみよう。

```shell
$ while true; do curl (TerraformのOutputのURL); echo; sleep 1; done
```

数分流した後に、```Lambda functions```で作ったLambda関数の情報を表示してみよう。

![キャプチャ7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/0ef76370-f681-2ae4-2118-8c0654f9d153.png)

すると、以下のようにサマリを確認することができる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/0743fa4c-7bf1-c787-6595-af88080a1d0e.png)

これで、New RelicでAWSの情報が参照できるようになった！

# 次回

https://qiita.com/neruneruo/items/643e27942b93a2acdb01