@cryptotaxi247 / infra-1 / commits / 68ad10ad

set up eventbridge webhook notifications for our s3 bucket

Arian van Putten committed Apr 27, 2026 at 12:11 UTC 68ad10ad6ea3a76c0644cc2feb1e77c204136d77
2 files changed +112
terraform-iam/README.md
+2
@@ -7,6 +7,8 @@ This terraform root module manages:
7 - IAM roles
8 - fastly log module
9 - infrastructure for archeologist team
10 +- Webhooks for the Cache bucket as our terraform code is awkwardly split and it
11 + requires iam:PassRole
12
13 ## Setup
14
terraform-iam/cache_eventbridge.tf new
+110
@@ -0,0 +1,110 @@
1 +# Forward S3 Object Created events on the nix-cache bucket to the
2 +# https://cache-updates.snix.store webhook via an EventBridge API destination.
3 +#
4 +# Lives in terraform-iam (rather than terraform/) because creating the
5 +# EventBridge target requires iam:PassRole on the IAM role below, which the
6 +# AWSPowerUserAccess SSO role used by terraform/ does not have.
7 +
8 +locals {
9 + cache_webhook_url = "https://cache-updates.snix.store"
10 + cache_webhook_header_key = "X-API-Key"
11 + cache_bucket_name = "nix-cache"
12 +}
13 +
14 +resource "secret_resource" "cache_webhook_api_key" {}
15 +
16 +# Cost: $1.00 per million events ingested. S3 EventBridge events are opt-in
17 +# data plane events, billed as custom events on the default bus.
18 +# https://aws.amazon.com/eventbridge/pricing/
19 +resource "aws_s3_bucket_notification" "cache" {
20 + provider = aws.us
21 + bucket = local.cache_bucket_name
22 + eventbridge = true
23 +}
24 +
25 +resource "aws_cloudwatch_event_connection" "cache_webhook" {
26 + provider = aws.us
27 + name = "cache-updates-snix-store"
28 + authorization_type = "API_KEY"
29 +
30 + auth_parameters {
31 + api_key {
32 + key = local.cache_webhook_header_key
33 + value = secret_resource.cache_webhook_api_key.value
34 + }
35 + }
36 +}
37 +
38 +# Cost: $0.20 per million invocations.
39 +# https://aws.amazon.com/eventbridge/pricing/
40 +resource "aws_cloudwatch_event_api_destination" "cache_webhook" {
41 + provider = aws.us
42 + name = "cache-updates-snix-store"
43 + invocation_endpoint = local.cache_webhook_url
44 + http_method = "POST"
45 + # Tweak this based on the amount of uploads per 24 hours?
46 + invocation_rate_limit_per_second = 300
47 + connection_arn = aws_cloudwatch_event_connection.cache_webhook.arn
48 +}
49 +
50 +resource "aws_cloudwatch_event_rule" "cache_object_created" {
51 + provider = aws.us
52 + name = "nix-cache-object-created"
53 + description = "S3 Object Created events on nix-cache forwarded to cache-updates.snix.store"
54 +
55 + event_pattern = jsonencode({
56 + source = ["aws.s3"]
57 + "detail-type" = ["Object Created"]
58 + detail = {
59 + bucket = {
60 + name = [local.cache_bucket_name]
61 + }
62 + }
63 + })
64 +}
65 +
66 +data "aws_iam_policy_document" "cache_webhook_assume" {
67 + statement {
68 + effect = "Allow"
69 + actions = ["sts:AssumeRole"]
70 + principals {
71 + type = "Service"
72 + identifiers = ["events.amazonaws.com"]
73 + }
74 + }
75 +}
76 +
77 +data "aws_iam_policy_document" "cache_webhook_invoke" {
78 + statement {
79 + effect = "Allow"
80 + actions = ["events:InvokeApiDestination"]
81 + resources = [aws_cloudwatch_event_api_destination.cache_webhook.arn]
82 + }
83 +}
84 +
85 +resource "aws_iam_role" "cache_webhook" {
86 + provider = aws.us
87 + name = "EventBridgeInvokeCacheWebhook"
88 + assume_role_policy = data.aws_iam_policy_document.cache_webhook_assume.json
89 +}
90 +
91 +resource "aws_iam_role_policy" "cache_webhook" {
92 + provider = aws.us
93 + name = "InvokeCacheWebhook"
94 + role = aws_iam_role.cache_webhook.id
95 + policy = data.aws_iam_policy_document.cache_webhook_invoke.json
96 +}
97 +
98 +resource "aws_cloudwatch_event_target" "cache_webhook" {
99 + provider = aws.us
100 + rule = aws_cloudwatch_event_rule.cache_object_created.name
101 + target_id = "cache-updates-snix-store"
102 + arn = aws_cloudwatch_event_api_destination.cache_webhook.arn
103 + role_arn = aws_iam_role.cache_webhook.arn
104 +
105 + # https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-rule-retry-policy.html
106 + retry_policy {
107 + maximum_event_age_in_seconds = 86400
108 + maximum_retry_attempts = 185
109 + }
110 +}