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
+}