Observability as Code は、オブザーバビリティツールの設定をコーディングすることによって自動化するプロセスです。略して o11y as code と呼ぶこともあります。すでにインフラストラクチャをコードとして管理しているのであれば、ダッシュボードをはじめとしたオブザーバビリティツールも、同じ方法で管理することを検討してみてください。

この3部構成のブログシリーズでは、Observability as Code を活用して、オブザーバビリティツールの設定を自動化するためのヒント、導入のガイダンスやコードサンプルをご紹介します。第1部はダッシュボードから始めます。第1部では、Terraform の基本、サンプルアプリをプロビジョニングする方法、実際にコードを用いてダッシュボードを作成する方法について説明します。

このシリーズでは、New Relic と HashiCorp 社の Terraform を使用して、実際に5つの具体的な実装サンプル(ダッシュボード、アラート、外形監視、タグ操作、そしてコードによるワークロード管理)の操作を行い、Observability as Code について理解を深めることができます。なお、​​​​​​作業にあたっては、​サンプルの FoodMe レストラン注文アプリのデータを使用します。

 

 

私たちが歩んだ道のりとInfrastructure as Code

10年以上前に Infrastructure as Code(IaCとも呼ばれる)が登場して以来、コードを使用したプロビジョニングは、現代のクラウド時代のコア要件になっています。「as Code」(コードとして)という用語は、一般的なプログラムのコードを扱うのと同じようにインフラストラクチャ設定を定義し、設定ファイルをソース管理システムにプッシュし、変更をインフラストラクチャ層に慎重に適用する、一連のプロセスを意味します。

最新の分散システムの台頭に伴って、システム停止も増えており、何か問題が発生した際に問題の根本原因を見つけるのが困難な場合があります。分散されたシステムの出力からシステムの内部状態を判断することがますます重要となる、この新しいパラダイムにおいて、オブザーバビリティは強力なツールとなります。オブザーバビリティは、トレース、ログ、メトリクスなどのさまざまなシステム出力を使用して、分散コンポーネントの内部状態を把握し、問題箇所を診断し、根本原因を特定します。

一方で、私たちが依存している運用慣行はそれほど変わっておらず、開発者や運用エンジニアは依然として何百ものアラートやダッシュボードを確認していることにお気づきの方もいるかもしれません。このようなアプローチは、再現性が低く標準化されていないダッシュボードが乱立したり、あるいはシグナル疲れやチームのベストプラクティスからの逸脱を避けるために、しばしば手動で多くのアラート条件を調整することにつながります。

しかし、Infrastructure as Code について知っていることと同様の手法で、オブザーバビリティを自動化することができます。オブザーバビリティの設定をコードとして扱う、この新しいアプローチが、今回紹介する Observability as Code です。Observability as code で業務をシンプルにする でも説明していますが、Observability as Code は、設定のメンテナンスと開発に必要な作業を削減して、監査可能なコード管理ソリューションへ移行することを意味します。

Terraformの基本情報

Hashicorp 社が提供する Terraform は、Infrastructure as Code のツールで、人間が読み書きしやすい形式の設定ファイルを使用して、インフラストラクチャリソースを定義し、管理できます。サービスを宣言的に管理して、それらのサービスへの変更を自動化できます。

ほとんどの例では、Terraform モジュールは1つのディレクトリにあるTerraform設定ファイルの集合です。その1つのディレクトリから直接 Terraform コマンドを実行する場合、そのディレクトリはルートモジュールと見なされます。Terraform のドキュメントでは、以下のような構成が示されています。

.
├── LICENSE
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf

このブログシリーズで使用するTerraformファイル

このブログシリーズの演習では、次の2つの重要なファイルに焦点を絞ります。

  • main.tf ファイルには、モジュールの主な設定が含まれています。他の設定ファイルを作成すると、プロジェクトに適した方法で整理することもできます。
  • variables.tf ファイルには、モジュール内で参照する変数定義が含まれています。他のユーザーや異なるプロジェクト間でモジュールを共有するような場合には、モジュールブロックの引数でそれぞれの変数定義ファイルを指定します。

