Post

iac-terraform-aws-demo

Overview

Terraform(IaC) framework to demonstrate some capabilitie for fundamental AWS design patterns:

VPC and AZ setup

VPC Architecture

APIGateway -> Lambda setup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# Lambda function
resource "aws_lambda_function" "payment_api_lambda_function" {
  function_name = "payment_api-lambda-function"
  runtime       = "python3.8"
  handler       = "payment_lambda_function.lambda_handler"
  filename      = "./lambda_functions/function.zip"
  role          = aws_iam_role.payment_lambda_function_execution_role.arn
}

# API Gateway
resource "aws_api_gateway_rest_api" "payment_api" {
  name        = "${var.vpc_suffix}-payment_api"
  description = "Payment API Gateway integrated with Lambda functions"

  endpoint_configuration {
    types = ["REGIONAL"]
  }
}

# API Gateway Resource
resource "aws_api_gateway_resource" "payment_api_resource" {
  rest_api_id = aws_api_gateway_rest_api.payment_api.id
  parent_id   = aws_api_gateway_rest_api.payment_api.root_resource_id
  path_part   = "payment"
}

# API Gateway Method
resource "aws_api_gateway_method" "payment_api_resource_method" {
  rest_api_id   = aws_api_gateway_rest_api.payment_api.id
  resource_id   = aws_api_gateway_resource.payment_api_resource.id
  http_method   = "ANY"
  authorization = "NONE"
}

# Lambda Integration with API Gateway
resource "aws_api_gateway_integration" "payment_api_integration" {
  rest_api_id             = aws_api_gateway_rest_api.payment_api.id
  resource_id             = aws_api_gateway_resource.payment_api_resource.id
  http_method             = aws_api_gateway_method.payment_api_resource_method.http_method
  integration_http_method = "POST" # Set your desired HTTP method
  # The type of integration with the specified backend. The valid value is
  #   http or http_proxy: for integration with an HTTP backend
  #   aws_proxy: for integration with AWS Lambda functions;
  #   aws: for integration with AWS Lambda functions or other AWS services, such as Amazon DynamoDB, Amazon Simple Notification Service or Amazon Simple Queue Service;
  #   mock: for integration with API Gateway without invoking any backend.

  type = "AWS_PROXY"
  uri  = aws_lambda_function.payment_api_lambda_function.invoke_arn
}

# API Gateway Deployment
resource "aws_api_gateway_deployment" "payment_api_deployment" {
  depends_on = [aws_api_gateway_integration.payment_api_integration]

  rest_api_id = aws_api_gateway_rest_api.payment_api.id
  stage_name  = "dev" # Set your desired stage name
}

# Used to give an external source (like an API Gateway, EventBridge Rule, SNS, or S3) permission to access the Lambda function
resource "aws_lambda_permission" "apigw_lambda" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.payment_api_lambda_function.function_name
  principal     = "apigateway.amazonaws.com"

  # More: https://repost.aws/questions/QUizsg_qznQLWKtqUD8Ruszw/api-gateway-lacks-permissions-to-trigger-lambda-when-made-by-terraform
  source_arn = "${aws_api_gateway_rest_api.payment_api.execution_arn}/*/*/*"
}


# API Gateway Resource
resource "aws_api_gateway_resource" "payment_api_notification_resource" {
  rest_api_id = aws_api_gateway_rest_api.payment_api.id
  parent_id   = aws_api_gateway_rest_api.payment_api.root_resource_id
  path_part   = "payment_notification"
}

# API Gateway Method
resource "aws_api_gateway_method" "payment_api_notification_resource_method" {
  rest_api_id   = aws_api_gateway_rest_api.payment_api.id
  resource_id   = aws_api_gateway_resource.payment_api_notification_resource.id
  http_method   = "ANY"
  authorization = "NONE"
}

# Lambda Integration with API Gateway
resource "aws_api_gateway_integration" "payment_api_notification_integration" {
  rest_api_id             = aws_api_gateway_rest_api.payment_api.id
  resource_id             = aws_api_gateway_resource.payment_api_notification_resource.id
  http_method             = aws_api_gateway_method.payment_api_notification_resource_method.http_method
  integration_http_method = "POST" # Set your desired HTTP method
  # The type of integration with the specified backend. The valid value is
  #   http or http_proxy: for integration with an HTTP backend
  #   aws_proxy: for integration with AWS Lambda functions;
  #   aws: for integration with AWS Lambda functions or other AWS services, such as Amazon DynamoDB, Amazon Simple Notification Service or Amazon Simple Queue Service;
  #   mock: for integration with API Gateway without invoking any backend.

  type = "AWS"
  uri  = aws_lambda_function.payment_api_lambda_function.invoke_arn
}

ALB -> EC2 / ECS Fargate setup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
resource "aws_launch_template" "ec2_launch_template" {
  name_prefix            = "${var.vpc_suffix}-ec2-launch_template"
  image_id               = "ami-00952f27cf14db9cd"
  instance_type          = "t3.micro"
  vpc_security_group_ids = [aws_security_group.ec2_sg.id]
  user_data              = filebase64("ec2-user-data.sh")
  tag_specifications {
    resource_type = "instance"
    tags = {
      Name = "${var.vpc_suffix}-asg-ec2-paymentservice"
    }
  }
}

