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".
|
||||
# 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" {
|
||||
version = "7.33.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"
|
||||
}
|
||||
|
||||
# Managed via Cloud Build CI/CD — not Terraform
|
||||
# resource "google_cloud_run_v2_service" "agent_service" {
|
||||
# name = var.cloud_run_service_name
|
||||
# location = var.cloud_run_region
|
||||
#
|
||||
# template {
|
||||
# service_account = google_service_account.agent_sa.email
|
||||
#
|
||||
# containers {
|
||||
# image = var.container_image
|
||||
# env {
|
||||
# name = "CUSTOMER_ID"
|
||||
# value = var.customer_id
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# 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"
|
||||
# }
|
||||
resource "google_cloud_run_v2_service" "agent_service" {
|
||||
name = var.cloud_run_service_name
|
||||
location = var.cloud_run_region
|
||||
|
||||
template {
|
||||
service_account = google_service_account.agent_sa.email
|
||||
|
||||
scaling {
|
||||
min_instance_count = 1
|
||||
max_instance_count = 10
|
||||
}
|
||||
|
||||
containers {
|
||||
image = var.container_image
|
||||
|
||||
resources {
|
||||
limits = {
|
||||
cpu = "1"
|
||||
memory = "1Gi"
|
||||
}
|
||||
}
|
||||
|
||||
env {
|
||||
name = "PROJECT_ID"
|
||||
value = var.project_id
|
||||
}
|
||||
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
|
||||
resource "google_bigquery_dataset" "logs" {
|
||||
|
|
@ -219,3 +245,102 @@ resource "google_project_iam_member" "log_sink_bq_writer" {
|
|||
role = "roles/bigquery.dataEditor"
|
||||
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}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,3 +55,9 @@ variable "container_image" {
|
|||
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"
|
||||
version = ">= 5.0"
|
||||
}
|
||||
archive = {
|
||||
source = "hashicorp/archive"
|
||||
version = ">= 2.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user