Approach: create a reusable Terraform module that builds a Launch Template (mixed instance types + spot overrides), an Auto Scaling Group with MixedInstancesPolicy, IAM role/policy for instance actions (SSM, cloudwatch, drain script), a Lifecycle Hook that triggers a Lambda (or SSM Run Command) to gracefully drain Spark executors before EC2 termination.Example key snippets (conceptual):Inputs:- name_prefix, vpc_subnet_ids, security_group_ids- launch_instance_types (list), on_demand_percentage, spot_allocation_strategy- ami_id, instance_profile_name, spark_master_endpoint, drain_timeout_seconds- desired_count, min_size, max_size, instance_types_overridesOutputs:- asg_id, launch_template_id, autoscaling_group_name, iam_instance_profileKey resources (conceptual HCL):hcl
resource "aws_launch_template" "spark_workers" {
name_prefix = "${var.name_prefix}-lt-"
image_id = var.ami_id
instance_type = var.default_instance_type
iam_instance_profile { name = aws_iam_instance_profile.spark.name }
user_data = base64encode(templatefile("${path.module}/userdata/drain.sh.tpl", {
spark_master = var.spark_master_endpoint
}))
tag_specifications { ... }
}
resource "aws_autoscaling_group" "spark_workers" {
name = "${var.name_prefix}-asg"
max_size = var.max_size
min_size = var.min_size
desired_capacity = var.desired_count
mixed_instances_policy {
launch_template {
launch_template_specification { launch_template_id = aws_launch_template.spark_workers.id, version = "$Latest" }
}
instances_distribution {
on_demand_percentage_above_base_capacity = var.on_demand_percentage
spot_allocation_strategy = var.spot_allocation_strategy
}
override = [
for t in var.launch_instance_types : { instance_type = t }
]
}
vpc_zone_identifier = var.vpc_subnet_ids
tag { key = "Name" value = "${var.name_prefix}-worker" propagate_at_launch = true }
lifecycle {
create_before_destroy = true
}
}
resource "aws_iam_role" "instance" { ... } # allow ssm:SendCommand, ec2:Describe*, cloudwatch logs
resource "aws_iam_instance_profile" "spark" { role = aws_iam_role.instance.name }
resource "aws_autoscaling_lifecycle_hook" "pre_terminate" {
name = "${var.name_prefix}-drain-hook"
autoscaling_group_name = aws_autoscaling_group.spark_workers.name
lifecycle_transition = "autoscaling:EC2_INSTANCE_TERMINATING"
heartbeat_timeout = var.drain_timeout_seconds
default_result = "CONTINUE"
notification_target_arn = aws_sns_topic.drains.arn # or SQS / EventBridge for Lambda
role_arn = aws_iam_role.lifecycle.arn
}
Drain flow & reasoning:- Lifecycle hook pauses termination; sends notification (SNS/EventBridge) to a Lambda or step-function.- Lambda calls SSM RunCommand targeting the instance to run a drain script (user-data places spark utilities). The drain script uses spark/yarn APIs to decommission executors, wait for in-flight tasks, then signals completion by calling CompleteLifecycleAction via AWS SDK.- IAM roles: instance profile (SSM, CloudWatch), lifecycle role (autoscaling:CompleteLifecycleAction), Lambda role (ssm:SendCommand, autoscaling:CompleteLifecycleAction).Edge cases & best practices:- Use multiple instance_type overrides to improve spot flexibility.- Set appropriate heartbeat_timeout > expected drain time + buffer.- Use SSM rather than SSH for robust command execution and security.- Emit metrics/logs to CloudWatch for failed drains and retry logic.- Ensure graceful decommission uses Spark/YARN recommended REST APIs to avoid data loss.