├── .dockerignore
├── .gitignore
├── .vscode
└── settings.json
├── Dockerfile
├── README.md
├── docker-compose.yml
├── docs
└── screenshots
│ ├── acm-certificates.png
│ ├── codepipeline-detail-view.png
│ ├── codepipeline-overview.png
│ ├── ecs-cluster-view.png
│ └── ecs-task-view.png
├── infra
└── aws
│ └── eu-west-1
│ └── production
│ ├── acm.tf
│ ├── alb.tf
│ ├── codebuild.tf
│ ├── codepipeline.tf
│ ├── ecs-instance.tf
│ ├── ecs-service.tf
│ ├── ecs.tf
│ ├── iam.tf
│ ├── locals.tf
│ ├── outputs.tf
│ ├── providers.tf
│ ├── rds.tf
│ ├── route53.tf
│ ├── ssm.tf
│ ├── sts.tf
│ ├── templates
│ ├── bootstrap.sh.tpl
│ ├── buildspec.yml
│ ├── codebuild_iam_policy.json
│ ├── codebuild_iam_role_policy.json
│ ├── codepipeline_iam_policy.json
│ ├── codepipeline_iam_role_policy.json
│ ├── ecs_container_instance_iam_role_policy.json
│ ├── ecs_execution_iam_policy.json
│ ├── ecs_tasks_iam_role_policy.json
│ └── taskdefinition.json
│ ├── terraform-state.tf
│ ├── variables.tf
│ └── vpc.tf
├── pom.xml
└── src
├── main
├── java
│ └── com
│ │ └── ruanbekker
│ │ └── cargarage
│ │ ├── CarGarageApplication.java
│ │ ├── controller
│ │ ├── CarController.java
│ │ └── IndexController.java
│ │ ├── exception
│ │ └── ResourceNotFoundException.java
│ │ ├── model
│ │ └── Car.java
│ │ └── repository
│ │ └── CarRepository.java
└── resources
│ ├── application.properties
│ └── logback.xml
└── test
├── java
└── com
│ └── ruanbekker
│ └── cargarage
│ └── CarGarageApplicationTests.java
└── resources
└── application.properties
/.dockerignore:
--------------------------------------------------------------------------------
1 | # ignore terraform from docker builds
2 | infra/
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # ignore terraform from git
2 | infra/*/*/*/.terraform/*
3 | infra/*/*/*/*.tfstate
4 | infra/*/*/*/*.tfstate.*
5 | infra/*/*/*/crash.log
6 | infra/*/*/*/*.tfvars
7 | infra/*/*/*/override.tf
8 | infra/*/*/*/override.tf.json
9 | infra/*/*/*/*_override.tf
10 | infra/*/*/*/*_override.tf.json
11 | infra/*/*/*/.terraformrc
12 | infra/*/*/*/terraform.rc
13 | infra/*/*/*/.terraform.lock.hcl
14 | # ignore target jar
15 | target/*
16 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "java.configuration.updateBuildConfiguration": "interactive"
3 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM adoptopenjdk/maven-openjdk11 as builder
2 | WORKDIR /app
3 | COPY . /app
4 | RUN --mount=type=cache,target=/root/.m2 mvn -f /app/pom.xml clean install
5 |
6 | FROM adoptopenjdk:11-jre-hotspot
7 | WORKDIR /app
8 | ARG JAR_FILE=/app/target/*.jar
9 | COPY --from=builder $JAR_FILE /app/app.jar
10 |
11 | EXPOSE 8080
12 | CMD ["java", "-jar", "/app/app.jar"]
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # aws-terraform-cicd-java-springboot
2 | Terraform: AWS CICD with CodePipeline, CodeBuild, ECS and a Springboot App
3 |
4 | The [all-in-one](https://github.com/ruanbekker/aws-terraform-cicd-java-springboot/tree/all-in-one) branch has the application code, application infrastructure and pipeline infrastructure in one repository.
5 |
6 | ## Description
7 |
8 | This is a demo on how to use Terraform to deploy your AWS Infrastructure for your Java Springboot application to run as a container on ECS.
9 |
10 | You will be able to boot your application locally using docker-compose as well as building the following infrastructure on AWS for this application:
11 |
12 | - ALB, Target Groups, 80 and 443 Target Group Listeners, with Listener Configurations
13 | - ACM Certificates, ACM Certificate Validation and Route53 Configuration
14 | - CI/CD Pipeline with CodePipeline, CodeBuild and Deployment to ECS with EC2 as target
15 | - Github Webhook (Pipeline will trigger on the main branch but configurable in `variables.tf`)
16 | - ECR Repository
17 | - ECS Container Instance with Userdata
18 | - ECS Cluster, ECS Service and ECS Task Definition with variables
19 | - S3 Buckets for CodePipeline and CodeBuild Cache
20 | - RDS MySQL Instance
21 | - SSM Parameters for RDS Password, Hostname etc, which we will place into the Task Definition as well
22 | - IAM Roles, Policies and Security Groups
23 |
24 | When I tested, terraform took `4m 24s` to deploy the infrastructure and when I made a commit to the `main` branch the pipeline took about 5 minutes to deploy.
25 |
26 | ## Deploy Local
27 |
28 | Boot our application with docker-compose:
29 |
30 | ```
31 | $ docker-compose up --build
32 | ```
33 |
34 | ## Test the Application Locally
35 |
36 | Make a request to view all cars:
37 |
38 | ```
39 | $ curl http://localhost:8080/api/cars
40 | []
41 | ```
42 |
43 | Create one car:
44 |
45 | ```
46 | $ curl -H "Content-Type: application/json" http://localhost:8080/api/cars -d '{"make":"bmw", "model": "m3"}'
47 | {"id":3,"make":"bmw","model":"m3","createdAt":"2021-03-01T14:12:07.624+00:00","updatedAt":"2021-03-01T14:12:07.624+00:00"}
48 | ```
49 |
50 | View all cars again:
51 |
52 | ```
53 | $ curl http://localhost:8080/api/cars
54 | [{"id":3,"make":"bmw","model":"m3","createdAt":"2021-03-01T14:12:08.000+00:00","updatedAt":"2021-03-01T14:12:08.000+00:00"}]
55 | ```
56 |
57 | View a specific car:
58 |
59 | ```
60 | $ curl http://localhost:8080/api/cars/3
61 | {"id":3,"make":"bmw","model":"m3","createdAt":"2021-03-01T14:12:08.000+00:00","updatedAt":"2021-03-01T14:12:08.000+00:00"}
62 | ```
63 |
64 | Delete a car:
65 |
66 | ```
67 | $ curl -XDELETE http://localhost:8080/api/cars/3
68 | ```
69 |
70 | View application status:
71 |
72 | ```
73 | $ curl -s http://localhost:8080/status | jq .
74 | {
75 | "status": "UP",
76 | "components": {
77 | "db": {
78 | "status": "UP",
79 | "details": {
80 | "database": "MySQL",
81 | "validationQuery": "isValid()"
82 | }
83 | },
84 | "diskSpace": {
85 | "status": "UP",
86 | "details": {
87 | "total": 62725623808,
88 | "free": 2183278592,
89 | "threshold": 10485760,
90 | "exists": true
91 | }
92 | },
93 | "ping": {
94 | "status": "UP"
95 | }
96 | }
97 | }
98 | ```
99 |
100 | Or the database status individually:
101 |
102 | ```
103 | $ curl -s http://localhost:8080/status/db
104 | {"status":"UP","details":{"database":"MySQL","validationQuery":"isValid()"}}
105 | ```
106 |
107 | ## Installing Terraform
108 |
109 | For Mac:
110 |
111 | ```
112 | $ wget https://releases.hashicorp.com/terraform/0.14.7/terraform_0.14.7_darwin_amd64.zip
113 | $ unzip terraform_0.14.7_darwin_amd64.zip
114 | $ mv ./terraform /usr/local/bin/terraform
115 | $ rm -rf terraform_0.14.7_darwin_amd64.zip
116 | ```
117 |
118 | View the version:
119 |
120 | ```
121 | $ terraform -version
122 | Terraform v0.14.7
123 | ```
124 |
125 | ## Assumptions for AWS
126 |
127 | For AWS, I have the current existing resources, which I will reference in terraform with the `data` source:
128 |
129 | ### vpc
130 | - vpc with the name "main", which is my non default-vpc
131 |
132 | ### subnets
133 | - 3 public subnets with tags Tier:public
134 | - 3 private subnets with tags Tier:private
135 |
136 | ### nat gateway
137 | - nat gw with eip for private range and added to my private routing table 0.0.0.0/0 to natgw
138 |
139 | ### rds
140 | - subnet group with the name "private" which is linked to my private subnets
141 |
142 | ### route53
143 | - existing hosted zone
144 |
145 | ### codestar connections
146 | - codestar connection linked to my github account:
147 | - https://eu-west-1.console.aws.amazon.com/codesuite/settings/connections
148 | - the connection id is defined in: var.codestar_connection_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
149 |
150 | ## Required Environment Variables
151 |
152 | Github Personal Access Token:
153 |
154 | Head over to https://github.com/settings/tokens/new and create a token with the following scopes:
155 | - `admin:repo_hook`
156 |
157 | Set the environment variable as:
158 |
159 | ```
160 | $ export TF_VAR_github_token=${your-github-pat}
161 | ```
162 |
163 | which will be referenced in `infra/aws/eu-west-1/production/locals.tf`:
164 |
165 | ```
166 | # locals.tf
167 | locals {
168 | github_token = var.github_token
169 | }
170 | ```
171 |
172 | Other variables that needs replacement resides in `infra/aws/eu-west-1/production/variables.tf`:
173 |
174 | ```
175 | variable "aws_region" {}
176 | variable "codebuild_docker_image" {}
177 | variable "codebuild_security_group_name" {}
178 | variable "codepipeline_build_stage_name" {}
179 | variable "codepipeline_deploy_stage_name" {}
180 | variable "codepipeline_source_stage_name" {}
181 | variable "codestar_connection_id" {}
182 | variable "container_desired_count" {}
183 | variable "container_port" {}
184 | variable "container_reserved_task_memory" {}
185 | variable "ecs_cluster_name" {}
186 | variable "ecs_container_instance_type" {}
187 | variable "ecs_tg_healthcheck_endpoint" {}
188 | variable "environment_name" {}
189 | variable "github_branch" {}
190 | variable "github_repo_name" {}
191 | variable "github_token" {}
192 | variable "github_username" {}
193 | variable "host_port" {}
194 | variable "platform_type" {}
195 | variable "rds_admin_username" {}
196 | variable "rds_instance_type" {}
197 | variable "rds_subnet_group_name" {}
198 | variable "route53_hosted_zone" {}
199 | variable "route53_record_set" {}
200 | variable "service_hostname" {}
201 | variable "service_name" {}
202 | variable "service_name_short" {}
203 | variable "ssh_keypair_name" {}
204 | variable "vpc_name" {}
205 | ```
206 |
207 | Also ensure your configuration matches your setup in:
208 | - `infra/aws/eu-west-1/production/providers.tf`
209 | - `infra/aws/eu-west-1/production/terraform-state.tf`
210 |
211 | ## Notes
212 |
213 | I am using the admin credentials for the application to use to authenticate against rds (for this demo), but you can use something like ansible and the local-exec provisioner to provision a rds username and password like [here](https://github.com/ruanbekker/terraformfiles/blob/master/aws-cicd-ecs-codepipeline/existing-vpc-ecs-rds-new-dbuser-ansible-ssm/infra/rds.tf#L11-L40).
214 |
215 | I am also using `String` as the type for SSM, if you save secret information, you should be using `SecureString` and encrypt it with KMS, but for the demo I won't be doing that.
216 |
217 |
218 | ## Deploy Infrastructure to AWS
219 |
220 | Validate:
221 |
222 | ```
223 | $ terraform validate
224 | Success! The configuration is valid.
225 | ```
226 |
227 | Variables isn't supported for backend, see [this issue](https://github.com/hashicorp/terraform/issues/13022#issuecomment-294262392), to use variables, you can [look at this example](https://github.com/ruanbekker/terraformfiles/tree/master/s3-backend-with-variables):
228 |
229 | Initialize:
230 |
231 | ```
232 | $ terraform init -input=false
233 | ```
234 |
235 | Plan:
236 |
237 | ```
238 | $ terraform plan
239 | ...
240 | # aws_acm_certificate.cert will be created
241 | # aws_acm_certificate_validation.validate will be created
242 | # aws_alb.ecs will be created
243 | # aws_alb_listener.http will be created
244 | # aws_alb_listener.https will be created
245 | # aws_alb_target_group.service_tg will be created
246 | # aws_cloudwatch_log_group.ecs will be created
247 | # aws_codebuild_project.build will be created
248 | # aws_codepipeline.pipeline will be created
249 | # aws_codepipeline_webhook.webhook will be created
250 | # aws_codestarconnections_connection.github will be created
251 | # aws_db_instance.prod will be created
252 | # aws_ecr_repository.repo will be created
253 | # aws_ecs_cluster.prod will be created
254 | # aws_ecs_service.service will be created
255 | # aws_ecs_task_definition.service will be created
256 | # aws_iam_instance_profile.ecs_instance will be created
257 | # aws_iam_role.codebuild_role will be created
258 | # aws_iam_role.codepipeline_role will be created
259 | # aws_iam_role.ecs_instance_role will be created
260 | # aws_iam_role.ecs_task_role will be created
261 | # aws_iam_role_policy.codebuild_policy will be created
262 | # aws_iam_role_policy.codepipeline_policy will be created
263 | # aws_iam_role_policy.ecs_instance_policy will be created
264 | # aws_iam_role_policy.ecs_task_policy will be created
265 | # aws_instance.ec2 will be created
266 | # aws_lb_listener_rule.forward_to_tg will be created
267 | # aws_route53_record.record["rbkr.xyz"] will be created
268 | # aws_route53_record.www will be created
269 | # aws_s3_bucket.codepipeline_artifact_store will be created
270 | # aws_security_group.alb will be created
271 | # aws_security_group.codebuild will be created
272 | # aws_security_group.ecs_instance will be created
273 | # aws_security_group.rds_instance will be created
274 | # aws_security_group_rule.alb_egress will be created
275 | # aws_security_group_rule.container_port will be created
276 | # aws_security_group_rule.ec2_egress will be created
277 | # aws_security_group_rule.http will be created
278 | # aws_security_group_rule.https will be created
279 | # aws_security_group_rule.mysql will be created
280 | # aws_security_group_rule.ssh will be created
281 | # aws_ssm_parameter.database_host will be created
282 | # aws_ssm_parameter.database_name will be created
283 | # aws_ssm_parameter.database_password will be created
284 | # aws_ssm_parameter.database_port will be created
285 | # aws_ssm_parameter.database_user will be created
286 | # github_repository_webhook.webhook will be created
287 | # random_password.db_admin_password will be created
288 | # random_shuffle.subnets will be created
289 | # random_string.secret will be created
290 | Plan: 50 to add, 0 to change, 0 to destroy.
291 | ```
292 |
293 | Apply:
294 |
295 | ```
296 | $ terraform apply -input=false -auto-approve
297 | Apply complete! Resources: 55 added, 0 changed, 0 destroyed.
298 | Releasing state lock. This may take a few moments...
299 |
300 | Outputs:
301 |
302 | account_id = "xxxxxxxxxxxx"
303 | alb_dns = "ecs-prod-alb-xxxxxxxxxx.eu-west-1.elb.amazonaws.com"
304 | caller_arn = "arn:aws:iam::xxxxxxxxxxxx:user/x"
305 | caller_user = "AXXXXXXXXXXXXXXXXXXXXXX"
306 | db_address = "ecs-prod-rds-instance.xxxxxxxxxxxx.eu-west-1.rds.amazonaws.com"
307 | environment_name = "prod"
308 | service_hostname = "www.rbkr.xyz"
309 |
310 | ~/aws-terraform-cicd-java-springboot/infra/aws/eu-west-1/production main* 4m 24s
311 | ```
312 |
313 | Now that your infrastructure is built, we can trigger our repo to start the pipeline:
314 |
315 | ```
316 | $ git commit --allow-empty --message "trigger pipeline"
317 | $ git push origin main
318 | ```
319 |
320 | ## A Tour through our Infra
321 |
322 | We can see our Pipeline when you navigate to CodePipeline:
323 |
324 | 
325 |
326 | When you select the pipeline to see our stages:
327 |
328 | 
329 |
330 | We can view our ECS Cluster:
331 |
332 | 
333 |
334 | Our task:
335 |
336 | 
337 |
338 | And also check that our ACM Certificates was validated (but terraform did that already):
339 |
340 | 
341 |
342 | ## Test the Application on AWS
343 |
344 | Make a request to view all the cars:
345 |
346 | ```
347 | ❯ curl -i https://www.rbkr.xyz/api/cars
348 | HTTP/2 200
349 | date: Wed, 03 Mar 2021 15:29:41 GMT
350 | content-type: application/json
351 |
352 | []
353 | ```
354 |
355 | Create a car:
356 |
357 | ```
358 | ❯ curl -i -H "Content-Type: application/json" -XPOST https://www.rbkr.xyz/api/cars -d '{"make": "bmw", "model": "m3"}'
359 | HTTP/2 200
360 | date: Wed, 03 Mar 2021 15:29:33 GMT
361 | content-type: application/json
362 |
363 | {"id":1,"make":"bmw","model":"m3","createdAt":"2021-03-03T15:29:33.707+00:00","updatedAt":"2021-03-03T15:29:33.707+00:00"}
364 | ```
365 |
366 | View all the cars:
367 |
368 | ```
369 | ❯ curl -i https://www.rbkr.xyz/api/cars
370 | HTTP/2 200
371 | date: Wed, 03 Mar 2021 15:29:41 GMT
372 | content-type: application/json
373 |
374 | [{"id":1,"make":"bmw","model":"m3","createdAt":"2021-03-03T15:29:34.000+00:00","updatedAt":"2021-03-03T15:29:34.000+00:00"}]
375 | ```
376 |
377 | View the application status:
378 |
379 | ```
380 | ❯ curl -s https://www.rbkr.xyz/status | jq .
381 | {
382 | "status": "UP",
383 | "components": {
384 | "db": {
385 | "status": "UP",
386 | "details": {
387 | "database": "MySQL",
388 | "validationQuery": "isValid()"
389 | }
390 | },
391 | "diskSpace": {
392 | "status": "UP",
393 | "details": {
394 | "total": 10501771264,
395 | "free": 9604685824,
396 | "threshold": 10485760,
397 | "exists": true
398 | }
399 | },
400 | "ping": {
401 | "status": "UP"
402 | }
403 | }
404 | }
405 | ```
406 |
407 | ## Destroy Infrastructure on AWS:
408 |
409 | Destroy:
410 |
411 | ```
412 | $ terraform destroy -auto-approve
413 | Destroy complete! Resources: 55 destroyed.
414 | Releasing state lock. This may take a few moments...
415 | ```
416 |
417 | ## Destroy Application running Locally:
418 |
419 | ```
420 | $ docker-compose down
421 | ```
422 |
423 | ## Resources
424 |
425 | ### AWS Resources
426 |
427 | - [Difference between Task Role and Execution Role](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_TaskDefinition.html)
428 |
429 | ### Terraform Resources
430 |
431 | - [Shunit Tests for User Data](https://alexharv074.github.io/2020/01/31/unit-testing-a-terraform-user_data-script-with-shunit2.html)
432 |
433 | ### Java Resources
434 | - [spring-testing-separate-data-source](https://www.baeldung.com/spring-testing-separate-data-source) and [github](https://github.com/eugenp/tutorials/tree/master/persistence-modules/spring-boot-persistence)
435 | - [testing-with-configuration-classes-and-profiles](https://spring.io/blog/2011/06/21/spring-3-1-m2-testing-with-configuration-classes-and-profiles)
436 | - [hibernate-ddl-auto-example](https://www.onlinetutorialspoint.com/hibernate/hbm2ddl-auto-example-hibernate-xml-config.html)
437 | - [cleaning-up-spring-boot-integration-tests-logs](https://ricardolsmendes.medium.com/cleaning-up-spring-boot-integration-tests-logs-5b2d0a5f29bc)
438 | - [docker-caching-strategies](https://testdriven.io/blog/faster-ci-builds-with-docker-cache/)
439 |
440 | ## Credit
441 |
442 | Huge thanks to [Cobus Bernard](https://github.com/cobusbernard/aws-containers-for-beginners) for his webinar back in 2019, and for sharing his terraform source code, as I learned a LOT from him, and this example is based off his terraform structure.
443 |
444 | Also great thanks to [callicoder](https://www.callicoder.com/spring-boot-rest-api-tutorial-with-mysql-jpa-hibernate/) for the rest api example which this example is based off.
445 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 |
3 | services:
4 | app:
5 | build:
6 | context: .
7 | dockerfile: ./Dockerfile
8 | container_name: app
9 | depends_on:
10 | - db
11 | environment:
12 | - M2_HOME=/root/.m2
13 | - MYSQL_HOST=db
14 | - MYSQL_USER=root
15 | - MYSQL_PASS=password
16 | - MYSQL_DATABASE=cargarage
17 | networks:
18 | - app
19 | ports:
20 | - 8080:8080
21 | volumes:
22 | - $HOME/.m2:/root/.m2
23 | restart: unless-stopped
24 |
25 | db:
26 | image: mysql:8.0
27 | container_name: db
28 | environment:
29 | - MYSQL_ROOT_PASSWORD=password
30 | - MYSQL_USERNAME=app
31 | - MYSQL_PASSWORD=app
32 | - MYSQL_DATABASE=cargarage
33 | - MYSQL_ROOT_HOST=%
34 | networks:
35 | - app
36 | volumes:
37 | - dbdata:/var/lib/mysql
38 | command: --default-authentication-plugin=mysql_native_password
39 |
40 | networks:
41 | app: {}
42 |
43 | volumes:
44 | dbdata: {}
--------------------------------------------------------------------------------
/docs/screenshots/acm-certificates.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ruanbekker/aws-terraform-cicd-java-springboot/1b393922ab485f33cbf2665fe230cb2f701fcfc0/docs/screenshots/acm-certificates.png
--------------------------------------------------------------------------------
/docs/screenshots/codepipeline-detail-view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ruanbekker/aws-terraform-cicd-java-springboot/1b393922ab485f33cbf2665fe230cb2f701fcfc0/docs/screenshots/codepipeline-detail-view.png
--------------------------------------------------------------------------------
/docs/screenshots/codepipeline-overview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ruanbekker/aws-terraform-cicd-java-springboot/1b393922ab485f33cbf2665fe230cb2f701fcfc0/docs/screenshots/codepipeline-overview.png
--------------------------------------------------------------------------------
/docs/screenshots/ecs-cluster-view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ruanbekker/aws-terraform-cicd-java-springboot/1b393922ab485f33cbf2665fe230cb2f701fcfc0/docs/screenshots/ecs-cluster-view.png
--------------------------------------------------------------------------------
/docs/screenshots/ecs-task-view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ruanbekker/aws-terraform-cicd-java-springboot/1b393922ab485f33cbf2665fe230cb2f701fcfc0/docs/screenshots/ecs-task-view.png
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/acm.tf:
--------------------------------------------------------------------------------
1 | resource "aws_acm_certificate" "cert" {
2 | domain_name = var.route53_hosted_zone
3 | subject_alternative_names = ["*.${var.route53_hosted_zone}"]
4 | validation_method = "DNS"
5 |
6 | tags = {
7 | Name = var.route53_hosted_zone
8 | Environment = var.environment_name
9 | }
10 |
11 | lifecycle {
12 | create_before_destroy = true
13 | }
14 | }
15 |
16 | data "aws_route53_zone" "zone" {
17 | name = var.route53_hosted_zone
18 | private_zone = false
19 | }
20 |
21 | resource "aws_route53_record" "record" {
22 | for_each = {
23 | for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => {
24 | name = dvo.resource_record_name
25 | record = dvo.resource_record_value
26 | type = dvo.resource_record_type
27 | }
28 | }
29 |
30 | allow_overwrite = true
31 | name = each.value.name
32 | records = [each.value.record]
33 | ttl = 60
34 | type = each.value.type
35 | zone_id = data.aws_route53_zone.zone.zone_id
36 | }
37 |
38 | resource "aws_acm_certificate_validation" "validate" {
39 | certificate_arn = aws_acm_certificate.cert.arn
40 | validation_record_fqdns = [for record in aws_route53_record.record : record.fqdn]
41 | }
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/alb.tf:
--------------------------------------------------------------------------------
1 | resource "aws_security_group" "alb" {
2 | name = "${var.environment_name}-${var.ecs_cluster_name}-alb-sg"
3 | description = "Allows Traffic from Internet to ALB SG"
4 | vpc_id = data.aws_vpc.main.id
5 | tags = {
6 | Name = "${var.environment_name}-${var.ecs_cluster_name}-alb-sg"
7 | }
8 | }
9 |
10 | resource "aws_security_group_rule" "https" {
11 | security_group_id = aws_security_group.alb.id
12 | description = "Allows Internet to Connect to TCP 443"
13 | type = "ingress"
14 | protocol = "tcp"
15 | from_port = 443
16 | to_port = 443
17 | cidr_blocks = ["0.0.0.0/0"]
18 | }
19 |
20 | resource "aws_security_group_rule" "http" {
21 | security_group_id = aws_security_group.alb.id
22 | description = "Allows Internet to Connect to TCP 80"
23 | type = "ingress"
24 | protocol = "tcp"
25 | from_port = 80
26 | to_port = 80
27 | cidr_blocks = ["0.0.0.0/0"]
28 | }
29 |
30 | resource "aws_security_group_rule" "alb_egress" {
31 | security_group_id = aws_security_group.alb.id
32 | description = "Allows Egress"
33 | type = "egress"
34 | protocol = "-1"
35 | from_port = 0
36 | to_port = 0
37 | cidr_blocks = ["0.0.0.0/0"]
38 | }
39 |
40 | resource "aws_alb" "ecs" {
41 | name = "ecs-${var.environment_name}-alb"
42 | internal = false
43 | load_balancer_type = "application"
44 | security_groups = [aws_security_group.alb.id]
45 | subnets = random_shuffle.public_subnets.result
46 | enable_deletion_protection = false
47 | tags = {
48 | Name = "ecs-${var.environment_name}-alb"
49 | Environment = var.environment_name
50 | }
51 | }
52 |
53 | resource "aws_alb_target_group" "service_tg" {
54 | name = "${var.environment_name}-${var.service_name_short}-ecs-tg"
55 | port = var.container_port
56 | protocol = "HTTP"
57 | vpc_id = data.aws_vpc.main.id
58 | target_type = "instance"
59 | deregistration_delay = 10
60 |
61 | health_check {
62 | interval = 15
63 | timeout = 5
64 | healthy_threshold = 2
65 | path = var.ecs_tg_healthcheck_endpoint
66 | matcher = "200"
67 | }
68 |
69 | lifecycle {
70 | create_before_destroy = true
71 | }
72 |
73 | depends_on = [ aws_alb.ecs ]
74 | }
75 |
76 | resource "aws_alb_listener" "http" {
77 | load_balancer_arn = aws_alb.ecs.arn
78 | port = "80"
79 | protocol = "HTTP"
80 |
81 | default_action {
82 | type = "redirect"
83 |
84 | redirect {
85 | port = "443"
86 | protocol = "HTTPS"
87 | status_code = "HTTP_301"
88 | }
89 | }
90 | }
91 |
92 | resource "aws_alb_listener" "https" {
93 | load_balancer_arn = aws_alb.ecs.arn
94 | port = 443
95 | protocol = "HTTPS"
96 | certificate_arn = aws_acm_certificate_validation.validate.certificate_arn
97 |
98 | default_action {
99 | type = "fixed-response"
100 |
101 | fixed_response {
102 | content_type = "text/plain"
103 | message_body = "Page cannot be found"
104 | status_code = "404"
105 | }
106 | }
107 | }
108 |
109 | resource "aws_lb_listener_rule" "forward_to_tg" {
110 | listener_arn = aws_alb_listener.https.arn
111 |
112 | action {
113 | type = "forward"
114 | target_group_arn = aws_alb_target_group.service_tg.arn
115 | }
116 |
117 | condition {
118 | source_ip {
119 | values = [
120 | "0.0.0.0/0"
121 | ]
122 | }
123 | }
124 |
125 | condition {
126 | host_header {
127 | values = ["${var.route53_record_set}.${var.route53_hosted_zone}"]
128 | }
129 | }
130 |
131 | }
132 |
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/codebuild.tf:
--------------------------------------------------------------------------------
1 | resource "aws_s3_bucket" "codebuild_cache" {
2 | bucket = "codebuild-${var.environment_name}-cache-${data.aws_caller_identity.current.account_id}"
3 | force_destroy = true
4 | }
5 |
6 | data "template_file" "codebuild_policy" {
7 | template = file("templates/codebuild_iam_policy.json")
8 |
9 | vars = {
10 | codepipeline_bucket_arn = aws_s3_bucket.codepipeline_artifact_store.arn
11 | codebuild_cache_bucket_arn = aws_s3_bucket.codebuild_cache.arn
12 | aws_region = var.aws_region
13 | aws_account_id = data.aws_caller_identity.current.account_id
14 | service_name = var.service_name
15 | environment_name = var.environment_name
16 | subnet_id1 = element(tolist(data.aws_subnet_ids.public.ids), 0)
17 | subnet_id2 = element(tolist(data.aws_subnet_ids.public.ids), 1)
18 | subnet_id3 = element(tolist(data.aws_subnet_ids.public.ids), 2)
19 | }
20 | }
21 |
22 | resource "aws_iam_role" "codebuild_role" {
23 | name = "${var.environment_name}-${var.service_name}-codebuild-role"
24 | path = "/service-role/"
25 | assume_role_policy = file("templates/codebuild_iam_role_policy.json")
26 | }
27 |
28 | resource "aws_iam_role_policy" "codebuild_policy" {
29 | name = "${var.environment_name}-${var.service_name}-codebuild-policy"
30 | role = aws_iam_role.codebuild_role.id
31 | policy = data.template_file.codebuild_policy.rendered
32 | }
33 |
34 | resource "aws_ecr_repository" "repo" {
35 | name = "${var.environment_name}-${var.service_name}"
36 | }
37 |
38 | data "template_file" "buildspec" {
39 | template = file("templates/buildspec.yml")
40 |
41 | vars = {
42 | container_name = "${var.environment_name}-${var.service_name}"
43 | repository_url = aws_ecr_repository.repo.repository_url
44 | region = var.aws_region
45 | aws_account_id = data.aws_caller_identity.current.account_id
46 | }
47 | }
48 |
49 | resource "aws_codebuild_project" "build" {
50 | name = "${var.service_name}-${var.environment_name}-build"
51 | build_timeout = "60"
52 | service_role = aws_iam_role.codebuild_role.arn
53 |
54 | artifacts {
55 | type = "CODEPIPELINE"
56 | }
57 |
58 | cache {
59 | type = "S3"
60 | location = "${aws_s3_bucket.codebuild_cache.bucket}/${var.service_name}/${var.environment_name}/cache"
61 | }
62 |
63 | environment {
64 | compute_type = "BUILD_GENERAL1_SMALL"
65 | image = var.codebuild_docker_image
66 | type = "LINUX_CONTAINER"
67 | privileged_mode = true
68 |
69 | environment_variable {
70 | name = "SERVICE_NAME"
71 | value = var.service_name
72 | }
73 |
74 | environment_variable {
75 | name = "ENVIRONMENT_NAME"
76 | value = var.environment_name
77 | }
78 | }
79 |
80 | logs_config {
81 | cloudwatch_logs {
82 | group_name = "/aws/codebuild/${var.environment_name}"
83 | stream_name = var.service_name
84 | status = "ENABLED"
85 | }
86 |
87 | s3_logs {
88 | status = "DISABLED"
89 | }
90 | }
91 |
92 | source {
93 | type = "CODEPIPELINE"
94 | buildspec = data.template_file.buildspec.rendered
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/codepipeline.tf:
--------------------------------------------------------------------------------
1 | resource "aws_s3_bucket" "codepipeline_artifact_store" {
2 | bucket = "codepipeline-${var.environment_name}-artifacts-${data.aws_caller_identity.current.account_id}"
3 | force_destroy = true
4 | }
5 |
6 | resource "aws_iam_role" "codepipeline_role" {
7 | name = "${var.service_name}-${var.environment_name}-codepipeline-role"
8 | path = "/service-role/"
9 | assume_role_policy = file("templates/codepipeline_iam_role_policy.json")
10 | }
11 |
12 | data "template_file" "codepipeline_policy" {
13 | template = file("templates/codepipeline_iam_policy.json")
14 |
15 | vars = {
16 | codepipeline_bucket_arn = aws_s3_bucket.codepipeline_artifact_store.arn
17 | }
18 | }
19 |
20 | resource "aws_iam_role_policy" "codepipeline_policy" {
21 | name = "${var.service_name}-${var.environment_name}-codepipeline_policy"
22 | role = aws_iam_role.codepipeline_role.id
23 | policy = data.template_file.codepipeline_policy.rendered
24 | }
25 |
26 | resource "aws_codepipeline" "pipeline" {
27 | name = "${var.service_name}-${var.environment_name}-pipeline"
28 | role_arn = aws_iam_role.codepipeline_role.arn
29 |
30 | artifact_store {
31 | location = aws_s3_bucket.codepipeline_artifact_store.bucket
32 | type = "S3"
33 | }
34 |
35 | stage {
36 | name = var.codepipeline_source_stage_name
37 |
38 | action {
39 | name = "Source"
40 | category = "Source"
41 | owner = "AWS"
42 | provider = "CodeStarSourceConnection"
43 | version = "1"
44 | output_artifacts = ["source"]
45 |
46 | configuration = {
47 | ConnectionArn = "arn:aws:codestar-connections:${var.aws_region}:${data.aws_caller_identity.current.account_id}:connection/${var.codestar_connection_id}"
48 | FullRepositoryId = "${var.github_username}/${var.github_repo_name}"
49 | BranchName = var.github_branch
50 | }
51 | }
52 | }
53 |
54 | stage {
55 | name = var.codepipeline_build_stage_name
56 |
57 | action {
58 | name = "Build"
59 | category = "Build"
60 | owner = "AWS"
61 | provider = "CodeBuild"
62 | version = "1"
63 | input_artifacts = ["source"]
64 | output_artifacts = ["imagedefinitions"]
65 |
66 | configuration = {
67 | ProjectName = "${var.service_name}-${var.environment_name}-build"
68 | }
69 | }
70 | }
71 |
72 | stage {
73 | name = var.codepipeline_deploy_stage_name
74 |
75 | action {
76 | name = "Deploy"
77 | category = "Deploy"
78 | owner = "AWS"
79 | provider = "ECS"
80 | input_artifacts = ["imagedefinitions"]
81 | version = "1"
82 |
83 | configuration = {
84 | ClusterName = "${var.environment_name}-${var.ecs_cluster_name}"
85 | ServiceName = "${var.environment_name}-${var.service_name}"
86 | FileName = "imagedefinitions.json"
87 | }
88 | }
89 | }
90 | }
91 |
92 | resource "aws_codepipeline_webhook" "webhook" {
93 | name = "${var.service_name}-${var.environment_name}-webhook-github"
94 | authentication = "GITHUB_HMAC"
95 | target_action = "Source"
96 | target_pipeline = aws_codepipeline.pipeline.name
97 |
98 | authentication_configuration {
99 | secret_token = local.webhook_secret
100 | }
101 |
102 | filter {
103 | json_path = "$.ref"
104 | match_equals = "refs/heads/{Branch}"
105 | }
106 | }
107 |
108 | resource "github_repository_webhook" "webhook" {
109 | repository = var.github_repo_name
110 |
111 | configuration {
112 | url = aws_codepipeline_webhook.webhook.url
113 | content_type = "json"
114 | insecure_ssl = true
115 | secret = local.webhook_secret
116 | }
117 |
118 | events = ["push"]
119 | }
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/ecs-instance.tf:
--------------------------------------------------------------------------------
1 | data "aws_ami" "latest_ecs" {
2 | most_recent = true
3 | owners = ["amazon"]
4 |
5 | filter {
6 | name = "name"
7 | values = ["amzn-ami-*-amazon-ecs-optimized"]
8 | }
9 |
10 | filter {
11 | name = "virtualization-type"
12 | values = ["hvm"]
13 | }
14 |
15 | filter {
16 | name = "root-device-type"
17 | values = ["ebs"]
18 | }
19 | }
20 |
21 | resource "aws_security_group" "ecs_instance" {
22 | name = "${var.environment_name}-${var.ecs_cluster_name}-ec2-sg"
23 | description = "${var.environment_name}-${var.ecs_cluster_name}-ec2-sg"
24 | vpc_id = data.aws_vpc.main.id
25 | tags = {
26 | Name = "${var.environment_name}-${var.ecs_cluster_name}-ec2-sg"
27 | }
28 | }
29 |
30 | resource "aws_security_group_rule" "ssh" {
31 | security_group_id = aws_security_group.ecs_instance.id
32 | description = "TCP/22 for VPC Range"
33 | type = "ingress"
34 | protocol = "tcp"
35 | from_port = 22
36 | to_port = 22
37 | cidr_blocks = ["10.0.0.0/16"]
38 | }
39 |
40 | resource "aws_security_group_rule" "container_port" {
41 | security_group_id = aws_security_group.ecs_instance.id
42 | description = "Allows ALB to Connect to Dynamic ECS Port"
43 | type = "ingress"
44 | protocol = "tcp"
45 | from_port = 0
46 | to_port = 65535
47 | source_security_group_id = aws_security_group.alb.id
48 | }
49 |
50 | resource "aws_security_group_rule" "ec2_egress" {
51 | security_group_id = aws_security_group.ecs_instance.id
52 | type = "egress"
53 | protocol = "-1"
54 | from_port = 0
55 | to_port = 0
56 | cidr_blocks = ["0.0.0.0/0"]
57 | }
58 |
59 | resource "aws_iam_instance_profile" "ecs_instance" {
60 | name = "${var.platform_type}-${var.environment_name}-instance-profile"
61 | role = aws_iam_role.ecs_container_instance_role.name
62 | }
63 |
64 | data "template_file" "userdata" {
65 | template = file("templates/bootstrap.sh.tpl")
66 | vars = {
67 | ecs_cluster_name = aws_ecs_cluster.prod.name
68 | environment_name = var.environment_name
69 | }
70 | }
71 |
72 | data "template_cloudinit_config" "userdata" {
73 | gzip = true
74 | base64_encode = true
75 |
76 | part {
77 | content_type = "text/x-shellscript"
78 | content = data.template_file.userdata.rendered
79 | }
80 | }
81 |
82 | resource "aws_instance" "ec2" {
83 | ami = data.aws_ami.latest_ecs.id
84 | instance_type = var.ecs_container_instance_type
85 | subnet_id = element(random_shuffle.private_subnets.result, 1)
86 | key_name = var.ssh_keypair_name
87 | vpc_security_group_ids = [aws_security_group.ecs_instance.id]
88 | associate_public_ip_address = false
89 | iam_instance_profile = aws_iam_instance_profile.ecs_container_instance_profile.name
90 |
91 | lifecycle {
92 | ignore_changes = [subnet_id,ami]
93 | }
94 |
95 | user_data_base64 = data.template_cloudinit_config.userdata.rendered
96 |
97 | tags = {
98 | Name = "${var.platform_type}-${var.environment_name}-ec2-instance"
99 | InstanceLifecycle = "ondemand"
100 | ECSClusterName = var.ecs_cluster_name
101 | Environment = var.environment_name
102 | ManagedBy = "terraform"
103 | }
104 |
105 | depends_on = [ aws_ecs_cluster.prod ]
106 |
107 | }
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/ecs-service.tf:
--------------------------------------------------------------------------------
1 | resource "aws_cloudwatch_log_group" "ecs" {
2 | name = "${var.environment_name}-${var.ecs_cluster_name}-logs"
3 | }
4 |
5 | data "template_file" "service_task" {
6 | template = file("templates/taskdefinition.json")
7 |
8 | vars = {
9 | aws_region = var.aws_region
10 | aws_account_id = data.aws_caller_identity.current.account_id
11 | environment_name = var.environment_name
12 | service_name = var.service_name
13 | image = aws_ecr_repository.repo.repository_url
14 | container_name = "${var.environment_name}-${var.service_name}"
15 | container_port = var.container_port
16 | host_port = var.host_port
17 | reserved_task_memory = var.container_reserved_task_memory
18 | log_group = aws_cloudwatch_log_group.ecs.name
19 | }
20 | }
21 |
22 | resource "aws_ecs_task_definition" "service" {
23 | family = "${var.environment_name}-${var.service_name}"
24 | container_definitions = data.template_file.service_task.rendered
25 | requires_compatibilities = ["EC2"]
26 | network_mode = "bridge"
27 | execution_role_arn = aws_iam_role.ecs_execution_role.arn
28 | task_role_arn = aws_iam_role.ecs_task_role.arn
29 |
30 | placement_constraints {
31 | type = "memberOf"
32 | expression = "attribute:environment == ${var.environment_name}"
33 | }
34 | }
35 |
36 | data "aws_ecs_task_definition" "service_current" {
37 | task_definition = aws_ecs_task_definition.service.family
38 | }
39 |
40 | resource "aws_ecs_service" "service" {
41 | name = "${var.environment_name}-${var.service_name}"
42 | task_definition = "${aws_ecs_task_definition.service.family}:${max(aws_ecs_task_definition.service.revision, data.aws_ecs_task_definition.service_current.revision)}"
43 |
44 | cluster = aws_ecs_cluster.prod.id
45 | launch_type = "EC2"
46 | desired_count = var.container_desired_count
47 |
48 | ordered_placement_strategy {
49 | type = "spread"
50 | field = "attribute:ecs.availability-zone"
51 | }
52 |
53 | load_balancer {
54 | target_group_arn = aws_alb_target_group.service_tg.arn
55 | container_name = "${var.environment_name}-${var.service_name}"
56 | container_port = var.container_port
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/ecs.tf:
--------------------------------------------------------------------------------
1 | resource "aws_ecs_cluster" "prod" {
2 | name = "${var.environment_name}-${var.ecs_cluster_name}"
3 | }
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/iam.tf:
--------------------------------------------------------------------------------
1 | data "aws_iam_policy" "AmazonEC2ContainerServiceforEC2Role" {
2 | arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role"
3 | }
4 |
5 | resource "aws_iam_role" "ecs_container_instance_role" {
6 | name = "${var.ecs_cluster_name}-${var.service_name}-${var.environment_name}-instance-role"
7 | path = "/service-role/"
8 | assume_role_policy = file("templates/ecs_container_instance_iam_role_policy.json")
9 | }
10 |
11 | resource "aws_iam_policy_attachment" "ecs_instance_policy_attach" {
12 | name = "ecs-instance-policy-attachment"
13 | roles = [aws_iam_role.ecs_container_instance_role.name]
14 | policy_arn = data.aws_iam_policy.AmazonEC2ContainerServiceforEC2Role.arn
15 | }
16 |
17 | resource "aws_iam_instance_profile" "ecs_container_instance_profile" {
18 | name = "${var.ecs_cluster_name}-${var.service_name}-${var.environment_name}-ecs-instance-profile"
19 | role = aws_iam_role.ecs_container_instance_role.name
20 | }
21 |
22 | data "aws_iam_policy" "AmazonECSTaskExecutionRolePolicy" {
23 | arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
24 | }
25 |
26 | resource "aws_iam_role" "ecs_task_role" {
27 | name = "${var.ecs_cluster_name}-${var.service_name}-${var.environment_name}-task-role"
28 | path = "/service-role/"
29 | assume_role_policy = file("templates/ecs_tasks_iam_role_policy.json")
30 | }
31 |
32 | resource "aws_iam_role_policy_attachment" "ecs_task_policy_attach" {
33 | role = aws_iam_role.ecs_task_role.name
34 | policy_arn = data.aws_iam_policy.AmazonECSTaskExecutionRolePolicy.arn
35 | }
36 |
37 | resource "aws_iam_role" "ecs_execution_role" {
38 | name = "${var.ecs_cluster_name}-${var.service_name}-${var.environment_name}-execution-role"
39 | path = "/service-role/"
40 | assume_role_policy = file("templates/ecs_tasks_iam_role_policy.json")
41 | }
42 |
43 | resource "aws_iam_role_policy_attachment" "ecs_execution_policy_attach" {
44 | role = aws_iam_role.ecs_execution_role.name
45 | policy_arn = data.aws_iam_policy.AmazonECSTaskExecutionRolePolicy.arn
46 | }
47 |
48 | data "template_file" "ecs_execution_role_policy" {
49 | template = file("templates/ecs_execution_iam_policy.json")
50 |
51 | vars = {
52 | aws_region = var.aws_region
53 | aws_account_id = data.aws_caller_identity.current.account_id
54 | service_name = var.service_name
55 | environment_name = var.environment_name
56 | }
57 | }
58 |
59 | resource "aws_iam_role_policy" "ecs_execution_role_policy" {
60 | name = "${var.ecs_cluster_name}-${var.service_name}-${var.environment_name}-execution-policy"
61 | role = aws_iam_role.ecs_execution_role.id
62 | policy = data.template_file.ecs_execution_role_policy.rendered
63 | }
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/locals.tf:
--------------------------------------------------------------------------------
1 | resource "random_string" "secret" {
2 | length = 16
3 | special = true
4 | override_special = "/@£$"
5 | }
6 |
7 | # https://github.com/settings/tokens/new
8 | # admin:repo_hook
9 |
10 | locals {
11 | webhook_secret = random_string.secret.result
12 | github_token = var.github_token
13 | github_username = var.github_username
14 | }
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/outputs.tf:
--------------------------------------------------------------------------------
1 | output "environment_name" {
2 | value = var.environment_name
3 | }
4 |
5 | output "alb_dns" {
6 | value = aws_alb.ecs.dns_name
7 | }
8 |
9 | output "service_hostname" {
10 | value = "${var.route53_record_set}.${var.route53_hosted_zone}"
11 | }
12 |
13 | output "db_address" {
14 | value = aws_db_instance.prod.address
15 | }
16 |
17 | output "account_id" {
18 | value = data.aws_caller_identity.current.account_id
19 | }
20 |
21 | output "caller_arn" {
22 | value = data.aws_caller_identity.current.arn
23 | }
24 |
25 | output "caller_user" {
26 | value = data.aws_caller_identity.current.user_id
27 | }
28 |
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/providers.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | aws = {
4 | version = ">= 2.7.0"
5 | source = "hashicorp/aws"
6 | }
7 | random = {
8 | version = "~> 3.0"
9 | }
10 | null = {
11 | version = "~> 3.0"
12 | }
13 | template = {
14 | version = "~> 2.1"
15 | }
16 | github = {
17 | source = "integrations/github"
18 | version = ">= 4.5.0"
19 | }
20 | }
21 | }
22 |
23 | provider "aws" {
24 | region = "eu-west-1"
25 | profile = "demo"
26 | shared_credentials_file = "~/.aws/credentials"
27 | }
28 |
29 | provider "github" {
30 | token = local.github_token
31 | organization = local.github_username
32 | }
33 |
34 | provider "template" {}
35 | provider "random" {}
36 | provider "null" {}
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/rds.tf:
--------------------------------------------------------------------------------
1 | resource "random_password" "db_admin_password" {
2 | length = 16
3 | special = true
4 | override_special = "_"
5 | }
6 |
7 | resource "aws_security_group" "rds_instance" {
8 | name = "${var.environment_name}-${var.ecs_cluster_name}-rds-sg"
9 | description = "${var.environment_name}-${var.ecs_cluster_name}-rds-sg"
10 | vpc_id = data.aws_vpc.main.id
11 | tags = {
12 | Name = "${var.environment_name}-${var.ecs_cluster_name}-rds-sg"
13 | }
14 | }
15 |
16 | resource "aws_security_group_rule" "mysql" {
17 | security_group_id = aws_security_group.rds_instance.id
18 | description = "TCP/3306 for ECS Instances"
19 | type = "ingress"
20 | protocol = "tcp"
21 | from_port = 3306
22 | to_port = 3306
23 | source_security_group_id = aws_security_group.ecs_instance.id
24 | }
25 |
26 | resource "aws_db_instance" "prod" {
27 | identifier = "${var.platform_type}-${var.environment_name}-rds-instance"
28 | allocated_storage = 10
29 | engine = "mysql"
30 | engine_version = "8.0.20"
31 | instance_class = var.rds_instance_type
32 | name = var.service_name_short
33 | username = var.rds_admin_username
34 | password = random_password.db_admin_password.result
35 | db_subnet_group_name = var.rds_subnet_group_name
36 | parameter_group_name = "default.mysql8.0"
37 | skip_final_snapshot = true
38 | publicly_accessible = false
39 | vpc_security_group_ids = [aws_security_group.rds_instance.id]
40 | tags = {
41 | Name = "${var.platform_type}-${var.environment_name}-rds-instance"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/route53.tf:
--------------------------------------------------------------------------------
1 | data "aws_route53_zone" "selected" {
2 | name = var.route53_hosted_zone
3 | private_zone = false
4 | }
5 |
6 | resource "aws_route53_record" "www" {
7 | zone_id = data.aws_route53_zone.selected.zone_id
8 | name = "${var.route53_record_set}.${data.aws_route53_zone.selected.name}"
9 | type = "CNAME"
10 | ttl = "300"
11 | records = [aws_alb.ecs.dns_name]
12 | }
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/ssm.tf:
--------------------------------------------------------------------------------
1 | resource "aws_ssm_parameter" "database_host" {
2 | name = "/${var.service_name}/${var.environment_name}/MYSQL_HOST"
3 | type = "String"
4 | value = aws_db_instance.prod.address
5 | }
6 |
7 | resource "aws_ssm_parameter" "database_port" {
8 | name = "/${var.service_name}/${var.environment_name}/MYSQL_PORT"
9 | type = "String"
10 | value = aws_db_instance.prod.port
11 | }
12 |
13 | resource "aws_ssm_parameter" "database_password" {
14 | name = "/${var.service_name}/${var.environment_name}/MYSQL_PASS"
15 | type = "String"
16 | value = random_password.db_admin_password.result
17 | }
18 |
19 | resource "aws_ssm_parameter" "database_name" {
20 | name = "/${var.service_name}/${var.environment_name}/MYSQL_DATABASE"
21 | type = "String"
22 | value = var.service_name_short
23 | }
24 |
25 | resource "aws_ssm_parameter" "database_user" {
26 | name = "/${var.service_name}/${var.environment_name}/MYSQL_USER"
27 | type = "String"
28 | value = var.rds_admin_username
29 | }
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/sts.tf:
--------------------------------------------------------------------------------
1 | data "aws_caller_identity" "current" {}
2 |
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/templates/bootstrap.sh.tpl:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # write ecs config
4 | echo "ECS_CLUSTER=${ecs_cluster_name}" >> /etc/ecs/ecs.config
5 | echo "ECS_AVAILABLE_LOGGING_DRIVERS=[\"json-file\",\"awslogs\"]" >> /etc/ecs/ecs.config
6 | echo "ECS_INSTANCE_ATTRIBUTES={\"environment\":\"${environment_name}\"}" >> /etc/ecs/ecs.config
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/templates/buildspec.yml:
--------------------------------------------------------------------------------
1 | # https://docs.aws.amazon.com/codebuild/latest/userguide/sample-runtime-versions.html
2 | # https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-available.html
3 | version: 0.2
4 | env:
5 | variables:
6 | M2_HOME: "/root/.m2"
7 |
8 | phases:
9 | install:
10 | runtime-versions:
11 | java: corretto11
12 | commands:
13 | - java -version
14 | pre_build:
15 | commands:
16 | - java -version
17 | - echo logging in to ECR
18 | - aws ecr get-login-password --region ${region} | docker login --username AWS --password-stdin ${aws_account_id}.dkr.ecr.${region}.amazonaws.com
19 | - REPOSITORY_URI=${repository_url}
20 | - IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
21 | build:
22 | commands:
23 | - echo build started on $(date)
24 | - mvn -e -X clean install
25 | - echo building the docker image
26 | - docker build -t $REPOSITORY_URI:latest .
27 | - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
28 | post_build:
29 | commands:
30 | - echo build completed on $(date)
31 | - echo pushing the docker images
32 | - docker push $REPOSITORY_URI:latest
33 | - docker push $REPOSITORY_URI:$IMAGE_TAG
34 | - echo writing image definitions file for deployment
35 | - printf '[{"name":"${container_name}","imageUri":"%s"}]' $REPOSITORY_URI:$IMAGE_TAG > imagedefinitions.json
36 |
37 | artifacts:
38 | files: imagedefinitions.json
39 |
40 | cache:
41 | paths:
42 | - '/root/.m2/**/*'
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/templates/codebuild_iam_policy.json:
--------------------------------------------------------------------------------
1 | {
2 | "Version": "2012-10-17",
3 | "Statement": [
4 | {
5 | "Effect": "Allow",
6 | "Resource": [
7 | "*"
8 | ],
9 | "Action": [
10 | "logs:CreateLogGroup",
11 | "logs:CreateLogStream",
12 | "logs:PutLogEvents",
13 | "ecr:GetAuthorizationToken",
14 | "ecr:InitiateLayerUpload",
15 | "ecr:UploadLayerPart",
16 | "ecr:CompleteLayerUpload",
17 | "ecr:BatchCheckLayerAvailability",
18 | "ecr:PutImage",
19 | "ecs:RunTask",
20 | "iam:PassRole"
21 | ]
22 | },
23 | {
24 | "Effect":"Allow",
25 | "Action": [
26 | "s3:GetObject",
27 | "s3:GetObjectVersion",
28 | "s3:GetBucketVersioning",
29 | "s3:List*",
30 | "s3:PutObject"
31 | ],
32 | "Resource": [
33 | "${codepipeline_bucket_arn}",
34 | "${codepipeline_bucket_arn}/*",
35 | "${codebuild_cache_bucket_arn}",
36 | "${codebuild_cache_bucket_arn}/*"
37 | ]
38 | },
39 | {
40 | "Effect": "Allow",
41 | "Action": [
42 | "ec2:CreateNetworkInterface",
43 | "ec2:DescribeDhcpOptions",
44 | "ec2:DescribeNetworkInterfaces",
45 | "ec2:DeleteNetworkInterface",
46 | "ec2:DescribeSubnets",
47 | "ec2:DescribeSecurityGroups",
48 | "ec2:DescribeVpcs"
49 | ],
50 | "Resource": "*"
51 | },
52 | {
53 | "Effect": "Allow",
54 | "Action": [
55 | "ec2:CreateNetworkInterfacePermission"
56 | ],
57 | "Resource": [
58 | "arn:aws:ec2:${aws_region}:${aws_account_id}:network-interface/*"
59 | ],
60 | "Condition": {
61 | "StringEquals": {
62 | "ec2:Subnet": [
63 | "arn:aws:ec2:${aws_region}:${aws_account_id}:subnet/${subnet_id1}",
64 | "arn:aws:ec2:${aws_region}:${aws_account_id}:subnet/${subnet_id2}",
65 | "arn:aws:ec2:${aws_region}:${aws_account_id}:subnet/${subnet_id3}"
66 | ],
67 | "ec2:AuthorizedService": "codebuild.amazonaws.com"
68 | }
69 | }
70 | },
71 | {
72 | "Effect": "Allow",
73 | "Action":[
74 | "ssm:GetParametersByPath",
75 | "ssm:GetParameters",
76 | "ssm:GetParameter"
77 | ],
78 | "Resource":[
79 | "arn:aws:ssm:${aws_region}:${aws_account_id}:parameter/${service_name}/${environment_name}/*",
80 | "arn:aws:ssm:${aws_region}:${aws_account_id}:parameter/codebuild/*"
81 | ]
82 | },
83 | {
84 | "Effect": "Allow",
85 | "Action":[
86 | "kms:Decrypt",
87 | "kms:GenerateDataKey"
88 | ],
89 | "Resource":[
90 | "*"
91 | ]
92 | }
93 | ]
94 | }
95 |
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/templates/codebuild_iam_role_policy.json:
--------------------------------------------------------------------------------
1 | {
2 | "Version": "2012-10-17",
3 | "Statement": [
4 | {
5 | "Effect": "Allow",
6 | "Principal": {
7 | "Service": "codebuild.amazonaws.com"
8 | },
9 | "Action": "sts:AssumeRole"
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/templates/codepipeline_iam_policy.json:
--------------------------------------------------------------------------------
1 | {
2 | "Version": "2012-10-17",
3 | "Statement": [
4 | {
5 | "Effect":"Allow",
6 | "Action": [
7 | "s3:GetObject",
8 | "s3:GetObjectVersion",
9 | "s3:GetBucketVersioning",
10 | "s3:List*",
11 | "s3:PutObject",
12 | "s3:PutObjectAcl"
13 | ],
14 | "Resource": [
15 | "${codepipeline_bucket_arn}",
16 | "${codepipeline_bucket_arn}/*"
17 | ]
18 | },
19 | {
20 | "Effect": "Allow",
21 | "Action": [
22 | "codebuild:BatchGetBuilds",
23 | "codebuild:StartBuild"
24 | ],
25 | "Resource": "*"
26 | },
27 | {
28 | "Action": [
29 | "ecs:*",
30 | "events:DescribeRule",
31 | "events:DeleteRule",
32 | "events:ListRuleNamesByTarget",
33 | "events:ListTargetsByRule",
34 | "events:PutRule",
35 | "events:PutTargets",
36 | "events:RemoveTargets",
37 | "iam:ListAttachedRolePolicies",
38 | "iam:ListInstanceProfiles",
39 | "iam:ListRoles",
40 | "logs:CreateLogGroup",
41 | "logs:DescribeLogGroups",
42 | "logs:FilterLogEvents"
43 | ],
44 | "Resource": "*",
45 | "Effect": "Allow"
46 | },
47 | {
48 | "Action": "iam:PassRole",
49 | "Effect": "Allow",
50 | "Resource": [
51 | "*"
52 | ],
53 | "Condition": {
54 | "StringLike": {
55 | "iam:PassedToService": "ecs-tasks.amazonaws.com"
56 | }
57 | }
58 | },
59 | {
60 | "Action": "iam:PassRole",
61 | "Effect": "Allow",
62 | "Resource": [
63 | "arn:aws:iam::*:role/ecsInstanceRole*"
64 | ],
65 | "Condition": {
66 | "StringLike": {
67 | "iam:PassedToService": [
68 | "ec2.amazonaws.com",
69 | "ec2.amazonaws.com.cn"
70 | ]
71 | }
72 | }
73 | },
74 | {
75 | "Effect": "Allow",
76 | "Action": "iam:CreateServiceLinkedRole",
77 | "Resource": "*",
78 | "Condition": {
79 | "StringLike": {
80 | "iam:AWSServiceName": [
81 | "ecs.amazonaws.com",
82 | "spot.amazonaws.com",
83 | "spotfleet.amazonaws.com"
84 | ]
85 | }
86 | }
87 | },
88 | {
89 | "Effect": "Allow",
90 | "Action": [
91 | "codestar-connections:UseConnection"
92 | ],
93 | "Resource": [
94 | "*"
95 | ]
96 | }
97 | ]
98 | }
99 |
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/templates/codepipeline_iam_role_policy.json:
--------------------------------------------------------------------------------
1 | {
2 | "Version": "2012-10-17",
3 | "Statement": [
4 | {
5 | "Effect": "Allow",
6 | "Principal": {
7 | "Service": "codepipeline.amazonaws.com"
8 | },
9 | "Action": "sts:AssumeRole"
10 | }
11 | ]
12 | }
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/templates/ecs_container_instance_iam_role_policy.json:
--------------------------------------------------------------------------------
1 | {
2 | "Version": "2008-10-17",
3 | "Statement": [
4 | {
5 | "Sid": "",
6 | "Effect": "Allow",
7 | "Principal": {
8 | "Service": "ec2.amazonaws.com"
9 | },
10 | "Action": "sts:AssumeRole"
11 | }
12 | ]
13 | }
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/templates/ecs_execution_iam_policy.json:
--------------------------------------------------------------------------------
1 | {
2 | "Version": "2012-10-17",
3 | "Statement": [
4 | {
5 | "Effect": "Allow",
6 | "Action":[
7 | "ssm:GetParametersByPath",
8 | "ssm:GetParameters",
9 | "ssm:GetParameter"
10 | ],
11 | "Resource":[
12 | "arn:aws:ssm:${aws_region}:${aws_account_id}:parameter/${service_name}/${environment_name}/*"
13 | ]
14 | },
15 | {
16 | "Effect": "Allow",
17 | "Action": [
18 | "logs:CreateLogGroup",
19 | "logs:CreateLogStream",
20 | "logs:PutLogEvents"
21 | ],
22 | "Resource": "*"
23 | }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/templates/ecs_tasks_iam_role_policy.json:
--------------------------------------------------------------------------------
1 | {
2 | "Version": "2012-10-17",
3 | "Statement": [
4 | {
5 | "Sid": "",
6 | "Effect": "Allow",
7 | "Principal": {
8 | "Service": "ecs-tasks.amazonaws.com"
9 | },
10 | "Action": "sts:AssumeRole"
11 | }
12 | ]
13 | }
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/templates/taskdefinition.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "${container_name}",
4 | "image": "${image}",
5 | "memoryReservation": ${reserved_task_memory},
6 | "portMappings": [
7 | {
8 | "containerPort": ${container_port},
9 | "hostPort": ${host_port}
10 | }
11 | ],
12 | "environment": [
13 | {
14 | "name": "AWS_DEFAULT_REGION",
15 | "value": "eu-west-1"
16 | }
17 | ],
18 | "secrets": [
19 | {
20 | "name": "MYSQL_HOST",
21 | "valueFrom": "arn:aws:ssm:${aws_region}:${aws_account_id}:parameter/${service_name}/${environment_name}/MYSQL_HOST"
22 | },
23 | {
24 | "name": "MYSQL_PORT",
25 | "valueFrom": "arn:aws:ssm:${aws_region}:${aws_account_id}:parameter/${service_name}/${environment_name}/MYSQL_PORT"
26 | },
27 | {
28 | "name": "MYSQL_PASS",
29 | "valueFrom": "arn:aws:ssm:${aws_region}:${aws_account_id}:parameter/${service_name}/${environment_name}/MYSQL_PASS"
30 | },
31 | {
32 | "name": "MYSQL_DATABASE",
33 | "valueFrom": "arn:aws:ssm:${aws_region}:${aws_account_id}:parameter/${service_name}/${environment_name}/MYSQL_DATABASE"
34 | },
35 | {
36 | "name": "MYSQL_USER",
37 | "valueFrom": "arn:aws:ssm:${aws_region}:${aws_account_id}:parameter/${service_name}/${environment_name}/MYSQL_USER"
38 | }
39 | ],
40 | "networkMode": "bridge",
41 | "essential": true,
42 | "logConfiguration": {
43 | "logDriver": "awslogs",
44 | "options": {
45 | "awslogs-group": "${log_group}",
46 | "awslogs-region": "${aws_region}",
47 | "awslogs-stream-prefix": "${service_name}"
48 | }
49 | }
50 | }
51 | ]
52 |
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/terraform-state.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | backend "s3" {
3 | encrypt = true
4 | bucket = "your-terraform-remote-state"
5 | key = "aws-terraform-cicd-java-springboot/prod/ecs/terraform.tfstate"
6 | region = "eu-west-1"
7 | profile = "demo"
8 | shared_credentials_file = "~/.aws/credentials"
9 | dynamodb_table = "terraform-remote-state"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/variables.tf:
--------------------------------------------------------------------------------
1 | variable "aws_region" {
2 | type = string
3 | default = "eu-west-1"
4 | }
5 |
6 | variable "environment_name" {
7 | type = string
8 | default = "prod"
9 | }
10 |
11 | variable "service_name" {
12 | type = string
13 | default = "cargarage-api"
14 | }
15 |
16 | variable "service_name_short" {
17 | type = string
18 | default = "cargarage"
19 | }
20 |
21 | variable "ecs_container_instance_type" {
22 | type = string
23 | default = "t2.micro"
24 | }
25 |
26 | variable "codebuild_security_group_name" {
27 | type = string
28 | default = "vpc-codebuild"
29 | }
30 |
31 | variable "container_port" {
32 | type = string
33 | default = "8080"
34 | }
35 |
36 | variable "host_port" {
37 | type = string
38 | default = "0"
39 | }
40 |
41 | variable "container_desired_count" {
42 | type = string
43 | default = "1"
44 | }
45 |
46 | variable "container_reserved_task_memory" {
47 | type = string
48 | default = "128"
49 | }
50 |
51 | variable "ecs_cluster_name" {
52 | type = string
53 | default = "ecs-cluster"
54 | }
55 |
56 | variable "ecs_tg_healthcheck_endpoint" {
57 | type = string
58 | default = "/status"
59 | }
60 |
61 | variable "github_username" {
62 | type = string
63 | default = "ruanbekker"
64 | }
65 |
66 | variable "github_repo_name" {
67 | type = string
68 | default = "aws-terraform-cicd-java-springboot"
69 | }
70 |
71 | variable "github_branch" {
72 | type = string
73 | default = "main"
74 | }
75 |
76 | variable "github_token" {
77 | type = string
78 | }
79 |
80 | variable "route53_hosted_zone" {
81 | type = string
82 | default = "example.com"
83 | }
84 |
85 | variable "route53_record_set" {
86 | type = string
87 | default = "www"
88 | }
89 |
90 | variable "service_hostname" {
91 | type = string
92 | default = "www.example.com"
93 | }
94 |
95 | variable "codestar_connection_id" {
96 | type = string
97 | default = ""
98 | }
99 |
100 | variable "codebuild_docker_image" {
101 | type = string
102 | default = "aws/codebuild/standard:5.0"
103 | }
104 |
105 | variable "codepipeline_source_stage_name" {
106 | type = string
107 | default = "Source"
108 | }
109 |
110 | variable "codepipeline_build_stage_name" {
111 | type = string
112 | default = "Build"
113 | }
114 |
115 | variable "codepipeline_deploy_stage_name" {
116 | type = string
117 | default = "Prod"
118 | }
119 |
120 | variable "rds_admin_username" {
121 | type = string
122 | default = "admin"
123 | }
124 |
125 | variable "rds_subnet_group_name" {
126 | type = string
127 | default = "private"
128 | }
129 |
130 | variable "rds_instance_type" {
131 | type = string
132 | default = "db.t2.micro"
133 | }
134 |
135 | variable "platform_type" {
136 | type = string
137 | default = "ecs"
138 | }
139 |
140 | variable "ssh_keypair_name" {
141 | type = string
142 | default = ""
143 | }
144 |
145 | variable "vpc_name" {
146 | type = string
147 | default = "main"
148 | }
--------------------------------------------------------------------------------
/infra/aws/eu-west-1/production/vpc.tf:
--------------------------------------------------------------------------------
1 | data "aws_vpc" "main" {
2 | default = false
3 | tags = {
4 | Name = var.vpc_name
5 | }
6 | }
7 |
8 | data "aws_subnet_ids" "private" {
9 | vpc_id = data.aws_vpc.main.id
10 | tags = {
11 | Tier = "private"
12 | }
13 | }
14 |
15 | data "aws_subnet_ids" "public" {
16 | vpc_id = data.aws_vpc.main.id
17 | tags = {
18 | Tier = "public"
19 | }
20 | }
21 |
22 | resource "random_shuffle" "private_subnets" {
23 | input = tolist(data.aws_subnet_ids.private.ids)
24 | result_count = 3
25 | }
26 |
27 | resource "random_shuffle" "public_subnets" {
28 | input = tolist(data.aws_subnet_ids.public.ids)
29 | result_count = 3
30 | }
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 | com.ruanbekker
5 | cargarage
6 | 0.0.1-SNAPSHOT
7 |
8 | org.springframework.boot
9 | spring-boot-starter-parent
10 | 2.3.3.RELEASE
11 |
12 | cargarage
13 | Car Garage API
14 |
15 | 11
16 | UTF-8
17 |
18 |
19 |
20 | org.springframework.boot
21 | spring-boot-starter-web
22 |
23 |
24 | org.springframework.boot
25 | spring-boot-starter-data-jpa
26 |
27 |
28 | org.springframework.boot
29 | spring-boot-starter-actuator
30 |
31 |
32 | org.springframework.boot
33 | spring-boot-starter-validation
34 |
35 |
36 | org.springframework.boot
37 | spring-boot-devtools
38 | runtime
39 |
40 |
41 | mysql
42 | mysql-connector-java
43 | runtime
44 |
45 |
46 | org.springframework.boot
47 | spring-boot-starter-test
48 | test
49 |
50 |
51 | com.h2database
52 | h2
53 |
54 |
55 |
56 | io.micrometer
57 | micrometer-registry-prometheus
58 | 1.9.4
59 |
60 |
61 |
83 |
84 |
85 |
86 |
87 | org.springframework.boot
88 | spring-boot-maven-plugin
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/src/main/java/com/ruanbekker/cargarage/CarGarageApplication.java:
--------------------------------------------------------------------------------
1 | package com.ruanbekker.cargarage;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
6 |
7 | import org.slf4j.Logger;
8 | import org.slf4j.LoggerFactory;
9 |
10 | @SpringBootApplication
11 | @EnableJpaAuditing
12 | public class CarGarageApplication {
13 |
14 | private static Logger log = LoggerFactory.getLogger(CarGarageApplication.class);
15 |
16 | public static void main(String[] args) {
17 | SpringApplication.run(CarGarageApplication.class, args);
18 | log.info("application is starting with info level logging");
19 | }
20 | }
--------------------------------------------------------------------------------
/src/main/java/com/ruanbekker/cargarage/controller/CarController.java:
--------------------------------------------------------------------------------
1 | package com.ruanbekker.cargarage.controller;
2 |
3 | import com.ruanbekker.cargarage.exception.ResourceNotFoundException;
4 | import com.ruanbekker.cargarage.model.Car;
5 | import com.ruanbekker.cargarage.repository.CarRepository;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.http.ResponseEntity;
8 | import org.springframework.web.bind.annotation.*;
9 |
10 | import javax.validation.Valid;
11 | import java.util.List;
12 | import org.slf4j.Logger;
13 | import org.slf4j.LoggerFactory;
14 |
15 | @RestController
16 | @RequestMapping("/api")
17 | public class CarController {
18 |
19 | private static final Logger log = LoggerFactory.getLogger(CarController.class);
20 |
21 | @Autowired
22 | CarRepository carRepository;
23 |
24 | @GetMapping("/cars")
25 | public List getAllCars() {
26 | log.info("listing all cars");
27 | return carRepository.findAll();
28 | }
29 |
30 | @PostMapping("/cars")
31 | public Car createCar(@Valid @RequestBody Car car) {
32 | log.info("creating entry for a new car");
33 | return carRepository.save(car);
34 | }
35 |
36 | @GetMapping("/cars/{id}")
37 | public Car getCarById(@PathVariable(value = "id") Long carId) {
38 | log.info("attempting to fetch car id {}", carId);
39 | return carRepository.findById(carId)
40 | .orElseThrow(() -> new ResourceNotFoundException("Car", "id", carId));
41 | }
42 |
43 | @PutMapping("/cars/{id}")
44 | public Car updateCar(@PathVariable(value = "id") Long carId, @Valid @RequestBody Car carDetails) {
45 |
46 | Car car = carRepository.findById(carId).orElseThrow(() -> new ResourceNotFoundException("Car", "id", carId));
47 | car.setMake(carDetails.getMake());
48 | car.setModel(carDetails.getModel());
49 | log.info("updating car id {}", carId);
50 | Car updatedCar = carRepository.save(car);
51 |
52 | return updatedCar;
53 | }
54 |
55 | @DeleteMapping("/cars/{id}")
56 | public ResponseEntity> deleteCar(@PathVariable(value = "id") Long carId) {
57 | Car car = carRepository.findById(carId).orElseThrow(() -> new ResourceNotFoundException("Car", "id", carId));
58 | carRepository.delete(car);
59 | log.info("deleting car id {}", carId);
60 |
61 | return ResponseEntity.ok().build();
62 | }
63 | }
--------------------------------------------------------------------------------
/src/main/java/com/ruanbekker/cargarage/controller/IndexController.java:
--------------------------------------------------------------------------------
1 | package com.ruanbekker.cargarage.controller;
2 |
3 | import org.springframework.web.bind.annotation.GetMapping;
4 | import org.springframework.web.bind.annotation.RequestMapping;
5 | import org.springframework.web.bind.annotation.RestController;
6 |
7 | @RestController
8 | @RequestMapping("/")
9 | public class IndexController {
10 |
11 | @GetMapping
12 | public String sayHello() {
13 | return "Hello and Welcome to the CarGarage application. You can create a new car entry by making a POST request to /api/cars endpoint.";
14 | }
15 | }
--------------------------------------------------------------------------------
/src/main/java/com/ruanbekker/cargarage/exception/ResourceNotFoundException.java:
--------------------------------------------------------------------------------
1 | package com.ruanbekker.cargarage.exception;
2 |
3 | import org.springframework.http.HttpStatus;
4 | import org.springframework.web.bind.annotation.ResponseStatus;
5 |
6 | @ResponseStatus(value = HttpStatus.NOT_FOUND)
7 | public class ResourceNotFoundException extends RuntimeException {
8 | private String resourceName;
9 | private String fieldName;
10 | private Object fieldValue;
11 |
12 | public ResourceNotFoundException( String resourceName, String fieldName, Object fieldValue) {
13 | super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue));
14 | this.resourceName = resourceName;
15 | this.fieldName = fieldName;
16 | this.fieldValue = fieldValue;
17 | }
18 |
19 | public String getResourceName() {
20 | return resourceName;
21 | }
22 |
23 | public String getFieldName() {
24 | return fieldName;
25 | }
26 |
27 | public Object getFieldValue() {
28 | return fieldValue;
29 | }
30 | }
--------------------------------------------------------------------------------
/src/main/java/com/ruanbekker/cargarage/model/Car.java:
--------------------------------------------------------------------------------
1 | package com.ruanbekker.cargarage.model;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4 | import org.springframework.data.annotation.CreatedDate;
5 | import org.springframework.data.annotation.LastModifiedDate;
6 | import org.springframework.data.jpa.domain.support.AuditingEntityListener;
7 |
8 | import javax.persistence.*;
9 | import javax.validation.constraints.NotBlank;
10 | import java.util.Date;
11 |
12 | @Entity
13 | @Table(name = "cars")
14 | @EntityListeners(AuditingEntityListener.class)
15 | @JsonIgnoreProperties(value = {"createdAt", "updatedAt"}, allowGetters = true)
16 | public class Car {
17 | @Id
18 | @GeneratedValue(strategy = GenerationType.IDENTITY)
19 | private Long id;
20 |
21 | @NotBlank
22 | private String make;
23 |
24 | @NotBlank
25 | private String model;
26 |
27 | @Column(nullable = false, updatable = false)
28 | @Temporal(TemporalType.TIMESTAMP)
29 | @CreatedDate
30 | private Date createdAt;
31 |
32 | @Column(nullable = false)
33 | @Temporal(TemporalType.TIMESTAMP)
34 | @LastModifiedDate
35 | private Date updatedAt;
36 |
37 | public Long getId() {
38 | return id;
39 | }
40 |
41 | public void setId(Long id) {
42 | this.id = id;
43 | }
44 |
45 | public String getMake() {
46 | return make;
47 | }
48 |
49 | public void setMake(String make) {
50 | this.make = make;
51 | }
52 |
53 | public String getModel() {
54 | return model;
55 | }
56 |
57 | public void setModel(String model) {
58 | this.model = model;
59 | }
60 |
61 | public Date getCreatedAt() {
62 | return createdAt;
63 | }
64 |
65 | public void setCreatedAt(Date createdAt) {
66 | this.createdAt = createdAt;
67 | }
68 |
69 | public Date getUpdatedAt() {
70 | return updatedAt;
71 | }
72 |
73 | public void setUpdatedAt(Date updatedAt) {
74 | this.updatedAt = updatedAt;
75 | }
76 |
77 | }
--------------------------------------------------------------------------------
/src/main/java/com/ruanbekker/cargarage/repository/CarRepository.java:
--------------------------------------------------------------------------------
1 | package com.ruanbekker.cargarage.repository;
2 |
3 | import com.ruanbekker.cargarage.model.Car;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 | import org.springframework.stereotype.Repository;
6 |
7 | @Repository
8 | public interface CarRepository extends JpaRepository {
9 |
10 | }
--------------------------------------------------------------------------------
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | # web
2 | management.server.port=8080
3 | management.endpoints.web.exposure.include=health,info,metrics,prometheus
4 | management.endpoints.web.base-path=/
5 | management.endpoints.web.path-mapping.health=status
6 | management.endpoint.health.show-details=always
7 |
8 | # jpa
9 | # (create, create-drop, validate, update)
10 | spring.jpa.hibernate.ddl-auto=update
11 | spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect
12 | spring.jpa.database-platform = org.hibernate.dialect.MySQL8Dialect
13 | spring.jpa.generate-ddl=true
14 | # datasource
15 | spring.datasource.url=jdbc:mysql://${MYSQL_HOST:127.0.0.1}/${MYSQL_DATABASE:cargarage}?autoReconnect=true&failOverReadOnly=false&maxReconnects=10
16 | spring.datasource.username=${MYSQL_USER:app}
17 | spring.datasource.password=${MYSQL_PASS:app}
18 | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
19 |
20 | # logging
21 | logging.level.com.ruanbekker.cargarage=INFO
22 | logging.level.org.springframework.web=INFO
--------------------------------------------------------------------------------
/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n%throwable
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/test/java/com/ruanbekker/cargarage/CarGarageApplicationTests.java:
--------------------------------------------------------------------------------
1 | package com.ruanbekker.cargarage;
2 |
3 | import org.junit.Test;
4 | import org.junit.runner.RunWith;
5 | import org.springframework.boot.test.context.SpringBootTest;
6 | import org.springframework.test.context.junit4.SpringRunner;
7 |
8 | @RunWith(SpringRunner.class)
9 | @SpringBootTest
10 | public class CarGarageApplicationTests {
11 |
12 | @Test
13 | public void contextLoads() {
14 | }
15 |
16 | }
--------------------------------------------------------------------------------
/src/test/resources/application.properties:
--------------------------------------------------------------------------------
1 | # https://www.baeldung.com/spring-testing-separate-data-source
2 | spring.datasource.driver-class-name=org.h2.Driver
3 | spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
4 | spring.datasource.username=admin
5 | spring.datasource.password=admin
--------------------------------------------------------------------------------