ordered_cache_behavior as variable with Terraform & Cloudfront - amazon-web-services

In Terraform, I would like to add ordered_cache_behaviors to a CloudFront distribution resource if I want to, or, by default, not add any ordered_cache_behavior. Is it possible to do it?
My current code:
resource "aws_cloudfront_distribution" "proxy_cdn" {
enabled = true
price_class = "PriceClass_100"
origin {
origin_id = var.cdn_proxy_origin_id
domain_name = var.cdn_domain_name
custom_origin_config {
origin_protocol_policy = "https-only"
http_port = "80"
https_port = "443"
origin_ssl_protocols = ["TLSv1", "TLSv1.1", "TLSv1.2"]
}
}
# current code:
ordered_cache_behavior {
# ordered behavior 1
}
ordered_cache_behavior {
# ordered behavior 2
}
# ...
default_cache_behavior {
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
cached_methods = ["GET", "HEAD"]
target_origin_id = var.cdn_proxy_origin_id
forwarded_values {
query_string = true
cookies {
forward = "all"
}
}
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
acm_certificate_arn = aws_acm_certificate.proxy_certificate.arn
ssl_support_method = "sni-only"
}
aliases = ["${var.proxy_subdomain}.myurl.com"]
depends_on = [
aws_acm_certificate_validation.proxy_certificate_validation,
]
}
I would like to include an array of ordered behaviors that can also be empty. Is it possible?

It is possible by using a combination of dynamic [1] and for_each [2]. I would suggest creating a variable which is not a list, rather a map, e.g:
variable "ordered_cache_behavior" {
type = map(object({
path_pattern = string
allowed_methods = list(string)
cached_methods = list(string)
target_origin_id = string
viewer_protocol_policy = string
}))
description = "Map of ordered cache behaviors."
default = {
"ordered_cache_behavior_1" = {
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
path_pattern = "/content/*"
target_origin_id = "myS3Origin"
viewer_protocol_policy = "redirect-to-https"
}
}
}
Note that this variable contains only required arguments. It can be expanded to use other arguments as well (e.g., min_ttl, max_ttl etc.). Then, in the resource itself (shortened for readability):
resource "aws_cloudfront_distribution" "proxy_cdn" {
.
.
.
dynamic "ordered_cache_behavior" {
for_each = var.ordered_cache_behavior
content {
path_pattern = oredered_cache_behavior.value.path_pattern
allowed_methods = oredered_cache_behavior.value.allowed_methods
cached_methods = oredered_cache_behavior.value.cached_methods
target_origin_id = oredered_cache_behavior.value.target_origin_id
viewer_protocol_policy = oredered_cache_behavior.value.viewer_protocol_policy
}
}
.
.
.
}
Setting the default value for the ordered_cache_behavior to be equal to {} will mean that no oreder_cache_behavior blocks will be created.
[1] https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks
[2] https://developer.hashicorp.com/terraform/language/meta-arguments/for_each

Related

Terraform error updating CloudFront Distribution InvalidLambdaFunctionAssociation: The function cannot have environment variables