New Relic Terraformプロバイダーの例

ここでは、Terraform のドキュメントで公開されている New Relic Terraform プロバイダー設定 を例として使用します。

# New Relic terraformプロバイダーを取得
terraform {
  required_version = "~> 1.0"
  required_providers {
    newrelic = {
      source  = "newrelic/newrelic"
    }
  }
}

# New Relicプロバイダーを設定
provider "newrelic" {
  account_id = <Your Account ID>
  api_key = <Your User API Key>    # 通常「NRAK」で始まる文字列です
  region = "US"                   # 有効な地域はUSとEUです
}

環境変数 を使用してプロバイダーを設定することもできます。これにより、provider ブロックを簡素化できます。各プロバイダーには、account_idapi_keyregionなどのキースキーマ属性があります。

覚えておくべきTerraformコマンド

Terraform の利用を始めるにあたって、はじめに次の4つのコマンドを覚えておいてください。

  • terraform init コマンドは、現在の作業ディレクトリを初期化して、Terraform で使用できるようにします。このコマンドは、後から構成を変更した場合など、作業ディレクトリを更新するために複数回実行しても安全です。
  • terraform plan コマンドは、構成ファイルを元に実行計画を作成し、Terraform がインフラストラクチャに加える変更をプレビューできます。このコマンドを使用すると、実際に変更を適用する前に、構文や変更内容に誤りがないかどうかを確認できます。
  • terraform apply コマンドは、構成ファイルを元に実行計画を自動的に作成し、その計画の承認を求めるプロンプトを表示し、指定されたアクションを実行します。プロンプトに従い、"yes" と答えて承認することで、Terraform によるリソースのプロビジョニングが開始されます。
  • terraform destroy コマンドは、特定の Terraform 構成ファイルによって管理されるすべてのリモートオブジェクトを一括で削除する便利な方法です。プロンプトに従いユーザーの承認を得たのち、Terraform がすべてのリソースを削除します。

Terraform コマンドの詳細については、Terraform のドキュメントを参照してください。

次のセクションでは、プロバイダー、データソース、リソースなど、Terraform の主要な概念について説明します。Terraform を使用して、サンプルの FoodMe レストランアプリのデータを表示するための New Relic ダッシュボードの作成を自動化してみましょう。

このブログ記事のデモでは、Terraform 構成ファイルで newrelic_one_dashboard リソースを使用しています。もう一つの方法として、newrelic_one_dashboard_jsonリソースを使用する場合は、TerraformとJSONテンプレートを使用したダッシュボードの作成を参照してください。

最初のTerraformモジュールのプロビジョニングを開始する前に

最初の Terraform モジュールをプロビジョニングする前に、New Relic アカウントIDとユーザーキーを取得し、正しいデータセンターを指定する必要があります。

このビデオチュートリアルでは、前提条件の作業について説明します。

サンプルアプリのプロビジョニング

Observability as Code を実装する前に、サンプルアプリのプロビジョニングから始めましょう。

