Struggling to automate terraform WAF - amazon-web-services

I'm trying to terraform WAF ACL and associated rules. The terraform stack I'm working on is identical in DEV, QA , and PROD, differences are all handled using different variables. So my idea is to store a list of CIDRs in a variable, and automatically create ALLOW rules for each. My limited knowledge is slowing me down though. It creates the ipsets perfectly, but the rules and ACL complain,
variable cloud_allowed_cidr_list = {type="list" default=["1.2.3.4/32","4.3.2.1/32"]}
resource "aws_waf_ipset" "ipset" {
count = "${length(var.cloud_allowed_cidr_list)}"
name = "ipset-${count.index}"
ip_set_descriptors {
type = "IPV4"
value = "${element(var.cloud_allowed_cidr_list, count.index)}"
}
}
resource "aws_waf_rule" "matchIPrule" {
count = "${length(var.cloud_allowed_cidr_list)}"
depends_on = ["aws_waf_ipset.ipset"]
name = "matchMancIPrule${count.index}"
metric_name = "matchMancIPrule${count.index}"
predicates {
data_id = "${aws_waf_ipset.ipset.*.id}"
negated = false
type = "IPMatch"
}
}
resource "aws_waf_web_acl" "waf_acl" {
depends_on = ["aws_waf_ipset.ipset", "aws_waf_rule.matchIPrule"]
name = "mancACL${count.index}"
metric_name = "mancACL${count.index}"
default_action {
type = "BLOCK"
}
rules {
action {
type = "ALLOW"
}
priority = "${count.index}"
rule_id = "${aws_waf_rule.matchIPrule.id}"
type = "REGULAR"
}
}
It fell apart when I realised that rules have multiple predicates, and the ACL has multiple rules .....how do you create that dynamically ? If anyone has any examples of doing something similar I'd be very grateful.

Since the release of 0.12 you can now do this using dynamic blocks.
No need to use count to iterate over your array.
resource "aws_waf_ipset" "ipset" {
name = "youripset"
dynamic "ip_set_descriptors" {
iterator = ip
for_each = var.cloud_allowed_cidr_list
content {
type = "IPV4"
value = ip.value
}
}
}

Related

Create Assume Role Policy with dynamic blocks

I am trying to create a Data Source for Assume Role Policy according to the terraform documentation,
Defining the "PRINCIPALS" code block and assigning the harcoded data works without a problem, but I want to assign the dynamic values from a tfvars file. When using the same dynamic block for "STATEMENT" it does not recognize the values even though it is similar to the initial block, I hope I have made myself understood.
variables.tf
variable "assume_role" {
description = "Assume example"
type = map(object({
sid = string
effect = string
actions = list(string)
principals = map(string)
}))
}
main.tf
data "aws_iam_policy_document" "assume-role-policy" {
dynamic "statement" {
for_each = var.assume_role
content {
actions = lookup(statement.value, "actions")
effect = lookup(statement.value, "effect")
sid = lookup(statement.value, "sid")
# this way works fine.. but with the dynamic block doesn't
# principals {
# type = "Service"
# identifiers = ["glue.amazonaws.com"]
# }
dynamic "principals" {
for_each = lookup(statement.value, "principals", {})
content {
type = lookup(principals.value, "type")
identifiers = lookup(principals.value, "identifiers")
}
}
}
}
}
resource "aws_iam_role" "instance" {
name = "instance_role_example"
assume_role_policy = data.aws_iam_policy_document.assume-role-policy.json
}
inn.tfvars
assume_role = {
assume = ({
sid = "1010"
effect = "Allow"
actions = ["sts:AssumeRole"]
principals = {
type = "Service"
identifiers = "glue.amazonaws.com"
}
})
}
Result:
Thank you very much in advance
It does not work, because there is nothing in principals to iterate over. You have only a single map, not a list of maps, nor a map of maps. You just directly access the fields (no iteration required):
dynamic "principals" {
for_each = lookup(statement.value, "principals", {})
content {
type = lookup(statement.value.principals, "type")
identifiers = [lookup(statement.value.principals, "identifiers")]
}
}

AWS Terraform passing of multiple arn for Cloudfront function_association

I like to do a dynamic function_assocation to an AWS CloudFront resource. Instead of defining each function, I did something like the below.
resource "aws_cloudfront_function" "functions" {
for_each = var.cf_lambda_functions
name = each.value.name
comment = each.value.description
runtime = each.value.runtime
publish = each.value.publish
code = each.value.code
}
and for the function_association, I did something like the below.
dynamic "function_association" {
for_each = aws_cloudfront_function.functions
content {
event_type = "viewer-request"
function_arn = each.value.arn
}
}
this gives me an error: each.value cannot be used in this context. How do you do this by passing multiple ARN of functions?
In dynamic blocks you can't use each. Instead it should be function_association in your case:
dynamic "function_association" {
for_each = aws_cloudfront_function.functions
content {
event_type = "viewer-request"
function_arn = function_association.value.arn
}
}

