diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 18c0033..adf0f76 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -32,6 +32,11 @@ jobs: - 'dev-team-account/**' stage: - 'stage-team-account/**' + security: + - 'security-team-account/**' + management: + - 'management-team-account/**' + - name: Build Matrix from Filter (with subdirs) @@ -51,18 +56,66 @@ jobs: ["prod"]="ROLE_ARN_PROD" ["dev"]="ROLE_ARN_DEV" ["stage"]="ROLE_ARN_STAGE" + ["security"]="ROLE_ARN_SECURITY" + ["management"]="ROLE_ARN_MANAGEMENT" ) - MATRIX_ITEMS=() + declare -A DEPENDENCY_MAP=( + ["prod-team-account/vpc"]="" + ["prod-team-account/iam"]="" + ["prod-team-account/acm"]="" + ["operation-team-account/ecr"]="prod-team-account/deploy/iam" + ["prod-team-account/alb"]="prod-team-account/deploy/vpc prod-team-account/deploy/acm" + ["prod-team-account/ecs"]="prod-team-account/deploy/vpc prod-team-account/deploy/iam prod-team-account/deploy/alb operation-team-account/deploy/ecr" + ["prod-team-account/codedeploy"]="prod-team-account/deploy/ecs" + ) - # 변경된 경로에 따라 matrix 구성 - for KEY in "${!ROLE_MAP[@]}"; do - VAR_NAME="FILTER_OUTPUTS_${KEY}" - VALUE="${!VAR_NAME}" - if [ "$VALUE" = "true" ]; then - BASE_DIR="${KEY}-team-account" + # Push 이벤트에 포함된 변경된 파일 목록을 호출 + echo "Comparing changes between ${{ github.event.before }} and ${{ github.event.after }}" + CHANGED_FILES=$(git diff --name-only ${{ github.event.before }} ${{ github.event.after }}) + + # 변경된 파일이 속한 서비스 폴더(backend.tf가 있는 폴더) 목록 검색 + CHANGED_DIRS=() + for file in $CHANGED_FILES; do + dir=$(dirname "$file") + while [ "$dir" != "." ]; do + if [ -f "$dir/backend.tf" ]; then + CHANGED_DIRS+=("$dir"); break; + fi; + dir=$(dirname "$dir"); + done + done + CHANGED_DIRS=($(echo "${CHANGED_DIRS[@]}" | tr ' ' '\n' | sort -u)) + + if [ ${#CHANGED_DIRS[@]} -eq 0 ]; then + echo "No terraform project directories with changes found."; echo "matrix=[]" >> $GITHUB_OUTPUT; exit 0; + fi + echo "Changed project directories: ${CHANGED_DIRS[@]}" + + # 변경된 폴더와 정의된 의존성을 기반으로 배포 순서를 결정 + TSORT_INPUT="" + ALL_DIRS_TO_CONSIDER="${CHANGED_DIRS[@]}" + for DIR in "${CHANGED_DIRS[@]}"; do + dependencies=${DEPENDENCY_MAP[$DIR]} + for DEP in $dependencies; do + TSORT_INPUT+="$DEP $DIR\n"; ALL_DIRS_TO_CONSIDER+=" $DEP"; + done + done + ALL_DIRS_TO_CONSIDER=($(echo "$ALL_DIRS_TO_CONSIDER" | tr ' ' '\n' | sort -u)) + + ORDERED_DIRS=$(echo -e "$TSORT_INPUT" | tsort 2>/dev/null || echo "$ALL_DIRS_TO_CONSIDER") + echo "Calculated execution order: $ORDERED_DIRS" + + # 실행할 최종 매트릭스를 JSON 형식으로 생성 + MATRIX_ITEMS=() + + for DIR in $ORDERED_DIRS; do + if [[ " ${CHANGED_DIRS[@]} " =~ " ${DIR} " ]]; then + ACCOUNT_PREFIX=$(echo $DIR | cut -d- -f1) + ROLE_KEY="${ROLE_MAP[$ACCOUNT_PREFIX]}" + MATRIX_ITEMS+=("{\"dir\":\"$DIR\",\"role_key\":\"$ROLE_KEY\"}") # 루트 디렉터리 검사 TF_COUNT_ROOT=$(find "$BASE_DIR" -maxdepth 1 -name '*.tf' | wc -l) @@ -79,6 +132,7 @@ jobs: fi fi done + fi done @@ -86,8 +140,7 @@ jobs: if [ ${#MATRIX_ITEMS[@]} -eq 0 ]; then echo "matrix=[]" >> $GITHUB_OUTPUT else - JSON="[$(IFS=,; echo "${MATRIX_ITEMS[*]}")]" - echo "matrix=$JSON" >> $GITHUB_OUTPUT + JSON="[$(IFS=,; echo "${MATRIX_ITEMS[*]}")]"; echo "matrix=$JSON" >> $GITHUB_OUTPUT; fi terraform-apply: @@ -96,9 +149,10 @@ jobs: runs-on: ubuntu-latest strategy: - matrix: # matrix 기반 반복 실행 + fail-fast: true + max-parallel: 1 + matrix: include: ${{ fromJson(needs.detect-changes.outputs.matrix) }} - fail-fast: false # 하나 실패해도 나머지 job은 계속 진행 steps: - name: Checkout repository diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b53d82..0ce1f12 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,5 @@ -name: Application-Deployment CI +name: Application-DeploymentCI + on: pull_request: @@ -66,6 +67,7 @@ jobs: echo "$DIR|$ROLE_KEY" >> $TMP_FILE fi fi + fi done diff --git a/.gitignore b/.gitignore index 1668f42..e95ad6e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,4 @@ *.tfstate.backup.* *.tfstate.backup.json *.tfstate.backup.json.* -.terraform.lock.hcl \ No newline at end of file +.terraform.lock.hcl diff --git a/operation-team-account/deploy/ecr/backend.tf b/operation-team-account/deploy/ecr/backend.tf new file mode 100644 index 0000000..54d2205 --- /dev/null +++ b/operation-team-account/deploy/ecr/backend.tf @@ -0,0 +1,9 @@ +terraform { + backend "s3" { + bucket = "cloudfence-operation-state" + key = "deploy/ecr.tfstate" + region = "ap-northeast-2" + dynamodb_table = "s3-operation-lock" + encrypt = true + } +} \ No newline at end of file diff --git a/operation-team-account/deploy/ecr/main.tf b/operation-team-account/deploy/ecr/main.tf new file mode 100644 index 0000000..b80ba7a --- /dev/null +++ b/operation-team-account/deploy/ecr/main.tf @@ -0,0 +1,62 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + +} + +provider "aws" { + region = "ap-northeast-2" +} + +data "terraform_remote_state" "iam" { + backend = "s3" + config = { + bucket = "cloudfence-prod-state" + key = "deploy/iam.tfstate" + region = "ap-northeast-2" + } +} + +# operation-team-account의 ECR 리포지토리 생성 및 정책 설정 +data "aws_iam_policy_document" "ecr_repo_policy_document" { + statement { + sid = "AllowCrossAccountPush" + effect = "Allow" + principals { + type = "AWS" + # prod 계정의 역할 ARN은 변수로 전달 + identifiers = [data.terraform_remote_state.iam.outputs.github_actions_role_arn] + } + actions = [ + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "ecr:BatchCheckLayerAvailability", + "ecr:PutImage", + "ecr:InitiateLayerUpload", + "ecr:UploadLayerPart", + "ecr:CompleteLayerUpload", + "ecr:GetAuthorizationToken" + ] + } +} + + +# ECR 리포지토리 생성 +resource "aws_ecr_repository" "app_ecr_repo" { + name = var.project_name + image_tag_mutability = "IMMUTABLE" + + image_scanning_configuration { + scan_on_push = true + } +} + +# 정책을 리포지토리에 연결 +resource "aws_ecr_repository_policy" "app_ecr_repo_policy" { + repository = aws_ecr_repository.app_ecr_repo.name + policy = data.aws_iam_policy_document.ecr_repo_policy_document.json +} \ No newline at end of file diff --git a/operation-team-account/deploy/ecr/outputs.tf b/operation-team-account/deploy/ecr/outputs.tf new file mode 100644 index 0000000..c2e7fe6 --- /dev/null +++ b/operation-team-account/deploy/ecr/outputs.tf @@ -0,0 +1,4 @@ +output "repository_url" { + description = "The URL of the ECR repository" + value = aws_ecr_repository.app_ecr_repo.repository_url +} \ No newline at end of file diff --git a/operation-team-account/deploy/ecr/variables.tf b/operation-team-account/deploy/ecr/variables.tf new file mode 100644 index 0000000..155f378 --- /dev/null +++ b/operation-team-account/deploy/ecr/variables.tf @@ -0,0 +1,5 @@ +variable "project_name" { + description = "The name of the project" + type = string + default = "cloudfence" +} diff --git a/prod-team-account/deploy/acm/backend.tf b/prod-team-account/deploy/acm/backend.tf new file mode 100644 index 0000000..029da1b --- /dev/null +++ b/prod-team-account/deploy/acm/backend.tf @@ -0,0 +1,9 @@ +terraform { + backend "s3" { + bucket = "cloudfence-prod-state" + key = "deploy/acm.tfstate" + region = "ap-northeast-2" + dynamodb_table = "s3-prod-lock" + encrypt = true + } +} \ No newline at end of file diff --git a/prod-team-account/deploy/acm/main.tf b/prod-team-account/deploy/acm/main.tf new file mode 100644 index 0000000..bd352b1 --- /dev/null +++ b/prod-team-account/deploy/acm/main.tf @@ -0,0 +1,44 @@ +terraform { + required_providers { + aws = { source = "hashicorp/aws", version = "~> 5.0" } + } +} + +provider "aws" { + region = "ap-northeast-2" +} + +# ACM 인증서 요청 +resource "aws_acm_certificate" "cert" { + domain_name = var.domain_name + subject_alternative_names = ["*.${var.domain_name}"] + validation_method = "DNS" + + lifecycle { + create_before_destroy = true + } +} + +# DNS 검증을 위한 Route 53 레코드 생성 +resource "aws_route53_record" "cert_validation" { + for_each = { + for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => { + name = dvo.resource_record_name + record = dvo.resource_record_value + type = dvo.resource_record_type + } + } + + allow_overwrite = true + name = each.value.name + records = [each.value.record] + ttl = 60 + type = each.value.type + zone_id = var.zone_id +} + +# DNS 검증이 완료될 때까지 대기하고 인증서 발급 완료 +resource "aws_acm_certificate_validation" "cert" { + certificate_arn = aws_acm_certificate.cert.arn + validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn] +} \ No newline at end of file diff --git a/prod-team-account/deploy/acm/outputs.tf b/prod-team-account/deploy/acm/outputs.tf new file mode 100644 index 0000000..b168a46 --- /dev/null +++ b/prod-team-account/deploy/acm/outputs.tf @@ -0,0 +1,4 @@ +output "certificate_arn" { + description = "The ARN of the validated ACM certificate" + value = aws_acm_certificate_validation.cert.certificate_arn +} \ No newline at end of file diff --git a/prod-team-account/deploy/acm/variables.tf b/prod-team-account/deploy/acm/variables.tf new file mode 100644 index 0000000..a97510b --- /dev/null +++ b/prod-team-account/deploy/acm/variables.tf @@ -0,0 +1,11 @@ +variable "domain_name" { + description = "The domain name for the SSL certificate" + type = string + default = "cloudfence.cloud" +} + +variable "zone_id" { + description = "The Route 53 Hosted Zone ID for the domain" + type = string + default = "Z0324594CRM7IYDEWX83" +} \ No newline at end of file diff --git a/prod-team-account/deploy/alb/backend.tf b/prod-team-account/deploy/alb/backend.tf new file mode 100644 index 0000000..467f717 --- /dev/null +++ b/prod-team-account/deploy/alb/backend.tf @@ -0,0 +1,9 @@ +terraform { + backend "s3" { + bucket = "cloudfence-prod-state" + key = "deploy/alb.tfstate" + region = "ap-northeast-2" + dynamodb_table = "s3-prod-lock" + encrypt = true + } +} \ No newline at end of file diff --git a/prod-team-account/deploy/alb/main.tf b/prod-team-account/deploy/alb/main.tf new file mode 100644 index 0000000..ea36832 --- /dev/null +++ b/prod-team-account/deploy/alb/main.tf @@ -0,0 +1,163 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + +} + +provider "aws" { + region = "ap-northeast-2" +} + +data "terraform_remote_state" "acm" { + backend = "s3" + config = { + bucket = "cloudfence-prod-state" + key = "deploy/acm.tfstate" + region = "ap-northeast-2" + } +} + +data "terraform_remote_state" "vpc" { + backend = "s3" + config = { + bucket = "cloudfence-prod-state" + key = "deploy/vpc.tfstate" + region = "ap-northeast-2" + } +} + +# WAF +resource "aws_wafv2_web_acl" "alb_waf" { + name = "${var.project_name}-alb-waf" + description = "WAF for ALB" + scope = "REGIONAL" + + default_action { + allow {} + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "waf-alb-metric" + sampled_requests_enabled = true + } + + rule { + name = "AWS-AWSManagedRulesCommonRuleSet" + priority = 1 + override_action { + none {} + } + statement { + managed_rule_group_statement { + vendor_name = "AWS" + name = "AWSManagedRulesCommonRuleSet" + } + } + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "AWSManagedRulesCommonRuleSet" + sampled_requests_enabled = true + } + } + + tags = { + Name = "${var.project_name}-alb-waf" + } +} + +# ALB +# 외부 사용자를 위한 로드 밸런서이므로 외부에 노출해야해서 tfsec 경고 무시 +#tfsec:ignore:aws-elb-alb-not-public +resource "aws_lb" "alb" { + name = "${var.project_name}-alb" + internal = false + load_balancer_type = "application" + security_groups = [data.terraform_remote_state.vpc.outputs.alb_security_group_id] + subnets = data.terraform_remote_state.vpc.outputs.public_subnet_ids + + + drop_invalid_header_fields = true + enable_deletion_protection = true + + tags = { + Name = "${var.project_name}-alb" + } +} + +# Target Group +resource "aws_lb_target_group" "blue" { + name = "${var.project_name}-blue-tg" + port = 80 + protocol = "HTTP" + vpc_id = data.terraform_remote_state.vpc.outputs.vpc_id + target_type = "instance" + health_check { + path = "/" + protocol = "HTTP" + interval = 30 + timeout = 5 + healthy_threshold = 2 + unhealthy_threshold = 2 + } + tags = { + Name = "${var.project_name}-blue-tg" + } +} + +resource "aws_lb_target_group" "green" { + name = "${var.project_name}-green-tg" + port = 80 + protocol = "HTTP" + vpc_id = data.terraform_remote_state.vpc.outputs.vpc_id + target_type = "instance" + health_check { + path = "/" + protocol = "HTTP" + interval = 30 + timeout = 5 + healthy_threshold = 2 + unhealthy_threshold = 2 + } + tags = { + Name = "${var.project_name}-green-tg" + } +} + +# ALB 리스너 +resource "aws_lb_listener" "https" { + load_balancer_arn = aws_lb.alb.arn + port = 443 + protocol = "HTTPS" + ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06" + certificate_arn = data.terraform_remote_state.acm.outputs.certificate_arn + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.blue.arn + } +} + +resource "aws_lb_listener" "https_redirect" { + load_balancer_arn = aws_lb.alb.arn + port = 80 + protocol = "HTTP" + default_action { + type = "redirect" + redirect { + port = "443" + protocol = "HTTPS" + status_code = "HTTP_301" + } + } +} + +# WAF와 ALB 연결 +resource "aws_wafv2_web_acl_association" "alb_association" { + resource_arn = aws_lb.alb.arn + web_acl_arn = aws_wafv2_web_acl.alb_waf.arn + depends_on = [aws_lb.alb] +} diff --git a/prod-team-account/deploy/alb/outputs.tf b/prod-team-account/deploy/alb/outputs.tf new file mode 100644 index 0000000..db59ab3 --- /dev/null +++ b/prod-team-account/deploy/alb/outputs.tf @@ -0,0 +1,24 @@ +output "dns_name" { + description = "The DNS name of the ALB" + value = aws_lb.alb.dns_name +} + +output "listener_arn" { + description = "The ARN of the ALB listener" + value = aws_lb_listener.https.arn +} + +output "blue_target_group_name" { + description = "The name of the blue target group" + value = aws_lb_target_group.blue.name +} + +output "green_target_group_name" { + description = "The name of the green target group" + value = aws_lb_target_group.green.name +} + +output "blue_target_group_arn" { + description = "The ARN of the blue target group" + value = aws_lb_target_group.blue.arn +} diff --git a/prod-team-account/deploy/alb/variables.tf b/prod-team-account/deploy/alb/variables.tf new file mode 100644 index 0000000..7c839dd --- /dev/null +++ b/prod-team-account/deploy/alb/variables.tf @@ -0,0 +1,5 @@ +variable "project_name" { + description = "The name of the project" + type = string + default = "cloudfence" +} \ No newline at end of file diff --git a/prod-team-account/deploy/codedeploy/backend.tf b/prod-team-account/deploy/codedeploy/backend.tf new file mode 100644 index 0000000..449e823 --- /dev/null +++ b/prod-team-account/deploy/codedeploy/backend.tf @@ -0,0 +1,9 @@ +terraform { + backend "s3" { + bucket = "cloudfence-prod-state" + key = "deploy/codedeploy.tfstate" + region = "ap-northeast-2" + dynamodb_table = "s3-prod-lock" + encrypt = true + } +} \ No newline at end of file diff --git a/prod-team-account/deploy/codedeploy/main.tf b/prod-team-account/deploy/codedeploy/main.tf new file mode 100644 index 0000000..b8708ca --- /dev/null +++ b/prod-team-account/deploy/codedeploy/main.tf @@ -0,0 +1,92 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + +} + +provider "aws" { + region = "ap-northeast-2" +} + +data "terraform_remote_state" "alb" { + backend = "s3" + config = { + bucket = "cloudfence-prod-state" + key = "deploy/alb.tfstate" + region = "ap-northeast-2" + } +} + +data "terraform_remote_state" "iam" { + backend = "s3" + config = { + bucket = "cloudfence-prod-state" + key = "deploy/iam.tfstate" + region = "ap-northeast-2" + } +} + +data "terraform_remote_state" "ecs" { + backend = "s3" + config = { + bucket = "cloudfence-prod-state" + key = "deploy/ecs.tfstate" + region = "ap-northeast-2" + } +} + +# CodeDeploy +resource "aws_codedeploy_app" "ecs_app" { + name = "${var.project_name}-ecs-app" + compute_platform = "ECS" +} + +resource "aws_codedeploy_deployment_group" "ecs_deployment_group" { + app_name = aws_codedeploy_app.ecs_app.name + deployment_group_name = "${var.project_name}-ecs-deployment-group" + service_role_arn = data.terraform_remote_state.iam.outputs.codedeploy_service_role_arn + + deployment_config_name = "CodeDeployDefault.ECSAllAtOnce" + + ecs_service { + cluster_name = data.terraform_remote_state.ecs.outputs.cluster_name + service_name = data.terraform_remote_state.ecs.outputs.service_name + } + + deployment_style { + deployment_type = "BLUE_GREEN" + deployment_option = "WITH_TRAFFIC_CONTROL" + } + + blue_green_deployment_config { + deployment_ready_option { + action_on_timeout = "CONTINUE_DEPLOYMENT" + } + terminate_blue_instances_on_deployment_success { + action = "TERMINATE" + termination_wait_time_in_minutes = 5 + } + } + load_balancer_info { + target_group_pair_info { + target_group { + name = data.terraform_remote_state.alb.outputs.blue_target_group_name + } + target_group { + name = data.terraform_remote_state.alb.outputs.green_target_group_name + } + prod_traffic_route { + listener_arns = [data.terraform_remote_state.alb.outputs.listener_arn] + } + } + } + auto_rollback_configuration { + enabled = true + events = ["DEPLOYMENT_FAILURE"] + } + +} \ No newline at end of file diff --git a/prod-team-account/deploy/codedeploy/outputs.tf b/prod-team-account/deploy/codedeploy/outputs.tf new file mode 100644 index 0000000..ac1099b --- /dev/null +++ b/prod-team-account/deploy/codedeploy/outputs.tf @@ -0,0 +1,9 @@ +output "application_name" { + description = "The name of the CodeDeploy application" + value = aws_codedeploy_app.ecs_app.name +} + +output "deployment_group_name" { + description = "The name of the CodeDeploy deployment group" + value = aws_codedeploy_deployment_group.ecs_deployment_group.deployment_group_name +} \ No newline at end of file diff --git a/prod-team-account/deploy/codedeploy/variables.tf b/prod-team-account/deploy/codedeploy/variables.tf new file mode 100644 index 0000000..155f378 --- /dev/null +++ b/prod-team-account/deploy/codedeploy/variables.tf @@ -0,0 +1,5 @@ +variable "project_name" { + description = "The name of the project" + type = string + default = "cloudfence" +} diff --git a/prod-team-account/deploy/ecs/backend.tf b/prod-team-account/deploy/ecs/backend.tf new file mode 100644 index 0000000..7486722 --- /dev/null +++ b/prod-team-account/deploy/ecs/backend.tf @@ -0,0 +1,9 @@ +terraform { + backend "s3" { + bucket = "cloudfence-prod-state" + key = "deploy/ecs.tfstate" + region = "ap-northeast-2" + dynamodb_table = "s3-prod-lock" + encrypt = true + } +} \ No newline at end of file diff --git a/prod-team-account/deploy/ecs/main.tf b/prod-team-account/deploy/ecs/main.tf new file mode 100644 index 0000000..f576105 --- /dev/null +++ b/prod-team-account/deploy/ecs/main.tf @@ -0,0 +1,201 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + +} + +provider "aws" { + region = "ap-northeast-2" +} + +data "terraform_remote_state" "vpc" { + backend = "s3" + config = { + bucket = "cloudfence-prod-state" + key = "deploy/vpc.tfstate" + region = "ap-northeast-2" + } +} + +data "terraform_remote_state" "alb" { + backend = "s3" + config = { + bucket = "cloudfence-prod-state" + key = "deploy/alb.tfstate" + region = "ap-northeast-2" + } +} + +data "terraform_remote_state" "iam" { + backend = "s3" + config = { + bucket = "cloudfence-prod-state" + key = "deploy/iam.tfstate" + region = "ap-northeast-2" + } +} + +data "terraform_remote_state" "ecr" { + backend = "s3" + config = { + bucket = "cloudfence-operation-state" + key = "deploy/ecr.tfstate" + region = "ap-northeast-2" + } +} + +data "aws_ami" "latest_shared_ami" { + most_recent = true + owners = [var.ami_owner_account_id] # operation-team-account의 AMI + filter { + name = "name" + values = ["WHS-CloudFence-*"] + } +} + +# ECS 클러스터 생성 +resource "aws_ecs_cluster" "ecs_cluster" { + name = "${var.project_name}-ecs-cluster" +} + +# ECS Launch Template +resource "aws_launch_template" "ecs_launch_template" { + name_prefix = "${var.project_name}-ecs-launch-template-" + image_id = data.aws_ami.latest_shared_ami.id + instance_type = "t3.micro" + + iam_instance_profile { + name = data.terraform_remote_state.iam.outputs.ecs_instance_profile_name + } + + metadata_options { + http_tokens = "required" # 토큰 기반의 IMDSv2만 허용하도록 설정 + http_endpoint = "enabled" + } + + network_interfaces { + associate_public_ip_address = false + security_groups = [data.terraform_remote_state.vpc.outputs.ecs_security_group_id] + } + + user_data = base64encode(<<-EOF + #!/bin/bash + echo ECS_CLUSTER=${aws_ecs_cluster.ecs_cluster.name} >> /etc/ecs/ecs.config + EOF + ) + + tags = { + Name = "${var.project_name}-ecs-launch-template" + } +} + +# ECS Auto Scaling Group +resource "aws_autoscaling_group" "ecs_auto_scaling_group" { + launch_template { + id = aws_launch_template.ecs_launch_template.id + version = "$Latest" + } + + min_size = 1 + max_size = 4 + desired_capacity = 2 + vpc_zone_identifier = [for subnet in data.terraform_remote_state.vpc.outputs.private_subnet_ids : subnet] + health_check_type = "EC2" + force_delete = true + protect_from_scale_in = true + + tag { + key = "ECS_Manage" + value = "${var.project_name}-ecs-auto-scaling-group" + propagate_at_launch = true + } + +} + +# ECS capacity provider +resource "aws_ecs_capacity_provider" "ecs_capacity_provider" { + name = "${var.project_name}-ecs-capacity-provider" + auto_scaling_group_provider { + auto_scaling_group_arn = aws_autoscaling_group.ecs_auto_scaling_group.arn + managed_termination_protection = "ENABLED" + managed_scaling { + status = "ENABLED" + target_capacity = 100 + } + } +} + +# Capacity provider association +resource "aws_ecs_cluster_capacity_providers" "ecs_cluster_capacity_providers" { + cluster_name = aws_ecs_cluster.ecs_cluster.name + capacity_providers = [aws_ecs_capacity_provider.ecs_capacity_provider.name] + default_capacity_provider_strategy { + capacity_provider = aws_ecs_capacity_provider.ecs_capacity_provider.name + weight = 100 + base = 1 + } +} + +# ECS Task Definition +resource "aws_ecs_task_definition" "ecs_task_definition" { + family = "${var.project_name}-ecs-task" + network_mode = "bridge" + requires_compatibilities = ["EC2"] + execution_role_arn = data.terraform_remote_state.iam.outputs.ecs_task_execution_role_arn + + container_definitions = jsonencode([ + { + name = "${var.project_name}-container" + image = "${data.terraform_remote_state.ecr.outputs.repository_url}:latest" + cpu = 256 + memory = 512 + essential = true + portMappings = [ + { + containerPort = 80 + hostPort = 80 + protocol = "tcp" + } + ] + } + ]) +} + +# ECS Service +resource "aws_ecs_service" "ecs_service" { + name = "${var.project_name}-ecs-service" + cluster = aws_ecs_cluster.ecs_cluster.id + task_definition = aws_ecs_task_definition.ecs_task_definition.arn + desired_count = 2 + + + capacity_provider_strategy { + capacity_provider = aws_ecs_capacity_provider.ecs_capacity_provider.name + weight = 100 + } + + load_balancer { + target_group_arn = data.terraform_remote_state.alb.outputs.blue_target_group_arn + container_name = "${var.project_name}-container" + container_port = 80 + } + + deployment_controller { + type = "CODE_DEPLOY" + } + + lifecycle { + ignore_changes = [task_definition, desired_count] + } + + health_check_grace_period_seconds = 60 + + tags = { + Name = "${var.project_name}-ecs-service" + } +} + \ No newline at end of file diff --git a/prod-team-account/deploy/ecs/outputs.tf b/prod-team-account/deploy/ecs/outputs.tf new file mode 100644 index 0000000..c23b71b --- /dev/null +++ b/prod-team-account/deploy/ecs/outputs.tf @@ -0,0 +1,9 @@ +output "cluster_name" { + description = "The name of the ECS cluster" + value = aws_ecs_cluster.ecs_cluster.name +} + +output "service_name" { + description = "The name of the ECS service" + value = aws_ecs_service.ecs_service.name +} \ No newline at end of file diff --git a/prod-team-account/deploy/ecs/variables.tf b/prod-team-account/deploy/ecs/variables.tf new file mode 100644 index 0000000..c765a7b --- /dev/null +++ b/prod-team-account/deploy/ecs/variables.tf @@ -0,0 +1,11 @@ +variable "project_name" { + description = "The name of the project for resource naming" + type = string + default = "cloudfence" +} + +variable "ami_owner_account_id" { + description = "The AWS Account ID of the account that owns the shared AMI" + type = string + default = "502676416967" # operation-team-account +} \ No newline at end of file diff --git a/prod-team-account/deploy/iam/backend.tf b/prod-team-account/deploy/iam/backend.tf new file mode 100644 index 0000000..4c3ee40 --- /dev/null +++ b/prod-team-account/deploy/iam/backend.tf @@ -0,0 +1,9 @@ +terraform { + backend "s3" { + bucket = "cloudfence-prod-state" + key = "deploy/iam.tfstate" + region = "ap-northeast-2" + dynamodb_table = "s3-prod-lock" + encrypt = true + } +} \ No newline at end of file diff --git a/prod-team-account/deploy/iam/main.tf b/prod-team-account/deploy/iam/main.tf new file mode 100644 index 0000000..878016c --- /dev/null +++ b/prod-team-account/deploy/iam/main.tf @@ -0,0 +1,96 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + +} + +provider "aws" { + region = "ap-northeast-2" +} + +data "aws_iam_role" "github_actions_role" { + name = "Application-Deployment-role2" +} + +# ECS 인스턴스가 사용할 IAM 역할 생성 +resource "aws_iam_role" "ecs_instance_role" { + name = "${var.project_name}-ecs-instance-role" + + # 이 역할을 EC2 인스턴스가 사용할 수 있도록 신뢰 정책 설정 + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Action = "sts:AssumeRole", + Effect = "Allow", + Principal = { + Service = "ec2.amazonaws.com" + } + } + ] + }) + + tags = { + Name = "${var.project_name}-ecs-instance-role" + } +} + +# AWS에서 관리하는 정책을 위에서 만든 역할에 연결 +resource "aws_iam_role_policy_attachment" "ecs_instance_role_attachment" { + role = aws_iam_role.ecs_instance_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role" +} + +# EC2 인스턴스에 역할을 부여하기 위한 인스턴스 프로파일 생성 +resource "aws_iam_instance_profile" "ecs_instance_profile" { + name = "${var.project_name}-ecs-instance-profile" + role = aws_iam_role.ecs_instance_role.name +} + + + +# ECS 작업 실행 역할 +resource "aws_iam_role" "ecs_task_execution_role" { + name = "${var.project_name}-ecs-task-execution-role" + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Action = "sts:AssumeRole", + Effect = "Allow", + Principal = { + Service = "ecs-tasks.amazonaws.com" + } + } + ] + }) +} +resource "aws_iam_role_policy_attachment" "ecs_task_execution_role_attachment" { + role = aws_iam_role.ecs_task_execution_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +} + +# CodeDeploy를 위한 IAM 역할 +resource "aws_iam_role" "codedeploy_role" { + name = "${var.project_name}-codedeploy-role" + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Action = "sts:AssumeRole", + Effect = "Allow", + Principal = { + Service = "codedeploy.amazonaws.com" + } + } + ] + }) +} +resource "aws_iam_role_policy_attachment" "codedeploy_role_attachment" { + role = aws_iam_role.codedeploy_role.name + policy_arn = "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS" +} \ No newline at end of file diff --git a/prod-team-account/deploy/iam/outputs.tf b/prod-team-account/deploy/iam/outputs.tf new file mode 100644 index 0000000..7f8b0bf --- /dev/null +++ b/prod-team-account/deploy/iam/outputs.tf @@ -0,0 +1,19 @@ +output "ecs_task_execution_role_arn" { + description = "ARN of the ECS Task Execution Role" + value = aws_iam_role.ecs_task_execution_role.arn +} + +output "codedeploy_service_role_arn" { + description = "The ARN of the IAM role for CodeDeploy" + value = aws_iam_role.codedeploy_role.arn +} + +output "ecs_instance_profile_name" { + description = "The name of the IAM instance profile for ECS container instances" + value = aws_iam_instance_profile.ecs_instance_profile.name +} + +output "github_actions_role_arn" { + description = "The ARN of the existing IAM role for GitHub Actions" + value = data.aws_iam_role.github_actions_role.arn +} \ No newline at end of file diff --git a/prod-team-account/deploy/iam/variables.tf b/prod-team-account/deploy/iam/variables.tf new file mode 100644 index 0000000..0704056 --- /dev/null +++ b/prod-team-account/deploy/iam/variables.tf @@ -0,0 +1,5 @@ +variable "project_name" { + description = "The name of the project for resource naming" + type = string + default = "cloudfence" +} \ No newline at end of file diff --git a/prod-team-account/deploy/vpc/backend.tf b/prod-team-account/deploy/vpc/backend.tf new file mode 100644 index 0000000..b0113bd --- /dev/null +++ b/prod-team-account/deploy/vpc/backend.tf @@ -0,0 +1,9 @@ +terraform { + backend "s3" { + bucket = "cloudfence-prod-state" + key = "deploy/vpc.tfstate" + region = "ap-northeast-2" + dynamodb_table = "s3-prod-lock" + encrypt = true + } +} \ No newline at end of file diff --git a/prod-team-account/deploy/vpc/main.tf b/prod-team-account/deploy/vpc/main.tf new file mode 100644 index 0000000..3811c80 --- /dev/null +++ b/prod-team-account/deploy/vpc/main.tf @@ -0,0 +1,185 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + +} + +provider "aws" { + region = "ap-northeast-2" +} + +# VPC +resource "aws_vpc" "vpc" { + cidr_block = var.vpc_cidr + enable_dns_support = true + enable_dns_hostnames = true + tags = { + Name = "${var.project_name}-vpc" + } +} + +# subnet(public) +# public 서브넷은 외부에서 접근 가능하도록 tfsec 경고 무시 +#tfsec:ignore:aws-ec2-no-public-ip-subnet +resource "aws_subnet" "public1" { + vpc_id = aws_vpc.vpc.id + cidr_block = "10.0.1.0/24" + availability_zone = "ap-northeast-2a" + map_public_ip_on_launch = true + tags = { + Name = "public_subnet1" + } +} + +#tfsec:ignore:aws-ec2-no-public-ip-subnet +resource "aws_subnet" "public2" { + vpc_id = aws_vpc.vpc.id + cidr_block = "10.0.2.0/24" + availability_zone = "ap-northeast-2b" + map_public_ip_on_launch = true + tags = { + Name = "public_subnet2" + } +} + +# subnet(private) + +resource "aws_subnet" "private1" { + vpc_id = aws_vpc.vpc.id + cidr_block = "10.0.101.0/24" + availability_zone = "ap-northeast-2a" + tags = { + Name = "private_subnet1" + } +} + +resource "aws_subnet" "private2" { + vpc_id = aws_vpc.vpc.id + cidr_block = "10.0.102.0/24" + availability_zone = "ap-northeast-2b" + tags = { + Name = "private_subnet2" + } +} + +# Internet Gateway +resource "aws_internet_gateway" "igw" { + vpc_id = aws_vpc.vpc.id + tags = { + Name = "${var.project_name}-igw" + } +} + +# Public Route Table +resource "aws_route_table" "public" { + vpc_id = aws_vpc.vpc.id + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.igw.id + } + tags = { + Name = "${var.project_name}-public-route-table" + } +} + +# Associate Public Subnets with Route Table +resource "aws_route_table_association" "public1" { + subnet_id = aws_subnet.public1.id + route_table_id = aws_route_table.public.id +} + +resource "aws_route_table_association" "public2" { + subnet_id = aws_subnet.public2.id + route_table_id = aws_route_table.public.id +} + +# NAT Gateway +resource "aws_eip" "nat" { + domain = "vpc" + tags = { + Name = "${var.project_name}-nat-eip" + } +} + +resource "aws_nat_gateway" "nat" { + allocation_id = aws_eip.nat.id + subnet_id = aws_subnet.public1.id + tags = { + Name = "${var.project_name}-nat-gateway" + } + depends_on = [aws_internet_gateway.igw] +} + +# Private Route Table +resource "aws_route_table" "private" { + vpc_id = aws_vpc.vpc.id + route { + cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.nat.id + } + tags = { + Name = "${var.project_name}-private-route-table" + } +} + +# Associate Private Subnets with Route Table +resource "aws_route_table_association" "private1" { + subnet_id = aws_subnet.private1.id + route_table_id = aws_route_table.private.id +} + +resource "aws_route_table_association" "private2" { + subnet_id = aws_subnet.private2.id + route_table_id = aws_route_table.private.id +} + +# security_group +# ALB를 위한 security group에서는 외부 사용자를위해 HTTPS(443) 포트만 열고 이후 tfsec 경고 무시 +#tfsec:ignore:aws-ec2-no-public-ingress-sgr +resource "aws_security_group" "alb_sg" { + name = "${var.project_name}-alb-sg" + description = "Security group for ALB" + vpc_id = aws_vpc.vpc.id + + ingress { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + description = "Allow HTTPS" + } + + egress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["10.0.0.0/16"] + } +} + +# ECS +# ECS의 security group은 ALB에서 오는 트래픽만 허용하고, 외부로의 모든 트래픽을 허용하므로 tfsec 경고 무시 +#tfsec:ignore:aws-ec2-no-public-egress-sgr +resource "aws_security_group" "ecs_sg" { + name = "${var.project_name}-ecs-sg" + description = "Security group for ECS tasks" + vpc_id = aws_vpc.vpc.id + + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + security_groups = [aws_security_group.alb_sg.id] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} diff --git a/prod-team-account/deploy/vpc/outputs.tf b/prod-team-account/deploy/vpc/outputs.tf new file mode 100644 index 0000000..0468288 --- /dev/null +++ b/prod-team-account/deploy/vpc/outputs.tf @@ -0,0 +1,32 @@ +# VPC의 ID + +output "vpc_id" { + description = "The ID of the VPC" + value = aws_vpc.vpc.id # network.tf 파일에 정의된 aws_vpc 리소스의 id 값 +} + +# Public Subnet들의 ID 목록을 출력 +output "public_subnet_ids" { + description = "A list of public subnet IDs" + # network.tf 파일에 정의된 public 서브넷 리소스들의 id를 리스트로 묶어서 출력 + value = [aws_subnet.public1.id, aws_subnet.public2.id] +} + +# Private Subnet들의 ID 목록을 출력 +output "private_subnet_ids" { + description = "A list of private subnet IDs" + value = [aws_subnet.private1.id, aws_subnet.private2.id] +} + +# ALB용 security group의 이름을 출력 +output "alb_security_group_id" { + description = "The ID of the security group for the ALB" + value = aws_security_group.alb_sg.id # security_group.tf 파일에 정의된 보안 그룹의 id +} + +# ECS용 보안 그룹의 ID를 출력 + +output "ecs_security_group_id" { + description = "The ID of the security group for the ECS tasks" + value = aws_security_group.ecs_sg.id # security_group.tf 파일에 정의된 보안 그룹의 id +} \ No newline at end of file diff --git a/prod-team-account/deploy/vpc/variables.tf b/prod-team-account/deploy/vpc/variables.tf new file mode 100644 index 0000000..083bd85 --- /dev/null +++ b/prod-team-account/deploy/vpc/variables.tf @@ -0,0 +1,17 @@ +variable "project_name" { + description = "The name of the project" + type = string + default = "cloudfence" +} + +variable "vpc_id" { + description = "The ID of the VPC where the ECS cluster will be deployed" + type = string + default = "cloudfence-vpc" +} + +variable "vpc_cidr" { + description = "The CIDR block for the VPC" + type = string + default = "10.0.0.0/16" +} \ No newline at end of file