Terraformで実現するNewRelic Dashboard as Code 〜マルチ環境対応の自動化実践〜

## はじめに
近年、システム運用においてオブザーバビリティ（可観測性）の重要性がますます高まっています。複雑化するシステムの状態を把握し、迅速に問題を特定・解決するためには、効果的なモニタリングツールが不可欠です。NewRelicは、このオブザーバビリティを実現する強力なプラットフォームとして広く認知されており、システム全体の状態をリアルタイムで可視化し、迅速なトラブルシューティングやパフォーマンス改善を可能にします。
しかし、NewRelicのダッシュボードを手動で作成・管理することは、特に複数環境（開発、ステージング、本番など）を扱う場合、以下のような大きな課題をもたらします：
+ **作業の非効率性**: 環境ごとに同じ設定を繰り返す手間が発生
+ **設定ミスのリスク**: 人為的エラーによる環境間の設定の不一致
+ **再利用性の欠如**: 既存ダッシュボードの他環境への展開が困難

これらの課題を解決するために、「Dashboard as Code」というアプローチが注目されています。Terraformを活用してNewRelicのダッシュボードをコード化することで、次のようなメリットが得られます：
+ **作業の効率化**: コードによる一貫した設定の複数環境への適用
+ **エラー防止**: 手動作業の排除による設定の再現性確保
+ **拡張性の向上**: 新規環境への迅速な展開

本記事では、Terraformを使用して開発環境でダッシュボードを調整し、その設定をステージング環境へ効率的に展開する手法を詳しく解説します。この方法により、オブザーバビリティの向上と運用作業の自動化を同時に実現できます。

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

## システム構成
### 全体アーキテクチャ
以下に 全体アーキテクチャ構成図 を示します。このアーキテクチャは大きく 2つの層 に分かれています：
![架构图.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71044/96f9cc0f-81b6-5c84-f0cf-17d9436aa709.png)

#### インフラストラクチャ層
この層では、AWS上にマルチ環境（DEV/STG） を構築します。各環境は以下の主要コンポーネントで構成されています：

+ ECSクラスタ：コンテナ化されたアプリケーションの実行環境。
+ VPCネットワーク：パブリックサブネットおよびプライベートサブネットによるネットワーク構成。
+ AWS CodePipeline：CI/CDパイプラインの自動化を実現。
+ AWS CodeBuild：アプリケーションのビルド環境。
+ Amazon CloudWatch：ログ管理および監視のための基盤。
これらの環境は Terraform を用いてコードベースで定義され、完全に自動化された方法 で構築・管理されます。これにより、環境ごとの設定の一貫性と再現性が確保されます。


#### ダッシュボード層
インフラストラクチャ層で構築された基盤環境の情報を、Terraform show を活用して取得します（例：ECS、ALB、RDSのリソース情報）。その取得した情報を基に、Terraform を用いて NewRelicダッシュボード を自動生成します。
Terraformを使用することで、DEV環境で調整したダッシュボード設定を、STG環境 にそのまま展開できるため、作業の効率化と設定ミスの削減が実現されます。

### マルチ環境ダッシュボード自動作成フロー
1. ダッシュボードに必要な情報の整理
    + 取得するリソースのメトリクスやログ、監視対象の項目を整理します（例：ECSのCPU・メモリ使用率、ALBのリクエスト数、RDSの接続数など）。
2. サンプル環境（Dev環境）でのダッシュボード作成
    + 手作業で Dev環境 のダッシュボードを作成し、必要なグラフやメトリクスを調整・整理します。
3. Terraform Dashboardモジュールの作成
    + Dev環境で作成したダッシュボードを基に、Terraformを用いて再利用可能な Dashboardモジュール を作成します。
4. 全環境へのダッシュボード自動展開
    + インフラストラクチャ層で構築された基盤情報（ECS、ALB、RDSなど）を Terraform show で取得し、その情報を利用して、DEV環境、STG環境 など複数環境のダッシュボードを自動生成します。

### terraformディレクトリ構成
Terraformを使用したNewRelicダッシュボードの自動生成プロジェクトは、以下のような構造で組織化されています。
```
newrelic-dashboards/
├── environments/
│   ├── dev.tfvars        # Dev環境用の変数定義ファイル
│   ├── stg.tfvars        # STG環境用の変数定義ファイル
│
└── modules/
    ├── dashboard/        # ダッシュボードを管理するモジュール
    │   ├── main.tf       # ダッシュボードリソースの定義
    │   ├── outputs.tf    # ダッシュボードの出力定義
    │   └── variables.tf  # モジュールの変数定義
    │
    ├── main.tf           # 基盤情報取得のモジュール定義
    ├── outputs.tf        # 環境情報の出力定義
    └── variables.tf      # 環境全体の変数定義
```
この構造により、環境ごとの設定を分離しつつ、ダッシュボード生成ロジックを再利用可能なモジュールとして実装することができます。terraform apply -var-file=environments/dev.tfvarsのようなコマンドで、特定の環境用のダッシュボードを生成することが可能となります。

