Tofu code and AWX first
Build and Publish Docker Image / Build and Push Docker Image (push) Successful in 2m13s Details

This commit is contained in:
Isaac Johnson 2026-09-21 19:03:45 -05:00
parent 94ef8585b8
commit df9960fe32
7 changed files with 243 additions and 121 deletions

42
.gitignore vendored
View File

@ -1,5 +1,5 @@
# Created by https://www.toptal.com/developers/gitignore/api/python,linux # Created by https://www.toptal.com/developers/gitignore/api/linux,python,terraform
# Edit at https://www.toptal.com/developers/gitignore?templates=python,linux # Edit at https://www.toptal.com/developers/gitignore?templates=linux,python,terraform
### Linux ### ### Linux ###
*~ *~
@ -188,4 +188,40 @@ poetry.toml
# LSP config files # LSP config files
pyrightconfig.json pyrightconfig.json
# End of https://www.toptal.com/developers/gitignore/api/python,linux ### Terraform ###
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
# Crash log files
crash.log
crash.*.log
# Exclude all .tfvars files, which are likely to contain sensitive data, such as
# password, private keys, and other secrets. These should not be part of version
# control as they are data points which are potentially sensitive and subject
# to change depending on the environment.
*.tfvars
*.tfvars.json
# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Include override files you do wish to add to version control using negated pattern
# !example_override.tf
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*
# Ignore CLI configuration files
.terraformrc
terraform.rc
# End of https://www.toptal.com/developers/gitignore/api/linux,python,terraform

167
awx/uptime_app.yaml Normal file
View File