I am trying to build a terraform template that creates an AWS S3 Bucket, Cloudfront Distribution and a Lambda function that should be associated with the Cloudfront Distribution.
As soon as I add "lambda_function_association" to the Cloudfront ressource I experience following error.
Error: error updating CloudFront Distribution (XXXXXXXXXXXXXXX): InvalidLambdaFunctionAssociation: The function cannot have environment variables. Function: arn:aws:lambda:us-east-1:XXXXXXXXXXXXX:function:testtools:4
status code: 400, request id: 3ce25af1-8341-41c0-8d35-4c3c91c2c001
with aws_cloudfront_distribution.testtools,
on main.tf line 42, in resource "aws_cloudfront_distribution" "testtools":
42: resource "aws_cloudfront_distribution" "testtools" {
lambda_function_association {
event_type = "origin-response"
lambda_arn = "${aws_lambda_function.testtools.qualified_arn}"
include_body = false
}
I think it is related to the lambda_arn that is used inside the function association.
resource "aws_cloudfront_distribution" "testtools" {
depends_on = [aws_s3_bucket.testtools, aws_lambda_function.testtools]
origin {
domain_name = aws_s3_bucket.testtools.bucket_regional_domain_name
origin_id = var.s3_origin_id
s3_origin_config {
origin_access_identity = aws_cloudfront_origin_access_identity.testtools.cloudfront_access_identity_path
}
}
enabled = true
is_ipv6_enabled = true
comment = "testtools"
default_root_object = "index.html"
provider = aws
logging_config {
include_cookies = false
bucket = "testtools.s3.amazonaws.com"
prefix = "testtools"
}
aliases = ["testtools.int.test.net"]
default_cache_behavior {
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
cached_methods = ["GET", "HEAD"]
target_origin_id = var.s3_origin_id
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "allow-all"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
lambda_function_association {
event_type = "origin-response"
lambda_arn = "${aws_lambda_function.testtools.qualified_arn}"
include_body = false
}
}
price_class = "PriceClass_200"
restrictions {
geo_restriction {
restriction_type = "whitelist"
locations = ["DE", "AU", "CH", "BG"]
}
}
tags = {
Environment = "production"
}
viewer_certificate {
acm_certificate_arn = var.ssl_cert_arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1"
}
}
resource "aws_lambda_function" "testtools" {
filename = "lambda_function_payload.zip"
function_name = "testtools"
role = aws_iam_role.testtools.arn
handler = "index.test"
publish = true
provider = aws.useast1
source_code_hash = filebase64sha256("lambda_function_payload.zip")
runtime = "nodejs12.x"
environment {
variables = {
foo = "bar"
}
}
}
When using a Lambda#edge, your lambda has a lot more restrictions that it has to adhere to. Some restrictions also depend on whether you're linking the lambda to origin req/res or viewer req/res.
One of these restrictions is that you can't use environment variables. You can find more info on this page: Lambda#Edge function restrictions

Terraform modify a resource once created

I am creating a cloudfront distribution using terraform and I get this error:
error creating CloudFront Distribution: CNAMEAlreadyExists: One or more aliases specified for the distribution includes an incorrectly configured DNS record that points to another CloudFront distribution. You must update the DNS record to correct the problem.
But once I created the cloudfront "without any alternative names (aliases in terraform language)" and once it is created and try to edit it and add the alternative names it doesn't give the above error.
So I thought of creating the cloudfront first and then create other resources and then after sometime update the created cloudfront with aliases
Is this possibe? What is the workaround? Why the first time it is failing (with same alternative name) but once created and add the same alternative domain again it works?
Here's my cloudfront.tf
resource "aws_cloudfront_distribution" "aws_cloudfront_distribution" {
# aliases = var.CF_ALIASES
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
compress = "true"
default_ttl = "0"
forwarded_values {
cookies {
forward = "none"
}
query_string = "false"
}
# lambda_function_association {
# event_type = "origin-response"
# include_body = "false"
# lambda_arn = var.CF_LAMBDA_ARN
# }
max_ttl = "0"
min_ttl = "0"
smooth_streaming = "false"
target_origin_id = var.CF_BUCKET_ORIGIN_ID
viewer_protocol_policy = "redirect-to-https"
}
enabled = "true"
http_version = "http2"
is_ipv6_enabled = "true"
origin {
custom_origin_config {
http_port = "80"
https_port = "443"
origin_keepalive_timeout = "5"
origin_protocol_policy = "http-only"
origin_read_timeout = "30"
origin_ssl_protocols = ["TLSv1", "TLSv1.1", "TLSv1.2"]
}
domain_name = var.CF_BUCKET_DOMAIN_NAME
origin_id = var.CF_BUCKET_ORIGIN_ID
}
price_class = "PriceClass_All"
restrictions {
geo_restriction {
restriction_type = "none"
}
}
retain_on_delete = "false"
tags = var.CF_TAGS_LIST
viewer_certificate {
acm_certificate_arn = var.CF_CERTFICATE_ARN
cloudfront_default_certificate = "false"
minimum_protocol_version = "TLSv1.2_2018"
ssl_support_method = "sni-only"
}
}
And how can I update the same cloudfront with alternative name in terraform?

