Terraform modify a resource once created - amazon-web-services

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?

Related

ordered_cache_behavior as variable with Terraform & Cloudfront

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

wafv2 firewall rules to restrict domain names

I created WAFV2 firewall rules to restrict few IP address to hit API gateways. it is working as expected.
# ipsets
resource "aws_wafv2_ip_set" "example-ip-sets" {
name = "ipsetsexample"
description = "Example IP set"
scope = "REGIONAL"
ip_address_version = "IPV4"
addresses = ["137.137.43.154/32", "40.121.120.150/32"]
tags = {
Tag1 = "Value1"
Tag2 = "Value2"
}
}
resource "aws_wafv2_web_acl" "firewall" {
name = "firewall"
scope = "REGIONAL"
default_action {
block {}
}
rule {
name = "ip-blacklist"
priority = 1
action {
allow {}
}
statement {
ip_set_reference_statement {
arn = aws_wafv2_ip_set.example-ip-sets.arn
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "BlacklistedIP"
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "Allowed"
sampled_requests_enabled = true
}
}
resource "aws_wafv2_web_acl_association" "this" {
resource_arn = aws_api_gateway_stage.api_stage.arn
web_acl_arn = aws_wafv2_web_acl.firewall.arn
}
but now new scenario came in to picture like, block some domain names instead of IP's (abcde.com, us1a.bcde.com) to invoke the API gateways. I have searched many terraform docs but didn't find any related.
is there any docs or source to restrict domain names to hit the API gateway ?

Terraform and AWS: Static website redirects to Amazon's S3 landing page

I'm trying to deploy a simple, static website to AWS using Terraform. The overall goal is to create an S3 bucket, upload my HTML files and other resources, then put that bucket behind a Cloudfront URL with an ACM-generated certificate so that I can view it in the browser. However, whenever I try and hit the Route 53-registered DNS name that should be pointing to my Cloudfront distribution, I end up at Amazon's S3 landing page (https://aws.amazon.com/s3/)
The S3 bucket name in this case would be something like "mysupercoolwebsite.com". The code to create the S3 buckets looks like this...
resource "aws_s3_bucket" "root_bucket" {
bucket = var.bucket_name
tags = var.common_tags
}
data "template_file" "root_bucket" {
template = file("templates/s3-policy.json.tmpl")
vars = {
bucket = var.bucket_name
}
}
resource "aws_s3_bucket_acl" "root_bucket" {
bucket = aws_s3_bucket.root_bucket.id
acl = "public-read"
}
resource "aws_s3_bucket_policy" "root_bucket" {
bucket = aws_s3_bucket.root_bucket.id
policy = data.template_file.root_bucket.rendered
}
resource "aws_s3_bucket_website_configuration" "root_bucket" {
bucket = aws_s3_bucket.root_bucket.id
index_document {
suffix = "index.html"
}
}
resource "aws_s3_bucket_cors_configuration" "root_bucket" {
bucket = aws_s3_bucket.root_bucket.id
cors_rule {
allowed_headers = ["Authorization", "Content-Length"]
allowed_methods = ["GET"]
allowed_origins = ["https://${var.domain_name}"]
max_age_seconds = 3000
}
}
resource "aws_s3_object" "html" {
for_each = fileset("../html/", "**/*")
bucket = aws_s3_bucket.root_bucket.id
key = each.value
source = "../html/${each.value}"
etag = filemd5("../html/${each.value}")
}
This seems correct, as I see the files uploaded to S3 as expected. I then try to create my Cloudfront distribution (I'll omit the Route 53 and ACM stuff):
resource "aws_cloudfront_distribution" "root_s3_distribution" {
origin {
domain_name = aws_s3_bucket_website_configuration.root_bucket.website_endpoint
origin_id = "S3-${var.bucket_name}"
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "http-only"
origin_ssl_protocols = ["TLSv1", "TLSv1.1", "TLSv1.2"]
}
}
enabled = true
is_ipv6_enabled = true
default_root_object = "index.html"
aliases = [var.domain_name]
custom_error_response {
error_caching_min_ttl = 0
error_code = 404
response_code = 200
response_page_path = "/404.html"
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "S3-${var.bucket_name}"
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "redirect-to-https"
min_ttl = 31536000
default_ttl = 31536000
max_ttl = 31536000
compress = true
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
acm_certificate_arn = aws_acm_certificate_validation.cert_validation.certificate_arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.1_2016"
}
tags = var.common_tags
}
Everything gets created via Terraform. No errors or warnings or anything. However, when I try to hit my DNS endpoint, I get redirected to the wrong page. What am I doing wrong here? My gut tells me it's something with the domain_name value specified in the Cloudfront origin stanza. Per the output of my Terraform, that value is:
mysupercoolwebsite.com.s3-website-us-east-1.amazonaws.com
where mysupercoolwebsite.com is the name of my S3 bucket. Any ideas what I'm missing or specifying incorrectly here that would cause this behavior?
EDIT: I should also point out that if I go to the Cloudfront domain name (.cloudfront.net) I see the expected HTML. So, I know it has to be something in how I'm connecting my Route 53 DNS record to my Cloudfront distribution.

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

Terraformed AWS API Gateway Custom Domain Names throws 403 Forbidden

I am trying to expose all the stages of my Regional API Gateway through a regional Custom Domain.
Problem
If I curl directly my API Gateway (ie. https://xx.execute-api.eu-west-3.amazonaws.com/default/users), it works, but I get a 403 if I curl de domain name (ie. https://api.acme.com/default/users).
Configuration
My Terraform files looks like that:
data "aws_route53_zone" "acme" {
name = "acme.com."
}
resource "aws_api_gateway_rest_api" "backend" {
name = "acme-backend-api"
description = "Backend API"
body = "SOMETHING"
endpoint_configuration {
types = ["REGIONAL"]
}
}
resource "aws_api_gateway_deployment" "backend" {
rest_api_id = aws_api_gateway_rest_api.backend.id
stage_name = "default"
lifecycle {
create_before_destroy = true
}
}
resource "aws_api_gateway_domain_name" "backend" {
domain_name = "api.acme.com"
regional_certificate_arn = "arn:aws:acm:xx:certificate/xx"
endpoint_configuration {
types = ["REGIONAL"]
}
}
resource "aws_route53_record" "backend" {
name = aws_api_gateway_domain_name.backend.domain_name
type = "A"
zone_id = data.aws_route53_zone.acme.id
alias {
evaluate_target_health = true
name = aws_api_gateway_domain_name.backend.regional_domain_name
zone_id = aws_api_gateway_domain_name.backend.regional_zone_id
}
}
resource "aws_api_gateway_base_path_mapping" "backend" {
api_id = aws_api_gateway_rest_api.backend.id
domain_name = aws_api_gateway_domain_name.backend.domain_name
# No stage_name: expose all stages
}
According to the Terraform api_gateway_domain_name and api_gateway_base_path_mapping examples, it should be ok.
I have also followed many howtos, and I have these elements:
The certificate
The A record to the API custom domain
The mapping to the deployed stage (which works if you call it directly)
What do I miss/do wrong?
This is v2 example working for me as off today, this "aws_apigatewayv2_api_mapping" is key to avoid port 80: Connection refused
or {"message":"Forbidden"} errors which I see you have but I did struggle with.
// ACM
resource "aws_acm_certificate" "cert_api" {
domain_name = var.api_domain
validation_method = "DNS"
tags = {
Name = var.api_domain
}
}
resource "aws_acm_certificate_validation" "cert_api" {
certificate_arn = aws_acm_certificate.cert_api.arn
}
// API Gateway V2
resource "aws_apigatewayv2_api" "lambda" {
name = "serverless_lambda_gw"
protocol_type = "HTTP"
}
resource "aws_apigatewayv2_stage" "lambda" {
api_id = aws_apigatewayv2_api.lambda.id
name = "serverless_lambda_stage"
auto_deploy = true
access_log_settings {
destination_arn = aws_cloudwatch_log_group.api_gw.arn
format = jsonencode({
requestId = "$context.requestId"
sourceIp = "$context.identity.sourceIp"
requestTime = "$context.requestTime"
protocol = "$context.protocol"
httpMethod = "$context.httpMethod"
resourcePath = "$context.resourcePath"
routeKey = "$context.routeKey"
status = "$context.status"
responseLength = "$context.responseLength"
integrationErrorMessage = "$context.integrationErrorMessage"
}
)
}
}
resource "aws_apigatewayv2_integration" "testimonials" {
api_id = aws_apigatewayv2_api.lambda.id
integration_uri = aws_lambda_function.testimonials.invoke_arn
integration_type = "AWS_PROXY"
integration_method = "POST"
}
resource "aws_apigatewayv2_route" "testimonials" {
api_id = aws_apigatewayv2_api.lambda.id
route_key = "GET /testimonials"
target = "integrations/${aws_apigatewayv2_integration.testimonials.id}"
}
resource "aws_cloudwatch_log_group" "api_gw" {
name = "/aws/api_gw/${aws_apigatewayv2_api.lambda.name}"
retention_in_days = 30
}
resource "aws_lambda_permission" "api_gw" {
statement_id = "AllowExecutionFromAPIGateway"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.testimonials.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_apigatewayv2_api.lambda.execution_arn}/*/*"
}
resource "aws_apigatewayv2_domain_name" "api" {
domain_name = var.api_domain
domain_name_configuration {
certificate_arn = aws_acm_certificate.cert_api.arn
endpoint_type = "REGIONAL"
security_policy = "TLS_1_2"
}
}
resource "aws_apigatewayv2_api_mapping" "api" {
api_id = aws_apigatewayv2_api.lambda.id
domain_name = aws_apigatewayv2_domain_name.api.id
stage = aws_apigatewayv2_stage.lambda.id
}
// Route53
resource "aws_route53_zone" "api" {
name = var.api_domain
}
resource "aws_route53_record" "cert_api_validations" {
allow_overwrite = true
count = length(aws_acm_certificate.cert_api.domain_validation_options)
zone_id = aws_route53_zone.api.zone_id
name = element(aws_acm_certificate.cert_api.domain_validation_options.*.resource_record_name, count.index)
type = element(aws_acm_certificate.cert_api.domain_validation_options.*.resource_record_type, count.index)
records = [element(aws_acm_certificate.cert_api.domain_validation_options.*.resource_record_value, count.index)]
ttl = 60
}
resource "aws_route53_record" "api-a" {
name = aws_apigatewayv2_domain_name.api.domain_name
type = "A"
zone_id = aws_route53_zone.api.zone_id
alias {
name = aws_apigatewayv2_domain_name.api.domain_name_configuration[0].target_domain_name
zone_id = aws_apigatewayv2_domain_name.api.domain_name_configuration[0].hosted_zone_id
evaluate_target_health = false
}
}