Skip to content

Commit 7f4305b

Browse files
authored
feat(terraform): add AD detector provisioning and lifecycle automation (#1680)
* feat(terraform): add AD detector provisioning and lifecycle automation - add scripts/terraform/main.tf to configure OpenSearch provider and detector variables - create detector via opensearch_anomaly_detection and output detector_id - add local-exec start/stop hooks to make detector job handling idempotent on apply/destroy - add scripts/terraform/README.md with prerequisites, configuration, and usage steps Testing done: - Verified Terraform can create and start the detector. Signed-off-by: kaituo <kaituo@amazon.com> * address comments Signed-off-by: kaituo <kaituo@amazon.com> --------- Signed-off-by: kaituo <kaituo@amazon.com>
1 parent a5e1ae3 commit 7f4305b

File tree

7 files changed

+388
-29
lines changed

7 files changed

+388
-29
lines changed

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,9 @@ out/
1515
.vscode
1616
bin/
1717
._.DS_Store
18-
src/test/resources/job-scheduler/
18+
src/test/resources/job-scheduler/
19+
20+
# Terraform local artifacts and secrets
21+
.terraform/
22+
*.tfstate*
23+
*.tfvars

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
1010
- Introduce Insights API ([1610](https://github.com/opensearch-project/anomaly-detection/pull/1610))
1111

1212
### Enhancements
13+
- feat(terraform): add AD detector provisioning and lifecycle automation ([1680](https://github.com/opensearch-project/anomaly-detection/pull/1680))
14+
1315
### Bug Fixes
1416
### Infrastructure
1517
- Add integTest coverage reporting and README badge ([1679](https://github.com/opensearch-project/anomaly-detection/pull/1679))

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[![AD Test](https://github.com/opensearch-project/anomaly-detection/workflows/Build%20and%20Test%20Anomaly%20detection/badge.svg)](https://github.com/opensearch-project/anomaly-detection/actions?query=workflow%3A%22Build+and+Test+Anomaly+detection%22+branch%3A%22main%22)
2-
[![codecov](https://codecov.io/gh/opensearch-project/anomaly-detection/branch/main/graph/badge.svg?flag=plugin)](https://codecov.io/gh/opensearch-project/anomaly-detection)
3-
[![codecov integTest](https://codecov.io/gh/opensearch-project/anomaly-detection/branch/main/graph/badge.svg?flag=plugin_integtest)](https://codecov.io/gh/opensearch-project/anomaly-detection)
2+
[![Plugin Coverage](https://img.shields.io/codecov/c/github/opensearch-project/anomaly-detection/main?flag=plugin&label=Plugin%20coverage)](https://codecov.io/gh/opensearch-project/anomaly-detection)
3+
[![IntegTest Coverage](https://img.shields.io/codecov/c/github/opensearch-project/anomaly-detection/main?flag=plugin_integtest&label=IntegTest%20coverage)](https://codecov.io/gh/opensearch-project/anomaly-detection)
44
[![Documentation](https://img.shields.io/badge/doc-reference-blue)](https://opensearch.org/docs/monitoring-plugins/ad/index/)
55
[![Forum](https://img.shields.io/badge/chat-on%20forums-blue)](https://forum.opensearch.org/)
66
![PRs welcome!](https://img.shields.io/badge/PRs-welcome!-success)

scripts/terraform/README.md

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# OpenSearch Anomaly Detection with Terraform
2+
3+
## Purpose
4+
5+
This project uses Terraform to manage an OpenSearch Anomaly Detection detector and its job lifecycle.
6+
7+
It does two things:
8+
9+
- Creates or updates a detector via `opensearch_anomaly_detection`.
10+
- Automatically stops and restarts the detector job on apply (and stops it on destroy) using `null_resource` + `local-exec` calls to the OpenSearch AD APIs.
11+
12+
## What This Configuration Creates
13+
14+
- 1 anomaly detector with:
15+
- configurable index pattern (`indices`)
16+
- configurable time field (`time_field`)
17+
- one feature using `max(<feature_field>)`
18+
- configurable detection interval and window delay
19+
- configurable result index
20+
- Output:
21+
- `detector_id`
22+
23+
## Prerequisites
24+
25+
- Terraform `>= 1.5.0`
26+
- OpenSearch cluster reachable from your machine
27+
- OpenSearch Anomaly Detection plugin/API available
28+
- `curl` installed (used by `local-exec` provisioners)
29+
30+
## Configuration
31+
32+
Defaults in [`main.tf`](main.tf) target local development:
33+
34+
- `opensearch_url = "http://localhost:9200"`
35+
- `opensearch_username = ""`
36+
- `opensearch_password = ""`
37+
38+
You can change `opensearch_url` to your remote OpenSearch endpoint, for example `https://your-cluster.example.com:9200`.
39+
40+
If your cluster has security enabled, prefer environment variables for credentials in production. `terraform.tfvars` and CLI flags also work, but they are less suitable for secrets.
41+
42+
Preferred example: environment variables
43+
44+
```bash
45+
export TF_VAR_opensearch_url='https://your-cluster.example.com:9200'
46+
export TF_VAR_opensearch_username='admin'
47+
export TF_VAR_opensearch_password='myStrongPassword123!'
48+
49+
terraform plan
50+
```
51+
52+
Example `terraform.tfvars`:
53+
54+
```hcl
55+
opensearch_url = "https://your-cluster.example.com:9200"
56+
opensearch_username = "admin"
57+
opensearch_password = "myStrongPassword123!"
58+
```
59+
60+
Example CLI flags:
61+
62+
```bash
63+
terraform plan \
64+
-var='opensearch_url=https://your-cluster.example.com:9200' \
65+
-var='opensearch_username=admin' \
66+
-var='opensearch_password=myStrongPassword123!'
67+
```
68+
69+
Avoid `-var` for passwords in production when possible, since command-line arguments can leak into shell history or CI logs.
70+
71+
If you use `terraform.tfvars`, make sure it is excluded from version control.
72+
73+
Important: this configuration currently stores connection settings in `null_resource` triggers so the destroy-time provisioner can stop the detector job. That means credentials may still be written to Terraform state even when provided via `TF_VAR_...` environment variables.
74+
75+
Common detector variables:
76+
77+
- `detector_name`
78+
- `indices`
79+
- `time_field`
80+
- `feature_field`
81+
- `detection_interval_minutes`
82+
- `window_delay_minutes`
83+
- `result_index`
84+
85+
## How To Use
86+
87+
1. Initialize providers:
88+
89+
```bash
90+
terraform init
91+
```
92+
93+
2. (Optional) Create `terraform.tfvars`:
94+
95+
```hcl
96+
opensearch_url = "http://localhost:9200"
97+
opensearch_username = ""
98+
opensearch_password = ""
99+
100+
detector_name = "tf-detector"
101+
indices = ["server-metrics"]
102+
time_field = "@timestamp"
103+
feature_field = "deny"
104+
detection_interval_minutes = 1
105+
window_delay_minutes = 1
106+
result_index = "opensearch-ad-plugin-result-tf"
107+
```
108+
109+
3. Review the plan:
110+
111+
```bash
112+
terraform plan
113+
```
114+
115+
4. Apply:
116+
117+
```bash
118+
terraform apply
119+
```
120+
121+
5. Get the detector ID:
122+
123+
```bash
124+
terraform output detector_id
125+
```
126+
127+
## Destroy
128+
129+
To remove resources:
130+
131+
```bash
132+
terraform destroy
133+
```
134+
135+
The configuration attempts to stop the detector job before deletion.
136+
137+
## Notes
138+
139+
- `null_resource.start_detector_job` is trigger-based and re-runs when detector config or connection settings change.
140+
- Credentials are optional for unsecured local clusters; provide them for secured clusters.

scripts/terraform/main.tf

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
terraform {
2+
required_version = ">= 1.5.0"
3+
4+
required_providers {
5+
opensearch = {
6+
source = "opensearch-project/opensearch"
7+
version = "~> 2.3.2"
8+
}
9+
null = {
10+
source = "hashicorp/null"
11+
version = "~> 3.2"
12+
}
13+
}
14+
}
15+
16+
############################
17+
# Connection (localhost)
18+
############################
19+
variable "opensearch_url" {
20+
type = string
21+
default = "http://localhost:9200"
22+
}
23+
24+
# Leave these empty if your local cluster has security disabled.
25+
variable "opensearch_username" {
26+
type = string
27+
default = "" # e.g., "admin"
28+
}
29+
30+
variable "opensearch_password" {
31+
type = string
32+
default = "" # e.g., "admin" or your configured password
33+
sensitive = true
34+
}
35+
36+
provider "opensearch" {
37+
url = var.opensearch_url
38+
username = var.opensearch_username
39+
password = var.opensearch_password
40+
}
41+
42+
############################
43+
# Detector config
44+
############################
45+
variable "detector_name" {
46+
type = string
47+
default = "tf-detector"
48+
}
49+
50+
variable "indices" {
51+
type = list(string)
52+
default = ["server-metrics"] # <-- change to your index / pattern
53+
}
54+
55+
variable "time_field" {
56+
type = string
57+
default = "@timestamp" # <-- change to your time field
58+
}
59+
60+
variable "feature_field" {
61+
type = string
62+
default = "deny" # <-- change to your numeric field
63+
}
64+
65+
variable "detection_interval_minutes" {
66+
type = number
67+
default = 1
68+
}
69+
70+
variable "window_delay_minutes" {
71+
type = number
72+
default = 1
73+
}
74+
75+
variable "result_index" {
76+
type = string
77+
default = "opensearch-ad-plugin-result-tf"
78+
}
79+
80+
locals {
81+
detector_body = {
82+
name = var.detector_name
83+
description = "Detector created by Terraform"
84+
time_field = var.time_field
85+
indices = var.indices
86+
87+
feature_attributes = [
88+
{
89+
feature_name = "feature_1"
90+
feature_enabled = true
91+
aggregation_query = {
92+
feature_1 = {
93+
max = { field = var.feature_field }
94+
}
95+
}
96+
}
97+
]
98+
99+
detection_interval = {
100+
period = {
101+
interval = var.detection_interval_minutes
102+
unit = "Minutes"
103+
}
104+
}
105+
106+
window_delay = {
107+
period = {
108+
interval = var.window_delay_minutes
109+
unit = "Minutes"
110+
}
111+
}
112+
113+
result_index = var.result_index
114+
}
115+
116+
detector_body_json = jsonencode(local.detector_body)
117+
detector_body_sha = sha256(local.detector_body_json)
118+
}
119+
120+
resource "opensearch_anomaly_detection" "detector" {
121+
body = local.detector_body_json
122+
}
123+
124+
############################
125+
# Start / stop the job
126+
############################
127+
resource "null_resource" "start_detector_job" {
128+
# Re-run if the detector is recreated OR its config changes.
129+
triggers = {
130+
detector_id = opensearch_anomaly_detection.detector.id
131+
detector_body_sha = local.detector_body_sha
132+
opensearch_url = var.opensearch_url
133+
opensearch_user = var.opensearch_username
134+
opensearch_pass = var.opensearch_password
135+
}
136+
137+
provisioner "local-exec" {
138+
interpreter = ["/bin/bash", "-c"]
139+
environment = {
140+
OPENSEARCH_URL = var.opensearch_url
141+
OPENSEARCH_USERNAME = var.opensearch_username
142+
OPENSEARCH_PASSWORD = var.opensearch_password
143+
DETECTOR_ID = self.triggers.detector_id
144+
}
145+
146+
command = <<EOT
147+
set -eo pipefail
148+
149+
# Make apply idempotent across re-runs/config updates:
150+
if [ -n "$OPENSEARCH_USERNAME" ] || [ -n "$OPENSEARCH_PASSWORD" ]; then
151+
curl -sS -XPOST "$OPENSEARCH_URL/_plugins/_anomaly_detection/detectors/$DETECTOR_ID/_stop" \
152+
-H 'Content-Type: application/json' \
153+
--user "$OPENSEARCH_USERNAME:$OPENSEARCH_PASSWORD" >/dev/null || true
154+
155+
curl -sSf -XPOST "$OPENSEARCH_URL/_plugins/_anomaly_detection/detectors/$DETECTOR_ID/_start" \
156+
-H 'Content-Type: application/json' \
157+
--user "$OPENSEARCH_USERNAME:$OPENSEARCH_PASSWORD" >/dev/null
158+
else
159+
curl -sS -XPOST "$OPENSEARCH_URL/_plugins/_anomaly_detection/detectors/$DETECTOR_ID/_stop" \
160+
-H 'Content-Type: application/json' >/dev/null || true
161+
162+
curl -sSf -XPOST "$OPENSEARCH_URL/_plugins/_anomaly_detection/detectors/$DETECTOR_ID/_start" \
163+
-H 'Content-Type: application/json' >/dev/null
164+
fi
165+
EOT
166+
}
167+
168+
# Stop before the detector is deleted (destroy order is reverse of depends_on).
169+
provisioner "local-exec" {
170+
when = destroy
171+
interpreter = ["/bin/bash", "-c"]
172+
environment = {
173+
OPENSEARCH_URL = self.triggers.opensearch_url
174+
OPENSEARCH_USERNAME = self.triggers.opensearch_user
175+
OPENSEARCH_PASSWORD = self.triggers.opensearch_pass
176+
DETECTOR_ID = self.triggers.detector_id
177+
}
178+
179+
command = <<EOT
180+
set -eo pipefail
181+
182+
if [ -n "$OPENSEARCH_USERNAME" ] || [ -n "$OPENSEARCH_PASSWORD" ]; then
183+
curl -sS -XPOST "$OPENSEARCH_URL/_plugins/_anomaly_detection/detectors/$DETECTOR_ID/_stop" \
184+
-H 'Content-Type: application/json' \
185+
--user "$OPENSEARCH_USERNAME:$OPENSEARCH_PASSWORD" >/dev/null || true
186+
else
187+
curl -sS -XPOST "$OPENSEARCH_URL/_plugins/_anomaly_detection/detectors/$DETECTOR_ID/_stop" \
188+
-H 'Content-Type: application/json' >/dev/null || true
189+
fi
190+
EOT
191+
}
192+
193+
depends_on = [opensearch_anomaly_detection.detector]
194+
}
195+
196+
output "detector_id" {
197+
value = opensearch_anomaly_detection.detector.id
198+
}

0 commit comments

Comments
 (0)