main
tf 302 lines 8.46 KB
Raw
1 locals {
2 channels_domain = "channels.nixos.org"
3
4 channels_index = templatefile("${path.module}/s3_listing.html.tpl", {
5 bucket_name = aws_s3_bucket.channels.bucket
6 bucket_url = "https://${aws_s3_bucket.channels.bucket_domain_name}"
7 bucket_website = "https://${local.channels_domain}"
8 })
9
10 # Use the website endpoint because the bucket is configured with website
11 # enabled. This also means we can't use TLS between Fastly and AWS because
12 # the website endpoint only has port 80 open.
13 channels_backend = "nix-channels.s3-website-us-east-1.amazonaws.com"
14 # TODO: Uncomment this once has been applied once. This is to work around fastly bug https://github.com/fastly/terraform-provider-fastly/issues/884
15 # channels_backend = aws_s3_bucket_website_configuration.channels.website_endpoint
16 }
17
18 resource "aws_s3_bucket" "channels" {
19 provider = aws.us
20 bucket = "nix-channels"
21 }
22
23 resource "aws_s3_bucket_website_configuration" "channels" {
24 provider = aws.us
25 bucket = aws_s3_bucket.channels.id
26
27 index_document {
28 suffix = "index.html"
29 }
30 }
31
32 import {
33 to = aws_s3_bucket_website_configuration.channels
34 id = aws_s3_bucket.channels.id
35 }
36
37 resource "aws_s3_bucket_cors_configuration" "channels" {
38 provider = aws.us
39 bucket = aws_s3_bucket.channels.id
40
41 cors_rule {
42 allowed_headers = ["*"]
43 allowed_methods = ["HEAD", "GET"]
44 allowed_origins = ["*"]
45 expose_headers = ["ETag"]
46 max_age_seconds = 3600
47 }
48 }
49
50 import {
51 to = aws_s3_bucket_cors_configuration.channels
52 id = aws_s3_bucket.channels.id
53
54 }
55
56 resource "aws_s3_bucket_object" "channels-index-html" {
57 provider = aws.us
58
59 acl = "public-read"
60 bucket = aws_s3_bucket.channels.bucket
61 content_type = "text/html"
62 etag = md5(local.channels_index)
63 key = "index.html"
64 content = local.channels_index
65 }
66
67 resource "aws_s3_bucket_policy" "channels" {
68 provider = aws.us
69 bucket = aws_s3_bucket.channels.id
70 policy = <<EOF
71 {
72 "Version": "2008-10-17",
73 "Statement": [
74 {
75 "Sid": "AllowPublicRead",
76 "Effect": "Allow",
77 "Principal": {
78 "AWS": "*"
79 },
80 "Action": "s3:GetObject",
81 "Resource": "arn:aws:s3:::nix-channels/*"
82 },
83 {
84 "Sid": "AllowPublicList",
85 "Effect": "Allow",
86 "Principal": {
87 "AWS": "*"
88 },
89 "Action": [
90 "s3:ListBucket",
91 "s3:GetBucketLocation"
92 ],
93 "Resource": "arn:aws:s3:::nix-channels"
94 },
95 {
96 "Sid": "AllowUpload",
97 "Effect": "Allow",
98 "Principal": {
99 "AWS": [
100 "arn:aws:iam::080433136561:user/s3-upload-releases",
101 "arn:aws:iam::065343343465:user/nixos-s3-upload-releases"
102 ]
103 },
104 "Action": [
105 "s3:PutObject",
106 "s3:PutObjectAcl"
107 ],
108 "Resource": "arn:aws:s3:::nix-channels/*"
109 }
110 ]
111 }
112 EOF
113 }
114
115 resource "fastly_service_vcl" "channels" {
116 name = local.channels_domain
117 default_ttl = 3600
118
119 backend {
120 address = local.channels_backend
121 auto_loadbalance = false
122 connect_timeout = 5000
123 name = local.channels_backend
124 override_host = local.channels_backend
125 request_condition = "not-flake-registry"
126 shield = "iad-va-us"
127 }
128
129 backend {
130 # https://github.com/NixOS/flake-registry/raw/master/flake-registry.json
131 name = "flake-registry"
132 address = "raw.githubusercontent.com"
133 auto_loadbalance = false
134 override_host = "raw.githubusercontent.com"
135 port = 443
136 use_ssl = true
137 ssl_check_cert = false
138 request_condition = "flake-registry"
139 }
140
141 request_setting {
142 name = "Redirect HTTP to HTTPS"
143 force_ssl = true
144 }
145
146 condition {
147 name = "Match /"
148 priority = 10
149 statement = "req.url ~ \"^/$\""
150 type = "REQUEST"
151 }
152
153 condition {
154 name = "not-flake-registry"
155 statement = "req.url != \"/NixOS/flake-registry/master/flake-registry.json\""
156 type = "REQUEST"
157 }
158
159 condition {
160 name = "flake-registry"
161 statement = "req.url == \"/NixOS/flake-registry/master/flake-registry.json\""
162 type = "REQUEST"
163 }
164
165 domain {
166 name = local.channels_domain
167 }
168
169 header {
170 action = "set"
171 destination = "url"
172 ignore_if_set = false
173 name = "Landing page"
174 priority = 10
175 request_condition = "Match /"
176 source = "\"/index.html\""
177 type = "request"
178 }
179
180 # Clean headers for caching
181 header {
182 destination = "http.x-amz-request-id"
183 type = "cache"
184 action = "delete"
185 name = "remove x-amz-request-id"
186 }
187 header {
188 destination = "http.x-amz-version-id"
189 type = "cache"
190 action = "delete"
191 name = "remove x-amz-version-id"
192 }
193 header {
194 destination = "http.x-amz-id-2"
195 type = "cache"
196 action = "delete"
197 name = "remove x-amz-id-2"
198 }
199
200 # Allow CORS GET requests.
201 header {
202 destination = "http.access-control-allow-origin"
203 type = "cache"
204 action = "set"
205 name = "CORS Allow"
206 source = "\"*\""
207 }
208
209 snippet {
210 content = "set req.url = querystring.remove(req.url);"
211 name = "Remove all query strings"
212 priority = 50
213 type = "recv"
214 }
215
216 snippet {
217 content = <<-EOT
218 if (beresp.status == 403) {
219 set beresp.status = 404;
220 set beresp.ttl = 86400s;
221 set beresp.grace = 0s;
222 set beresp.cacheable = true;
223 }
224 if (req.url ~ "/flake-registry.json") {
225 set beresp.stale_if_error = 1000000s;
226 }
227 EOT
228 name = "Change 403 from S3 to 404"
229 priority = 100
230 type = "fetch"
231 }
232
233 snippet {
234 name = "flake-registry"
235 content = <<-EOT
236 if (req.url == "/flake-registry.json") {
237 set req.url = "/NixOS/flake-registry/master/flake-registry.json";
238 }
239 EOT
240 type = "recv"
241 }
242
243 snippet {
244 content = <<-EOT
245 # S3 object-level redirects can only be 301s. We use them to point
246 # "latest" versions of various channel/release artifacts to the correct
247 # location. First, mark these redirects as temporary. Second, disable
248 # caching, since some of the artifacts need to have matching versions
249 # (e.g. a .iso and its checksum), which is near-impossible to guarantee
250 # with caching unless we explicitly perform invalidations.
251 #
252 # Note: we need to match on 301s and 302s here, since Fastly has multiple
253 # layers, and otherwise a redirect might still get cached at the second
254 # layer after the first layer turned a 301 into a 302.
255 #
256 # Additionally, this also implements the "Lockable HTTP Tarball Protocol"
257 # to use nixexprs.tar.zst (or any other nixexprs.tar.*) with Flakes and
258 # have it locked properly.
259 if (beresp.status == 301 || beresp.status == 302) {
260 set beresp.status = 302;
261 set beresp.ttl = 0s;
262 set beresp.grace = 0s;
263 set beresp.cacheable = false;
264 if (req.backend.is_origin && std.prefixof(bereq.url.basename, "nixexprs.tar.")) {
265 # pass redirect location into special flake "immutable tarball" header
266 set beresp.http.link = "<" + beresp.http.location + {">; rel="immutable""};
267 # clear query string from redirect destination as precaution in case
268 # legacy consumers can't handle flake attributes like "?rev=" in it
269 set beresp.http.location = querystring.remove(beresp.http.location);
270 }
271 return (pass);
272 }
273 EOT
274 name = "Change 301 from S3 to 302"
275 # Keep close to last, since it conditionally returns.
276 priority = 999
277 type = "fetch"
278 }
279
280 logging_s3 {
281 name = "${local.channels_domain}-to-s3"
282 bucket_name = local.fastlylogs["bucket_name"]
283 compression_codec = "zstd"
284 domain = local.fastlylogs["s3_domain"]
285 format = local.fastlylogs["format"]
286 format_version = 2
287 path = "${local.channels_domain}/"
288 period = local.fastlylogs["period"]
289 message_type = "blank"
290 s3_iam_role = local.fastlylogs["iam_role_arn"]
291 }
292 }
293
294 resource "fastly_tls_subscription" "channels-2025-11" {
295 domains = [for domain in fastly_service_vcl.channels.domain : domain.name]
296 configuration_id = local.fastly_tls13_quic_configuration_id
297 certificate_authority = "lets-encrypt"
298 }
299
300 output "channels-managed_dns_challenge" {
301 value = fastly_tls_subscription.channels-2025-11.managed_dns_challenges
302 }