## 実装手順
### 事前準備
+ AWSアカウント：申請済みであり、IAMユーザーが払い出されていること。
+ GitHub環境：アクセス可能な環境を準備しておくこと。
+ NewRelicアカウント：登録済みであること。
+ NewRelicでのAWSインテグレーション設定：
    + Amazon CloudWatch Metric StreamsまたはAPIポーリングの2つの方法があります。筆者は推奨されている Amazon CloudWatch Metric Streams を利用しています。
    + 設定方法については、下記の記事をご参考ください。

https://docs.newrelic.com/jp/docs/infrastructure/amazon-integrations/get-started/introduction-aws-integrations/

https://qiita.com/nr-mito/items/ec595a70336ceb1db84a

### AWSマルチ基盤環境構築


+ GitHubで公開されているサンプルコードを利用します。  

https://github.com/aws-samples/aws-ecs-cicd-terraform/tree/master

+ Terraformコードを修正してマルチ環境対応にします。
    + 元のコードはマルチ環境向けに設計されていないため、リポジトリを2つのフォルダ（`dev-aws-ecs-cicd-terraform-master` と `stg-aws-ecs-cicd-terraform-master`）にコピーします。  
    + **`/dev-aws-ecs-cicd-terraform-master/terraform/terraform.tfvars`** を以下のように修正します：  
        ```hcl
        aws_region="ap-northeast-1"
        stack="dev"
        fargate-task-service-role="dev-task-role"
        fargate-task-execution-role="dev-execution-role"
        aws_ecr="dev-petclinic"
        aws_profile="default"
        source_repo_name="dev-petclinic"
        source_repo_branch="master"
        image_repo_name="dev-petclinic"
        family="dev-family"
        ```
    + **`/stg-aws-ecs-cicd-terraform-master/terraform/terraform.tfvars`** を以下のように修正します：  
        ```hcl
        aws_region="ap-northeast-1"
        stack="stg"
        fargate-task-service-role="stg-task-role"
        fargate-task-execution-role="stg-execution-role"
        aws_ecr="stg-petclinic"
        aws_profile="default"
        source_repo_name="stg-petclinic"
        source_repo_branch="master"
        image_repo_name="stg-petclinic"
        family="stg-family"
        ```

+ それぞれのフォルダに移動し、`README.md` に記載されている手順に従い環境を構築します。

:::note warn
**注意点**：  
Terraformコードで環境構築後、ALBのセキュリティグループはデフォルトで `0.0.0.0/0` が許可されています。このままではセキュリティリスクがあるため、必ずIP（例：自宅のIP）を制限してください。  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71044/f6ae30a5-5982-8632-e399-9858b9c073a2.png)
:::

### 開発環境（Dev環境）でのダッシュボード設定
#### ダッシュボードのグラフ整理
| **グラフ名（リソース名）**                                      | **内容**                                                                 | **目的**                                    |
|-----------------------------------------------------------------|--------------------------------------------------------------------------|--------------------------------------------|
| **Total Request Count (${var.alb_arn})**                       | ALBを通じたリクエスト数（AZごと）を表示                                  | トラフィック状況と負荷分散の状態を把握する   |
| **HTTP Status Codes (${var.alb_arn})**                         | HTTPステータスコード（2XX、3XX、4XX、5XX）ごとの数を表示                  | エラーレスポンスの発生状況を監視する         |
| **ECS-CPU utilization (%) (${var.ecs_cluster_name})**          | ECSクラスタのCPU使用率（平均、最小、最大、合計）を表示                    | コンテナのCPUリソース使用状況を監視する       |
| **ECS-Memory utilization (%) (${var.ecs_cluster_name})**       | ECSクラスタのメモリ使用率（平均、最小、最大、合計）を表示                 | メモリ不足やリソース過剰利用を確認する       |
| **Database connections (${var.rds_instance_identifier})**      | RDSデータベースへの接続数を表示                                          | データベースの負荷状況を把握する             |
| **DB Throughput (bytes/s) (${var.rds_instance_identifier})**   | RDSデータベースの読み取り・書き込みスループット（bytes/s）を表示         | データベースのパフォーマンスを監視する       |
| **DB CPU utilization (${var.rds_instance_identifier})**        | RDSデータベースのCPU使用率（平均、最小、最大）を表示                     | データベース処理能力のボトルネックを監視する |
| **CodeBuild Failed Builds (${var.codebuild_project_name})**    | CodeBuildプロジェクトのビルド失敗数を時間帯ごとに表示                    | CI/CDパイプラインの問題を特定する            |

