Terraform | Cyclic dependency in "apply" stage when using "depends_on" for module

4.5k views Asked by At

I'm struggling with cyclic dependency issue in terraform apply stage when using depends_on on a module.

The error I got in apply is:

* Cycle: aws_appautoscaling_policy.queue_depth_based_scale_out_policy, module.my_module.aws_ecs_task_definition.task_definition (destroy), aws_appautoscaling_policy.queue_depth_based_scale_in_policy

The plan stage looked exactly fine and there are no errors in the plan stage.

I have tried to identify the cycle in graph using below command,

terraform graph -draw-cycles -module-depth=0 -type=plan  | dot -Tsvg > graph-plan.svg

There is no cycle shown in plan graph. Then, tried to identify cycle in apply using,

terraform graph -draw-cycles -module-depth=0 -type=apply  | dot -Tsvg > graph-apply.svg

Sadly, this commands is unable to show cycle in graph.

Fortunately, I am able to see the cycle in apply stage graph using these below commands,

terraform plan -out tfplan
terraform graph -draw-cycles -module-depth=0 tfplan | dot -Tsvg > graph-apply.svg

The cycle in my graph looks like this,

Graph showing cycle in apply stage

Although, I am still unable to make out for the reason for this cycle in graph.

Moreover, it seems issue is specifically with adding depends_on on a module. Since I already have few more aws_appautoscaling_policy in my module which do depends on aws_appautoscaling_target which depends on aws_ecs_service and hence eventually depends on aws_ecs_task_definition but apply for this works fine.

There are some aws_appautoscaling_policy which are specifically related to a particular app hence I am adding them separately (and not as part of module), but since auto scaling policy can only be added once service is registered as scalable target, hence I am adding depends_on on module, since aws_appautoscaling_target is defined in the module.

Here is my code snippet for module,

resource "aws_ecs_task_definition" "task_definition" {
  family                = "${var.service_name}"
  container_definitions = "${var.container_definitions}"
  task_role_arn         = "${aws_iam_role.task_role.arn}"
  lifecycle {
    create_before_destroy = true
  }
}
resource "aws_ecs_service" "service" {
  name                               = "${var.service_name}"
  cluster                            = "${data.aws_ecs_cluster.ecs_cluster.arn}"
  task_definition                    = "${aws_ecs_task_definition.task_definition.arn}"
  deployment_minimum_healthy_percent = 50
  deployment_maximum_percent         = 100
  lifecycle {
    ignore_changes = ["desired_count"]
  }
}
resource "aws_appautoscaling_target" "ecs_target" {
  max_capacity       = "${var.max_scalabe_capacity}"
  min_capacity       = "${var.min_scalabe_capacity}"
  resource_id        = "service/${var.ecs_cluster_name}/${aws_ecs_service.service.name}"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"
}
resource "aws_appautoscaling_policy" "cpu_based_scale_in_policy" {
  name               = "${var.service_name}-${var.env}-cpu-based-scale-in-policy"
  policy_type        = "StepScaling"
  resource_id        = "service/${var.ecs_cluster_name}/${var.service_name}"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"
  step_scaling_policy_configuration {
    adjustment_type         = "ChangeInCapacity"
    cooldown                = "${var.scale_in_cooldown_period}"
    metric_aggregation_type = "Average"
    step_adjustment {
      metric_interval_upper_bound = "${var.scale_in_step_adjustment_upper_bound}"
      scaling_adjustment          = "${var.scale_in_step_adjustment_scaling_adjustment}"
    }
  }
  depends_on = ["aws_appautoscaling_target.ecs_target"]
}

And here is the usage of module,

module "my_module" {
  source = "GIT_URL_FOR_MODULE"
  VARIABLES_AS_NEEDED_BY_MODULE
}
resource "aws_appautoscaling_policy" "queue_depth_based_scale_in_policy" {
  name               = "${local.service_name}-${local.env}-queue-scale-in-policy-new"
  policy_type        = "StepScaling"
  resource_id        = "service/${local.ecs_cluster_name}/${local.service_name}"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"
  step_scaling_policy_configuration {
    adjustment_type         = "ChangeInCapacity"
    cooldown                = "${local.queue_scale_in_cooldown_period}"
    metric_aggregation_type = "Average"
    step_adjustment {
      metric_interval_upper_bound = "${local.queue_scale_in_step_adjustment_upper_bound}"
      scaling_adjustment          = "${local.queue_scale_in_step_adjustment_scaling_adjustment}"
    }
  }
  depends_on = ["module.my_module"]
}

