Skip to content

Commit 677c5d9

Browse files
committed
chore: adding the initial commit
0 parents  commit 677c5d9

File tree

7 files changed

+407
-0
lines changed

7 files changed

+407
-0
lines changed

.github/workflows/release.yml

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
name: Release
3+
4+
on:
5+
push:
6+
tags:
7+
- "v*"
8+
9+
jobs:
10+
release:
11+
uses: appvia/appvia-cicd-workflows/.github/workflows/terraform-module-release.yml@main
12+
name: GitHub Release

.github/workflows/terraform.yml

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
name: Terraform
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
11+
jobs:
12+
module-validation:
13+
uses: appvia/appvia-cicd-workflows/.github/workflows/terraform-module-validation.yml@main
14+
name: Module Validation
15+
with:
16+
working-directory: .

.terraform.lock.hcl

+85
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<!-- BEGIN_TF_DOCS -->
2+
## Requirements
3+
4+
| Name | Version |
5+
|------|---------|
6+
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.0 |
7+
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | ~> 5.0 |
8+
9+
## Providers
10+
11+
| Name | Version |
12+
|------|---------|
13+
| <a name="provider_aws"></a> [aws](#provider\_aws) | 5.40.0 |
14+
15+
## Modules
16+
17+
| Name | Source | Version |
18+
|------|--------|---------|
19+
| <a name="module_slack_notfications"></a> [slack\_notfications](#module\_slack\_notfications) | terraform-aws-modules/notify-slack/aws | 6.1.1 |
20+
| <a name="module_sns"></a> [sns](#module\_sns) | terraform-aws-modules/sns/aws | v6.0.1 |
21+
22+
## Resources
23+
24+
| Name | Type |
25+
|------|------|
26+
| [aws_ce_anomaly_monitor.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ce_anomaly_monitor) | resource |
27+
| [aws_ce_anomaly_subscription.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ce_anomaly_subscription) | resource |
28+
| [aws_sns_topic_subscription.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic_subscription) | resource |
29+
| [aws_sns_topic.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/sns_topic) | data source |
30+
31+
## Inputs
32+
33+
| Name | Description | Type | Default | Required |
34+
|------|-------------|------|---------|:--------:|
35+
| <a name="input_create_sns_topic"></a> [create\_sns\_topic](#input\_create\_sns\_topic) | Indicates whether to create an SNS topic for notifications | `bool` | `true` | no |
36+
| <a name="input_monitors"></a> [monitors](#input\_monitors) | A collection of cost anomaly monitors to create | <pre>list(object({<br> name = string<br> monitor_type = optional(string, "DIMENSIONAL")<br> monitor_dimension = optional(string, "SERVICE")<br> monitor_specification = optional(string, null)<br><br> notify = optional(object({<br> frequency = string<br> threshold_expression = optional(any, null)<br> }), {<br> frequency = "DAILY"<br> })<br> }))</pre> | n/a | yes |
37+
| <a name="input_notification"></a> [notification](#input\_notification) | The configuration of the notification | <pre>object({<br> email = optional(object({<br> addresses = list(string)<br> }), null)<br> slack = optional(object({<br> channel = string<br> webhook_url = string<br> }), null)<br> teams = optional(object({<br> webhook_url = string<br> }), null)<br> })</pre> | n/a | yes |
38+
| <a name="input_sns_topic_name"></a> [sns\_topic\_name](#input\_sns\_topic\_name) | The name of an existing or new SNS topic for notifications | `string` | `"cost-anomaly-notifications"` | no |
39+
| <a name="input_tags"></a> [tags](#input\_tags) | A map of tags to add to all resources | `map(string)` | n/a | yes |
40+
41+
## Outputs
42+
43+
No outputs.
44+
<!-- END_TF_DOCS -->

main.tf

+186
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
#
2+
## Related to the provisioning of services
3+
#
4+
5+
locals {
6+
# The SNS topic ARN to use for the cost anomaly detection
7+
sns_topic_arn = var.create_sns_topic ? module.sns[0].topic_arn : data.aws_sns_topic.current[0].arn
8+
}
9+
10+
#
11+
## We need to lookup the SNS topic ARN, if it exists
12+
#
13+
data "aws_sns_topic" "current" {
14+
count = var.create_sns_topic ? 0 : 1
15+
name = var.sns_topic_name
16+
}
17+
18+
#
19+
## Provision the SNS topic for the cost anomaly detection, if required
20+
#
21+
module "sns" {
22+
source = "terraform-aws-modules/sns/aws"
23+
version = "v6.0.1"
24+
count = var.create_sns_topic ? 1 : 0
25+
26+
name = var.sns_topic_name
27+
tags = var.tags
28+
topic_policy_statements = {
29+
"AllowBudgetsToNotifySNSTopic" = {
30+
actions = ["sns:Publish"]
31+
effect = "Allow"
32+
principals = [{
33+
type = "Service"
34+
identifiers = ["budgets.amazonaws.com"]
35+
}]
36+
}
37+
"AllowLambda" = {
38+
actions = [
39+
"sns:Subscribe",
40+
]
41+
effect = "Allow"
42+
principals = [{
43+
type = "Service"
44+
identifiers = ["lambda.amazonaws.com"]
45+
}]
46+
}
47+
}
48+
}
49+
50+
#
51+
## Provision the cost anomaly detection for services
52+
#
53+
resource "aws_ce_anomaly_monitor" "this" {
54+
for_each = { for x in var.monitors : x.name => x }
55+
56+
name = each.value.name
57+
monitor_type = each.value.monitor_type
58+
monitor_dimension = each.value.monitor_dimension
59+
#monitor_specification = each.value.monitor_specification != "" ? jsonencode(each.value.monitor_specification) : ""
60+
tags = var.tags
61+
}
62+
63+
#
64+
## Provision the subscriptions to the anomaly detection monitors
65+
#
66+
resource "aws_ce_anomaly_subscription" "this" {
67+
for_each = { for x in var.monitors : x.name => x }
68+
69+
name = each.value.name
70+
frequency = each.value.notify.frequency
71+
monitor_arn_list = [aws_ce_anomaly_monitor.this[each.key].arn]
72+
tags = var.tags
73+
74+
dynamic "threshold_expression" {
75+
for_each = [each.value.notify.threshold_expression != null ? [1] : []]
76+
content {
77+
dynamic "dimension" {
78+
for_each = [for x in each.value.notify.threshold_expression : x if lookup(x, "dimension", null) != null]
79+
content {
80+
key = dimension.value.key
81+
match_options = dimension.value.match_options
82+
values = dimension.value.values
83+
}
84+
}
85+
86+
dynamic "cost_category" {
87+
for_each = [for x in each.value.notify.threshold_expression : x if lookup(x, "cost_category", null) != null]
88+
content {
89+
key = cost_category.value.key
90+
match_options = cost_category.value.match_options
91+
values = cost_category.value.values
92+
}
93+
}
94+
95+
dynamic "tags" {
96+
for_each = [for x in each.value.notify.threshold_expression : x if lookup(x, "tags", null) != null]
97+
content {
98+
key = tags.value.key
99+
match_options = tags.value.match_options
100+
values = tags.value.values
101+
}
102+
}
103+
104+
dynamic "and" {
105+
for_each = [for x in each.value.notify.threshold_expression : x if lookup(x, "and", null) != null]
106+
content {
107+
dynamic "dimension" {
108+
for_each = and.value.and.dimension != null ? [and.value.and.dimension] : []
109+
content {
110+
key = dimension.value.key
111+
match_options = dimension.value.match_options
112+
values = dimension.value.values
113+
}
114+
}
115+
}
116+
}
117+
118+
dynamic "or" {
119+
for_each = [for x in each.value.notify.threshold_expression : x if lookup(x, "or", null) != null]
120+
content {
121+
dynamic "dimension" {
122+
for_each = or.value.or.dimension != null ? [or.value.or.dimension] : []
123+
content {
124+
key = dimension.value.key
125+
match_options = dimension.value.match_options
126+
values = dimension.value.values
127+
}
128+
}
129+
}
130+
}
131+
132+
dynamic "not" {
133+
for_each = [for x in each.value.notify.threshold_expression : x if lookup(x, "not", null) != null]
134+
content {
135+
dynamic "dimension" {
136+
for_each = not.value.not.dimension != null ? [not.value.not.dimension] : []
137+
content {
138+
key = dimension.value.key
139+
match_options = dimension.value.match_options
140+
values = dimension.value.values
141+
}
142+
}
143+
}
144+
}
145+
}
146+
}
147+
148+
subscriber {
149+
address = local.sns_topic_arn
150+
type = "SNS"
151+
}
152+
153+
depends_on = [module.sns]
154+
}
155+
156+
#
157+
## Provision any additional notification subscriptions (email)
158+
#
159+
resource "aws_sns_topic_subscription" "main" {
160+
for_each = { for x in var.notification.email.addresses : x => x }
161+
162+
endpoint = each.value
163+
protocol = "email"
164+
topic_arn = local.sns_topic_arn
165+
}
166+
167+
168+
#
169+
## Provision a slack notification if required
170+
#
171+
# tfsec:ignore:aws-lambda-enable-tracing
172+
# tfsec:ignore:aws-lambda-restrict-source-arn
173+
module "slack_notfications" {
174+
count = var.notification.slack != null ? 1 : 0
175+
source = "terraform-aws-modules/notify-slack/aws"
176+
version = "6.1.1"
177+
178+
create_sns_topic = false
179+
lambda_function_name = "cost-anomaly-detection"
180+
slack_channel = var.notification.slack.channel
181+
slack_username = ":aws: (Cost Anomaly Detection)"
182+
slack_webhook_url = var.notification.slack.webhook_url
183+
sns_topic_name = var.sns_topic_name
184+
tags = var.tags
185+
}
186+

variables.tf

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
2+
variable "tags" {
3+
description = "A map of tags to add to all resources"
4+
type = map(string)
5+
}
6+
7+
variable "monitors" {
8+
description = "A collection of cost anomaly monitors to create"
9+
type = list(object({
10+
name = string
11+
monitor_type = optional(string, "DIMENSIONAL")
12+
monitor_dimension = optional(string, "SERVICE")
13+
monitor_specification = optional(string, null)
14+
15+
notify = optional(object({
16+
frequency = string
17+
threshold_expression = optional(any, null)
18+
}), {
19+
frequency = "DAILY"
20+
})
21+
}))
22+
}
23+
24+
variable "notification" {
25+
description = "The configuration of the notification"
26+
type = object({
27+
email = optional(object({
28+
addresses = list(string)
29+
}), null)
30+
slack = optional(object({
31+
channel = string
32+
webhook_url = string
33+
}), null)
34+
teams = optional(object({
35+
webhook_url = string
36+
}), null)
37+
})
38+
}
39+
40+
variable "create_sns_topic" {
41+
description = "Indicates whether to create an SNS topic for notifications"
42+
type = bool
43+
default = true
44+
}
45+
46+
variable "sns_topic_name" {
47+
description = "The name of an existing or new SNS topic for notifications"
48+
type = string
49+
default = "cost-anomaly-notifications"
50+
}

0 commit comments

Comments
 (0)