mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 10:24:02 +00:00
refactor(bot): replace reminder polling with scheduled dispatches
This commit is contained in:
@@ -4,6 +4,8 @@ import * as pulumi from '@pulumi/pulumi'
|
||||
|
||||
const config = new pulumi.Config()
|
||||
const awsConfig = new pulumi.Config('aws')
|
||||
const region = awsConfig.get('region') || aws.getRegionOutput().name
|
||||
const accountId = aws.getCallerIdentityOutput().accountId
|
||||
|
||||
const appName = config.get('appName') ?? 'household'
|
||||
const environment = config.get('environment') ?? pulumi.getStack()
|
||||
@@ -23,6 +25,10 @@ const logLevel = config.get('logLevel') ?? 'info'
|
||||
const purchaseParserModel = config.get('purchaseParserModel') ?? 'gpt-4o-mini'
|
||||
const assistantModel = config.get('assistantModel') ?? 'gpt-4o-mini'
|
||||
const topicProcessorModel = config.get('topicProcessorModel') ?? 'gpt-4o-mini'
|
||||
const scheduledDispatchGroupName =
|
||||
config.get('scheduledDispatchGroupName') ?? 'scheduled-dispatches'
|
||||
const lambdaFunctionName = `${appName}-${environment}-bot`
|
||||
const scheduledDispatchTargetLambdaArn = pulumi.interpolate`arn:aws:lambda:${region}:${accountId}:function:${lambdaFunctionName}`
|
||||
|
||||
const telegramBotToken = config.requireSecret('telegramBotToken')
|
||||
const telegramWebhookSecret = config.requireSecret('telegramWebhookSecret')
|
||||
@@ -58,6 +64,18 @@ new aws.iam.RolePolicyAttachment(`${appName}-${environment}-lambda-basic-exec`,
|
||||
policyArn: 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
|
||||
})
|
||||
|
||||
const schedulerGroup = new aws.scheduler.ScheduleGroup(`${appName}-${environment}-dispatches`, {
|
||||
name: scheduledDispatchGroupName,
|
||||
tags
|
||||
})
|
||||
|
||||
const schedulerInvokeRole = new aws.iam.Role(`${appName}-${environment}-scheduler-invoke-role`, {
|
||||
assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({
|
||||
Service: 'scheduler.amazonaws.com'
|
||||
}),
|
||||
tags
|
||||
})
|
||||
|
||||
const secretSpecs = [
|
||||
{
|
||||
key: 'telegramBotToken',
|
||||
@@ -160,6 +178,7 @@ new aws.s3.BucketPolicy(`${appName}-${environment}-miniapp-policy`, {
|
||||
})
|
||||
|
||||
const lambda = new aws.lambda.Function(`${appName}-${environment}-bot`, {
|
||||
name: lambdaFunctionName,
|
||||
packageType: 'Image',
|
||||
imageUri: botImage.imageUri,
|
||||
role: lambdaRole.arn,
|
||||
@@ -175,6 +194,11 @@ const lambda = new aws.lambda.Function(`${appName}-${environment}-bot`, {
|
||||
TELEGRAM_WEBHOOK_PATH: config.get('telegramWebhookPath') ?? '/webhook/telegram',
|
||||
DATABASE_URL: databaseUrl ?? '',
|
||||
SCHEDULER_SHARED_SECRET: schedulerSharedSecret ?? '',
|
||||
SCHEDULED_DISPATCH_PROVIDER: 'aws-eventbridge',
|
||||
AWS_SCHEDULED_DISPATCH_REGION: region,
|
||||
AWS_SCHEDULED_DISPATCH_TARGET_LAMBDA_ARN: scheduledDispatchTargetLambdaArn,
|
||||
AWS_SCHEDULED_DISPATCH_ROLE_ARN: schedulerInvokeRole.arn,
|
||||
AWS_SCHEDULED_DISPATCH_GROUP_NAME: schedulerGroup.name,
|
||||
OPENAI_API_KEY: openaiApiKey ?? '',
|
||||
MINI_APP_URL: miniAppUrl,
|
||||
MINI_APP_ALLOWED_ORIGINS: miniAppAllowedOrigins.join(','),
|
||||
@@ -186,6 +210,43 @@ const lambda = new aws.lambda.Function(`${appName}-${environment}-bot`, {
|
||||
tags
|
||||
})
|
||||
|
||||
new aws.iam.RolePolicy(`${appName}-${environment}-lambda-scheduler-policy`, {
|
||||
role: lambdaRole.id,
|
||||
policy: schedulerInvokeRole.arn.apply((schedulerInvokeRoleArn) =>
|
||||
JSON.stringify({
|
||||
Version: '2012-10-17',
|
||||
Statement: [
|
||||
{
|
||||
Effect: 'Allow',
|
||||
Action: ['scheduler:CreateSchedule', 'scheduler:DeleteSchedule', 'scheduler:GetSchedule'],
|
||||
Resource: '*'
|
||||
},
|
||||
{
|
||||
Effect: 'Allow',
|
||||
Action: ['iam:PassRole'],
|
||||
Resource: schedulerInvokeRoleArn
|
||||
}
|
||||
]
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
new aws.iam.RolePolicy(`${appName}-${environment}-scheduler-invoke-policy`, {
|
||||
role: schedulerInvokeRole.id,
|
||||
policy: lambda.arn.apply((lambdaArn) =>
|
||||
JSON.stringify({
|
||||
Version: '2012-10-17',
|
||||
Statement: [
|
||||
{
|
||||
Effect: 'Allow',
|
||||
Action: ['lambda:InvokeFunction'],
|
||||
Resource: lambdaArn
|
||||
}
|
||||
]
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const functionUrl = new aws.lambda.FunctionUrl(`${appName}-${environment}-bot-url`, {
|
||||
functionName: lambda.name,
|
||||
authorizationType: 'NONE',
|
||||
@@ -199,8 +260,6 @@ const functionUrl = new aws.lambda.FunctionUrl(`${appName}-${environment}-bot-ur
|
||||
}
|
||||
})
|
||||
|
||||
const region = awsConfig.get('region') || aws.getRegionOutput().name
|
||||
|
||||
export const botOriginUrl = functionUrl.functionUrl
|
||||
export const miniAppBucketName = bucket.bucket
|
||||
export const miniAppWebsiteUrl = pulumi.interpolate`http://${bucket.websiteEndpoint}`
|
||||
|
||||
@@ -12,21 +12,6 @@ locals {
|
||||
|
||||
artifact_location = coalesce(var.artifact_repository_location, var.region)
|
||||
|
||||
reminder_jobs = {
|
||||
utilities = {
|
||||
schedule = var.scheduler_utilities_cron
|
||||
path = "/jobs/reminder/utilities"
|
||||
}
|
||||
rent-warning = {
|
||||
schedule = var.scheduler_rent_warning_cron
|
||||
path = "/jobs/reminder/rent-warning"
|
||||
}
|
||||
rent-due = {
|
||||
schedule = var.scheduler_rent_due_cron
|
||||
path = "/jobs/reminder/rent-due"
|
||||
}
|
||||
}
|
||||
|
||||
runtime_secret_ids = toset(compact([
|
||||
var.telegram_webhook_secret_id,
|
||||
var.scheduler_shared_secret_id,
|
||||
@@ -37,6 +22,7 @@ locals {
|
||||
|
||||
api_services = toset([
|
||||
"artifactregistry.googleapis.com",
|
||||
"cloudtasks.googleapis.com",
|
||||
"cloudscheduler.googleapis.com",
|
||||
"iam.googleapis.com",
|
||||
"iamcredentials.googleapis.com",
|
||||
|
||||
@@ -58,10 +58,18 @@ resource "google_service_account" "mini_runtime" {
|
||||
display_name = "${local.name_prefix} mini runtime"
|
||||
}
|
||||
|
||||
resource "google_service_account" "scheduler_invoker" {
|
||||
project = var.project_id
|
||||
account_id = "${var.environment}-scheduler"
|
||||
display_name = "${local.name_prefix} scheduler invoker"
|
||||
resource "google_cloud_tasks_queue" "scheduled_dispatches" {
|
||||
project = var.project_id
|
||||
location = var.region
|
||||
name = var.scheduled_dispatch_queue_name
|
||||
|
||||
depends_on = [google_project_service.enabled]
|
||||
}
|
||||
|
||||
resource "google_project_iam_member" "bot_runtime_cloud_tasks_enqueuer" {
|
||||
project = var.project_id
|
||||
role = "roles/cloudtasks.enqueuer"
|
||||
member = "serviceAccount:${google_service_account.bot_runtime.email}"
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret" "runtime" {
|
||||
@@ -169,8 +177,12 @@ module "bot_api_service" {
|
||||
var.bot_mini_app_url == null ? {} : {
|
||||
MINI_APP_URL = var.bot_mini_app_url
|
||||
},
|
||||
{
|
||||
SCHEDULER_OIDC_ALLOWED_EMAILS = google_service_account.scheduler_invoker.email
|
||||
var.scheduled_dispatch_public_base_url == null ? {} : {
|
||||
SCHEDULED_DISPATCH_PROVIDER = "gcp-cloud-tasks"
|
||||
SCHEDULED_DISPATCH_PUBLIC_BASE_URL = var.scheduled_dispatch_public_base_url
|
||||
GCP_SCHEDULED_DISPATCH_PROJECT_ID = var.project_id
|
||||
GCP_SCHEDULED_DISPATCH_LOCATION = var.region
|
||||
GCP_SCHEDULED_DISPATCH_QUEUE = google_cloud_tasks_queue.scheduled_dispatches.name
|
||||
}
|
||||
)
|
||||
|
||||
@@ -192,6 +204,8 @@ module "bot_api_service" {
|
||||
|
||||
depends_on = [
|
||||
google_project_service.enabled,
|
||||
google_cloud_tasks_queue.scheduled_dispatches,
|
||||
google_project_iam_member.bot_runtime_cloud_tasks_enqueuer,
|
||||
google_secret_manager_secret.runtime,
|
||||
google_secret_manager_secret_iam_member.bot_runtime_access
|
||||
]
|
||||
@@ -218,54 +232,6 @@ module "mini_app_service" {
|
||||
depends_on = [google_project_service.enabled]
|
||||
}
|
||||
|
||||
resource "google_cloud_run_v2_service_iam_member" "scheduler_invoker" {
|
||||
project = var.project_id
|
||||
location = var.region
|
||||
name = module.bot_api_service.name
|
||||
role = "roles/run.invoker"
|
||||
member = "serviceAccount:${google_service_account.scheduler_invoker.email}"
|
||||
}
|
||||
|
||||
resource "google_service_account_iam_member" "scheduler_token_creator" {
|
||||
service_account_id = google_service_account.scheduler_invoker.name
|
||||
role = "roles/iam.serviceAccountTokenCreator"
|
||||
member = "serviceAccount:service-${data.google_project.current.number}@gcp-sa-cloudscheduler.iam.gserviceaccount.com"
|
||||
}
|
||||
|
||||
resource "google_cloud_scheduler_job" "reminders" {
|
||||
for_each = local.reminder_jobs
|
||||
|
||||
project = var.project_id
|
||||
region = var.region
|
||||
name = "${local.name_prefix}-${each.key}"
|
||||
schedule = each.value.schedule
|
||||
time_zone = var.scheduler_timezone
|
||||
paused = var.scheduler_paused
|
||||
|
||||
http_target {
|
||||
uri = "${module.bot_api_service.uri}${each.value.path}"
|
||||
http_method = "POST"
|
||||
|
||||
headers = {
|
||||
"Content-Type" = "application/json"
|
||||
}
|
||||
|
||||
body = base64encode(jsonencode({
|
||||
dryRun = var.scheduler_dry_run
|
||||
jobId = "${local.name_prefix}-${each.key}"
|
||||
}))
|
||||
|
||||
oidc_token {
|
||||
service_account_email = google_service_account.scheduler_invoker.email
|
||||
audience = module.bot_api_service.uri
|
||||
}
|
||||
}
|
||||
|
||||
depends_on = [
|
||||
module.bot_api_service,
|
||||
google_service_account_iam_member.scheduler_token_creator
|
||||
]
|
||||
}
|
||||
|
||||
resource "google_service_account" "github_deployer" {
|
||||
count = var.create_workload_identity ? 1 : 0
|
||||
|
||||
@@ -23,11 +23,6 @@ output "mini_app_service_url" {
|
||||
value = module.mini_app_service.uri
|
||||
}
|
||||
|
||||
output "scheduler_job_names" {
|
||||
description = "Cloud Scheduler jobs for reminders"
|
||||
value = { for name, job in google_cloud_scheduler_job.reminders : name => job.name }
|
||||
}
|
||||
|
||||
output "runtime_secret_ids" {
|
||||
description = "Secret Manager IDs expected by runtime"
|
||||
value = sort([for secret in google_secret_manager_secret.runtime : secret.secret_id])
|
||||
|
||||
@@ -28,12 +28,8 @@ alert_notification_emails = [
|
||||
"alerts@example.com"
|
||||
]
|
||||
|
||||
scheduler_utilities_cron = "0 9 * * *"
|
||||
scheduler_rent_warning_cron = "0 9 * * *"
|
||||
scheduler_rent_due_cron = "0 9 * * *"
|
||||
scheduler_timezone = "Asia/Tbilisi"
|
||||
scheduler_paused = true
|
||||
scheduler_dry_run = true
|
||||
scheduled_dispatch_queue_name = "scheduled-dispatches"
|
||||
scheduled_dispatch_public_base_url = "https://api.example.com"
|
||||
|
||||
create_workload_identity = true
|
||||
github_repository = "whekin/household-bot"
|
||||
|
||||
@@ -165,40 +165,17 @@ variable "openai_api_key_secret_id" {
|
||||
nullable = true
|
||||
}
|
||||
|
||||
variable "scheduler_timezone" {
|
||||
description = "Scheduler timezone"
|
||||
variable "scheduled_dispatch_queue_name" {
|
||||
description = "Cloud Tasks queue name for one-shot reminder dispatches"
|
||||
type = string
|
||||
default = "Asia/Tbilisi"
|
||||
default = "scheduled-dispatches"
|
||||
}
|
||||
|
||||
variable "scheduler_utilities_cron" {
|
||||
description = "Cron expression for the utilities reminder scheduler job. Daily cadence is recommended because the app filters per household."
|
||||
variable "scheduled_dispatch_public_base_url" {
|
||||
description = "Public bot base URL used by Cloud Tasks callbacks for scheduled dispatches"
|
||||
type = string
|
||||
default = "0 9 * * *"
|
||||
}
|
||||
|
||||
variable "scheduler_rent_warning_cron" {
|
||||
description = "Cron expression for the rent warning scheduler job. Daily cadence is recommended because the app filters per household."
|
||||
type = string
|
||||
default = "0 9 * * *"
|
||||
}
|
||||
|
||||
variable "scheduler_rent_due_cron" {
|
||||
description = "Cron expression for the rent due scheduler job. Daily cadence is recommended because the app filters per household."
|
||||
type = string
|
||||
default = "0 9 * * *"
|
||||
}
|
||||
|
||||
variable "scheduler_dry_run" {
|
||||
description = "Whether scheduler jobs should invoke the bot in dry-run mode"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "scheduler_paused" {
|
||||
description = "Whether scheduler should be paused initially"
|
||||
type = bool
|
||||
default = true
|
||||
default = null
|
||||
nullable = true
|
||||
}
|
||||
|
||||
variable "bot_min_instances" {
|
||||
|
||||
Reference in New Issue
Block a user