main.tf 18 KB

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