resource "aws_autoscaling_group" "asg" {
  name = "${var.vpc_suffix}-asg"
  #  availability_zones   = local.availability_zones
  desired_capacity     = 2
  max_size             = 4
  min_size             = 1
  health_check_type    = "EC2"
  termination_policies = ["OldestInstance"]
  vpc_zone_identifier  = aws_subnet.private_subnet.*.id
  target_group_arns    = [aws_lb_target_group.ec2_tg.arn]

  launch_template {
    id      = aws_launch_template.ec2_launch_template.id
    version = "$Latest"
  }
}

# Create a new load balancer attachment
resource "aws_autoscaling_attachment" "asg_alb" {
  autoscaling_group_name = aws_autoscaling_group.asg.id
  lb_target_group_arn    = aws_lb_target_group.ec2_tg.arn
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
resource "aws_alb" "ecs_services_alb" {
  name            = "${var.vpc_suffix}-ecs-paymentservice-alb"
  subnets         = aws_subnet.public_subnet.*.id
  security_groups = [aws_security_group.alb_sg.id]
}

resource "aws_alb_target_group" "ecs_tg" {
  name        = "ecs-paymentservice-tg"
  port        = 80
  protocol    = "HTTP"
  vpc_id      = aws_vpc.vpc.id
  target_type = "ip" # for Fargate

  #  health_check {
  #    healthy_threshold   = "3"
  #    interval            = "30"
  #    protocol            = "HTTP"
  #    matcher             = "200"
  #    timeout             = "3"
  #    path                = var.health_check_path
  #    unhealthy_threshold = "2"
  #  }
}

# aws_lb_target_group_attachment. That is primarily for attaching EC2 instances to a target group if you have no auto-scaling in place.
#For auto-scaled EC2 instance, or ECS managed containers, you let those service manage the target group attachment for you.


# Redirect all traffic from the ALB to the target group
resource "aws_alb_listener" "ec2_services_alb_listener" {
  load_balancer_arn = aws_alb.ecs_services_alb.id
  port              = 80
  protocol          = "HTTP"

  default_action {
    target_group_arn = aws_alb_target_group.ecs_tg.arn
    type             = "forward"
  }
}

# ECR repo
resource "aws_ecr_repository" "main" {
  name                 = "${var.vpc_suffix}-ecr"
  image_tag_mutability = "MUTABLE"
}

resource "aws_ecr_lifecycle_policy" "main" {
  repository = aws_ecr_repository.main.name

  policy = jsonencode({
    rules = [{
      rulePriority = 1
      description  = "keep last 10 images"
      action = {
        type = "expire"
      }
      selection = {
        tagStatus   = "any"
        countType   = "imageCountMoreThan"
        countNumber = 10
      }
    }]
  })
}

# ECS Cluster
resource "aws_ecs_cluster" "fargate_cluster" {
  name = "${var.vpc_suffix}-ecs-cluster"
}

# ECS Task definition
resource "aws_ecs_task_definition" "fargate_task" {
  family                   = "paymentservice-task"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]

  cpu           = "256"                                    # Set CPU units for the task
  memory        = "512"                                    # Set memory for the task
  task_role_arn = aws_iam_role.ecs_task_execution_role.arn # Specify your task execution role ARN

  container_definitions = jsonencode([{
    name  = "${var.vpc_suffix}-container-paymentservice"
    image = "nginx:latest"
    portMappings = [{
      containerPort = 80
      hostPort      = 80
    }]
  }])
}

resource "aws_ecs_service" "main" {
  name                               = "${var.vpc_suffix}-service-${var.env}"
  cluster                            = aws_ecs_cluster.fargate_cluster.id
  task_definition                    = aws_ecs_task_definition.fargate_task.arn
  desired_count                      = 3
  deployment_minimum_healthy_percent = 50
  deployment_maximum_percent         = 200
  launch_type                        = "FARGATE"

  network_configuration {
    security_groups  = [aws_security_group.ecs_sg.id]
    subnets          = aws_subnet.private_subnet.*.id
    assign_public_ip = false
  }

  load_balancer { # For ECS this configuration is used instead of aws_lb_target_group_attachment
    target_group_arn = aws_alb_target_group.ecs_tg.arn
    container_name   = "${var.vpc_suffix}-container-paymentservice"
    container_port   = 80
  }

  lifecycle {
    ignore_changes = [task_definition, desired_count]
  }
}

Commands

1
2
3
4
5
6
7
8
9
10
11
    $ terraform init

    $ terraform validate

    $ terraform fmt

    $ terraform plan -var-file="./dev/terraform.tfvars"

    $ terraform apply -var-file="./dev/terraform.tfvars"

    $ terraform destroy -var-file="./dev/terraform.tfvars"
This post is licensed under CC BY 4.0 by the author.