:::note info
実際のプロジェクトでは、**APM（Application Performance Monitoring）** 指標や **ECSタスク数**、**アプリケーションのレスポンス時間** など、さらに重要なメトリクスを追加する必要があります。ここでは指標の網羅性については議論しません。

:::

#### Dev環境用サンプルダッシュボード作成

+ **手作業でサンプル用ダッシュボードを作成します。**  
    1. **[Dashboards] -> [Create a dashboard] -> [Create a new dashboard]**  
       ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71044/46decf8e-e2b8-c5f2-cf70-dfd1af7242d3.png)

+ **NewRelicが提供するグラフを活用し、0からクエリを作成する工数を削減できます。**  
    例：**DB CPU使用率のグラフ**  
    1. **[Amazon Web Services]** もしくは **[ALL Entities]** から対象のDBインスタンスを選択します。  
    2. **Metrics** から **CPU utilization** のグラフを探し、右上の **[...]** をクリックして **[Add to Dashboard]** を選択します。  
       - 既存のダッシュボード（例：sample）を選択し、グラフをコピーします。  
       ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71044/886b21d0-a3c9-981e-5fcd-5ee6f82d74f9.png)  

+ **ダッシュボードのグラフを編集して、リソース名を特定します。**  
    1. ダッシュボード（sample）に移動し、グラフの右上 **[...]** をクリックして **[Edit]** を選択します。  
       ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71044/12ac08ed-2f1f-3a1e-6ff7-55924265580e.png)  
    2. クエリ内のDBインスタンスを特定する部分（`WHERE`句）をAWSリソース名に変更します。  
       - **変更前**：  
         ```sql
         SELECT min(`aws.rds.CPUUtilization`), max(`aws.rds.CPUUtilization`), average(`aws.rds.CPUUtilization`) 
         FROM Metric 
         WHERE (entity.guid = 'NjI2NTg2M3xJTkZSQXxOQXwxMDIzMTI3ODkzMzM2ODE4NzU4') 
         TIMESERIES AUTO
         ```  
       - **変更後**：  
         ```sql
         SELECT min(`aws.rds.CPUUtilization`), max(`aws.rds.CPUUtilization`), average(`aws.rds.CPUUtilization`) 
         FROM Metric 
         WHERE aws.rds.DBInstanceIdentifier = 'dev-petclinic' 
         TIMESERIES AUTO
         ```  
    3. **変更理由**：  
       - 今後Terraform化する際に、**リソース名（`aws.rds.DBInstanceIdentifier`）** は比較的に変換しやすいため、管理が容易になります。  
+ **サンプルダッシュボードのJSON情報を取得**  
    - NewRelicで作成したサンプルダッシュボードのJSON情報を取得し、Terraformファイル作成の参考にします。  
      ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71044/a4ee8721-631e-5fda-1e75-70eeaafe8aec.png)  