@ -0,0 +1,167 @@
---
- name: Deploy Uptime Kuma on GCP and Configure Non-Interactively
hosts: localhost
connection: local
gather_facts: false
vars:
# GCP Configuration
gcp_project: "your-gcp-project-id"
gcp_region: "us-central1"
gcp_zone: "us-central1-a"
instance_name: "uptime-kuma-vm"
machine_type: "e2-small" # 2GB RAM is plenty for Uptime Kuma
static_ip_name: "uptime-kuma-ip"
firewall_rule_name: "allow-uptime-kuma-3001"
# Uptime Kuma Credentials & Target
kuma_admin_user: "admin"
kuma_admin_pass: "ChangeMeImmediately123!"
target_monitor_name: "Target Web App"
target_monitor_url: "https://example.com"
tasks:
# -------------------------------------------------------------
# 1. Local Prerequisites
# -------------------------------------------------------------
- name: Ensure uptime-kuma-api Python library is installed locally
ansible.builtin.pip:
name: uptime-kuma-api
state: present
# -------------------------------------------------------------
# 2. Allocate Static Public IP (Idempotent)
# -------------------------------------------------------------
- name: Check if static IP exists
ansible.builtin.command: >
gcloud compute addresses describe {{ static_ip_name }}
--region={{ gcp_region }}
--project={{ gcp_project }}
register: ip_check
failed_when: false
changed_when: false
- name: Allocate static external IP in GCP
ansible.builtin.command: >
gcloud compute addresses create {{ static_ip_name }}
--region={{ gcp_region }}
--project={{ gcp_project }}
--description="Static IP for Uptime Kuma"
when: ip_check.rc != 0
# -------------------------------------------------------------
# 3. Create Firewall Rule (Port 3001)
# -------------------------------------------------------------
- name: Check if firewall rule exists
ansible.builtin.command: >
gcloud compute firewall-rules describe {{ firewall_rule_name }}
--project={{ gcp_project }}
register: fw_check
failed_when: false
changed_when: false
- name: Allow TCP traffic to port 3001
ansible.builtin.command: >
gcloud compute firewall-rules create {{ firewall_rule_name }}
--project={{ gcp_project }}
--allow=tcp:3001
--target-tags=uptime-kuma
--source-ranges=0.0.0.0/0
--description="Allow traffic to Uptime Kuma UI & API"
when: fw_check.rc != 0
# -------------------------------------------------------------
# 4. Provision VM with Container-Optimized OS
# -------------------------------------------------------------
- name: Check if instance already exists
ansible.builtin.command: >
gcloud compute instances describe {{ instance_name }}
--zone={{ gcp_zone }}
--project={{ gcp_project }}
register: instance_check
failed_when: false
changed_when: false
- name: Launch Compute Engine VM with Uptime Kuma container
ansible.builtin.command: >
gcloud compute instances create-with-container {{ instance_name }}
--project={{ gcp_project }}
--zone={{ gcp_zone }}
--machine-type={{ machine_type }}
--tags=uptime-kuma
--address={{ static_ip_name }}
--boot-disk-size=20GB
--container-image=louislam/uptime-kuma:1
--container-mount-host-path=host-path=/var/uptime-kuma,mount-path=/app/data,mode=rw
when: instance_check.rc != 0
# -------------------------------------------------------------
# 5. Fetch Public IP and Wait for Service Readiness
# -------------------------------------------------------------
- name: Fetch instance public IP address
ansible.builtin.command: >
gcloud compute instances describe {{ instance_name }}
--zone={{ gcp_zone }}
--project={{ gcp_project }}
--format="value(networkInterfaces[0].accessConfigs[0].natIP)"
register: vm_ip
changed_when: false
- name: Display Uptime Kuma URL
ansible.builtin.debug:
msg: "Uptime Kuma is booting at: http://{{ vm_ip.stdout.strip() }}:3001"
- name: Wait for Uptime Kuma to respond with HTTP 200
ansible.builtin.uri:
url: "http://{{ vm_ip.stdout.strip() }}:3001"
status_code: 200
register: readiness
until: readiness.status == 200
retries: 30
delay: 6
# -------------------------------------------------------------
# 6. Non-Interactive Configuration via API
# -------------------------------------------------------------
- name: Perform headless setup and add monitor
ansible.builtin.command:
cmd: >
python3 -c "
import sys
from uptime_kuma_api import UptimeKumaApi, MonitorType
kuma_url = 'http://{{ vm_ip.stdout.strip() }}:3001'
api = UptimeKumaApi(kuma_url)
# 1. Initial admin account creation if not yet initialized
if api.need_setup():
api.setup('{{ kuma_admin_user }}', '{{ kuma_admin_pass }}')
print('ADMIN_CREATED')
# 2. Login
api.login('{{ kuma_admin_user }}', '{{ kuma_admin_pass }}')
# 3. Add monitor idempotently
current_monitors = [m['name'] for m in api.get_monitors()]
if '{{ target_monitor_name }}' not in current_monitors:
api.add_monitor(
type=MonitorType.HTTP,
name='{{ target_monitor_name }}',
url='{{ target_monitor_url }}',
interval=60
)
print('MONITOR_ADDED')
else:
print('MONITOR_ALREADY_EXISTS')
api.disconnect()
"
register: setup_output
changed_when: "'MONITOR_ADDED' in setup_output.stdout"
- name: Show setup summary
ansible.builtin.debug:
msg:
- "Setup Status: {{ setup_output.stdout.strip() }}"
- "Access UI at: http://{{ vm_ip.stdout.strip() }}:3001"
- "Username: {{ kuma_admin_user }}"

View File

