2 |
3 | Simple Spring Boot app
4 |
5 |
6 | What's your name?
7 |
8 |
12 |
13 |
14 | Hello,
15 |
16 | List names:
17 | {{#messages}}
18 |
19 | {{id}}
20 | {{name}}
21 |
22 | {{/messages}}
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/target/app-1.0-SNAPSHOT.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timurgaleev/ecs-spring-boot-rds-tf/66598bd7b587524e26c0b76c18fa441a66dd8639/target/app-1.0-SNAPSHOT.jar
--------------------------------------------------------------------------------
/terraform/.gitignore:
--------------------------------------------------------------------------------
1 | # Скомпилированные файлы
2 | * .tfstate
3 | * .tfstate.backup
4 | .terraform.tfstate.lock.info
5 | # Каталог модулей
6 | .terraform /
7 |
8 | .swp
9 |
--------------------------------------------------------------------------------
/terraform/alb.tf:
--------------------------------------------------------------------------------
1 | resource "aws_alb" "main" {
2 | name = "sb-load-balancer"
3 | subnets = "${aws_subnet.public.*.id}"
4 | security_groups = ["${aws_security_group.lb.id}"]
5 | }
6 |
7 | resource "aws_alb_target_group" "app" {
8 | name = "sb-target-group"
9 | port = 8080
10 | protocol = "HTTP"
11 | vpc_id = "${aws_vpc.main.id}"
12 | target_type = "ip"
13 |
14 | health_check {
15 | healthy_threshold = "3"
16 | interval = "30"
17 | protocol = "HTTP"
18 | matcher = "200"
19 | timeout = "3"
20 | path = "${var.health_check_path}"
21 | unhealthy_threshold = "2"
22 | }
23 | }
24 |
25 | # Redirect all traffic from the ALB to the target group
26 | resource "aws_alb_listener" "front_end" {
27 | load_balancer_arn = "${aws_alb.main.id}"
28 | port = "80"
29 | protocol = "HTTP"
30 |
31 | default_action {
32 | target_group_arn = "${aws_alb_target_group.app.id}"
33 | type = "forward"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/terraform/auto_scaling.tf:
--------------------------------------------------------------------------------
1 | resource "aws_appautoscaling_target" "target" {
2 | service_namespace = "ecs"
3 | resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.main.name}"
4 | scalable_dimension = "ecs:service:DesiredCount"
5 | role_arn = "${var.ecs_autoscale_role} "
6 | min_capacity = 3
7 | max_capacity = 6
8 | }
9 |
10 | # Automatically scale capacity up by one
11 | resource "aws_appautoscaling_policy" "up" {
12 | name = "sb_scale_up"
13 | service_namespace = "ecs"
14 | resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.main.name}"
15 | scalable_dimension = "ecs:service:DesiredCount"
16 |
17 | step_scaling_policy_configuration {
18 | adjustment_type = "ChangeInCapacity"
19 | cooldown = 60
20 | metric_aggregation_type = "Maximum"
21 |
22 | step_adjustment {
23 | metric_interval_lower_bound = 0
24 | scaling_adjustment = 1
25 | }
26 | }
27 |
28 | depends_on = ["aws_appautoscaling_target.target"]
29 | }
30 |
31 | # Automatically scale capacity down by one
32 | resource "aws_appautoscaling_policy" "down" {
33 | name = "sb_scale_down"
34 | service_namespace = "ecs"
35 | resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.main.name}"
36 | scalable_dimension = "ecs:service:DesiredCount"
37 |
38 | step_scaling_policy_configuration {
39 | adjustment_type = "ChangeInCapacity"
40 | cooldown = 60
41 | metric_aggregation_type = "Maximum"
42 |
43 | step_adjustment {
44 | metric_interval_lower_bound = 0
45 | scaling_adjustment = -1
46 | }
47 | }
48 |
49 | depends_on = ["aws_appautoscaling_target.target"]
50 | }
51 |
52 | # Cloudwatch alarm that triggers the autoscaling up policy
53 | resource "aws_cloudwatch_metric_alarm" "service_cpu_high" {
54 | alarm_name = "sb_cpu_utilization_high"
55 | comparison_operator = "GreaterThanOrEqualToThreshold"
56 | evaluation_periods = "2"
57 | metric_name = "CPUUtilization"
58 | namespace = "AWS/ECS"
59 | period = "60"
60 | statistic = "Average"
61 | threshold = "85"
62 |
63 | dimensions = {
64 | ClusterName = "${aws_ecs_cluster.main.name}"
65 | ServiceName = "${aws_ecs_service.main.name}"
66 | }
67 |
68 | alarm_actions = ["${aws_appautoscaling_policy.up.arn}"]
69 | }
70 |
71 | # Cloudwatch alarm that triggers the autoscaling down policy
72 | resource "aws_cloudwatch_metric_alarm" "service_cpu_low" {
73 | alarm_name = "sb_cpu_utilization_low"
74 | comparison_operator = "LessThanOrEqualToThreshold"
75 | evaluation_periods = "2"
76 | metric_name = "CPUUtilization"
77 | namespace = "AWS/ECS"
78 | period = "60"
79 | statistic = "Average"
80 | threshold = "10"
81 |
82 | dimensions = {
83 | ClusterName = "${aws_ecs_cluster.main.name}"
84 | ServiceName = "${aws_ecs_service.main.name}"
85 | }
86 |
87 | alarm_actions = ["${aws_appautoscaling_policy.down.arn}"]
88 | }
89 |
--------------------------------------------------------------------------------
/terraform/ecs.tf:
--------------------------------------------------------------------------------
1 | resource "aws_ecs_cluster" "main" {
2 | name = "sb-cluster"
3 | }
4 |
5 | data "template_file" "sb_app" {
6 | template = "${file("templates/ecs/production_app.json")}"
7 |
8 | vars = {
9 | app_image = "${var.app_image}"
10 | fargate_cpu = "${var.fargate_cpu}"
11 | fargate_memory = "${var.fargate_memory}"
12 | aws_region = "${var.aws_region}"
13 | app_port = "${var.app_port}"
14 | db_hostname = "${aws_db_instance.mysql.endpoint}"
15 | db_port = "${var.db_port}"
16 | db_name = "${var.db_name}"
17 | db_username = "${var.db_username}"
18 | db_password = "${aws_ssm_parameter.secret.value}"
19 | }
20 | }
21 |
22 | resource "aws_ecs_task_definition" "app" {
23 | family = "sb-app-task"
24 | execution_role_arn = "${var.ecs_task_execution_role}"
25 | network_mode = "awsvpc"
26 | requires_compatibilities = ["FARGATE"]
27 | cpu = "${var.fargate_cpu}"
28 | memory = "${var.fargate_memory}"
29 | container_definitions = "${data.template_file.sb_app.rendered}"
30 | }
31 |
32 | resource "aws_ecs_service" "main" {
33 | name = "sb-service"
34 | cluster = "${aws_ecs_cluster.main.id}"
35 | task_definition = "${aws_ecs_task_definition.app.arn}"
36 | desired_count = "${var.app_count}"
37 | launch_type = "FARGATE"
38 |
39 | network_configuration {
40 | security_groups = ["${aws_security_group.ecs_tasks.id}"]
41 | subnets = "${aws_subnet.private.*.id}"
42 | assign_public_ip = true
43 | }
44 |
45 | load_balancer {
46 | target_group_arn = "${aws_alb_target_group.app.id}"
47 | container_name = "sb-app"
48 | container_port = "${var.app_port}"
49 | }
50 |
51 | depends_on = [
52 | "aws_alb_listener.front_end",
53 | ]
54 | }
55 |
--------------------------------------------------------------------------------
/terraform/logs.tf:
--------------------------------------------------------------------------------
1 | # Set up cloudwatch group and log stream and retain logs for 30 days
2 | resource "aws_cloudwatch_log_group" "sb_log_group" {
3 | name = "/ecs/sb-app"
4 | retention_in_days = 30
5 |
6 | tags = {
7 | Name = "sb-log-group"
8 | }
9 | }
10 |
11 | resource "aws_cloudwatch_log_stream" "sb_log_stream" {
12 | name = "sb-log-stream"
13 | log_group_name = "${aws_cloudwatch_log_group.sb_log_group.name}"
14 | }
15 |
--------------------------------------------------------------------------------
/terraform/network.tf:
--------------------------------------------------------------------------------
1 | # Fetch AZs in the current region
2 | data "aws_availability_zones" "available" {}
3 |
4 | resource "aws_vpc" "main" {
5 | cidr_block = "172.17.0.0/16"
6 | tags = {
7 | name = "vpc-sb-main"
8 | }
9 | }
10 |
11 | # Create var.az_count private subnets, each in a different AZ
12 | resource "aws_subnet" "private" {
13 | count = "${var.az_count}"
14 | cidr_block = "${cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)}"
15 | availability_zone = "${data.aws_availability_zones.available.names[count.index]}"
16 | vpc_id = "${aws_vpc.main.id}"
17 | tags = {
18 | name = "vpc-sb-private-subnet"
19 | }
20 | }
21 |
22 | # Create var.az_count public subnets, each in a different AZ
23 | resource "aws_subnet" "public" {
24 | count = "${var.az_count}"
25 | cidr_block = "${cidrsubnet(aws_vpc.main.cidr_block, 8, var.az_count + count.index)}"
26 | availability_zone = "${data.aws_availability_zones.available.names[count.index]}"
27 | vpc_id = "${aws_vpc.main.id}"
28 | map_public_ip_on_launch = true
29 | tags = {
30 | Name = "vpc-sb-public-subnet"
31 | }
32 | }
33 |
34 | # IGW for the public subnet
35 | resource "aws_internet_gateway" "gw" {
36 | vpc_id = "${aws_vpc.main.id}"
37 | tags = {
38 | Name = "vpc-sb-internet-gateway"
39 | }
40 | }
41 |
42 | # Route the public subnet trafic through the IGW
43 | resource "aws_route" "internet_access" {
44 | route_table_id = "${aws_vpc.main.main_route_table_id}"
45 | destination_cidr_block = "0.0.0.0/0"
46 | gateway_id = "${aws_internet_gateway.gw.id}"
47 | }
48 |
49 | # Create a NAT gateway with an EIP for each private subnet to get internet connectivity
50 | resource "aws_eip" "gw" {
51 | count = "${var.az_count}"
52 | vpc = true
53 | depends_on = ["aws_internet_gateway.gw"]
54 | }
55 |
56 | resource "aws_nat_gateway" "gw" {
57 | count = "${var.az_count}"
58 | subnet_id = "${element(aws_subnet.public.*.id, count.index)}"
59 | allocation_id = "${element(aws_eip.gw.*.id, count.index)}"
60 | }
61 |
62 | # Create a new route table for the private subnets, make it route non-local traffic through the NAT gateway to the internet
63 | resource "aws_route_table" "private" {
64 | count = "${var.az_count}"
65 | vpc_id = "${aws_vpc.main.id}"
66 |
67 | route {
68 | cidr_block = "0.0.0.0/0"
69 | nat_gateway_id = "${element(aws_nat_gateway.gw.*.id, count.index)}"
70 | #gateway_id = "${aws_internet_gateway.gw.id}"
71 | }
72 | tags = {
73 | Name = "vpc-sb-private-route-table"
74 | }
75 | }
76 |
77 | # Explicitly associate the newly created route tables to the private subnets (so they don't default to the main route table)
78 | resource "aws_route_table_association" "private" {
79 | count = "${var.az_count}"
80 | subnet_id = "${element(aws_subnet.private.*.id, count.index)}"
81 | route_table_id = "${element(aws_route_table.private.*.id, count.index)}"
82 | }
83 |
--------------------------------------------------------------------------------
/terraform/outputs.tf:
--------------------------------------------------------------------------------
1 | output "alb_hostname" {
2 | value = "${aws_alb.main.dns_name}"
3 | }
4 | output "db_endpoint" {
5 | value = "${aws_db_instance.mysql.endpoint}"
6 | }
7 |
--------------------------------------------------------------------------------
/terraform/provider.tf:
--------------------------------------------------------------------------------
1 | # provider.tf - Specify the provider and access details
2 |
3 | provider "aws" {
4 | shared_credentials_file = "$HOME/.aws/credentials"
5 | profile = "default"
6 | region = "${var.aws_region}"
7 | }
8 |
--------------------------------------------------------------------------------
/terraform/rds.tf:
--------------------------------------------------------------------------------
1 | /**
2 | * RDS instance
3 | */
4 | resource "aws_db_instance" "mysql" {
5 | identifier = "${var.project}-${var.environment}"
6 | allocated_storage = 20
7 | storage_type = "gp2"
8 | engine = "mysql"
9 | engine_version = "5.7.22"
10 | instance_class = "db.t2.micro"
11 | name = "${var.db_name}"
12 | username = "${var.db_username}"
13 | password = "${aws_ssm_parameter.secret.value}"
14 | port = "${var.db_port}"
15 | publicly_accessible = false
16 | security_group_names = []
17 | vpc_security_group_ids = ["${aws_security_group.mysql.id}"]
18 | db_subnet_group_name = "${aws_db_subnet_group.mysql.id}"
19 | parameter_group_name = "${aws_db_parameter_group.mysql.name}"
20 | multi_az = false
21 | backup_retention_period = 0
22 | backup_window = "05:20-05:50"
23 | maintenance_window = "sun:04:00-sun:04:30"
24 | final_snapshot_identifier = "${var.project}-${var.environment}-final"
25 | skip_final_snapshot = true
26 |
27 | tags = {
28 | Name = "${var.project}-${var.environment}"
29 | Group = "${var.project}"
30 | }
31 | }
32 |
33 | resource "aws_ssm_parameter" "secret" {
34 | name = "/${var.environment}/database/password/master"
35 | description = "The parameter description"
36 | type = "SecureString"
37 | value = "${var.database_master_password}"
38 |
39 | tags = {
40 | environment = "${var.environment}"
41 | }
42 | }
43 |
44 | /**
45 | * Parameter group for MySQL
46 | */
47 | resource "aws_db_parameter_group" "mysql" {
48 | name = "${var.project}-${var.environment}-pg"
49 | family = "mysql5.7"
50 | description = "RDS parameter group for ${var.project}"
51 |
52 | parameter {
53 | name = "character_set_server"
54 | value = "utf8"
55 | }
56 |
57 | parameter {
58 | name = "character_set_client"
59 | value = "utf8"
60 | }
61 | }
62 |
63 | /**
64 | * Subnet group for RDS
65 | */
66 | resource "aws_db_subnet_group" "mysql" {
67 | name = "${var.project}-${var.environment}"
68 | description = "${var.project} group of subnets"
69 | #subnet_ids = concat("${aws_subnet.private.*.id}", "${aws_subnet.public.*.id}")
70 | subnet_ids = "${aws_subnet.private.*.id}"
71 | tags = {
72 | Name = "${var.project} DB subnet group"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/terraform/security.tf:
--------------------------------------------------------------------------------
1 | # ALB Security Group: Edit this to restrict access to the application
2 | resource "aws_security_group" "lb" {
3 | name = "sb-load-balancer-security-group"
4 | description = "controls access to the ALB"
5 | vpc_id = "${aws_vpc.main.id}"
6 |
7 | ingress {
8 | protocol = "tcp"
9 | from_port = 80
10 | to_port = 80
11 | cidr_blocks = ["0.0.0.0/0"]
12 | }
13 |
14 | egress {
15 | protocol = "-1"
16 | from_port = 0
17 | to_port = 0
18 | cidr_blocks = ["0.0.0.0/0"]
19 | }
20 | }
21 |
22 | # Traffic to the ECS cluster should only come from the ALB
23 | resource "aws_security_group" "ecs_tasks" {
24 | name = "sb-ecs-tasks-security-group"
25 | description = "allow inbound access from the ALB only"
26 | vpc_id = "${aws_vpc.main.id}"
27 |
28 | ingress {
29 | protocol = "tcp"
30 | from_port = "8080"
31 | to_port = "8080"
32 | security_groups = ["${aws_security_group.lb.id}"]
33 | }
34 |
35 | egress {
36 | protocol = "-1"
37 | from_port = 0
38 | to_port = 0
39 | cidr_blocks = ["0.0.0.0/0"]
40 | }
41 | }
42 | /**
43 | * Security group for MySQL
44 | */
45 | resource "aws_security_group" "mysql" {
46 | name = "${var.project}-${var.environment}-mysql"
47 | description = "security group for ${var.project} mysql"
48 | vpc_id = "${aws_vpc.main.id}"
49 | tags = {
50 | Name = "${var.environment}-rds-mysql"
51 | Environment = "${var.environment}"
52 | }
53 |
54 | ingress {
55 | from_port = 3306
56 | to_port = 3306
57 | protocol = "tcp"
58 | cidr_blocks = ["0.0.0.0/0"]
59 | }
60 |
61 | egress {
62 | from_port = 0
63 | to_port = 0
64 | protocol = "-1"
65 | cidr_blocks = ["0.0.0.0/0"]
66 | }
67 | }
68 |
69 |
--------------------------------------------------------------------------------
/terraform/templates/ecs/production_app.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "sb-app",
4 | "image": "${app_image}",
5 | "cpu": ${fargate_cpu},
6 | "memory": ${fargate_memory},
7 | "networkMode": "awsvpc",
8 | "logConfiguration": {
9 | "logDriver": "awslogs",
10 | "options": {
11 | "awslogs-group": "/ecs/sb-app",
12 | "awslogs-region": "${aws_region}",
13 | "awslogs-stream-prefix": "ecs"
14 | }
15 | },
16 | "portMappings": [
17 | {
18 | "containerPort": ${app_port},
19 | "hostPort": ${app_port}
20 | }
21 | ],
22 | "environment": [
23 | {
24 | "name": "rds.username",
25 | "value": "${db_username}"
26 | },
27 | {
28 | "name": "rds.hostname",
29 | "value": "${db_hostname}"
30 | },
31 | {
32 | "name": "rds.password",
33 | "value": "${db_password}"
34 | },
35 | { "name": "rds.port",
36 | "value": "${db_port}"
37 | },
38 | {
39 | "name": "rds.db.name",
40 | "value": "${db_name}"
41 | }
42 | ]
43 | }
44 | ]
45 |
--------------------------------------------------------------------------------
/terraform/variables.tf:
--------------------------------------------------------------------------------
1 | variable "aws_region" {
2 | description = "The AWS region things are created in"
3 | default = "us-west-2"
4 | }
5 |
6 | variable "az_count" {
7 | description = "Number of AZs to cover in a given region"
8 | default = "2"
9 | }
10 |
11 | variable "app_image" {
12 | description = "Docker image to run in the ECS cluster"
13 | default = "035898547283.dkr.ecr.us-west-2.amazonaws.com/provectus_app:latest"
14 | }
15 |
16 | variable "app_port" {
17 | description = "Port exposed by the docker image to redirect traffic to"
18 | default = 8080
19 | }
20 |
21 | variable "app_count" {
22 | description = "Number of docker containers to run"
23 | default = 3
24 | }
25 |
26 | variable "ecs_autoscale_role" {
27 | description = "Role arn for the ecsAutocaleRole"
28 | default = "arn:aws:iam::035898547283:role/ecsAutoscaleRole"
29 | }
30 |
31 | variable "ecs_task_execution_role" {
32 | description = "Role arn for the ecsTaskExecutionRole"
33 | default = "arn:aws:iam::035898547283:role/ecsTaskExecutionRole"
34 | }
35 |
36 | variable "health_check_path" {
37 | default = "/"
38 | }
39 |
40 | variable "fargate_cpu" {
41 | description = "Fargate instance CPU units to provision (1 vCPU = 1024 CPU units)"
42 | default = "1024"
43 | }
44 |
45 | variable "fargate_memory" {
46 | description = "Fargate instance memory to provision (in MiB)"
47 | default = "2048"
48 | }
49 |
50 | # A project name
51 | variable "project" {
52 | default = "spring-boot-project"
53 | }
54 |
55 | # Environment name
56 | variable "environment" {
57 | default = "production"
58 | }
59 |
60 | # User name for RDS
61 | variable "db_username" {
62 | default = "superuser"
63 | }
64 |
65 | # The DB name in the RDS instance. Note that this cannot contain -'s
66 | variable "db_name" {
67 | default = "sb_db"
68 | }
69 |
70 | variable "db_port" {
71 | default = "3306"
72 | }
73 |
74 | # SSM password
75 | variable "database_master_password" {
76 | default = "secret123456789-"
77 | }
78 |
--------------------------------------------------------------------------------