main.tf 18 KB

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