Steps followed in pipeline are:

terraform get -update=true
terraform init
terraform taint -allow-missing -module=${MODULE_NAME} aws_ecs_task_definition.task_definition
terraform plan -out tfplan -input=false
terraform apply -input=false tfplan

Would be happy to learn the reason behind this cycle?

Another point to highlight is that terraform apply is successful when we just destroy everything and recreate from scratch. The cycle is only observed when I taint my task-definition and have some updates in my scaling policies which are placed outside of module.

Note: In my pipeline, I do taint the previous task definition, to ensure services are started with new task definition instantaneously, otherwise task(s) will not be immediately rolled out with new task definition.

1

There are 1 answers

0
Aditya On BEST ANSWER

I have managed to get rid of cyclic dependency. Here is the approach used,

Rather than having a dependency on the entire module, I have added output for aws_appautoscaling_target in the module. And then I am just using this output in the scaling policy to ensure there is an implicit dependency created.

Here is the sample code,

Module

resource "aws_ecs_task_definition" "task_definition" {
  family                = "${var.service_name}"
  container_definitions = "${var.container_definitions}"
  task_role_arn         = "${aws_iam_role.task_role.arn}"
  lifecycle {
    create_before_destroy = true
  }
}
resource "aws_ecs_service" "service" {
  name                               = "${var.service_name}"
  cluster                            = "${data.aws_ecs_cluster.ecs_cluster.arn}"
  task_definition                    = "${aws_ecs_task_definition.task_definition.arn}"
  deployment_minimum_healthy_percent = 50
  deployment_maximum_percent         = 100
  lifecycle {
    ignore_changes = ["desired_count"]
  }
}
resource "aws_appautoscaling_target" "ecs_target" {
  max_capacity       = "${var.max_scalabe_capacity}"
  min_capacity       = "${var.min_scalabe_capacity}"
  resource_id        = "service/${var.ecs_cluster_name}/${aws_ecs_service.service.name}"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"
}
resource "aws_appautoscaling_policy" "cpu_based_scale_in_policy" {
  name               = "${var.service_name}-${var.env}-cpu-based-scale-in-policy"
  policy_type        = "StepScaling"
  resource_id        = "service/${var.ecs_cluster_name}/${var.service_name}"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"
  step_scaling_policy_configuration {
    adjustment_type         = "ChangeInCapacity"
    cooldown                = "${var.scale_in_cooldown_period}"
    metric_aggregation_type = "Average"
    step_adjustment {
      metric_interval_upper_bound = "${var.scale_in_step_adjustment_upper_bound}"
      scaling_adjustment          = "${var.scale_in_step_adjustment_scaling_adjustment}"
    }
  }
  depends_on = ["aws_appautoscaling_target.ecs_target"]
}
output "scalable_target_id" {
  value = "${aws_appautoscaling_target.ecs_target.id}"
}

Note the output code block added inside the module. And here is the usage of module, where output of module is consumed in scaling policy name.

module "my_module" {
  source = "GIT_URL_FOR_MODULE"
  VARIABLES_AS_NEEDED_BY_MODULE
}
resource "aws_appautoscaling_policy" "queue_depth_based_scale_in_policy" {
  name               = "${local.service_name}-${local.env}-queue-scale-in-policy-new-${module.my_module.scalable_target_id}"
  policy_type        = "StepScaling"
  resource_id        = "service/${local.ecs_cluster_name}/${local.service_name}"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"
  step_scaling_policy_configuration {
    adjustment_type         = "ChangeInCapacity"
    cooldown                = "${local.queue_scale_in_cooldown_period}"
    metric_aggregation_type = "Average"
    step_adjustment {
      metric_interval_upper_bound = "${local.queue_scale_in_step_adjustment_upper_bound}"
      scaling_adjustment          = "${local.queue_scale_in_step_adjustment_scaling_adjustment}"
    }
  }      
}

Although, I am still unable to figure out why the cycle was there in the first place.