Terraform for loop inside for_each argument

Can someone please explain what a for loop inside a for_each argument does in Terraform? I am trying to create an AWS SSL certificate. I've seen other code like the below but I don't understand it:
resource "aws_acm_certificate" "nonprod_cert" {
domain_name = var.phz_domain_name
validation_method = "DNS"
}
resource "aws_route53_record" "nonprod_cert_record" {
for_each = {
for dvo in aws_acm_certificate.nonprod_cert.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
zone_id = var.phz_id
name = each.value.name
type = each.value.type
records = [each.value.record]
ttl = 60
}
resource "aws_acm_certificate_validation" "nonprod_cert_validated" {
certificate_arn = aws_acm_certificate.nonprod_cert.arn
validation_record_fqdns = [for record in aws_route53_record.nonprod_cert_record : record.fqdn]
depends_on = [
aws_acm_certificate.nonprod_cert,
aws_route53_record.nonprod_cert_record
]
}
The specific line that I don't understand is the one in the route53 record. I get that a for_each argument can be used to create multiple resources from a single block, but I can't find anywhere that explains what this for loop is doing inside of it. If someone could explain that would be great!
The inner for "loop" creates the data that the for_each then iterates over. Specifically the each.key will be the dvo.domain_name and the each.value will be the
{
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
You could simply move that into a locals block beforehand and not have it in a single line:
locals {
records = {
for dvo in aws_acm_certificate.nonprod_cert.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
}
resource "aws_route53_record" "nonprod_cert_record" {
for_each = local.records
zone_id = var.phz_id
name = each.value.name
type = each.value.type
records = [each.value.record]
ttl = 60
}
That will have the exact same effect.
First they are using a for expression to convert one type of object into another type. In this case they are converting the list of domain_validation_options into a list of objects that can be used for creating aws_route53_record resources.
Next they are using for_each to create a new aws_route53_record resource for each element of the list that was generated by the for expression.
The key things to be aware of here are:
for is used to convert a list of objects into a list of different objects.
for_each is used to create multiple resources from a list, set, or map of values.
I highly recommend spending the time to go through the Terraform learning site, or at least the Terraform documentation to learn the basic keywords and overall syntax.

Terraform for_each use case

I am creating a bunch of Route 53 resolver rules in each region and this works great with for_each loop:
resource "aws_route53_resolver_rule" "resolver-rule" {
for_each = var.resolver-rules
domain_name = each.value.domain-name
name = format("core-%s", each.value.domain-label)
rule_type = "FORWARD"
resolver_endpoint_id = aws_route53_resolver_endpoint.outbound-endpoint.id
target_ip {
ip = each.value.forwarder1
}
target_ip {
ip = each.value.forwarder2
}
tags = merge(var.tags, map("Name", format("core-%s-%s-%s", var.team_name, var.environment, each.value.domain-label)))
}
My var looks like this:
variable "resolver-rules" {
description = "A map of parameters needed for each resolver rule"
type = map(object({
domain-name = string
domain-label = string
forwarder1 = string
forwarder2 = string
}))
}
resolver-rules = {
"resolver-rule1" = {
domain-name = "10.in-addr.arpa."
domain-label = "10-in-addr-arpa"
forwarder1 = "10.10.1.100"
forwarder2 = "10.10.2.100"
}
"resolver-rule2" = {
domain-name = "mycompany.com."
domain-label = "mycompany-com"
forwarder1 = "10.10.1.100"
forwarder2 = "10.10.2.100"
}
}
Now I need to associate those rules with resource share (not posting here):
resource "aws_ram_resource_association" "rule-association" {
for_each = var.resolver-rules
resource_arn = aws_route53_resolver_rule.resolver-rule.arn
resource_share_arn = aws_ram_resource_share.rte53-resolver-share.arn
}
Question: how do I modify (enumerate) my resource association part, so that each association would be created for each rule I define (it has to be 1 to 1 association). I just can't wrap my head around it :)
Your aws_route53_resolver_rule.resolver-rule will be a map with keys of resolver-rule1 and resolver-rule2.
Therefore to access its arn in your rule-association you could do the following:
resource "aws_ram_resource_association" "rule-association" {
for_each = var.resolver-rules
resource_arn = aws_route53_resolver_rule.resolver-rule[each.key].arn
resource_share_arn = aws_ram_resource_share.rte53-resolver-share.arn
}
In your question, aws_ram_resource_share.rte53-resolver-share.arn is not shown, thus I don't know if you also require some changes to it or not.

Terraform create a Cloudwatch rule with multiple targets

From the Terraform docs - https://www.terraform.io/docs/providers/aws/r/cloudwatch_event_target.html
I don't see an option to map multiple targets to the same Cloudwatch rule. It only takes an arn field which accepts one resource. I'm trying to map 5 Lambdas to the same Cloudwatch rule. Does Terraform support this?
EDIT: How can I attach only 5 lambdas? If I've created 15 lambdas, I want to attach 5 each to 3 cloudwatch rules.
Got it working! I had to divide the count of the rules by 5 when I assigned targets to rules. This is roughly what it looks like:
resource "aws_cloudwatch_event_rule" "arule" {
count = "${ceil(length(var.lambda_arns) / 5.0)}" // Needs to be 5.0 to force float computation
name = "${var.rule_name}${format("-%d", count.index)}"
is_enabled = true
}
resource "aws_cloudwatch_event_target" "atarget" {
depends_on = ["aws_cloudwatch_event_rule.arule"]
count = "${length(var.lambda_arns)}"
rule = "${aws_cloudwatch_event_rule.arule.*.name[count.index / 5]}"
arn = "${var.lambda_arns[count.index]}"
}
I created the event rules based on the number of lambdas (i.e., if there are 10 lambdas, 2 rules are created).
I created the targets based on number of lambdas (i.e., if there are 10 lambdas, 10 targets are created).
I assigned the targets proportionally among the rules by dividing the count.index by 5 (the same logic used to determine the count of rules).
Assuming you created all your lambas using the same terraform resource with count, you can use count on this as well:
resource "aws_cloudwatch_event_target" "cw_target" {
count = length(aws_lambda_function.my_lambdas)
rule = "${aws_cloudwatch_event_rule.my_rule.name}"
arn = "${aws_lambda_function.my_lambdas.*.arn[count.index]}"
}
Here is what I did. I ignore the "target_id" of resource "aws_cloudwatch_event_target" (very important), and use local variables (define your local vars, this example: "targets"), and looping for the local vars, and creating multiple aws_cloudwatch_event_target, and multiple assessment templates.
locals {
stack_name_prefix = "Inspector"
rules_package_arn_cis = "arn:aws:inspector:ap-southeast-2:454640832652:rulespackage/0-Vkd2Vxjq"
default_target = {
rules : [local.rules_package_arn_cis],
duration : 3600
}
targets = [
merge(local.default_target, {
name : "data_indexer",
tags : {
Function = "LR - DX"
},
}),
merge(local.default_target, {
name : "ai_engine",
tags : {
Function = "LR - AIE"
},
}),
merge(local.default_target, {
name : "data_processor",
tags : {
Function = "LR - Data Processor"
},
}),
merge(local.default_target, {
name : "platform_manager",
tags : {
Function = "LR - PM"
},
})
]
}
resource "aws_inspector_assessment_template" "assessment_template" {
count = length(local.targets)
name = "${local.stack_name_prefix}_${local.targets[count.index]["name"]}_assessment_template"
target_arn = aws_inspector_assessment_target.assessment[count.index].arn
duration = local.default_target.duration
rules_package_arns = [local.rules_package_arn_cis]
}
resource "aws_cloudwatch_event_target" "event_target_for_inspector_assessment_template" {
count = length(local.targets)
rule = aws_cloudwatch_event_rule.event_rule_for_inspector_assessment_template.name
// target_id = "amazon_inspector_assessment" ## Don't USE target_id, it will mess up the cloudwatch event target, and only generated one target instead of 4
arn = aws_inspector_assessment_template.assessment_template[count.index].arn
role_arn = aws_iam_role.inspector_assessment_template.arn
}
module "eventbridgetarget" {
for_each = var.rule_and_target_details
source = "git::ssh://git#bitbucket.int.ally.com/tf/terraform-modules-aws-eventbridge.git//modules/target?ref=v1"
rule_arn = module.eventbridgerule.rule.arn
name = each.value.name
namespace = module.namespace.lower_short_name
tags = module.namespace.tags
#arn = module.lambda.arn
arn = each.value.arn
}
Now in the tfvars pass the value like below:
rule_and_target_details = {
"firsttarget" = {
name = "getentities"
arn = "arn:aws:execute-api:us-east-1:2XXXXXXX:92mchkioeh/api/GET/getEntities"
}
"secondtarget" = {
name = "getactivitylog"
arn = "arn:aws:execute-api:us-east-1:2XXXXXX:92mchkioeh/api/GET/getActivityLog"
}
"thirdtarget" = {
name = "searchactivitylog"
arn = "arn:aws:execute-api:us-east-1:XXXXXX:92mchkioeh/api/GET/searchActivityLog"
}
}