feat(tf): convert all remaining scripts to terraform
This commit is contained in:
parent
9092f4e002
commit
4c5034df88
|
|
@ -1,6 +1,27 @@
|
||||||
# This file is maintained automatically by "terraform init".
|
# This file is maintained automatically by "terraform init".
|
||||||
# Manual edits may be lost in future updates.
|
# Manual edits may be lost in future updates.
|
||||||
|
|
||||||
|
provider "registry.terraform.io/hashicorp/archive" {
|
||||||
|
version = "2.8.0"
|
||||||
|
constraints = ">= 2.2.0"
|
||||||
|
hashes = [
|
||||||
|
"h1:jdmKm+xl6ZcQrijxapnZ94RVuz/G4vk7hsIa1N0VT5Q=",
|
||||||
|
"zh:0d14713fdc259fb377d0b899ad3c650a34194bd52194c863303ef22a65a580e2",
|
||||||
|
"zh:369b56040c7a8085d04e7e8ffac1e2b321a3170e502f788819bc34b868ec016f",
|
||||||
|
"zh:4d1a3b983ed6af5a52bfe12794674ae55cbadfa6021b37106ade68b433ad216a",
|
||||||
|
"zh:5c547549e26e083573c78a966ca68ce6d7df6bb8f3948f66a575f07da46b74ea",
|
||||||
|
"zh:6de093e62a975eb19a5e3017ce38e6e3cb639c17b79648d2000e0a8348f0e997",
|
||||||
|
"zh:7267936c2cdbc448efeb594d73e6b56a53d6a7ae14fe88cdd2a4133adc3302f0",
|
||||||
|
"zh:7482f023050ed426b4b45116e1761643bc33b1fd4ce4a6fab207ae2571f35940",
|
||||||
|
"zh:76bbd93b234e5a2927d98b511d86565700f549b570871a194c35f944b96cefb7",
|
||||||
|
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||||
|
"zh:c6afc4bc1f002bac9c173007dd4da05fde788cd14c2916089f958c33fedb0dfa",
|
||||||
|
"zh:d3ba40bd806a3a08e9237dece679193c99afb2085de6b45d7f5d1f673cfcd368",
|
||||||
|
"zh:e1ad7ded53ecd6f0e5b473a3b44eae2b2e885653a56050ab583d387332be02e4",
|
||||||
|
"zh:e93e78575ce82be6084cc153c24ba8f385dc8d6880888ee66e918460c870953d",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
provider "registry.terraform.io/hashicorp/google" {
|
provider "registry.terraform.io/hashicorp/google" {
|
||||||
version = "7.33.0"
|
version = "7.33.0"
|
||||||
constraints = ">= 5.0.0"
|
constraints = ">= 5.0.0"
|
||||||
|
|
|
||||||
94
infrastructure/terraform/cost-guard-fn/main.py
Normal file
94
infrastructure/terraform/cost-guard-fn/main.py
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
import functions_framework
|
||||||
|
from google.cloud import secretmanager
|
||||||
|
|
||||||
|
|
||||||
|
def get_webhook_url(project_id: str) -> str | None:
|
||||||
|
try:
|
||||||
|
client = secretmanager.SecretManagerServiceClient()
|
||||||
|
name = f"projects/{project_id}/secrets/webhook-url/versions/latest"
|
||||||
|
response = client.access_secret_version(request={"name": name})
|
||||||
|
return response.payload.data.decode("UTF-8")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def send_webhook(url: str, message: str) -> None:
|
||||||
|
payload = json.dumps({"text": message}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
data=payload,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req, timeout=10):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@functions_framework.cloud_event
|
||||||
|
def billing_alert(cloud_event):
|
||||||
|
project_id = os.environ.get("PROJECT_ID", "")
|
||||||
|
service = os.environ.get("CLOUD_RUN_SERVICE", "")
|
||||||
|
region = os.environ.get("REGION", "")
|
||||||
|
|
||||||
|
# Dekod Pub/Sub-melding
|
||||||
|
data = base64.b64decode(cloud_event.data["message"]["data"]).decode()
|
||||||
|
alert = json.loads(data)
|
||||||
|
|
||||||
|
budget_amount = alert.get("budgetAmount", "?")
|
||||||
|
cost_amount = alert.get("costAmount", "?")
|
||||||
|
alert_threshold = alert.get("alertThresholdExceeded", 0)
|
||||||
|
|
||||||
|
print(f"Billing alert: {cost_amount} / {budget_amount} ({alert_threshold*100:.0f}%)")
|
||||||
|
|
||||||
|
webhook_url = get_webhook_url(project_id)
|
||||||
|
base_msg = (
|
||||||
|
f"🚨 *OSVauco Cost Alert*
|
||||||
|
"
|
||||||
|
f"Prosjekt: `{project_id}`
|
||||||
|
"
|
||||||
|
f"Forbruk: `{cost_amount}` av budsjett `{budget_amount}`
|
||||||
|
"
|
||||||
|
f"Terskel: `{alert_threshold*100:.0f}%`"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Kjør auto-teardown kun ved 100% terskel
|
||||||
|
if alert_threshold >= 1.0:
|
||||||
|
print("100% terskel nådd — starter auto-teardown")
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"gcloud", "run", "services", "delete", service,
|
||||||
|
f"--region={region}",
|
||||||
|
f"--project={project_id}",
|
||||||
|
"--quiet",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
teardown_msg = base_msg + "
|
||||||
|
|
||||||
|
ℹ️ Auto-teardown utført: Cloud Run stoppet."
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
teardown_msg = base_msg + f"
|
||||||
|
|
||||||
|
⚠️ Auto-teardown FEILET: {e.stderr.decode()}"
|
||||||
|
msg = teardown_msg
|
||||||
|
else:
|
||||||
|
msg = base_msg + "
|
||||||
|
|
||||||
|
⚠️ Ingen tiltak nå — men pass på forbruket."
|
||||||
|
|
||||||
|
if webhook_url:
|
||||||
|
try:
|
||||||
|
send_webhook(webhook_url, msg)
|
||||||
|
print("Webhook-alert sendt")
|
||||||
|
except Exception as ex:
|
||||||
|
print(f"Webhook feilet: {ex}")
|
||||||
|
else:
|
||||||
|
print("Ingen webhook-url i Secret Manager — hopper over varsling")
|
||||||
2
infrastructure/terraform/cost-guard-fn/requirements.txt
Normal file
2
infrastructure/terraform/cost-guard-fn/requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
functions-framework==3.*
|
||||||
|
google-cloud-secret-manager>=2.0.0
|
||||||
|
|
@ -139,33 +139,59 @@ resource "google_artifact_registry_repository" "repo" {
|
||||||
format = "DOCKER"
|
format = "DOCKER"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Managed via Cloud Build CI/CD — not Terraform
|
resource "google_cloud_run_v2_service" "agent_service" {
|
||||||
# resource "google_cloud_run_v2_service" "agent_service" {
|
name = var.cloud_run_service_name
|
||||||
# name = var.cloud_run_service_name
|
location = var.cloud_run_region
|
||||||
# location = var.cloud_run_region
|
|
||||||
#
|
template {
|
||||||
# template {
|
service_account = google_service_account.agent_sa.email
|
||||||
# service_account = google_service_account.agent_sa.email
|
|
||||||
#
|
scaling {
|
||||||
# containers {
|
min_instance_count = 1
|
||||||
# image = var.container_image
|
max_instance_count = 10
|
||||||
# env {
|
}
|
||||||
# name = "CUSTOMER_ID"
|
|
||||||
# value = var.customer_id
|
containers {
|
||||||
# }
|
image = var.container_image
|
||||||
# }
|
|
||||||
# }
|
resources {
|
||||||
#
|
limits = {
|
||||||
# depends_on = [google_project_service.apis]
|
cpu = "1"
|
||||||
# }
|
memory = "1Gi"
|
||||||
#
|
}
|
||||||
# resource "google_cloud_run_v2_service_iam_member" "invoker" {
|
}
|
||||||
# project = var.project_id
|
|
||||||
# location = google_cloud_run_v2_service.agent_service.location
|
env {
|
||||||
# name = google_cloud_run_v2_service.agent_service.name
|
name = "PROJECT_ID"
|
||||||
# role = "roles/run.invoker"
|
value = var.project_id
|
||||||
# member = "user:chris.christiansen@vauco.no"
|
}
|
||||||
# }
|
env {
|
||||||
|
name = "REGION"
|
||||||
|
value = var.region
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout = "300s"
|
||||||
|
max_instance_request_concurrency = 80
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
labels = {
|
||||||
|
env = "prod"
|
||||||
|
team = "osvaucoe"
|
||||||
|
agent = "orchestrator"
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [google_project_service.apis]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_cloud_run_v2_service_iam_member" "invoker" {
|
||||||
|
project = var.project_id
|
||||||
|
location = google_cloud_run_v2_service.agent_service.location
|
||||||
|
name = google_cloud_run_v2_service.agent_service.name
|
||||||
|
role = "roles/run.invoker"
|
||||||
|
member = "user:chris.christiansen@vauco.no"
|
||||||
|
}
|
||||||
|
|
||||||
# BigQuery — Cloud Run structured logs
|
# BigQuery — Cloud Run structured logs
|
||||||
resource "google_bigquery_dataset" "logs" {
|
resource "google_bigquery_dataset" "logs" {
|
||||||
|
|
@ -219,3 +245,102 @@ resource "google_project_iam_member" "log_sink_bq_writer" {
|
||||||
role = "roles/bigquery.dataEditor"
|
role = "roles/bigquery.dataEditor"
|
||||||
member = google_logging_project_sink.cloudrun_to_bq.writer_identity
|
member = google_logging_project_sink.cloudrun_to_bq.writer_identity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# From 04-observability-setup.sh
|
||||||
|
resource "google_monitoring_notification_channel" "email" {
|
||||||
|
display_name = "OSVauco Alert Email"
|
||||||
|
type = "email"
|
||||||
|
labels = {
|
||||||
|
email_address = var.alert_email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_logging_metric" "agent_error_count" {
|
||||||
|
name = "agent-error-count"
|
||||||
|
description = "Count of ERROR severity logs from agent"
|
||||||
|
filter = "resource.type=\"cloud_run_revision\" severity=ERROR"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_monitoring_uptime_check_config" "cloud_run_uptime" {
|
||||||
|
display_name = "${var.cloud_run_service_name}-uptime"
|
||||||
|
http_check {
|
||||||
|
path = "/health"
|
||||||
|
port = 443
|
||||||
|
use_ssl = true
|
||||||
|
}
|
||||||
|
monitored_resource {
|
||||||
|
type = "uptime_url"
|
||||||
|
labels = {
|
||||||
|
host = replace(google_cloud_run_v2_service.agent_service.uri, "https://", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
timeout = "10s"
|
||||||
|
period = "60s"
|
||||||
|
}
|
||||||
|
|
||||||
|
# From 10-cost-guard.sh
|
||||||
|
data "archive_file" "cost_guard_fn_zip" {
|
||||||
|
type = "zip"
|
||||||
|
source_dir = "${path.module}/cost-guard-fn"
|
||||||
|
output_path = "${path.module}/cost-guard-fn.zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_storage_bucket_object" "cost_guard_fn_source" {
|
||||||
|
name = "cost-guard-fn-source.zip"
|
||||||
|
bucket = google_storage_bucket.agent_staging.name
|
||||||
|
source = data.archive_file.cost_guard_fn_zip.output_path
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_cloudfunctions2_function" "billing_auto_teardown" {
|
||||||
|
name = "billing-auto-teardown"
|
||||||
|
location = var.region
|
||||||
|
|
||||||
|
build_config {
|
||||||
|
runtime = "python312"
|
||||||
|
entry_point = "billing_alert"
|
||||||
|
source {
|
||||||
|
storage_source {
|
||||||
|
bucket = google_storage_bucket.agent_staging.name
|
||||||
|
object = google_storage_bucket_object.cost_guard_fn_source.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service_config {
|
||||||
|
service_account_email = google_service_account.agent_sa.email
|
||||||
|
environment_variables = {
|
||||||
|
PROJECT_ID = var.project_id
|
||||||
|
CLOUD_RUN_SERVICE = var.cloud_run_service_name
|
||||||
|
REGION = var.region
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event_trigger {
|
||||||
|
trigger_region = "global"
|
||||||
|
event_type = "google.cloud.pubsub.topic.v1.messagePublished"
|
||||||
|
pubsub_topic = google_pubsub_topic.billing_alerts.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data "google_project" "project" {}
|
||||||
|
|
||||||
|
resource "google_project_iam_member" "function_sa_run_admin" {
|
||||||
|
project = var.project_id
|
||||||
|
role = "roles/run.admin"
|
||||||
|
member = "serviceAccount:${data.google_project.project.number}-compute@developer.gserviceaccount.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_project_iam_member" "function_sa_secret_accessor" {
|
||||||
|
project = var.project_id
|
||||||
|
role = "roles/secretmanager.secretAccessor"
|
||||||
|
member = "serviceAccount:${data.google_project.project.number}-compute@developer.gserviceaccount.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
# From 11-billing-iam-hardening.sh
|
||||||
|
resource "google_billing_account_iam_member" "billing_viewers" {
|
||||||
|
for_each = toset(var.billing_viewer_emails)
|
||||||
|
|
||||||
|
billing_account_id = var.billing_account_id
|
||||||
|
role = "roles/billing.viewer"
|
||||||
|
member = "user:${each.key}"
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
project_id = "propane-will-491900-m5"
|
project_id = "propane-will-491900-m5"
|
||||||
region = "europe-west1"
|
region = "europe-west1"
|
||||||
alert_email = "chris.christiansen@vauco.no"
|
alert_email = "chris.christiansen@vauco.no"
|
||||||
billing_account_id = "0171F6-057E6B-A260BA"
|
billing_account_id = "0171F6-057E6B-A260BA"
|
||||||
cloud_run_region = "europe-west1"
|
cloud_run_region = "europe-west1"
|
||||||
artifact_region = "europe-west1"
|
artifact_region = "europe-west1"
|
||||||
cloud_run_service_name = "osvauco-agent"
|
cloud_run_service_name = "osvauco-agent"
|
||||||
artifact_repo_name = "agent-images"
|
artifact_repo_name = "agent-images"
|
||||||
|
|
@ -55,3 +55,9 @@ variable "container_image" {
|
||||||
default = "europe-west1-docker.pkg.dev/propane-will-491900-m5/agent-images/osvauco-agent:latest"
|
default = "europe-west1-docker.pkg.dev/propane-will-491900-m5/agent-images/osvauco-agent:latest"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "billing_viewer_emails" {
|
||||||
|
description = "A list of user emails to be granted billing viewer role."
|
||||||
|
type = list(string)
|
||||||
|
default = ["chris.christiansen@vauco.no", "jason.vauger@vauco.no"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,9 @@ terraform {
|
||||||
source = "hashicorp/google"
|
source = "hashicorp/google"
|
||||||
version = ">= 5.0"
|
version = ">= 5.0"
|
||||||
}
|
}
|
||||||
|
archive = {
|
||||||
|
source = "hashicorp/archive"
|
||||||
|
version = ">= 2.2.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user