| 1 | locals { |
| 2 | releases_domain = "releases.nixos.org" |
| 3 | |
| 4 | releases_index = templatefile("${path.module}/s3_listing.html.tpl", { |
| 5 | bucket_name = aws_s3_bucket.releases.bucket |
| 6 | bucket_url = "https://${aws_s3_bucket.releases.bucket_domain_name}" |
| 7 | bucket_website = "https://${local.releases_domain}" |
| 8 | }) |
| 9 | |
| 10 | releases_backend = "nix-releases.s3-eu-west-1.amazonaws.com" |
| 11 | } |
| 12 | |
| 13 | resource "aws_s3_bucket" "releases" { |
| 14 | bucket = "nix-releases" |
| 15 | } |
| 16 | |
| 17 | resource "aws_s3_bucket_lifecycle_configuration" "releases" { |
| 18 | bucket = aws_s3_bucket.releases.id |
| 19 | |
| 20 | |
| 21 | transition_default_minimum_object_size = "varies_by_storage_class" |
| 22 | rule { |
| 23 | id = "tf-s3-lifecycle-20230907091915137900000001" |
| 24 | status = "Enabled" |
| 25 | |
| 26 | filter { |
| 27 | prefix = "" |
| 28 | } |
| 29 | |
| 30 | transition { |
| 31 | days = 365 |
| 32 | storage_class = "STANDARD_IA" |
| 33 | } |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | import { |
| 38 | id = aws_s3_bucket.releases.id |
| 39 | to = aws_s3_bucket_lifecycle_configuration.releases |
| 40 | } |
| 41 | |
| 42 | resource "aws_s3_bucket_cors_configuration" "releases" { |
| 43 | bucket = aws_s3_bucket.releases.id |
| 44 | |
| 45 | cors_rule { |
| 46 | allowed_headers = ["*"] |
| 47 | allowed_methods = ["HEAD", "GET"] |
| 48 | allowed_origins = ["*"] |
| 49 | expose_headers = ["ETag"] |
| 50 | max_age_seconds = 3600 |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | import { |
| 55 | to = aws_s3_bucket_cors_configuration.releases |
| 56 | id = aws_s3_bucket.releases.id |
| 57 | } |
| 58 | |
| 59 | resource "aws_s3_bucket_object" "releases-index-html" { |
| 60 | acl = "public-read" |
| 61 | bucket = aws_s3_bucket.releases.bucket |
| 62 | content_type = "text/html" |
| 63 | etag = md5(local.releases_index) |
| 64 | key = "index.html" |
| 65 | content = local.releases_index |
| 66 | } |
| 67 | |
| 68 | resource "aws_s3_bucket_policy" "releases" { |
| 69 | bucket = aws_s3_bucket.releases.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-releases/*" |
| 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-releases" |
| 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-releases/*" |
| 109 | } |
| 110 | ] |
| 111 | } |
| 112 | EOF |
| 113 | } |
| 114 | |
| 115 | resource "fastly_service_vcl" "releases" { |
| 116 | name = local.releases_domain |
| 117 | default_ttl = 86400 |
| 118 | |
| 119 | backend { |
| 120 | address = local.releases_backend |
| 121 | auto_loadbalance = false |
| 122 | between_bytes_timeout = 10000 |
| 123 | connect_timeout = 5000 |
| 124 | error_threshold = 0 |
| 125 | first_byte_timeout = 15000 |
| 126 | max_conn = 200 |
| 127 | name = local.releases_backend |
| 128 | override_host = local.releases_backend |
| 129 | port = 443 |
| 130 | shield = "dub-dublin-ie" |
| 131 | ssl_cert_hostname = local.releases_backend |
| 132 | ssl_check_cert = true |
| 133 | use_ssl = true |
| 134 | weight = 100 |
| 135 | } |
| 136 | |
| 137 | request_setting { |
| 138 | name = "Redirect HTTP to HTTPS" |
| 139 | force_ssl = true |
| 140 | } |
| 141 | |
| 142 | condition { |
| 143 | name = "Generated by synthetic response for 404 page" |
| 144 | priority = 0 |
| 145 | statement = "beresp.status == 404" |
| 146 | type = "CACHE" |
| 147 | } |
| 148 | |
| 149 | condition { |
| 150 | name = "Match /" |
| 151 | priority = 10 |
| 152 | statement = "req.url ~ \"^/$\"" |
| 153 | type = "REQUEST" |
| 154 | } |
| 155 | |
| 156 | domain { |
| 157 | name = local.releases_domain |
| 158 | } |
| 159 | |
| 160 | header { |
| 161 | action = "set" |
| 162 | destination = "url" |
| 163 | ignore_if_set = false |
| 164 | name = "Landing page" |
| 165 | priority = 10 |
| 166 | request_condition = "Match /" |
| 167 | source = "\"/index.html\"" |
| 168 | type = "request" |
| 169 | } |
| 170 | |
| 171 | # Clean headers for caching |
| 172 | header { |
| 173 | destination = "http.x-amz-request-id" |
| 174 | type = "cache" |
| 175 | action = "delete" |
| 176 | name = "remove x-amz-request-id" |
| 177 | } |
| 178 | header { |
| 179 | destination = "http.x-amz-version-id" |
| 180 | type = "cache" |
| 181 | action = "delete" |
| 182 | name = "remove x-amz-version-id" |
| 183 | } |
| 184 | header { |
| 185 | destination = "http.x-amz-id-2" |
| 186 | type = "cache" |
| 187 | action = "delete" |
| 188 | name = "remove x-amz-id-2" |
| 189 | } |
| 190 | |
| 191 | # Allow CORS GET requests. |
| 192 | header { |
| 193 | destination = "http.access-control-allow-origin" |
| 194 | type = "cache" |
| 195 | action = "set" |
| 196 | name = "CORS Allow" |
| 197 | source = "\"*\"" |
| 198 | } |
| 199 | |
| 200 | response_object { |
| 201 | cache_condition = "Generated by synthetic response for 404 page" |
| 202 | content = "404" |
| 203 | content_type = "text/html" |
| 204 | name = "Generated by synthetic response for 404 page" |
| 205 | response = "Not Found" |
| 206 | status = 404 |
| 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 | # Work around the 2GB size limit for large files |
| 217 | # |
| 218 | # See https://docs.fastly.com/en/guides/segmented-caching |
| 219 | snippet { |
| 220 | content = <<-EOT |
| 221 | if (req.url.path ~ "^/nixos/") { |
| 222 | set req.enable_segmented_caching = true; |
| 223 | } |
| 224 | EOT |
| 225 | name = "Enable segment caching for ISOs and friends" |
| 226 | priority = 60 |
| 227 | type = "recv" |
| 228 | } |
| 229 | |
| 230 | snippet { |
| 231 | content = <<-EOT |
| 232 | if (beresp.status == 403) { |
| 233 | set beresp.status = 404; |
| 234 | set beresp.ttl = 86400s; |
| 235 | set beresp.grace = 0s; |
| 236 | set beresp.cacheable = true; |
| 237 | } |
| 238 | EOT |
| 239 | name = "Change 403 from S3 to 404" |
| 240 | priority = 100 |
| 241 | type = "fetch" |
| 242 | } |
| 243 | |
| 244 | logging_s3 { |
| 245 | name = "${local.releases_domain}-to-s3" |
| 246 | bucket_name = local.fastlylogs["bucket_name"] |
| 247 | compression_codec = "zstd" |
| 248 | domain = local.fastlylogs["s3_domain"] |
| 249 | format = local.fastlylogs["format"] |
| 250 | format_version = 2 |
| 251 | path = "${local.releases_domain}/" |
| 252 | period = local.fastlylogs["period"] |
| 253 | message_type = "blank" |
| 254 | s3_iam_role = local.fastlylogs["iam_role_arn"] |
| 255 | } |
| 256 | } |
| 257 | |
| 258 | resource "fastly_tls_subscription" "releases-2025-11" { |
| 259 | domains = [for domain in fastly_service_vcl.releases.domain : domain.name] |
| 260 | configuration_id = local.fastly_tls13_quic_configuration_id |
| 261 | certificate_authority = "lets-encrypt" |
| 262 | } |
| 263 | |
| 264 | output "releases-managed_dns_challenge" { |
| 265 | value = fastly_tls_subscription.releases-2025-11.managed_dns_challenges |
| 266 | } |