@ -1,42 +1,6 @@
# This file is maintained automatically by "tofu init". # This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates. # Manual edits may be lost in future updates.
provider "registry.opentofu.org/hashicorp/archive" {
version = "2.8.1"
hashes = [
"h1:+zxWNwWTN6nQH4UsAHVU2dHFeQ1dbyti1imUwXo2UkM=",
"h1:0nQbGAtw2FkzedC/AoP5stVvIEQT7ae04UDUE7K+rkQ=",
"h1:4BH/NzcaIQ+HtfGhBo9boDdYTYLimD9Aw81bXyx4PQ8=",
"h1:6cIzOvUMhFPYYCVpuraJCp1OBKQfIdt+vweg/t90nqY=",
"h1:D0qy1mIABEFzQEwuASo4g+oGRgxodYsHhbU2VzmafkY=",
"h1:Dwonz53sAPonmKnk+YNwbjw0uEatDVPTj/dtfCwBQVw=",
"h1:FQLyzsDwRhDvov/FU4I/HTnt9jdNFivK1hmBVxhgj5Y=",
"h1:FwIDfXJo5yqRq9DjC8u3cXfGVQhHje3bpdxUufTT4wI=",
"h1:M561F8TZn57Si9KJ8ycdyGGOIDSx6uzeILfn3ODTZ7I=",
"h1:Yg4us0ttDVnZnECv/eo1Scg2D00bZ8ENxD47TVt/HYQ=",
"h1:l7EcsA0WkfQ1ylEm7q1xfJei3bUMFriPybZLIuRCkzg=",
"h1:oOm3HeeUHp36HGT2EXEoLX+B3mGz+MIF8BazNsAOB7I=",
"h1:rhohSLLoDQyaUzetRvvlrbjZ2LAEfVCyUdD/Or10jos=",
"h1:wcukYOssMr3IzGWQUMQkX7On5WeBmBTNIpvPsK3ac90=",
"h1:xTRO4rh/BOjz11omsMID2q0MCchmCTX+tgI6v5/OMQw=",
"zh:167dcb2f3dfef941d300e4899c6b1852592e98cd5e20782289369bf0c81512f0",
"zh:1e0bbf0e538631a49746ff23c7008c7de7d0850e4477a1aee7685cc9881a942a",
"zh:4310e3fc60442f17165fef1a97eae8be3778ae9611a526eb5b19647ffbab98bc",
"zh:5700d38d2dbf82f1c88dd72aae3cadd12b177554a4ef99bea9bb949fb1961412",
"zh:69e2209971a4f87e45c01d3e7581f6864abb1502e7c44809b4b0ffdfdc1d3d5d",
"zh:6adbd69f1279d29dfe17fc7eab8e72fffa9ab0c562f87a3f73aaf5d63fe352d8",
"zh:76e3cdadb03e6c8a7a5d535d7b3c2509a7e5ba9c37fac66ca360d271f5dc5c17",
"zh:8a5bb2ce0746b28f12770e473022076c55e32ea942b514150cf2b07fa673d48a",
"zh:9612186dd905850d20144101c722da59a2de660c2bf931a786ac63fe3766f5a6",
"zh:a77922d3edcc1288d52e5d225e0b98d6ae8b3ef8ef4f27a40a6c11136489b886",
"zh:ac70801bfaa2d39e339c39d4989199981b4d4dcbd0cf1c65a9755940ef2a80ee",
"zh:af382792a3ca715ef9f740f18ee1b1c4256e757e93fd5204ab5f6c1757ba2555",
"zh:c2bd89b4fc81883f410851f3e4499ee9b2d14039b97a04a2edc9fb4577bcb38e",
"zh:f9a8ee2c68f67bc6eefbce642594d4fd51247542599bed8ac47ec8139615c2ce",
"zh:fa64f6907c6daf715ad0c435b410fc352567118297f143ee2760230b4592bfde",
]
}
provider "registry.opentofu.org/hashicorp/azurerm" { provider "registry.opentofu.org/hashicorp/azurerm" {
version = "4.81.0" version = "4.81.0"
constraints = "~> 4.0" constraints = "~> 4.0"

Binary file not shown.

View File

@ -28,13 +28,6 @@ resource "azurerm_storage_account" "example" {
account_replication_type = var.sa_account_replication_type account_replication_type = var.sa_account_replication_type
} }
# Create a storage container
resource "azurerm_storage_container" "example" {
name = "example-flexcontainer"
storage_account_id = azurerm_storage_account.example.id
container_access_type = "private"
}
# Create a Log Analytics workspace for Application Insights # Create a Log Analytics workspace for Application Insights
resource "azurerm_log_analytics_workspace" "example" { resource "azurerm_log_analytics_workspace" "example" {
name = coalesce(var.ws_name, random_string.name.result) name = coalesce(var.ws_name, random_string.name.result)
@ -53,82 +46,36 @@ resource "azurerm_application_insights" "example" {
workspace_id = azurerm_log_analytics_workspace.example.id workspace_id = azurerm_log_analytics_workspace.example.id
} }
# Create a service plan # Create a service plan. Custom container images require a Dedicated (or Premium)
# plan - Consumption/Flex Consumption plans don't support bring-your-own-container.
resource "azurerm_service_plan" "example" { resource "azurerm_service_plan" "example" {
name = coalesce(var.asp_name, random_string.name.result) name = coalesce(var.asp_name, random_string.name.result)
resource_group_name = azurerm_resource_group.example.name resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location location = azurerm_resource_group.example.location
sku_name = "FC1" sku_name = var.asp_sku_name
os_type = "Linux" os_type = "Linux"
} }
data "archive_file" "function_app" { # Create a function app that runs the prebuilt container image straight from Docker Hub
type = "zip" resource "azurerm_linux_function_app" "example" {
source_dir = "${path.module}/.."
output_path = "${path.module}/function_app.zip"
excludes = [".git", ".gitea", "iac", "helm-chart", ".terraform*", ".vscode"]
}
resource "azurerm_storage_blob" "appcode" {
name = "app-${random_string.name.result}.zip"
storage_account_name = azurerm_storage_account.example.name
storage_container_name = azurerm_storage_container.example.name
type = "Block"
source = data.archive_file.function_app.output_path
}
data "azurerm_storage_account_sas" "sas" {
connection_string = azurerm_storage_account.example.primary_connection_string
https_only = true
resource_types {
service = false
container = false
object = true
}
services {
blob = true
queue = false
table = false
file = false
}
start = "2023-01-01T00:00:00Z"
expiry = "2029-01-01T00:00:00Z"
permissions {
read = true
write = false
delete = false
list = false
add = false
create = false
update = false
process = false
tag = false
filter = false
}
}
# Create a function app
resource "azurerm_function_app_flex_consumption" "example" {
name = coalesce(var.fa_name, random_string.name.result) name = coalesce(var.fa_name, random_string.name.result)
resource_group_name = azurerm_resource_group.example.name resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location location = azurerm_resource_group.example.location
service_plan_id = azurerm_service_plan.example.id service_plan_id = azurerm_service_plan.example.id
storage_container_type = "blobContainer" storage_account_name = azurerm_storage_account.example.name
storage_container_endpoint = "${azurerm_storage_account.example.primary_blob_endpoint}${azurerm_storage_container.example.name}" storage_account_access_key = azurerm_storage_account.example.primary_access_key
storage_authentication_type = "StorageAccountConnectionString"
storage_access_key = azurerm_storage_account.example.primary_access_key
runtime_name = var.runtime_name
runtime_version = var.runtime_version
maximum_instance_count = 50
instance_memory_in_mb = 2048
site_config { site_config {
application_insights_connection_string = azurerm_application_insights.example.connection_string application_insights_connection_string = azurerm_application_insights.example.connection_string
application_stack {
docker {
registry_url = "https://index.docker.io"
image_name = var.container_image_name
image_tag = var.container_image_tag
}
}
} }
app_settings = { app_settings = {
@ -136,6 +83,8 @@ resource "azurerm_function_app_flex_consumption" "example" {
"OLLAMA_MODEL" = "gemma4:e4b" "OLLAMA_MODEL" = "gemma4:e4b"
"JOKE_PROMPT" = "tell me a dad joke about programming" "JOKE_PROMPT" = "tell me a dad joke about programming"
"PORT" = "80" "PORT" = "80"
"WEBSITE_RUN_FROM_PACKAGE" = "${azurerm_storage_account.example.primary_blob_endpoint}${azurerm_storage_container.example.name}/${azurerm_storage_blob.appcode.name}${data.azurerm_storage_account_sas.sas.sas}" "WEBSITES_PORT" = "80"
"WEBSITES_ENABLE_APP_SERVICE_STORAGE" = "false"
} }
} }

View File

@ -12,9 +12,9 @@ output "asp_name" {
} }
output "fa_name" { output "fa_name" {
value = azurerm_function_app_flex_consumption.example.name value = azurerm_linux_function_app.example.name
} }
output "fa_url" { output "fa_url" {
value = "https://${azurerm_function_app_flex_consumption.example.name}.azurewebsites.net" value = "https://${azurerm_linux_function_app.example.name}.azurewebsites.net"
} }

View File

@ -89,14 +89,20 @@ variable "fa_name" {
default = "" default = ""
} }
variable "runtime_name" { variable "asp_sku_name" {
description = "The name of the language worker runtime." description = "The SKU of the App Service Plan hosting the Function App. Must be a Dedicated or Premium SKU to support custom containers (e.g. B1, P1v2, EP1)."
type = string type = string
default = "python" # Allowed: dotnet-isolated, java, node, powershell, python default = "B1"
} }
variable "runtime_version" { variable "container_image_name" {
description = "The version of the language worker runtime." description = "The Docker Hub image name (without tag) to deploy to the Function App."
type = string type = string
default = "3.11" # Supported versions: see https://aka.ms/flexfxversions default = "idjohnson/dadjokeapp"
}
variable "container_image_tag" {
description = "The tag of the Docker Hub image to deploy to the Function App."
type = string
default = "0.1"
} }