diff --git a/.gitignore b/.gitignore index 48efa8d1..8459b00b 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,11 @@ nginx.vars global.env .*.bak* + + +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* +terraform/environment.tf \ No newline at end of file diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl new file mode 100644 index 00000000..2c8b1d50 --- /dev/null +++ b/terraform/.terraform.lock.hcl @@ -0,0 +1,40 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "4.34.0" + hashes = [ + "h1:SDqaa/BVMQMzQ1bWQfrcsC4jfaywFeUq03jsojDNnyY=", + "zh:2bdc9b908008c1e874d8ba7e2cfabd856cafb63c52fef51a1fdeef2f5584bffd", + "zh:43c5364e3161be3856e56494cbb8b21d513fc05875f1b40e66e583602154dd0a", + "zh:44e763adae92489f223f65866c1f8b5342e7e85b95daa8d1f483a2afb47f7db3", + "zh:62bfabb3a1a31814cb3fadc356539d8253b95abacfffd90984affb54c9a53a86", + "zh:6495ce67897d2d5648d466c09e8588e837c2878322988738a95c06926044b05d", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:b1546b2ac61d7cc27a8eba160ae1b6ac581d2c4af824a400d6511e4998da398a", + "zh:c8c362c5527f0533bde581e41cdb2bdf42aea557762f326dbfa122fdf001fb10", + "zh:c8cc28fb41f73ca09f590bace2204ea325f6116be0bbce6abfebd393d028f12c", + "zh:db0601c9bd12ca028d60ac87e85320285ebc64857715f7908dd6a283e5f44d45", + "zh:e64d2193236d05348ba2e8b99650d9274e5af80be39b3ee28bbe442b0684d8a3", + "zh:ff6228f3751e1f0ee7dc086d09e32d39ca6556f0b5267f36aae67881d29ace94", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.4.3" + hashes = [ + "h1:saZR+mhthL0OZl4SyHXZraxyaBNVMxiZzks78nWcZ2o=", + "zh:41c53ba47085d8261590990f8633c8906696fa0a3c4b384ff6a7ecbf84339752", + "zh:59d98081c4475f2ad77d881c4412c5129c56214892f490adf11c7e7a5a47de9b", + "zh:686ad1ee40b812b9e016317e7f34c0d63ef837e084dea4a1f578f64a6314ad53", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:84103eae7251384c0d995f5a257c72b0096605048f757b749b7b62107a5dccb3", + "zh:8ee974b110adb78c7cd18aae82b2729e5124d8f115d484215fd5199451053de5", + "zh:9dd4561e3c847e45de603f17fa0c01ae14cae8c4b7b4e6423c9ef3904b308dda", + "zh:bb07bb3c2c0296beba0beec629ebc6474c70732387477a65966483b5efabdbc6", + "zh:e891339e96c9e5a888727b45b2e1bb3fcbdfe0fd7c5b4396e4695459b38c8cb1", + "zh:ea4739860c24dfeaac6c100b2a2e357106a89d18751f7693f3c31ecf6a996f8d", + "zh:f0c76ac303fd0ab59146c39bc121c5d7d86f878e9a69294e29444d4c653786f8", + "zh:f143a9a5af42b38fed328a161279906759ff39ac428ebcfe55606e05e1518b93", + ] +} diff --git a/terraform/Makefile b/terraform/Makefile new file mode 100644 index 00000000..3e106660 --- /dev/null +++ b/terraform/Makefile @@ -0,0 +1,87 @@ +GIT_DESCR = $(shell git describe --tags --always) +DOCKER_REGISTRY = 205035295823.dkr.ecr.eu-central-1.amazonaws.com +DOCKER_TAG = $(GIT_DESCR) +AWS_PROFILE = apeunit + +define build_project + @echo installing dependencies and building + sh -c 'cd ../$(1) && yarn install && yarn build' + @echo done +endef + +define build_docker_image + @echo building docker image for $(1) + sh -c 'cd .. && docker build --platform linux/amd64 --build-arg DOCKER_TAG='$(DOCKER_TAG)' -t $(1) -f $(1)/Dockerfile .' + @echo done +endef + +define push_docker_image + @echo publishing docker image for $(1) + docker tag $(1):latest $(DOCKER_REGISTRY)/$(2):$(DOCKER_TAG) + docker tag $(1):latest $(DOCKER_REGISTRY)/$(2):latest + docker push --all-tags $(DOCKER_REGISTRY)/$(2) + @echo done +endef + +define redeploy_ecs_service + @echo building docker image for $(1) + aws --profile $(AWS_PROFILE) ecs update-service --cluster kanvas-ecs-cluster --service $(1)-service --force-new-deployment + @echo done +endef + +default: create-ecr-repos docker-build-all docker-push-all + +create-ecr-repos: + @echo copy resources + terraform apply -target="aws_ecr_repository.kanvas_ecr_repos" + @echo done + +docker-build-store-front: + $(call build_docker_image,store-front) + +docker-build-admin-front: + $(call build_docker_image,admin-front) + +docker-build-store-api-server: + $(call build_docker_image,store-api-server) + +docker-build-admin-api-server: + $(call build_docker_image,admin-api-server) + +docker-build-all: docker-build-store-front docker-build-admin-front docker-build-store-api-server docker-build-admin-api-server + +docker-login: + # docker login $(DOCKER_PROVIDER) + aws --profile $(AWS_PROFILE) ecr get-login-password --region eu-central-1 | docker login --username AWS --password-stdin $(DOCKER_REGISTRY) + docker login $(DOCKER_REGISTRY) + +docker-push-store-front: docker-login + $(call push_docker_image,store-front,kanvas-store-front) + +docker-push-admin-front: docker-login + $(call push_docker_image,admin-front,kanvas-admin-front) + +docker-push-store-api-server: docker-login + $(call push_docker_image,store-api-server,kanvas-store-api) + +docker-push-admin-api-server: docker-login + $(call push_docker_image,admin-api-server,kanvas-admin-api) + +docker-push-all: docker-login docker-push-store-front docker-push-admin-front docker-push-store-api-server docker-push-admin-api-server + + + + + +ecs-redeploy-store-front: + $(call redeploy_ecs_service,store-front) +ecs-redeploy-admin-front: + $(call redeploy_ecs_service,admin-front) + +ecs-redeploy-store-api-server: + $(call redeploy_ecs_service,store-api) + +ecs-redeploy-admin-api-server: + $(call redeploy_ecs_service,admin-api) + +ecs-redeploy-all: ecs-redeploy-store-front ecs-redeploy-admin-front ecs-redeploy-store-api-server ecs-redeploy-admin-api-server \ No newline at end of file diff --git a/terraform/environment.tf.sample b/terraform/environment.tf.sample new file mode 100644 index 00000000..1d03b617 --- /dev/null +++ b/terraform/environment.tf.sample @@ -0,0 +1,194 @@ +variable "service_environments" { + type = list(object({ + key = string + environment = list(object({ + name = string + value = string + })) + })) + default = [ + { + key = "store-front" + environment = [ + { + name = "REACT_APP_PORT" + value = "" + }, + { + name = "REACT_APP_API_SERVER_BASE_URL" + value = "" + }, + { + name = "REACT_APP_STRIPE_PK_KEY" + value = "" + } + ] + }, + { + key = "admin-front" + environment = [ + { + name = "REACT_APP_PORT" + value = "" + } + ] + }, + { + key = "admin-api" + environment = [ + { + name = "ADMIN_PRIVATE_KEY" + value = "" + }, + { + name = "AWS_S3_BUCKET" + value = "" + }, + { + name = "AWS_S3_ACCESS_KEY" + value = "" + }, + { + name = "AWS_S3_KEY_SECRET" + value = "" + }, + { + name = "JWT_EXPIRATION_TIME" + value = "" + }, + { + name = "JWT_SECRET" + value = "" + }, + { + name = "BEHIND_PROXY" + value = "" + }, + { + name = "STORE_API" + value = "" + } + ] + }, + { + key = "store-api" + environment = [ + { + name = "JWT_EXPIRATION_TIME" + value = "" + }, + { + name = "JWT_SECRET" + value = "" + }, + { + name = "AWS_S3_BUCKET" + value = "" + }, + { + name = "AWS_S3_ACCESS_KEY" + value = "" + }, + { + name = "AWS_S3_KEY_SECRET" + value = "" + }, + { + name = "STRIPE_SECRET" + value = "" + }, + { + name = "STRIPE_WEBHOOK_SECRET" + value = "" + }, + { + name = "PINATA_API_KEY" + value = "" + }, + { + name = "PINATA_API_SECRET" + value = "" + }, + { + name = "CART_EXPIRATION_MILLI_SECS" + value = "" + }, + { + name = "ORDER_EXPIRATION_MILLI_SECS" + value = "" + }, + { + name = "MINTER_TZ_ADDRESS" + value = "" + }, + { + name = "ADMIN_PUBLIC_KEY" + value = "" + }, + { + name = "BEHIND_PROXY" + value = "" + }, + { + name = "PROFILE_PICTURES_ENABLED" + value = "" + }, + { + name = "KANVAS_CONTRACT" + value = "" + }, + { + name = "TEZOS_NETWORK" + value = "" + }, + { + name = "CACHE_TTL" + value = "" + }, + { + name = "CART_MAX_ITEMS" + value = "" + }, + { + name = "API_KEY_SECRET" + value = "" + }, + { + name = "TEZPAY_PAYPOINT_ADDRESS" + value = "" + }, + { + name = "KANVAS_API_PORT" + value = "" + } + ] + }, + { + key = "peppermint" + environment = [ + { + name = "REACT_APP_PORT" + value = "" + } + ] + }, + { + key = "admin-quepasa" + environment = [ + { + name = "REACT_APP_PORT" + value = "" + } + ] + }, + { + key = "store-quepasa" + environment = [ + { + name = "CONTRACT_SETTINGS" + value = "" + } + ] + } + ] +} diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 00000000..d2b85ba7 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,272 @@ +provider "aws" { + profile = "apeunit" + region = "eu-central-1" # Setting my region to London. Use your own region here +} + +locals { + service_map = { for v in var.services_list : v.key => v } + db_map = { for v in var.db_list : v.key => v } + environment_variables = { for v in var.service_environments : v.key => v } + image_repos = { for v in var.services_list : v.key => v if !can(length(v.image)) } +} + +resource "aws_ecr_repository" "kanvas_ecr_repos" { + for_each = local.image_repos + name = "kanvas-${each.key}" +} + + +resource "aws_ecs_cluster" "kanvas_ecs_cluster" { + name = "kanvas-ecs-cluster" # Naming the cluster +} + +data "aws_iam_policy_document" "assume_role_policy" { + statement { + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["ecs-tasks.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "ecsTaskExecutionRole" { + name = "ecsTaskExecutionRole" + assume_role_policy = data.aws_iam_policy_document.assume_role_policy.json +} + +resource "aws_iam_role_policy_attachment" "ecsTaskExecutionRole_policy" { + role = aws_iam_role.ecsTaskExecutionRole.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +} + +resource "aws_cloudwatch_log_group" "kanvas_cloudwatch_group" { + name = "kanvas" +} + +# Providing a reference to our default VPC +resource "aws_default_vpc" "default_vpc" { +} + +# Providing a reference to our default subnets +resource "aws_default_subnet" "default_subnet_a" { + availability_zone = "eu-central-1a" +} + +resource "aws_default_subnet" "default_subnet_b" { + availability_zone = "eu-central-1b" +} + +resource "aws_default_subnet" "default_subnet_c" { + availability_zone = "eu-central-1c" +} + +resource "random_string" "uddin-db-password" { + length = 32 + upper = true + numeric = true + special = false +} +resource "aws_security_group" "kanvas_db" { + vpc_id = aws_default_vpc.default_vpc.id + name = "kanvas-db" + description = "Allow all inbound for Postgres" + ingress { + from_port = 5432 + to_port = 5432 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } +} + +locals { + db_credentials = { for v in var.db_list : v.key => { "username" : "${v.key}_user", "password" : random_string.uddin-db-password.result } } +} + +resource "aws_db_instance" "kanvas_dbs" { + for_each = local.db_map + allocated_storage = 10 + db_name = "kanvas_${each.key}" + engine = each.value.engine + engine_version = each.value.version + instance_class = "db.t3.micro" + skip_final_snapshot = true + publicly_accessible = true + vpc_security_group_ids = [aws_security_group.kanvas_db.id] + username = local.db_credentials[each.key].username + password = local.db_credentials[each.key].password +} + +locals { + db_envs = { for v in var.db_list : v.key => [{ "name" : "PGHOST", "value" : aws_db_instance.kanvas_dbs[v.key].address }, { "name" : "PGPORT", "value" : aws_db_instance.kanvas_dbs[v.key].port }, { "name" : "PGUSER", "value" : local.db_credentials[v.key].username }, { "name" : "PGPASSWORD", "value" : local.db_credentials[v.key].password }, { "name" : "PGDATABASE", "value" : aws_db_instance.kanvas_dbs[v.key].db_name }, { "name" : "DATABASE_URL", "value" : "postgres://${local.db_credentials[v.key].username}:${local.db_credentials[v.key].password}@${aws_db_instance.kanvas_dbs[v.key].endpoint}/${aws_db_instance.kanvas_dbs[v.key].db_name}" }] } +} + +resource "aws_ecs_task_definition" "kanvas_ecs_task" { + for_each = local.service_map + family = "${each.key}-task" # Naming our first task + container_definitions = < 0) ? [ for v in each.value.db : [ for k, val in local.db_envs[v.type] : { "name" : "${v.prefix}${val.name}", "value" : tostring(val.value) } ] ]: []]))}, + "essential": true, + "portMappings": [ + { + "containerPort": ${each.value.port}, + "hostPort": ${each.value.port} + } + ], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "${aws_cloudwatch_log_group.kanvas_cloudwatch_group.id}", + "awslogs-region": "eu-central-1", + "awslogs-stream-prefix": "ecs" + } + }, + "entryPoint": ${jsonencode(can(length(each.value.entrypoint) > 0) ? ["bash", "-c", file(each.value.entrypoint)] : [])}, + "memory": ${each.value.memory}, + "cpu": ${each.value.cpu} + } + ] + DEFINITION + requires_compatibilities = ["FARGATE"] # Stating that we are using ECS Fargate + network_mode = "awsvpc" # Using awsvpc as our network mode as this is required for Fargate + memory = each.value.memory # Specifying the memory our container requires + cpu = each.value.cpu # Specifying the CPU our container requires + execution_role_arn = aws_iam_role.ecsTaskExecutionRole.arn +} + +resource "aws_ecs_service" "kanvas_ecs_service" { + for_each = local.service_map + name = "${each.key}-service" # Naming our first service + cluster = aws_ecs_cluster.kanvas_ecs_cluster.id # Referencing our created Cluster + task_definition = aws_ecs_task_definition.kanvas_ecs_task[each.key].arn # Referencing the task our service will spin up + launch_type = "FARGATE" + desired_count = 1 + force_new_deployment = true # Setting the number of containers to 3 + + load_balancer { + target_group_arn = aws_lb_target_group.kanvas_target_groups[each.key].arn # Referencing our target group + container_name = aws_ecs_task_definition.kanvas_ecs_task[each.key].family + container_port = each.value.port # Specifying the container port + } + + network_configuration { + subnets = ["${aws_default_subnet.default_subnet_a.id}", "${aws_default_subnet.default_subnet_b.id}", "${aws_default_subnet.default_subnet_c.id}"] + assign_public_ip = true # Providing our containers with public IPs + security_groups = ["${aws_security_group.service_security_group.id}"] # Setting the security group + } +} + +# resource "aws_lb" "application_load_balancer" { +# name = "kanvas-lb" # Naming our load balancer +# load_balancer_type = "application" +# subnets = [ # Referencing the default subnets +# "${aws_default_subnet.default_subnet_a.id}", +# "${aws_default_subnet.default_subnet_b.id}", +# "${aws_default_subnet.default_subnet_c.id}" +# ] +# # Referencing the security group +# security_groups = ["${aws_security_group.load_balancer_security_group.id}"] +# } + +resource "aws_lb" "application_load_balancers" { + for_each = local.service_map + name = "kanvas-${each.key}-lb" # Naming our load balancer + load_balancer_type = "application" + subnets = [ # Referencing the default subnets + "${aws_default_subnet.default_subnet_a.id}", + "${aws_default_subnet.default_subnet_b.id}", + "${aws_default_subnet.default_subnet_c.id}" + ] + # Referencing the security group + security_groups = ["${aws_security_group.load_balancer_security_group.id}"] +} + +# Creating a security group for the load balancer: +resource "aws_security_group" "load_balancer_security_group" { + ingress { + from_port = 80 # Allowing traffic in from port 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] # Allowing traffic in from all sources + } + + egress { + from_port = 0 # Allowing any incoming port + to_port = 0 # Allowing any outgoing port + protocol = "-1" # Allowing any outgoing protocol + cidr_blocks = ["0.0.0.0/0"] # Allowing traffic out to all IP addresses + } +} + + +resource "aws_lb_target_group" "kanvas_target_groups" { + for_each = local.service_map + name = "${each.key}-target-group" + port = 80 + protocol = "HTTP" + target_type = "ip" + vpc_id = aws_default_vpc.default_vpc.id # Referencing the default VPC + health_check { + matcher = "200,301,302" + path = "/" + } +} + +resource "aws_lb_listener" "kanvas_lb_listeners" { + for_each = local.service_map + load_balancer_arn = aws_lb.application_load_balancers[each.key].arn # Referencing our load balancer + port = "80" + protocol = "HTTP" + + # default_action { + # type = "fixed-response" + # fixed_response { + # content_type = "text/plain" + # message_body = "No routes defined" + # status_code = "200" + # } + # } + + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.kanvas_target_groups[each.key].arn # Referencing our tagrte group + } +} + + +# resource "aws_lb_listener_rule" "kanvas_lb_listener_rules" { +# for_each = local.service_map +# listener_arn = aws_lb_listener.kanvas_lb_listeners[each.key].arn + +# action { +# type = "forward" +# target_group_arn = aws_lb_target_group.kanvas_target_groups[each.key].arn +# } +# condition { +# path_pattern { +# values = ["/${each.key}*"] +# } +# } +# } + +resource "aws_security_group" "service_security_group" { + ingress { + from_port = 0 + to_port = 0 + protocol = "-1" + # Only allowing traffic in from the load balancer security group + security_groups = ["${aws_security_group.load_balancer_security_group.id}"] + } + + egress { + from_port = 0 # Allowing any incoming port + to_port = 0 # Allowing any outgoing port + protocol = "-1" # Allowing any outgoing protocol + cidr_blocks = ["0.0.0.0/0"] # Allowing traffic out to all IP addresses + } +} diff --git a/terraform/scripts/admin-api.entrypoint.sh b/terraform/scripts/admin-api.entrypoint.sh new file mode 100644 index 00000000..33217750 --- /dev/null +++ b/terraform/scripts/admin-api.entrypoint.sh @@ -0,0 +1,10 @@ + set -e -x + ./script/wait-db + INIT_QUEPASA=false ./script/migrate + if [[ "`psql -c \"select count(1) from kanvas_user\" -tA`" == "0" ]]; then + yarn seed + ./script/setup-replication-sub + fi + if [ "${ADMIN_API_ENABLED:-yes}" == "yes" ]; then + yarn run start:prod + fi \ No newline at end of file diff --git a/terraform/scripts/admin-quepasa.entrypoint.sh b/terraform/scripts/admin-quepasa.entrypoint.sh new file mode 100644 index 00000000..98bbbeab --- /dev/null +++ b/terraform/scripts/admin-quepasa.entrypoint.sh @@ -0,0 +1,5 @@ +set -e + while ! psql -d "$DATABASE_URL" -c "select 1" ; do + sleep 1 + done + /que-pasa/que-pasa --bcd-enable \ No newline at end of file diff --git a/terraform/scripts/peppermint.entrypoint.sh b/terraform/scripts/peppermint.entrypoint.sh new file mode 100644 index 00000000..45026e3a --- /dev/null +++ b/terraform/scripts/peppermint.entrypoint.sh @@ -0,0 +1,7 @@ + set -e -x + while ! psql -d "$DATABASE_URL" -c "select 1 from peppermint.operations" ; do + sleep 1 + done + + cp /config/peppermint.json config.json + node app.mjs \ No newline at end of file diff --git a/terraform/scripts/store-quepasa.entrypoint.sh b/terraform/scripts/store-quepasa.entrypoint.sh new file mode 100644 index 00000000..98bbbeab --- /dev/null +++ b/terraform/scripts/store-quepasa.entrypoint.sh @@ -0,0 +1,5 @@ +set -e + while ! psql -d "$DATABASE_URL" -c "select 1" ; do + sleep 1 + done + /que-pasa/que-pasa --bcd-enable \ No newline at end of file diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 00000000..9773a5d9 --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,121 @@ + +variable "db_list" { + type = list(object({ + key = string + engine = string + version = number + })) + default = [ + { + key = "store_db" + engine = "postgres" + version = 13 + }, + { + key = "admin_db" + engine = "postgres" + version = 13 + } + ] +} + + +variable "services_list" { + type = list(object({ + key = string + port = number + memory = number + cpu = number + image = optional(string) + db = optional(list(object({ + type = string + prefix = optional(string) + }))) + entrypoint = optional(string) + })) + default = [ + { + key = "store-api" + port = 3000 + memory = 2048 + cpu = 256 + db = [ + { + type = "store_db" + prefix = "" + } + ] + }, + { + key = "store-front" + port = 3000 + memory = 512 + cpu = 256 + }, + { + key = "admin-front" + port = 4000 + memory = 512 + cpu = 256 + }, + { + key = "admin-api" + port = 3001 + memory = 2048 + cpu = 256 + db = [ + { + type = "store_db" + prefix = "STORE_" + }, + { + type = "admin_db" + prefix = "" + } + ], + entrypoint = "./scripts/admin-api.entrypoint.sh" + }, + { + key = "peppermint" + port = 3001 + memory = 2048 + cpu = 256 + image = "ghcr.io/tzconnectberlin/peppermint:1.2" + db = [ + { + type = "store_db" + prefix = "" + } + ] + entrypoint = "./scripts/peppermint.entrypoint.sh" + }, + { + key = "admin-quepasa" + port = 3001 + memory = 2048 + cpu = 256 + image = "ghcr.io/tzconnectberlin/que-pasa:1.2.5" + db = [ + { + type = "admin_db" + prefix = "" + } + ], + entrypoint = "./scripts/peppermint.entrypoint.sh" + }, + { + key = "store-quepasa" + port = 3001 + memory = 2048 + cpu = 256 + image = "ghcr.io/tzconnectberlin/que-pasa:1.2.5" + db = [ + { + type = "store_db" + prefix = "" + } + ], + entrypoint = "./scripts/store-quepasa.entrypoint.sh" + } + ] +}