+ **Terraformでダッシュボードをモジュール化**  
    - 以下のパスに `main.tf` を作成します：  
      **`/newrelic-dashboards/modules/dashboard/main.tf`**  
    - ダッシュボード定義をTerraformコードとして記述します。  
    ```hcl
    terraform {
      required_providers {
        newrelic = {
          source  = "newrelic/newrelic"
          version = "~> 3.0"
        }
      }
    }
    
    resource "newrelic_one_dashboard" "infrastructure_dashboard" {
      name        = "${var.environment} Infrastructure Dashboard"
      permissions = "public_read_only"
    
      page {
        name = "Load Balancer Metrics"
    
        widget_line {
          title  = "Total Request Count (${var.alb_arn})"
          row    = 1
          column = 1
          width  = 3
          height = 3
    
          nrql_query {
            query = "SELECT sum(`aws.applicationelb.RequestCount.byAlb`) as 'Requests with AZ' FROM Metric WHERE `aws.applicationelb.LoadBalancer` ='${var.alb_arn}' AND aws.applicationelb.AvailabilityZone IS NOT NULL TIMESERIES auto SINCE 30 minutes ago UNTIL now"
          }
        }
    
        widget_line {
          title  = "HTTP Status Codes (${var.alb_arn})"
          row    = 1
          column = 4
          width  = 3
          height = 3
    
          nrql_query {
            query = "FROM Metric SELECT sum(aws.applicationelb.HTTPCode_Target_2XX_Count) as '2XX', sum(aws.applicationelb.HTTPCode_Target_3XX_Count) as '3XX', sum(aws.applicationelb.HTTPCode_Target_4XX_Count) as '4XX', sum(aws.applicationelb.HTTPCode_Target_5XX_Count) as '5XX' WHERE aws.applicationelb.LoadBalancer='${var.alb_arn}' SINCE 30 minutes ago TIMESERIES"
          }
        }
    
        widget_line {
          title  = "ECS-CPU utilization (%) (${var.ecs_cluster_name})"
          row    = 1
          column = 7
          width  = 3
          height = 3
    
          nrql_query {
            query = "SELECT average(`aws.ecs.CPUUtilization.byService`), min(`aws.ecs.CPUUtilization.byService`), max(`aws.ecs.CPUUtilization.byService`), sum(`aws.ecs.CPUUtilization.byService`) FROM Metric WHERE (aws.ecs.ClusterName='${var.ecs_cluster_name}') TIMESERIES AUTO"
          }
        }
    
        widget_line {
          title  = "ECS-Memory utilization (%) (${var.ecs_cluster_name})"
          row    = 1
          column = 10
          width  = 3
          height = 3
    
          nrql_query {
            query = "SELECT average(`aws.ecs.MemoryUtilization.byService`), min(`aws.ecs.MemoryUtilization.byService`), max(`aws.ecs.MemoryUtilization.byService`), sum(`aws.ecs.MemoryUtilization.byService`) FROM Metric WHERE (aws.ecs.ClusterName='${var.ecs_cluster_name}') TIMESERIES AUTO"
          }
        }
    
        widget_line {
          title  = "Database connections (${var.rds_instance_identifier})"
          row    = 4
          column = 1
          width  = 3
          height = 3
    
          nrql_query {
            query = "SELECT average(`aws.rds.DatabaseConnections`) as 'Connections' FROM Metric WHERE aws.rds.DBInstanceIdentifier = '${var.rds_instance_identifier}' TIMESERIES auto"
          }
        }
    
        widget_line {
          title  = "DB Throughput (bytes/s) (${var.rds_instance_identifier})"
          row    = 4
          column = 4
          width  = 3
          height = 3
    
          nrql_query {
            query = "SELECT average(`aws.rds.ReadThroughput`) as 'Read', average(`aws.rds.WriteThroughput`) as 'Write' FROM Metric WHERE aws.rds.DBInstanceIdentifier = '${var.rds_instance_identifier}' TIMESERIES"
          }
        }
    
        widget_line {
          title  = "DB CPU utilization (${var.rds_instance_identifier})"
          row    = 4
          column = 7
          width  = 3
          height = 3
    
          nrql_query {
            query = "SELECT min(`aws.rds.CPUUtilization`), max(`aws.rds.CPUUtilization`), average(`aws.rds.CPUUtilization`) FROM Metric WHERE aws.rds.DBInstanceIdentifier = '${var.rds_instance_identifier}' TIMESERIES AUTO"
          }
        }
    
        widget_line {
          title  = "CodeBuild Failed Builds (${var.codebuild_project_name})"
          row    = 4
          column = 10
          width  = 3
          height = 3
    
          nrql_query {
            query = "SELECT average(`aws.codebuild.FailedBuilds`) FROM Metric FACET `aws.codebuild.ProjectName` WHERE `aws.codebuild.ProjectName` = '${var.codebuild_project_name}' SINCE 6 HOURS AGO TIMESERIES"
          }
        }
      }
    }

+ **環境変数ファイル（tfvars）を作成**  
    - **`newrelic-dashboards/environments/dev.tfvars`** に環境情報を設定します：  

    ```hcl
    environment               = "dev"
    alb_arn                   = "app/dev-alb/52e8a82b796e3001"
    ecs_cluster_name          = "dev-Cluster"
    rds_instance_identifier   = "dev-petclinic"
    codebuild_project_name    = "codebuild-dev-petclinic-master"
    ```  

    **補足**：  
    上記のリソース情報は、インフラ基盤環境構築後に **`terraform show`** を実行することで取得可能です。  

+ **Terraformを実行してダッシュボードをデプロイ**  
    - 以下のコマンドでDEV環境のダッシュボードをデプロイします：  

    ```bash
    terraform apply -var-file="environments/dev.tfvars"
    ```

+ **デプロイされたダッシュボードの確認**  
    デプロイ後のダッシュボードは以下の通りです：  
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71044/86c2109c-c49f-bd98-9422-8c0f326093e5.png)

### 本番ステージング環境（STG環境）への展開
今回の検証では、**STG環境のダッシュボードはDEV環境と同じ内容** に設定します。そのため、STG環境への展開は非常に簡単です。  
もしカスタマイズが必要な場合は、TerraformコードでSTG専用のカスタマイズを追加すれば対応可能です。  
ただし、今回は **マルチ環境への適用効率** を検証する目的で、カスタマイズ部分は割愛しています。  

1. **環境変数ファイル（tfvars）の作成**  
    以下の環境情報を設定し、**`newrelic-dashboards/environments/stg.tfvars`** を作成します：  

    ```hcl
    environment               = "stg"
    alb_arn                   = "app/stg-alb/45a2fb743b4efc17"
    ecs_cluster_name          = "stg-Cluster"
    rds_instance_identifier   = "stg-petclinic"
    codebuild_project_name    = "codebuild-stg-petclinic-master"
    ```

2. **Terraformを実行してダッシュボードをデプロイ**  
    以下のコマンドを実行して、STG環境のダッシュボードをデプロイします：  

    ```bash
    terraform apply -var-file="environments/stg.tfvars"
    ```

3. **デプロイされたダッシュボードの確認**  
    デプロイ後のダッシュボードは以下の通りです：  
    - ダッシュボード名やリソース名は **STG環境** のものに置き換わっています。  

    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71044/a6af67ea-a7f8-b407-0c07-28b83cc70cdc.png)  

## 実践のメリットと課題

### 実践のメリット  
1. **多環境への効率的な展開**  
   - Terraformを活用することで、DEV環境で作成した設定をそのままSTGや本番環境に適用可能となり、作業の手間を大幅に削減できます。
   - 再利用可能なモジュール化により、新たな環境の追加も容易です。

2. **設定ミスの削減**  
   - ダッシュボードをコード化することで、手動設定によるエラーや環境間の設定不一致を防ぐことができます。

3. **将来の拡張性**  
   - コードベースで管理することで、将来的にダッシュボード設定をCI/CDパイプラインに組み込み、自動化された運用が可能となります。

### 課題と注意点  
1. **コードの複雑性**  
   - 多環境への適用時にカスタマイズが必要な場合、Terraformコードが複雑化する可能性があります。
   - 特に、環境ごとに異なるリソース（例：メトリクスやカスタムグラフ）の柔軟な対応には、追加のスクリプトやモジュール設計が必要です。

2. **キー管理のリスク**  
   - NewRelicのAPIキーやアカウント情報が流出しないよう、十分な管理が必要です。  
   - **Secret Manager** や **環境変数** を活用し、直接コードに書き込まない運用が推奨されます。

3. **運用面での注意点**  
   - 環境変数やリソース情報の管理方法を標準化する必要があります。
   - ダッシュボード設定の変更管理プロセス（例：コードレビューやテスト）の確立が求められます。


## まとめ

本記事では、Terraformを活用したNewRelic Dashboardの自動生成について詳しく解説しました。**Dashboard as Code**というアプローチを採用することで、複数環境でのダッシュボード管理を効率化し、設定品質を向上できることを実証しました。

このアプローチを導入することで、**システム監視の効率化** と **オブザーバビリティの向上** を同時に達成することが可能となります。特に、多環境を管理する運用チームにとって、本手法は非常に有効なツールとなるでしょう。

### 今後の展望

#### CI/CDパイプラインとの統合
- **GitHub Actions** や **Jenkins** を活用したダッシュボードの自動デプロイ基盤の構築  
- **プルリクエストベースのレビュー** を導入し、コード変更の透明性を確保  
- 継続的デリバリーによる、迅速かつ安全なデプロイの実現  

#### 自動化の更なる可能性
- **アラート設定の自動化** により、異常検知の効率化および精度向上  
- **再利用可能なカスタムダッシュボードテンプレート** の整備  
- チーム間で共有可能な、汎用性の高いモジュールの開発と展開  

---

このように、TerraformとNewRelicを組み合わせることで、運用効率を大幅に向上させ、システムの安定性を高めることが期待できます。