├── .gitignore ├── Dockerfile ├── Jenkinsfile ├── README.md ├── docker-compose.yaml ├── pom.xml ├── src └── main │ ├── java │ └── com │ │ └── example │ │ └── app │ │ ├── Application.java │ │ ├── GreetingController.java │ │ ├── domain │ │ └── Message.java │ │ └── repos │ │ └── MessageRepo.java │ └── resources │ ├── application.properties │ └── templates │ └── main.mustache ├── target └── app-1.0-SNAPSHOT.jar └── terraform ├── .gitignore ├── alb.tf ├── auto_scaling.tf ├── ecs.tf ├── logs.tf ├── network.tf ├── outputs.tf ├── provider.tf ├── rds.tf ├── security.tf ├── templates └── ecs │ └── production_app.json └── variables.tf /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled files 2 | *.tfstate 3 | *.tfstate.backup 4 | 5 | # Module directory 6 | .terraform/ 7 | 8 | # Sensitive Files 9 | /secrets.tf 10 | 11 | .swp 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8 2 | 3 | MAINTAINER timur_galeev@outlook.com 4 | 5 | EXPOSE 8080 6 | EXPOSE 3306 7 | VOLUME /tmp 8 | ADD target/app-1.0-SNAPSHOT.jar app.jar 9 | CMD ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"] 10 | 11 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | node { 2 | stage('SCM checkout'){ 3 | checkout scm 4 | } 5 | stage 'Docker build' 6 | docker.build('sb_app') 7 | 8 | stage 'Docker push' 9 | docker.withRegistry('https://035898547283.dkr.ecr.us-west-2.amazonaws.com', 'ecr:us-west-2:ecr-credentials') { 10 | docker.image('sb_app').push('latest') 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CI/CD spring application linked MySQL(RDS) via Jenkins pipeline to AWS cloud(ECS) 2 | 3 | In **Jenkins** (Install required plugins (if not already installed)) 4 | - [Pipeline](https://wiki.jenkins-ci.org/display/JENKINS/Pipeline+Plugin) 5 | - [Docker Pipeline Plugin](https://wiki.jenkins-ci.org/display/JENKINS/CloudBees+Docker+Pipeline+Plugin) 6 | - [Amazon ECR Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Amazon+ECR) 7 | 8 | 1. Add AWS Credentials to Jenkins 9 | From the home screen, hit the Credentials link in the left-side bar. 10 | 2. Determine where you want to put your credentials. If unsure, go into the Global credentials. You may want to do some reading on credential management for a production/widespread use. 11 | 3. Click the Add Credentials link in the left-side navigation. 12 | 4. For Kind, select AWS Credentials. 13 | 5. Enter the Access ID and Secret Access Key for the AWS user that has access to the ECR repository. 14 | 6. In the Advanced button, specify an ID that will make sense to you (so you don’t have to remember a randomly generated UUID). 15 | 16 | **Terraform** 17 | The first thing we’ll do is specify the provider. This can be done in different ways but here we’re telling Terraform our provider is AWS and that it can find our credentials in `$HOME/.aws/credentials` which is the default location for AWS credentials on Mac and Linux. 18 | 19 | To get the minimal amount of high availability, we’ll deploy our ECS cluster to run on at least two Availability Zones (AZs). The load balancer also needs at least 2 public subnets in different AZs. 20 | 21 | Initial command 22 | `terraform init terraform/` 23 | This gets it all set up and ready to apply. 24 | 25 | Finally, run 26 | `terraform apply terraform/` 27 | to get this bad boy deployed! It will refresh state for all your resources and show you what will be created/removed/updated. 28 | 29 | When it spits out your load balancer url, go ahead and visit it at port :8080 and you should see the JSON returned from the initial block in your deployed into ECR app. 30 | 31 | In this example, wherever it says `sb` is my application name. 32 | 33 | ## To run locally 34 | **Note**: I was unable to run new versions of spring-cloud-aws locally. It seems it tries always to perform some autoconfiguration assuming it is deployed on AWS, and as long as it doesn't find a valid instance id, it raises an exception and stops. 35 | 36 | Some configurations are required in your AWS account for this sample to work. Additionally, we need an IAM user with access key and programmatic access to AWS API so that we can access AWS resources from our development machine. 37 | 38 | ## Create an IAM User 39 | - Enable programmatic access 40 | - Generate an access key for the user 41 | - Give the user the following permissions: ** AmazonRDSFullAccess 42 | 43 | ## To run on EC2 44 | **Create an IAM role** 45 | Create an IAM role with the following properties: 46 | 47 | - EC2 role (i.e., a role to be attached to EC2 instances) 48 | - Policies: ** AmazonRDSFullAccess 49 | ## Create an EC2 instance - if you want to create instance in AWS 50 | It has been tested with an instance with the following properties: 51 | 52 | - AMI: Ubuntu 18.04 53 | - Type: t2.micro 54 | - Storage: 20Gb 55 | - Security group: choose or create one with ports 22 and 8080 opened 56 | - Attach the IAM role created previously 57 | 58 | ## What you'll need 59 | - Docker 60 | - Jenkins 61 | - AWS ECS 62 | - AWS RDS 63 | - AWS VPC 64 | - AWS ERC 65 | - AWS ALB 66 | - Terraform 67 | 68 | ## Stack 69 | - AWS 70 | - Docker 71 | - Docker-Compose 72 | - Java 73 | - Spring Boot 74 | - MySQL 75 | - Tomcat 76 | - Maven 77 | 78 | You can check the log by 79 | ~~~ 80 | Open http://[your DNS name]:8080 in your browser. 81 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | mysql-docker: 4 | container_name: 5 | mysql 6 | image: mysql/mysql-server:5.7 7 | environment: 8 | MYSQL_ROOT_PASSWORD: secret123456789- 9 | MYSQL_DATABASE: sb_db 10 | MYSQL_USER: superuser 11 | MYSQL_PASSWORD: secret123456789- 12 | MYSQL_ROOT_HOST: '%' 13 | ports: 14 | - 3306 15 | restart: always 16 | 17 | app: 18 | container_name: 19 | demo-app 20 | depends_on: 21 | - mysql-docker 22 | build: 23 | context: . 24 | restart: always 25 | links: 26 | - mysql-docker 27 | ports: 28 | - 8080:8080 29 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.example 8 | app 9 | 1.0-SNAPSHOT 10 | 11 | 12 | org.springframework.boot 13 | spring-boot-starter-parent 14 | 2.1.2.RELEASE 15 | 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-mustache 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-web 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-data-jpa 29 | 30 | 31 | mysql 32 | mysql-connector-java 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-devtools 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-test 42 | test 43 | 44 | 45 | 46 | 1.8 47 | UTF-8 48 | 49 | 50 | 51 | 52 | 53 | 54 | org.springframework.boot 55 | spring-boot-maven-plugin 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/main/java/com/example/app/Application.java: -------------------------------------------------------------------------------- 1 | package com.example.app; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/example/app/GreetingController.java: -------------------------------------------------------------------------------- 1 | package com.example.app; 2 | 3 | import com.example.app.domain.Message; 4 | import com.example.app.repos.MessageRepo; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.RequestParam; 10 | 11 | import java.util.Map; 12 | 13 | @Controller 14 | public class GreetingController { 15 | 16 | @Autowired 17 | private MessageRepo messageRepo; 18 | 19 | @GetMapping 20 | public String main(Map model) { 21 | Iterable messages = messageRepo.findAll(); 22 | model.put("messages", messages); 23 | return "main"; 24 | 25 | } 26 | 27 | @PostMapping 28 | public String add(@RequestParam String text, Map model){ 29 | Message message = new Message(text); 30 | 31 | messageRepo.save(message); 32 | 33 | Iterable messages = messageRepo.findAll (); 34 | model.put("messages", messages); 35 | return "main"; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/example/app/domain/Message.java: -------------------------------------------------------------------------------- 1 | package com.example.app.domain; 2 | import javax.persistence.Entity; 3 | import javax.persistence.GeneratedValue; 4 | import javax.persistence.GenerationType; 5 | import javax.persistence.Id; 6 | 7 | @Entity 8 | public class Message { 9 | @Id 10 | @GeneratedValue(strategy= GenerationType.AUTO) 11 | private Integer id; 12 | 13 | private String name; 14 | 15 | public Message() { 16 | } 17 | 18 | public Message(String name) { 19 | this.name = name; 20 | } 21 | 22 | public Integer getId() { 23 | return id; 24 | } 25 | 26 | public void setId(Integer id) { 27 | this.id = id; 28 | } 29 | 30 | public String getName() { 31 | return name; 32 | } 33 | 34 | public void setName(String name) { 35 | this.name = name; 36 | } 37 | 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/example/app/repos/MessageRepo.java: -------------------------------------------------------------------------------- 1 | package com.example.app.repos; 2 | 3 | import com.example.app.domain.Message; 4 | import org.springframework.data.repository.CrudRepository; 5 | 6 | public interface MessageRepo extends CrudRepository { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.driver-class-name=com.mysql.jdbc.Driver 2 | spring.jpa.hibernate.ddl-auto=create 3 | 4 | spring.datasource.url=jdbc:mysql://${rds.hostname}/${rds.db.name}?useSSL=false 5 | spring.datasource.username=${rds.username} 6 | spring.datasource.password=${rds.password} 7 | 8 | #cloud.aws.credentials.accessKey=your_access_key 9 | #cloud.aws.credentials.secretKey=your_secret_key 10 | #cloud.aws.region.static=us-west-2 11 | 12 | 13 | # Disable auto cloudfromation 14 | #cloud.aws.stack.auto=false 15 | 16 | #instanceProfile=false 17 | 18 | #cloud.aws.rds.dbInstanceIdentifier=spring-boot-project-production 19 | #cloud.aws.rds.spring-boot-project-production.password=secret123456789- 20 | #cloud.aws.rds.spring-boot-project-production.username=superuser 21 | #cloud.aws.rds.spring-boot-project-production.databaseName=sb_db 22 | 23 | -------------------------------------------------------------------------------- /src/main/resources/templates/main.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | Simple Spring Boot app 4 | 5 | 6 |
What's your name?
7 |
8 |
9 | 10 | 11 |
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 | --------------------------------------------------------------------------------