Terraform Error: Invalid value for module argument when running tf plan for cloudfront module

I have a module which creates submodules and each submodule have a custom_error_response that land to a different response_page_path.
In the main module I have added;
custom_error_response = [
{
error_code = "403"
error_caching_min_ttl = "30"
response_code = "200"
response_page_path = "/${var.default_root_object}"
},
]
and the variable default_root_object is:
variable "default_root_object" {
description = "Default root object at origin for CloudFront distribution"
}
In the submodules I have added the following in the variables.tf:
variable "custom_error_response" {
description = "(Optional) - List of one or more custom error response element maps"
type = "list"
default = []
}
In the main.tf of each sub-module I have also defined the custom_error_response like this:
dynamic "custom_error_response" {
for_each = var.custom_error_response
content {
error_caching_min_ttl = lookup(custom_error_response.value, "error_caching_min_ttl", null)
error_code = custom_error_response.value.error_code
response_code = lookup(custom_error_response.value, "response_code", null)
response_page_path = lookup(custom_error_response.value, "response_page_path", null)
}
}
When I am running terraform init it is okay, but when I run terraform plan it gives me the same error 8 times for the 8 sub modules I have made.
Error: Invalid value for module argument
on portal_distribution/main.tf line 32, in module "**mainmodule**":
32: custom_error_response = [
33: {
34: error_code = "403"
35: error_caching_min_ttl = "30"
36: response_code = "200"
37: response_page_path = "/${var.default_root_object}"
38: },
39: ]
The given value is not suitable for child module variable
"custom_error_response" defined at
.terraform/modules/**submodule**_portal.**mainmodule**/modules/aws-terraform-cloudfront_s3_origin/variables.tf:302,1-33:
element 0: string required.
How do I fix the error?
The type of your variable is a list. The fact that you're doing map lookups suggest you're trying to use a map or something. What I'd recommend: use a map of objects.
If you need it to be a var:
variable myinput {
type = map(list(object({
error_caching_min_ttl = string
error_code = number
response_code = number
response_page_path = string
})))
}
In the code I used to prove this on my machine, I didnt' use a var. So where the for_each is, just put the var there instead and populate the var with your values. The solution is to use a map of objects. I like maps over lists because you can give each item in the collection a descriptive title. This helps with giving names in the state that are meaningul.
dynamic "custom_error_response" {
for_each = map(
"descriptiveTitleForThis", {
error_caching_min_ttl = "60"
error_code = 403
response_code = 11
response_page_path = "page1"
},
"551directToPage2", {
error_caching_min_ttl = "60"
error_code = 551
response_code = 10
response_page_path = "page2"
}
)
content {
error_caching_min_ttl = custom_error_response.value.error_caching_min_ttl
error_code = custom_error_response.value.error_code
response_code = custom_error_response.value.response_code
response_page_path = custom_error_response.value.response_page_path
}
}
My full code below if you want it:
provider aws {
profile = "myprofile"
region = "us-west-2"
}
resource "aws_s3_bucket" "b" {
bucket = "mybucket"
acl = "private"
tags = {
Name = "My bucket"
}
}
locals {
s3_origin_id = "myS3Origin"
}
resource "aws_cloudfront_distribution" "s3_distribution" {
enabled = true
origin {
domain_name = aws_s3_bucket.b.bucket_regional_domain_name
origin_id = local.s3_origin_id
}
default_cache_behavior {
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
cached_methods = ["GET", "HEAD"]
target_origin_id = local.s3_origin_id
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "allow-all"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
}
restrictions {
geo_restriction {
restriction_type = "whitelist"
locations = ["US", "CA", "GB", "DE"]
}
}
viewer_certificate {
cloudfront_default_certificate = true
}
dynamic "custom_error_response" {
for_each = map(
"descriptiveTitleForThis", {
error_caching_min_ttl = "60"
error_code = 403
response_code = 11
response_page_path = "page1"
},
"551directToPage2", {
error_caching_min_ttl = "60"
error_code = 551
response_code = 10
response_page_path = "page2"
},
"552directToPage3", {
error_caching_min_ttl = "60"
error_code = 552
response_code = 12
response_page_path = "page3"
},
"553directToPage4", {
error_caching_min_ttl = "60"
error_code = 553
response_code = 13
response_page_path = "page4"
},
)
content {
error_caching_min_ttl = custom_error_response.value.error_caching_min_ttl
error_code = custom_error_response.value.error_code
response_code = custom_error_response.value.response_code
response_page_path = custom_error_response.value.response_page_path
}
}
}
Edit: I forgot to print the plan
Terraform will perform the following actions:
# aws_cloudfront_distribution.s3_distribution will be created
+ resource "aws_cloudfront_distribution" "s3_distribution" {
<..ommitted for brevity...>
+ custom_error_response {
+ error_caching_min_ttl = 60
+ error_code = 403
+ response_code = 11
+ response_page_path = "page1"
}
+ custom_error_response {
+ error_caching_min_ttl = 60
+ error_code = 551
+ response_code = 10
+ response_page_path = "page2"
}
+ custom_error_response {
+ error_caching_min_ttl = 60
+ error_code = 552
+ response_code = 12
+ response_page_path = "page3"
}
+ custom_error_response {
+ error_caching_min_ttl = 60
+ error_code = 553
+ response_code = 13
+ response_page_path = "page4"
}
<..ommitted for brevity...>
}
# aws_s3_bucket.b will be created
+ resource "aws_s3_bucket" "b" {
<..ommitted for brevity...>
}
Plan: 2 to add, 0 to change, 0 to destroy.
terraform version
Terraform v0.12.20
provider.aws v3.9.0