1. このGlitchリンク(glitch.com/edit/#!/remix/nr-devrel-o11yascode)を使用して、FoodMe サンプルアプリの一意のURLを生成します。

2. 環境変数を設定します。.env に移動し、以下の値を挿入します。

  • LICENSE_KEY:New Relic にデータを送信するための Ingest API キーを挿入します。
  • APP_NAME: FoodMe-XXX のように、自身の名前またはイニシャルなど、任意の文字列をアプリ名に挿入します(FoodMe-Janなど)。

3. URIをプレビューします。

Tools (パネル下部)に移動し、Preview in a new windowを選択します。

4. URLを記録します。

新しく生成されたURLをメモします。シリーズの第2部で、Observability as Code による外形監視の設定を行う際に使用します。

 

5. ワークロードをいくつか生成します。ダッシュボードでデータを表示するためには、ある程度データが記録されれている必要があります。先程のURLにアクセスすると、サンプルアプリが表示されているので、任意の名前や配達先住所を入力し、"Find Restaurants!" を選択します。メインページに移動したら、ページ内のリンクをクリックして、サンプルアプリのワークロードをいくつか生成します。

Terraformを使用したダッシュボード作成

Observability as Code に関する最初のチュートリアルの準備が整いました。New Relic カスタムダッシュボードを使用すると、New Relic で表示したい特定のデータを収集して視覚化できます。Terraform を使用して、New Relic でダッシュボードの設定を自動化する方法を説明します。

主な手順は3つあります。このセクションで説明する内容をすべて確認するには、この動画をご覧ください。詳細については、New Relic と Terraform を使い始めるをご覧ください。これらの手順に沿って、GitHub のコードサンプルと Instruqt の実践的なワークショップを活用することもできます。

Terraform の各リソースブロックでは、ダッシュボード、アラート、通知ワークフロー、ワークロードなどの1つ以上のオブザーバビリティ・オブジェクトを記述します。Resource: newrelic_one_dashboardの例を使用します。

1. resource ブロックを作成し、名前(exampledash)とリソースタイプ(newrelic_one_dashboard)を宣言します。リソースのタイプと名前はリソースの識別子であるため、モジュール内で一意である必要があります。Resource: newrelic_one_dashboard に基づいて、New Relic にダッシュボードをデプロイする簡単な例を紹介します。

# New Relic ダッシュボードのリソース定義
resource "newrelic_one_dashboard" "exampledash" {
	# ダッシュボードのタイトル
  name = "New Relic Terraform Example"

	# ネストされたブロック内でダッシュボードのページ設定を記述します
  page {
		#  ページ名
    name = "New Relic Terraform Example"

		# ネストされたブロック内で Billboard ウィジェットの設定を記述します
    widget_billboard {
      title = "Requests per minute"
      row = 1
      column = 1
      width = 6
      height = 3

			# ネストされたブロック内で NRQL クエリを記述します
      nrql_query {
        query = "FROM Transaction SELECT rate(count(*), 1 minute)"
      }
    }
  }
}

各ブロックに記載している属性の詳細については、Terraform New Relic プロバイダーのドキュメントを参照してください。

また、設定内で記述している New Relicのクエリ言語(NRQL)についての詳細は、NRQL リファレンスを参照してください。

次に、Terraform に variables.tf ファイルを含めます。入力変数を使用すると、ソースコードを変更せずに、Terraform モジュールをカスタマイズできます。これにより、ユーザーやアカウントの異なる他の構成との間で、Terraform モジュールを簡単に共有したり再利用したりできます。このセクションの最後に、variables.tf ファイルの例を記載します。

3. 最後に、New Relic プロバイダーの設定リソース設定が記述された main.tf ファイル、および対応する variables.tf ファイルを組み合わせて、ダッシュボードをデプロイします。

次の2つの main.tf ファイルと variables.tf ファイルでは、Google 社の提唱するサイト信頼性エンジニアリングにおいて「4つのゴールデンシグナル」として説明されている概念( レイテンシ、トラフィック、エラー、スループット)を、ダッシュボードとして可視化するための例を示しています。これらの例は、Terraform New Relic プロバイダーのドキュメントに記載しているコードサンプルをベースとしています。

main.tfファイルの完全なコードの例

 

# get the New Relic terraform provider
terraform {
  required_version = "~> 1.0"
  required_providers {
    newrelic = {
      source  = "newrelic/newrelic"
    }
  }
}

# configure the New Relic provider
provider "newrelic" {
  account_id = (var.nr_account_id)
  api_key = (var.nr_api_key)    # usually prefixed with 'NRAK'
  region = (var.nr_region)      # Valid regions are US and EU
}

# resource to create, update, and delete dashboards in New Relic
resource "newrelic_one_dashboard" "dashboard_name" {
  name = "O11y_asCode-FoodMe-Dashboards-TF"

  # determines who can see the dashboard in an account
  permissions = "public_read_only"

  page {
    name = "Dashboards as Code"

    widget_markdown {
      title = "Golden Signals - Latency"
      row = 1
      column = 1
      width = 4
      height = 3

      text = "## The Four Golden Signals - Latency\n---\n#### The time it takes to service a request. It’s important to distinguish between the latency of successful requests and the latency of failed requests. \n\n#### For example, an HTTP 500 error triggered due to loss of connection to a database or other critical backend might be served very quickly; however, as an HTTP 500 error indicates a failed request, factoring 500s into your overall latency might result in misleading calculations. \n\n#### On the other hand, a slow error is even worse than a fast error! Therefore, it’s important to track error latency, as opposed to just filtering out errors."
    }

    widget_line {
      title = "Golden Signals - Latency - FoodMe - Line"
      row = 1
      column = 5
      width = 4
      height = 3

      nrql_query {
        query = "SELECT average(apm.service.overview.web) * 1000 as 'Latency' FROM Metric WHERE appName like '%FoodMe%' since 30 minutes ago TIMESERIES AUTO"
      }
    }

    widget_stacked_bar {
      title = "Golden Signals - Latency - FoodMe - Stacked Bar"
      row = 1
      column = 9
      width = 4
      height = 3

      nrql_query {
        query = "SELECT average(apm.service.overview.web) * 1000 as 'Latency' FROM Metric WHERE appName like '%FoodMe%' since 30 minutes ago TIMESERIES AUTO"
      }
    }

    widget_markdown {
      title = "Golden Signals - Errors"
      row = 4
      column = 1
      width = 4
      height = 3

      text = "## The Four Golden Signals - Errors\n---\n\n#### The rate of requests that fail, either explicitly (e.g., HTTP 500s), implicitly (for example, an HTTP 200 success response, but coupled with the wrong content), or by policy (for example, \"If you committed to one-second response times, any request over one second is an error\").\n \n#### Where protocol response codes are insufficient to express all failure conditions, secondary (internal) protocols may be necessary to track partial failure modes. \n\n#### Monitoring these cases can be drastically different: catching HTTP 500s at your load balancer can do a decent job of catching all completely failed requests, while only end-to-end system tests can detect that you’re serving the wrong content."
    }

    widget_area {
      title = "Golden Signals - Errors - FoodMe - Area"
      row = 4
      column = 5
      width = 4
      height = 3

      nrql_query {
        query = "SELECT (count(apm.service.error.count) / count(apm.service.transaction.duration))*100 as 'Errors' FROM Metric WHERE (appName like '%FoodMe%') AND (transactionType = 'Web') SINCE 30 minutes ago TIMESERIES AUTO"
      }
    }

    widget_billboard {
      title = "Golden Signals - Errors - FoodMe - Billboard Compare With"
      row = 4
      column = 9
      width = 4
      height = 3

      nrql_query {
        query = "SELECT (count(apm.service.error.count) / count(apm.service.transaction.duration))*100 as 'Errors' FROM Metric WHERE (appName like '%FoodMe%') AND (transactionType = 'Web') SINCE 30 minutes ago COMPARE WITH 30 minutes ago"
      }
    }

    widget_markdown {
      title = "Golden Signals - Traffic"
      row = 7
      column = 1
      width = 4
      height = 3

      text = "## The Four Golden Signals - Traffic\n---\n\n#### A measure of how much demand is being placed on your system, measured in a high-level system-specific metric. \n\n#### For a web service, this measurement is usually HTTP requests per second, perhaps broken out by the nature of the requests (e.g., static versus dynamic content). \n\n#### For an audio streaming system, this measurement might focus on network I/O rate or concurrent sessions. \n\n#### For a key-value storage system, this measurement might be transactions and retrievals per second."
    }

    widget_table {
      title = "Golden Signals - Traffic - FoodMe - Table"
      row = 7
      column = 5
      width = 4
      height = 3

      nrql_query {
        query = "SELECT rate(count(apm.service.transaction.duration), 1 minute) as 'Traffic' FROM Metric WHERE (appName LIKE '%FoodMe%') AND (transactionType = 'Web') FACET path SINCE 30 minutes ago"
      }
    }

    widget_pie {
      title = "Golden Signals - Traffic - FoodMe - Pie"
      row = 7
      column = 9
      width = 4
      height = 3

      nrql_query {
        query = "SELECT rate(count(apm.service.transaction.duration), 1 minute) as 'Traffic' FROM Metric WHERE (appName LIKE '%FoodMe%') AND (transactionType = 'Web') FACET path SINCE 30 minutes ago"
      }
    }

    widget_markdown {
      title = "Golden Signals - Saturation"
      row = 10
      column = 1
      width = 4
      height = 3

      text = "## The Four Golden Signals - Saturation\n---\n\n#### How \"full\" your service is. A measure of your system fraction, emphasizing the resources that are most constrained (e.g., in a memory-constrained system, show memory; in an I/O-constrained system, show I/O). Note that many systems degrade in performance before they achieve 100% utilization, so having a utilization target is essential.\n\n#### In complex systems, saturation can be supplemented with higher-level load measurement: can your service properly handle double the traffic, handle only 10% more traffic, or handle even less traffic than it currently receives? For very simple services that have no parameters that alter the complexity of the request (e.g., \"Give me a nonce\" or \"I need a globally unique monotonic integer\") that rarely change configuration, a static value from a load test might be adequate. \n\n#### As discussed in the previous paragraph, however, most services need to use indirect signals like CPU utilization or network bandwidth that have a known upper bound. Latency increases are often a leading indicator of saturation. Measuring your 99th percentile response time over some small window (e.g., one minute) can give a very early signal of saturation.\n\n#### Finally, saturation is also concerned with predictions of impending saturation, such as \"It looks like your database will fill its hard drive in 4 hours.\""
    }

    widget_line {
      title = "Golden Signals - Saturation - CPU & Memory - Multi-Queries"
      row = 10
      column = 5
      width = 4
      height = 3

      nrql_query {
        query = "SELECT rate(sum(apm.service.cpu.usertime.utilization), 1 second) * 100 as 'cpuUsed' FROM Metric WHERE appName LIKE '%FoodMe%' SINCE 30 minutes ago TIMESERIES AUTO"
      }

      nrql_query {
        query = "SELECT average(apm.service.memory.physical) * rate(count(apm.service.instance.count), 1 minute) / 1000 as 'memoryUsed %' FROM Metric WHERE appName LIKE '%FoodMe%' SINCE 30 minutes ago TIMESERIES AUTO"
      }
    }

    widget_line {
      title = "Golden Signals - Saturation - Memory - Line Compare With"
      row = 10
      column = 9
      width = 4
      height = 3

      nrql_query {
        query = "SELECT average(apm.service.memory.physical) * rate(count(apm.service.instance.count), 1 minute) / 1000 as 'memoryUsed %' FROM Metric WHERE appName LIKE '%FoodMe%' SINCE 30 minutes ago COMPARE WITH 20 minutes ago TIMESERIES AUTO"
      }
    }
  }
}

variables.tfファイルの完全なコードの例

# your unique New Relic account ID
variable "nr_account_id" {
  default = "XXXXX"
}
# your User API key
variable "nr_api_key" {
  default = "XXXXX"
}

# valid regions are US and EU
variable "nr_region" {
  default = "US"
}

最終的なデプロイ結果の確認

Terraform でダッシュボードをデプロイしたら、New Relic の UI 上でも確認してみましょう。以下のようなダッシュボードが作成されているはずです。