diff --git a/tests/terraform/.gitignore b/tests/terraform/.gitignore new file mode 100644 index 00000000..ea1f477c --- /dev/null +++ b/tests/terraform/.gitignore @@ -0,0 +1,2 @@ +terraform.tfstate* +.terraform diff --git a/tests/terraform/README.md b/tests/terraform/README.md new file mode 100644 index 00000000..a664586d --- /dev/null +++ b/tests/terraform/README.md @@ -0,0 +1,35 @@ +## An example of ecspresso deployment with terraform + +This example shows how to deploy an ECS service by ecspresso with terraform. + +### Prerequisites + +- [Terraform](https://www.terraform.io/) >= v1.0.0 +- [ecspresso](https://github.com/kayac/ecspresso) >= v2.0.0 + +#### Environment variables + +- `AWS_REGION` for AWS region. (e.g. `ap-northeast-1`) +- `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`, or `AWS_PROFILE` for AWS credentials. +- `AWS_SDK_LOAD_CONFIG=true` may be required if you use `AWS_PROFILE` and `~/.aws/config`. + +### Usage + +```console +$ terraform init +$ terraform apply +$ ecspresso deploy +``` + +After completing the deployment, you can access the service via ALB. + +```console +$ curl -s "http://$(terraform output -raw alb_dns_name)/" +``` + +### Cleanup + +```console +$ ecspresso delete --terminate +$ terraform destroy +``` diff --git a/tests/terraform/alb.tf b/tests/terraform/alb.tf new file mode 100644 index 00000000..94a16e07 --- /dev/null +++ b/tests/terraform/alb.tf @@ -0,0 +1,58 @@ +resource "aws_lb" "main" { + name = var.project + internal = false + load_balancer_type = "application" + security_groups = [ + aws_security_group.alb.id, + aws_security_group.default.id, + ] + subnets = [ + aws_subnet.public-a.id, + aws_subnet.public-c.id, + aws_subnet.public-d.id, + ] + tags = { + Name = var.project + } +} + +resource "aws_lb_target_group" "http" { + name = "${var.project}-http" + port = 80 + target_type = "ip" + vpc_id = aws_vpc.main.id + protocol = "HTTP" + deregistration_delay = 5 + + health_check { + path = "/" + port = "traffic-port" + protocol = "HTTP" + healthy_threshold = 2 + unhealthy_threshold = 10 + timeout = 5 + interval = 6 + } + tags = { + Name = "${var.project}-http" + } +} + +resource "aws_lb_listener" "http" { + load_balancer_arn = aws_lb.main.arn + port = 80 + protocol = "HTTP" + + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.http.arn + } + + tags = { + Name = "${var.project}-http" + } +} + +output "alb_dns_name" { + value = aws_lb.main.dns_name +} diff --git a/tests/terraform/config.tf b/tests/terraform/config.tf new file mode 100644 index 00000000..b02573cb --- /dev/null +++ b/tests/terraform/config.tf @@ -0,0 +1,27 @@ +variable "project" { + type = string + default = "ecspresso" +} + +provider "aws" { + region = "ap-northeast-1" + default_tags { + tags = { + "env" = "${var.project}" + } + } +} + +terraform { + required_version = ">= 1.4.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.65.0" + } + } +} + +data "aws_caller_identity" "current" { +} diff --git a/tests/terraform/ecs-service-def.jsonnet b/tests/terraform/ecs-service-def.jsonnet new file mode 100644 index 00000000..4d8a5b10 --- /dev/null +++ b/tests/terraform/ecs-service-def.jsonnet @@ -0,0 +1,48 @@ +{ + deploymentConfiguration: { + deploymentCircuitBreaker: { + enable: false, + rollback: false, + }, + maximumPercent: 200, + minimumHealthyPercent: 100, + }, + deploymentController: { + type: 'ECS', + }, + desiredCount: 1, + enableECSManagedTags: false, + enableExecuteCommand: true, + healthCheckGracePeriodSeconds: 0, + launchType: 'FARGATE', + loadBalancers: [ + { + containerName: 'nginx', + containerPort: 80, + targetGroupArn: '{{or (env `TARGET_GROUP_ARN` ``) (tfstate `aws_lb_target_group.http.arn`) }}', + }, + ], + networkConfiguration: { + awsvpcConfiguration: { + assignPublicIp: 'ENABLED', + securityGroups: [ + '{{or (env `SECURITY_GROUP_ID` ``) (tfstate `aws_security_group.default.id`) }}', + ], + subnets: [ + '{{or (env `SUBNET_ID_AZ_A` ``) (tfstate `aws_subnet.public-a.id`) }}', + '{{or (env `SUBNET_ID_AZ_C` ``) (tfstate `aws_subnet.public-c.id`) }}', + '{{or (env `SUBNET_ID_AZ_D` ``) (tfstate `aws_subnet.public-d.id`) }}', + ], + }, + }, + platformFamily: 'Linux', + platformVersion: 'LATEST', + propagateTags: 'SERVICE', + schedulingStrategy: 'REPLICA', + tags: [ + { + key: 'env', + value: 'ecspresso', + }, + ], +} diff --git a/tests/terraform/ecs-task-def.jsonnet b/tests/terraform/ecs-task-def.jsonnet new file mode 100644 index 00000000..9226dcf8 --- /dev/null +++ b/tests/terraform/ecs-task-def.jsonnet @@ -0,0 +1,79 @@ +{ + containerDefinitions: [ + { + cpu: 0, + essential: true, + image: 'nginx:latest', + logConfiguration: { + logDriver: 'awslogs', + options: { + 'awslogs-create-group': 'true', + 'awslogs-group': '{{tfstate `aws_cloudwatch_log_group.main.name`}}', + 'awslogs-region': '{{ must_env `AWS_REGION` }}', + 'awslogs-stream-prefix': 'nginx', + }, + }, + name: 'nginx', + portMappings: [ + { + appProtocol: '', + containerPort: 80, + hostPort: 80, + protocol: 'tcp', + }, + ], + }, + { + command: [ + 'tail', + '-f', + '/dev/null', + ], + cpu: 0, + essential: true, + image: 'debian:bullseye-slim', + logConfiguration: { + logDriver: 'awslogs', + options: { + 'awslogs-create-group': 'true', + 'awslogs-group': '{{tfstate `aws_cloudwatch_log_group.main.name`}}', + 'awslogs-region': '{{ must_env `AWS_REGION` }}', + 'awslogs-stream-prefix': 'bash', + }, + }, + name: 'bash', + secrets: [ + { + name: 'FOO', + valueFrom: '{{tfstate `aws_ssm_parameter.foo.name`}}' + }, + { + name: 'BAR', + valueFrom: '{{tfstate `aws_secretsmanager_secret.bar.arn`}}' + }, + { + name: 'JSON_KEY', + valueFrom: '{{tfstate `aws_secretsmanager_secret.json.arn`}}:key::' + }, + ], + }, + ], + cpu: '256', + ephemeralStorage: { + sizeInGiB: 30, + }, + executionRoleArn: '{{tfstate `aws_iam_role.ecs-task.arn`}}', + family: 'ecspresso', + memory: '512', + networkMode: 'awsvpc', + requiresCompatibilities: [ + 'FARGATE', + ], + tags: [ + { + key: 'env', + value: 'ecspresso', + }, + ], + taskRoleArn: '{{tfstate `aws_iam_role.ecs-task.arn`}}', +} diff --git a/tests/terraform/ecs.tf b/tests/terraform/ecs.tf new file mode 100644 index 00000000..f21db2e9 --- /dev/null +++ b/tests/terraform/ecs.tf @@ -0,0 +1,6 @@ +resource "aws_ecs_cluster" "main" { + name = var.project + tags = { + Name = var.project + } +} diff --git a/tests/terraform/ecspresso.jsonnet b/tests/terraform/ecspresso.jsonnet new file mode 100644 index 00000000..d29094d6 --- /dev/null +++ b/tests/terraform/ecspresso.jsonnet @@ -0,0 +1,16 @@ +{ + region: '{{ must_env `AWS_REGION` }}', + cluster: 'ecspresso', + service: 'ecspresso', + service_definition: 'ecs-service-def.jsonnet', + task_definition: 'ecs-task-def.jsonnet', + timeout: '10m0s', + plugins: [ + { + name: 'tfstate', + config: { + path: 'terraform.tfstate', + }, + } + ], +} diff --git a/tests/terraform/iam.tf b/tests/terraform/iam.tf new file mode 100644 index 00000000..cdf01f9b --- /dev/null +++ b/tests/terraform/iam.tf @@ -0,0 +1,71 @@ +resource "aws_iam_role" "ecs-task" { + name = "${var.project}-ecs-task" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Principal = { + Service = "ecs-tasks.amazonaws.com" + } + Effect = "Allow" + Sid = "" + } + ] + }) +} + +resource "aws_iam_policy" "ecs-task" { + name = var.project + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "logs:CreateLogStream", + "logs:PutLogEvents", + "ssm:GetParameter", + "ssm:GetParameters", + "secretsmanager:GetSecretValue", + "ssmmessages:CreateControlChannel", + "ssmmessages:CreateDataChannel", + "ssmmessages:OpenControlChannel", + "ssmmessages:OpenDataChannel", + ] + Effect = "Allow" + Resource = "*" + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "ecs-task" { + role = aws_iam_role.ecs-task.name + policy_arn = aws_iam_policy.ecs-task.arn +} + +resource "aws_iam_role" "ecs-task-execution" { + name = "${var.project}-ecs-task-execution" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Principal = { + Service = "ecs-tasks.amazonaws.com" + } + Effect = "Allow" + Sid = "" + } + ] + }) +} + +data "aws_iam_policy" "ecs-task-exection" { + arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +} + +resource "aws_iam_role_policy_attachment" "ecs-task-execution" { + role = aws_iam_role.ecs-task-execution.name + policy_arn = data.aws_iam_policy.ecs-task-exection.arn +} diff --git a/tests/terraform/logs.tf b/tests/terraform/logs.tf new file mode 100644 index 00000000..64431cad --- /dev/null +++ b/tests/terraform/logs.tf @@ -0,0 +1,4 @@ +resource "aws_cloudwatch_log_group" "main" { + name = var.project + retention_in_days = 7 +} diff --git a/tests/terraform/secrets.tf b/tests/terraform/secrets.tf new file mode 100644 index 00000000..4a5d21aa --- /dev/null +++ b/tests/terraform/secrets.tf @@ -0,0 +1,26 @@ + +resource "aws_ssm_parameter" "foo" { + name = "/${var.project}/foo" + type = "SecureString" + value = "FOO" +} + +resource "aws_secretsmanager_secret" "bar" { + name = "${var.project}-bar" +} + +resource "aws_secretsmanager_secret_version" "bar" { + secret_id = aws_secretsmanager_secret.bar.id + secret_string = "BAR" +} + +resource "aws_secretsmanager_secret" "json" { + name = "${var.project}-json" +} + +resource "aws_secretsmanager_secret_version" "json" { + secret_id = aws_secretsmanager_secret.json.id + secret_string = jsonencode({ + key = "value" + }) +} diff --git a/tests/terraform/sg.tf b/tests/terraform/sg.tf new file mode 100644 index 00000000..196349d2 --- /dev/null +++ b/tests/terraform/sg.tf @@ -0,0 +1,33 @@ +resource "aws_security_group" "default" { + name = "${var.project}-default" + vpc_id = aws_vpc.main.id + ingress { + self = true + from_port = 0 + to_port = 0 + protocol = "-1" + } + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_security_group" "alb" { + name = "${var.project}-alb" + vpc_id = aws_vpc.main.id + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} diff --git a/tests/terraform/vpc.tf b/tests/terraform/vpc.tf new file mode 100644 index 00000000..d21edf93 --- /dev/null +++ b/tests/terraform/vpc.tf @@ -0,0 +1,66 @@ +resource "aws_vpc" "main" { + cidr_block = "10.0.0.0/16" + tags = { + Name = var.project + } +} + +resource "aws_subnet" "public-a" { + vpc_id = aws_vpc.main.id + cidr_block = "10.0.1.0/24" + availability_zone = "ap-northeast-1a" + tags = { + Name = "${var.project}-public-a" + } +} + +resource "aws_subnet" "public-c" { + vpc_id = aws_vpc.main.id + cidr_block = "10.0.2.0/24" + availability_zone = "ap-northeast-1c" + tags = { + Name = "${var.project}-public-c" + } +} + +resource "aws_subnet" "public-d" { + vpc_id = aws_vpc.main.id + cidr_block = "10.0.3.0/24" + availability_zone = "ap-northeast-1d" + tags = { + Name = "${var.project}-public-d" + } +} + +resource "aws_internet_gateway" "main" { + vpc_id = aws_vpc.main.id + tags = { + Name = var.project + } +} + +resource "aws_route_table" "public" { + vpc_id = aws_vpc.main.id + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.main.id + } + tags = { + Name = "${var.project}-public" + } +} + +resource "aws_route_table_association" "public-a" { + subnet_id = aws_subnet.public-a.id + route_table_id = aws_route_table.public.id +} + +resource "aws_route_table_association" "public-c" { + subnet_id = aws_subnet.public-c.id + route_table_id = aws_route_table.public.id +} + +resource "aws_route_table_association" "public-d" { + subnet_id = aws_subnet.public-d.id + route_table_id = aws_route_table.public.id +}