Error when using aws wafv2 create-web-acl

I am using version 2.0.50 of the AWS cli on Linux, and trying to create a v2 AWS WAF. Running the command aws wafv2 create-web-acl --cli-input-json file://waf.json results in the following response:
An error occurred (WAFInvalidParameterException) when calling the CreateWebACL operation: Error reason: Your statement has multiple values set for a field that requires exactly one value., field: RULE, parameter: Rule
Can somebody identify what is wrong with the following JSON, or confirm that they are seeing the same issue?
{
"DefaultAction": {
"Allow": {}
},
"Name": "test-web-acl",
"Rules": [
{
"Name": "rule-one",
"Priority": 1,
"Statement": {
"ManagedRuleGroupStatement": {
"Name": "AWSManagedRulesUnixRuleSet",
"VendorName": "AWS"
}
},
"VisibilityConfig": {
"CloudWatchMetricsEnabled": false,
"MetricName": "rule-one-metric",
"SampledRequestsEnabled": false
}
}
],
"Scope": "REGIONAL",
"VisibilityConfig": {
"CloudWatchMetricsEnabled": false,
"MetricName": "test-web-acl-metric",
"SampledRequestsEnabled": false
}
}
I can't see what is incorrect about the JSON according to the syntax described here CreateWebACL
The answer is that the OverrideAction attribute is missing from the Rule object. When adding "OverrideAction":{"None":{}} to the Rule object, then the ACL was created. The error message is misleading.
# resources.tf
resource "aws_wafv2_ip_set" "ip_whitelist" {
name = var.waf_name
scope = var.waf_scope
ip_address_version = var.waf_ip_address_version
addresses = [var.waf_addresses]
}
resource "aws_wafv2_web_acl" "web_acl" {
name = var.waf_web_acl_name
description = var.waf_description
scope = var.waf_scope
default_action {
allow {}
}
# ipsets
rule {
name = var.waf_name
priority = 0
action {
allow {}
}
statement {
ip_set_reference_statement {
arn = aws_wafv2_ip_set.ip_whitelist.arn
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = var.waf_name
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = var.waf_name
sampled_requests_enabled = true
}
dynamic "rule" {
for_each = var.rules
content {
name = rule.value.name
priority = rule.value.priority
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = rule.value.managed_rule_group_statement_name
vendor_name = rule.value.managed_rule_group_statement_vendor_name
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = rule.value.metric_name
sampled_requests_enabled = true
}
}
}
}
resource "aws_wafv2_web_acl_association" "waf_alb" {
resource_arn = aws_alb.alb.arn
web_acl_arn = aws_wafv2_web_acl.web_acl.arn
}
# variables.tf
# waf ipset name
variable "waf_name" {
type = string
default = "ip-whitelist"
}
variable "waf_scope" {
type = string
default = "REGIONAL"
}
variable "waf_ip_address_version" {
default = "IPV4"
type=string
}
variable "waf_addresses" {
default = "your-ip/32"
type = string
}
# waf details
variable "waf_web_acl_name" {
type=string
default = "waf-rules"
}
variable "waf_description" {
type=string
default = "waf rules"
}
# waf multiple rules
variable "rules" {
type = list(any)
default = [
{
name = "AWS-AWSManagedRulesAdminProtectionRuleSet"
priority = 1
managed_rule_group_statement_name = "AWSManagedRulesAdminProtectionRuleSet"
managed_rule_group_statement_vendor_name = "AWS"
metric_name = "foo_name"
},
{
name = "AWS-AWSManagedRulesPHPRuleSet"
priority = 2
managed_rule_group_statement_name = "AWSManagedRulesPHPRuleSet"
managed_rule_group_statement_vendor_name = "AWS"
metric_name = "foo_name"
},
{
name = "AWS-AWSManagedRulesLinuxRuleSet"
priority = 3
managed_rule_group_statement_name = "AWSManagedRulesLinuxRuleSet"
managed_rule_group_statement_vendor_name = "AWS"
metric_name = "foo_name"
},
{
name = "AWS-AWSManagedRulesAmazonIpReputationList"
priority = 4
managed_rule_group_statement_name = "AWSManagedRulesAmazonIpReputationList"
managed_rule_group_statement_vendor_name = "AWS"
metric_name = "foo_name"
},
{
name = "AWS-AWSManagedRulesSQLiRuleSet"
priority = 5
managed_rule_group_statement_name = "AWSManagedRulesSQLiRuleSet"
managed_rule_group_statement_vendor_name = "AWS"
metric_name = "foo_name"
},
{
name = "AWS-AWSManagedRulesUnixRuleSet"
priority = 6
managed_rule_group_statement_name = "AWSManagedRulesUnixRuleSet"
managed_rule_group_statement_vendor_name = "AWS"
metric_name = "foo_name"
},
{
name = "AWS-AWSManagedRulesCommonRuleSet"
priority = 7
managed_rule_group_statement_name = "AWSManagedRulesCommonRuleSet"
managed_rule_group_statement_vendor_name = "AWS"
metric_name = "foo_name"
}
]
}

Error creating CloudFront Distribution: NoSuchOrigin:

I am trying to deploy a Cloudfront distribution with Terraform and getting an error while specifying the origin_id
Cloudfront is pointing at a load balancer via a Route53 lookup.
resource "aws_cloudfront_distribution" "my-app" {
origin {
custom_origin_config {
http_port = 443
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols = ["TLSv1.2"]
}
domain_name = "${var.domain_name}"
origin_id = "Custom-${var.domain_name}"
}
...
default_cache_behavior {
allowed_methods = ["GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT", "DELETE"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "${local.origin_id}"
...
where var.domain_name is a route53 record and local.origin_id is a unique id.
When performing the terraform apply I get this error:
aws_cloudfront_distribution.my-app: error creating CloudFront Distribution: NoSuchOrigin: One or more of your origins or origin groups do not exist.
The documentation states: origin_id (Required) - A unique identifier for the origin. which it is.
The error relates to the cache behaviour.
You need to make sure that the target_origin_id relates to an origin_id within a cache behaviour.
Like so:
resource "aws_cloudfront_distribution" "my-app" {
origin {
custom_origin_config {
http_port = 443
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols = ["TLSv1.2"]
}
domain_name = "${var.domain_name}"
origin_id = "Custom-${var.domain_name}"
}
...
default_cache_behavior {
allowed_methods = ["GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT", "DELETE"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "Custom-${var.domain_name}"
...