main.tf 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  1. locals {
  2. name = var.name != "" ? var.name : var.hostname
  3. backend_name = var.backend_name != "" ? var.backend_name : "${var.hostname} - backend"
  4. ssl_hostname = var.ssl_hostname != "" ? var.ssl_hostname : var.hostname
  5. healthcheck_host = var.healthcheck_host != "" ? var.healthcheck_host : var.hostname
  6. healthcheck_name = var.healthcheck_name != "" ? var.healthcheck_name : "${var.hostname} - healthcheck"
  7. media_backend_name = var.media_backend["name"] != "" ? var.media_backend["name"] : "${local.backend_name} - media"
  8. media_ssl_hostname = var.media_backend["ssl_hostname"] != "" ? var.media_backend["ssl_hostname"] : var.media_backend["address"]
  9. edge_security_dict_name = "Edge_Security"
  10. datadog_format = replace(file("${path.module}/logging/datadog.json"), "__service__", var.datadog_service)
  11. fastly_globeviz_format = file("${path.module}/logging/fastly_globeviz.json")
  12. associated_domain_response = file("${path.module}/responses/associated_domain.json")
  13. deep_link_response = file("${path.module}/responses/deep_link.json")
  14. vcl_main = file("${path.module}/vcl/main.vcl")
  15. vcl_sigsci_config = templatefile("${path.module}/vcl/sigsci_config.vcl", {
  16. host = var.signal_science_host,
  17. shared_key = var.signal_science_shared_key
  18. })
  19. vcl_apex_error = templatefile("${path.module}/vcl/apex_error.vcl", { hostname = var.hostname })
  20. vcl_apex_redirect = templatefile("${path.module}/vcl/apex_redirect.vcl", { hostname = var.hostname })
  21. vcl_backend_403 = file("${path.module}/vcl/backend_403.vcl")
  22. vcl_block_user_agents = file("${path.module}/vcl/block_user_agents.vcl")
  23. vcl_custom_error_redirect = file("${path.module}/vcl/custom_error_redirect.vcl")
  24. vcl_custom_error = templatefile("${path.module}/vcl/custom_error.vcl", { hostname = var.hostname })
  25. vcl_static_cache_control = file("${path.module}/vcl/static_cache_control.vcl")
  26. vcl_tarpit = file("${path.module}/vcl/tarpit.vcl")
  27. vcl_globeviz = templatefile("${path.module}/vcl/globeviz.vcl", { service = var.globeviz_service })
  28. vcl_media_redirect = templatefile("${path.module}/vcl/media_redirect.vcl", {
  29. backend = "F_${replace(local.media_backend_name, " ", "_")}",
  30. redirect = var.media_backend["bucket_prefix"]
  31. })
  32. }
  33. resource "fastly_service_vcl" "app_service" {
  34. name = local.name
  35. default_ttl = var.default_ttl
  36. http3 = true
  37. stale_if_error = true
  38. domain {
  39. name = var.hostname
  40. }
  41. dynamic "domain" {
  42. for_each = var.domains
  43. content {
  44. name = domain.value
  45. }
  46. }
  47. # Backend
  48. backend {
  49. name = local.backend_name
  50. address = var.backend_address
  51. auto_loadbalance = false
  52. between_bytes_timeout = var.backend_between_bytes_timeout
  53. first_byte_timeout = var.backend_first_byte_timeout
  54. healthcheck = local.healthcheck_name
  55. keepalive_time = 0
  56. override_host = local.ssl_hostname
  57. port = var.backend_port
  58. max_conn = var.max_conn
  59. min_tls_version = var.min_tls_version
  60. shield = var.shield_region
  61. ssl_ca_cert = var.backend_ca_cert
  62. ssl_check_cert = var.backend_ssl_check
  63. ssl_cert_hostname = var.backend_ssl_check ? local.ssl_hostname : ""
  64. ssl_sni_hostname = local.ssl_hostname
  65. use_ssl = var.use_ssl
  66. }
  67. # Media backend
  68. dynamic "backend" {
  69. for_each = var.media_backend["address"] != "" ? [1] : []
  70. content {
  71. address = var.media_backend["address"]
  72. name = var.media_backend["name"] != "" ? var.media_backend["name"] : "${local.backend_name} - media"
  73. auto_loadbalance = false
  74. between_bytes_timeout = var.backend_between_bytes_timeout
  75. first_byte_timeout = var.backend_first_byte_timeout
  76. keepalive_time = 0
  77. override_host = var.media_backend["address"]
  78. port = var.backend_port
  79. max_conn = var.max_conn
  80. min_tls_version = var.min_tls_version
  81. request_condition = var.media_backend["condition"] != "" ? var.media_backend["condition_name"] : ""
  82. ssl_check_cert = var.media_backend["ssl_check"]
  83. ssl_cert_hostname = var.media_backend["ssl_check"] ? local.media_ssl_hostname : ""
  84. ssl_sni_hostname = var.media_backend["ssl_check"] ? local.media_ssl_hostname : ""
  85. use_ssl = var.use_ssl
  86. }
  87. }
  88. dynamic "condition" {
  89. for_each = var.media_backend["condition"] != "" ? [1] : []
  90. content {
  91. name = var.media_backend["condition_name"]
  92. statement = var.media_backend["condition"]
  93. type = "REQUEST"
  94. priority = 10
  95. }
  96. }
  97. # Healthcheck
  98. healthcheck {
  99. name = local.healthcheck_name
  100. host = local.healthcheck_host
  101. path = var.healthcheck_path
  102. check_interval = 60000
  103. expected_response = var.healthcheck_expected_response
  104. initial = 1
  105. method = var.healthcheck_method
  106. threshold = 1
  107. timeout = 5000
  108. window = 2
  109. }
  110. # Datadog logging
  111. dynamic "logging_datadog" {
  112. for_each = var.datadog ? [1] : []
  113. content {
  114. name = "Datadog ${var.datadog_region}"
  115. format = local.datadog_format
  116. token = var.datadog_token
  117. region = var.datadog_region
  118. }
  119. }
  120. # Fastly global visualization logging
  121. dynamic "logging_https" {
  122. for_each = var.fastly_globeviz_url != "" ? [1] : []
  123. content {
  124. name = "fastly-globeviz"
  125. format = local.fastly_globeviz_format
  126. url = var.fastly_globeviz_url
  127. content_type = "text/plain"
  128. method = "POST"
  129. placement = "none"
  130. }
  131. }
  132. # Force TLS/HSTS settings
  133. # Creates similar objects to what the GUI switch creates.
  134. dynamic "request_setting" {
  135. for_each = var.force_tls_hsts ? [1] : []
  136. content {
  137. name = "Generated by force TLS and enable HSTS"
  138. bypass_busy_wait = false
  139. force_miss = false
  140. force_ssl = true
  141. max_stale_age = 0
  142. timer_support = false
  143. xff = ""
  144. }
  145. }
  146. dynamic "header" {
  147. for_each = var.force_tls_hsts ? [1] : []
  148. content {
  149. action = "set"
  150. destination = "http.Strict-Transport-Security"
  151. name = "Generated by force TLS and enable HSTS"
  152. type = "response"
  153. ignore_if_set = false
  154. priority = 100
  155. source = "\"max-age=${var.hsts_duration}\""
  156. }
  157. }
  158. # Dynamic compression
  159. dynamic "header" {
  160. for_each = var.dynamic_compression ? [1] : []
  161. content {
  162. action = "set"
  163. destination = "http.X-Compress-Hint"
  164. name = "Dynamic Compression"
  165. type = "response"
  166. priority = 100
  167. source = "\"on\""
  168. }
  169. }
  170. # Signal Sciences integration
  171. # We need to enable a custom main VCL file in order to do what we need here
  172. dynamic "vcl" {
  173. for_each = var.signal_science_host != "" && var.signal_science_shared_key != "" ? [1] : []
  174. content {
  175. name = "Main VCL File"
  176. content = local.vcl_main
  177. main = true
  178. }
  179. }
  180. dynamic "vcl" {
  181. for_each = (var.signal_science_host != "") && (var.signal_science_shared_key != "") ? [1] : []
  182. content {
  183. name = "sigsci_config"
  184. content = local.vcl_sigsci_config
  185. }
  186. }
  187. # Redirect www
  188. dynamic "snippet" {
  189. for_each = var.apex_redirect ? [1] : []
  190. content {
  191. name = "Redirect www to APEX - ERROR"
  192. content = local.vcl_apex_error
  193. type = "error"
  194. priority = 100
  195. }
  196. }
  197. dynamic "snippet" {
  198. for_each = var.apex_redirect ? [1] : []
  199. content {
  200. name = "Redirect www to APEX - RECV"
  201. content = local.vcl_apex_redirect
  202. type = "recv"
  203. priority = 100
  204. }
  205. }
  206. # Cache control for static files
  207. dynamic "snippet" {
  208. for_each = var.static_cache_control ? [1] : []
  209. content {
  210. name = "Add cache-control headers for static files"
  211. content = local.vcl_static_cache_control
  212. type = "fetch"
  213. priority = 100
  214. }
  215. }
  216. # Mastodon official error page
  217. dynamic "snippet" {
  218. for_each = var.mastodon_error_page ? [1] : []
  219. content {
  220. name = "Custom 503 error page"
  221. content = local.vcl_custom_error
  222. type = "error"
  223. priority = 100
  224. }
  225. }
  226. dynamic "snippet" {
  227. for_each = var.mastodon_error_page ? [1] : []
  228. content {
  229. name = "Redirect 503 to custom error page"
  230. content = local.vcl_custom_error_redirect
  231. type = "fetch"
  232. priority = 100
  233. }
  234. }
  235. # Tarpit
  236. dynamic "snippet" {
  237. for_each = var.tarpit ? [1] : []
  238. content {
  239. name = "Enable tarpit"
  240. content = local.vcl_tarpit
  241. type = "deliver"
  242. priority = 100
  243. }
  244. }
  245. dynamic "snippet" {
  246. for_each = var.tarpit ? [1] : []
  247. content {
  248. name = "Custom header for source 403"
  249. content = local.vcl_backend_403
  250. type = "fetch"
  251. priority = 100
  252. }
  253. }
  254. # Fastly Globeviz integration
  255. dynamic "snippet" {
  256. for_each = var.globeviz_service != "" ? [1] : []
  257. content {
  258. name = "Collect Globeviz data"
  259. content = local.vcl_globeviz
  260. type = "init"
  261. priority = 100
  262. }
  263. }
  264. # Media backend rewrite
  265. dynamic "snippet" {
  266. for_each = var.media_backend["bucket_prefix"] != "" ? [1] : []
  267. content {
  268. name = "Rewrite assets requests to Exoscale"
  269. content = local.vcl_media_redirect
  270. type = "miss"
  271. priority = 100
  272. }
  273. }
  274. #snippet {
  275. # name = "Block outdated user agents"
  276. # content = local.vcl_block_user_agents
  277. # type = "revc"
  278. # priority = 100
  279. #}
  280. # User-defined custom VCL snippets
  281. dynamic "snippet" {
  282. for_each = var.vcl_snippets
  283. content {
  284. content = snippet.value["content"]
  285. name = snippet.value["name"]
  286. type = snippet.value["type"]
  287. priority = snippet.value["priority"]
  288. }
  289. }
  290. # gzip default policy
  291. dynamic "gzip" {
  292. for_each = var.gzip_default_policy ? [1] : []
  293. content {
  294. content_types = [
  295. "text/html",
  296. "application/x-javascript",
  297. "text/css",
  298. "application/javascript",
  299. "text/javascript",
  300. "application/json",
  301. "application/vnd.ms-fontobject",
  302. "application/x-font-opentype",
  303. "application/x-font-truetype",
  304. "application/x-font-ttf",
  305. "application/xml",
  306. "font/eot",
  307. "font/opentype",
  308. "font/otf",
  309. "image/svg+xml",
  310. "image/vnd.microsoft.icon",
  311. "text/plain",
  312. "text/xml",
  313. ]
  314. extensions = [
  315. "css",
  316. "js",
  317. "html",
  318. "eot",
  319. "ico",
  320. "otf",
  321. "ttf",
  322. "json",
  323. "svg",
  324. ]
  325. name = "Generated by default compression policy"
  326. }
  327. }
  328. # Additional products
  329. product_enablement {
  330. brotli_compression = var.product_enablement.brotli_compression
  331. domain_inspector = var.product_enablement.domain_inspector
  332. image_optimizer = var.product_enablement.image_optimizer
  333. origin_inspector = var.product_enablement.origin_inspector
  334. websockets = var.product_enablement.websockets
  335. }
  336. # Support Apple Associated Domains
  337. dynamic "condition" {
  338. for_each = var.apple_associated_domain ? [1] : []
  339. content {
  340. name = "Associated domain file is requested"
  341. statement = "req.url.path == \"/.well-known/apple-app-site-association\""
  342. type = "REQUEST"
  343. priority = 10
  344. }
  345. }
  346. dynamic "response_object" {
  347. for_each = var.apple_associated_domain ? [1] : []
  348. content {
  349. name = "Associated domain"
  350. content = local.associated_domain_response
  351. content_type = "application/json"
  352. request_condition = "Associated domain file is requested"
  353. response = "OK"
  354. status = 200
  355. }
  356. }
  357. dynamic "dictionary" {
  358. for_each = var.edge_security ? [1] : []
  359. content {
  360. name = local.edge_security_dict_name
  361. }
  362. }
  363. # Android deep link
  364. dynamic "condition" {
  365. for_each = var.android_deep_link ? [1] : []
  366. content {
  367. name = "Android Deep Link is requested"
  368. statement = "req.url.path == \"/.well-known/assetlinks.json\""
  369. type = "REQUEST"
  370. priority = 10
  371. }
  372. }
  373. dynamic "response_object" {
  374. for_each = var.android_deep_link ? [1] : []
  375. content {
  376. name = "Android Deep Link"
  377. content = local.deep_link_response
  378. content_type = "application/json"
  379. request_condition = "Android Deep Link is requested"
  380. response = "OK"
  381. status = 200
  382. }
  383. }
  384. # IP Blocklist settings
  385. # Creates similar objects & resources to what the GUI IP Blocklist creates.
  386. dynamic "acl" {
  387. for_each = var.ip_blocklist ? [1] : []
  388. content {
  389. name = var.ip_blocklist_name
  390. }
  391. }
  392. dynamic "condition" {
  393. for_each = var.ip_blocklist ? [1] : []
  394. content {
  395. name = "Generated by IP block list"
  396. priority = 0
  397. statement = "client.ip ~ ${replace(var.ip_blocklist_name, " ", "_")}"
  398. type = "REQUEST"
  399. }
  400. }
  401. dynamic "response_object" {
  402. for_each = var.ip_blocklist ? [1] : []
  403. content {
  404. name = "Generated by IP block list"
  405. content_type = "text/html"
  406. request_condition = "Generated by IP block list"
  407. response = "Forbidden"
  408. status = 403
  409. }
  410. }
  411. # AS Blocklist settings
  412. # Any AS numbers that need to be blocked are added to a dictionary, and the
  413. # related condition/request objects are created.
  414. dynamic "dictionary" {
  415. for_each = var.as_blocklist ? [1] : []
  416. content {
  417. name = var.as_blocklist_name
  418. }
  419. }
  420. dynamic "dictionary" {
  421. for_each = var.as_blocklist ? [1] : []
  422. content {
  423. name = var.as_request_blocklist_name
  424. }
  425. }
  426. dynamic "condition" {
  427. for_each = var.as_blocklist ? [1] : []
  428. content {
  429. name = "Generated by ${var.as_blocklist_name}"
  430. priority = 10
  431. statement = "table.lookup(${replace(var.as_blocklist_name, " ", "_")}, client.as.number) == \"block\""
  432. type = "REQUEST"
  433. }
  434. }
  435. dynamic "condition" {
  436. for_each = var.as_blocklist ? [1] : []
  437. content {
  438. name = "Generated by ${var.as_request_blocklist_name}"
  439. priority = 10
  440. statement = "table.lookup(${replace(var.as_request_blocklist_name, " ", "_")}, client.as.number) == \"block\" && req.url.path ~ \"^/(api/|explore)\""
  441. type = "REQUEST"
  442. }
  443. }
  444. dynamic "response_object" {
  445. for_each = var.as_blocklist ? [1] : []
  446. content {
  447. name = "Generated by ${var.as_blocklist_name}"
  448. content_type = "text/html"
  449. request_condition = "Generated by ${var.as_blocklist_name}"
  450. response = "Forbidden"
  451. status = 403
  452. }
  453. }
  454. dynamic "response_object" {
  455. for_each = var.as_blocklist ? [1] : []
  456. content {
  457. name = "Generated by ${var.as_request_blocklist_name}"
  458. content_type = "text/html"
  459. request_condition = "Generated by ${var.as_request_blocklist_name}"
  460. response = "Forbidden"
  461. status = 403
  462. }
  463. }
  464. # JA3 Blocklist settings
  465. # Any requests that contain a TLS JA3 in the dictionary is blocked.
  466. dynamic "dictionary" {
  467. for_each = var.ja3_blocklist ? [1] : []
  468. content {
  469. name = var.ja3_blocklist_name
  470. }
  471. }
  472. dynamic "condition" {
  473. for_each = var.ja3_blocklist ? [1] : []
  474. content {
  475. name = "Generated by ${var.ja3_blocklist_name}"
  476. priority = 10
  477. statement = "table.lookup(${replace(var.ja3_blocklist_name, " ", "_")}, client.as.number) == \"block\""
  478. type = "REQUEST"
  479. }
  480. }
  481. dynamic "response_object" {
  482. for_each = var.ja3_blocklist ? [1] : []
  483. content {
  484. name = "Generated by ${var.ja3_blocklist_name}"
  485. content_type = "text/html"
  486. request_condition = "Generated by ${var.ja3_blocklist_name}"
  487. response = "Forbidden"
  488. status = 403
  489. }
  490. }
  491. }
  492. # Edge Security
  493. resource "fastly_service_dictionary_items" "edge_security" {
  494. for_each = {
  495. for d in fastly_service_vcl.app_service.dictionary : d.name => d if d.name == local.edge_security_dict_name
  496. }
  497. service_id = fastly_service_vcl.app_service.id
  498. dictionary_id = each.value.dictionary_id
  499. items = { Enabled = 100 }
  500. }
  501. # IP Blocklist entries
  502. resource "fastly_service_acl_entries" "ip_blocklist_entries" {
  503. for_each = {
  504. for d in fastly_service_vcl.app_service.acl : d.name => d if d.name == var.ip_blocklist_name
  505. }
  506. service_id = fastly_service_vcl.app_service.id
  507. acl_id = each.value.acl_id
  508. manage_entries = length(var.ip_blocklist_items) > 0 ? true : false
  509. dynamic "entry" {
  510. for_each = var.ip_blocklist_items
  511. content {
  512. ip = split("/", entry.value)[0]
  513. subnet = length(split("/", entry.value)) == 1 ? "32" : split("/", entry.value)[1]
  514. negated = false
  515. comment = "Generated by IP block list"
  516. }
  517. }
  518. }
  519. # AS Blocklist dictionary entries
  520. resource "fastly_service_dictionary_items" "as_blocklist_entries" {
  521. for_each = {
  522. for d in fastly_service_vcl.app_service.dictionary : d.name => d if d.name == var.as_blocklist_name
  523. }
  524. service_id = fastly_service_vcl.app_service.id
  525. dictionary_id = each.value.dictionary_id
  526. manage_items = length(var.as_blocklist_items) > 0 ? true : false
  527. items = { for i in var.as_blocklist_items : i => "block" }
  528. }
  529. resource "fastly_service_dictionary_items" "as_request_blocklist_entries" {
  530. for_each = {
  531. for d in fastly_service_vcl.app_service.dictionary : d.name => d if d.name == var.as_request_blocklist_name
  532. }
  533. service_id = fastly_service_vcl.app_service.id
  534. dictionary_id = each.value.dictionary_id
  535. manage_items = length(var.as_request_blocklist_items) > 0 ? true : false
  536. items = { for i in var.as_request_blocklist_items : i => "block" }
  537. }
  538. # JA3 Blocklist dictionary entries
  539. resource "fastly_service_dictionary_items" "ja_blocklist_entries" {
  540. for_each = {
  541. for d in fastly_service_vcl.app_service.dictionary : d.name => d if d.name == var.ja3_blocklist_name
  542. }
  543. service_id = fastly_service_vcl.app_service.id
  544. dictionary_id = each.value.dictionary_id
  545. manage_items = length(var.ja3_blocklist_items) > 0 ? true : false
  546. items = { for i in var.ja3_blocklist_items : i => "block" }
  547. }