diff --git a/.gitignore b/.gitignore index 6f6a360..d8e555d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,28 +1,19 @@ -# Terraform 관련 파일 무시 -.terraform/ # terraform init 시 생성되는 폴더 -*.tfstate # 상태 파일 (리소스 실제 정보 포함) -*.tfstate.* # 상태 파일 백업 -terraform.tfstate -terraform.tfstate.* +# Terraform 상태 및 캐시 파일 +*.tfstate +*.tfstate.* + +# Terraform 변수 파일 (민감정보 방지) *.tfvars -*.tfstate.backup -.terraform.lock.hcl -terraform.tfvars # 민감 정보 입력용 파일 -*.auto.tfvars -crash.log # Terraform 충돌 로그 -.terraform -override.tf -override.tf.json -# AWS CLI 자격 증명 -.aws/ # ~/.aws/credentials, config 등 -~/.aws/ -.aws -# 시스템 자동 생성 파일 (Windows/macOS) -.DS_Store # macOS -Thumbs.db # Windows -ehthumbs.db # Windows -*.log # 일반 로그 파일 -*.tmp # 임시 파일 -# VSCode 설정 (선택사항) -.vscode/ +# Terraform 실행 계획 파일 +*.tfplan + +# Crash log +crash.log + +# Provider 바이너리/캐시 디렉토리 +.terraform/ + +# IDE 또는 시스템 파일 +.DS_Store +Thumbs.db diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl new file mode 100644 index 0000000..97e27e1 --- /dev/null +++ b/.terraform.lock.hcl @@ -0,0 +1,43 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.3.0" + hashes = [ + "h1:yaA7q6gSIJaFEtUcrGrjuI8z2wzwBxIyJeCocYiKD2A=", + "zh:0502dc1889cca94c89bfc00b214970bffa2d81a2cdb55e05ab6192484ddb1532", + "zh:0a009c6f643410dc29fe2c07aee57e726ac86335fad84788fc7412abbd3a55be", + "zh:0ddd577e5f23dc0be23b87d62dff1f5694b88b1fbc01bdd3046b4b51cc18a00c", + "zh:1b2754cb01fa2c1a6a59c4195212f6bd4b3d1602e3f4ffb94ab609e01f2ea11a", + "zh:2bc0edb35a1411670d74e827db58ef32a07e11757fdaa17934dce5451511e55a", + "zh:703415b5c58d9232bdb686816e90525dfe96b0a374062bd8e27bec553cac5538", + "zh:8c4f1f41722aacb4b128dfb269f5b3f0aa1239a5742f22abb012f87095b2244c", + "zh:9815c0cc480acfef7c9b6b31505070bb0247a0982d98b4b6e51b1923b3a65f7e", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:b3563ce1e4c40fa139c045a1db06c3308fcf8aa9722c0a586a18bfbcedc111b5", + "zh:bbcf01aa5188416cb0f31425c2dfc3a4df41248d4dce9ebab709d416177a3011", + "zh:bc49559699e6a03ff57675172fc367db9993df74a502e0c6f273127af82990a9", + "zh:c89bbeee5db6bbe80ce152481b85a4d44b733d7c1e1a37924f36c9cde0b7ce2d", + "zh:d26793472e127a98dfa5d32a71adc4c960b573afc427604c9815bae9cda31a72", + "zh:eb8db004ccbf52b3ed8b15189c59560c233abd2c2f5ac5ee68768841c3c8e206", + ] +} + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.1.0" + hashes = [ + "h1:y9cHrgcuaZt592In6xQzz1lx7k/B9EeWrAb8K7QqOgU=", + "zh:14c35d89307988c835a7f8e26f1b83ce771e5f9b41e407f86a644c0152089ac2", + "zh:2fb9fe7a8b5afdbd3e903acb6776ef1be3f2e587fb236a8c60f11a9fa165faa8", + "zh:35808142ef850c0c60dd93dc06b95c747720ed2c40c89031781165f0c2baa2fc", + "zh:35b5dc95bc75f0b3b9c5ce54d4d7600c1ebc96fbb8dfca174536e8bf103c8cdc", + "zh:38aa27c6a6c98f1712aa5cc30011884dc4b128b4073a4a27883374bfa3ec9fac", + "zh:51fb247e3a2e88f0047cb97bb9df7c228254a3b3021c5534e4563b4007e6f882", + "zh:62b981ce491e38d892ba6364d1d0cdaadcee37cc218590e07b310b1dfa34be2d", + "zh:bc8e47efc611924a79f947ce072a9ad698f311d4a60d0b4dfff6758c912b7298", + "zh:c149508bd131765d1bc085c75a870abb314ff5a6d7f5ac1035a8892d686b6297", + "zh:d38d40783503d278b63858978d40e07ac48123a2925e1a6b47e62179c046f87a", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:fb07f708e3316615f6d218cec198504984c0ce7000b9f1eebff7516e384f4b54", + ] +} diff --git a/README.md b/README.md index 47d8243..1eeccb9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,59 @@ -# Application-Development -DevSecOps Architecture for a Virtual Enterprise – Application Development Repo +# 🛠️ Terraform 기반 AWS CI/CD 인프라 구축 + +이 프로젝트는 **Terraform IaC(Infra as Code)** 기반으로 AWS에 Jenkins 중심의 CI/CD 파이프라인 및 보안 분석 도구 환경을 자동화하는 구성을 담고 있습니다. +**모듈화 설계**로 유지보수성과 확장성을 확보하였으며, AMI 기반의 EC2 생성 방식으로 신속한 인스턴스 배포가 가능합니다. + +--- + +## 🧭 전체 구성도 + +```plaintext + + webhook + ↓ +GitHub → Jenkins(CI) → ECR → Codedeploy(ECS) + ↓ ↓(pull) + [SAST] [SCA] [DAST] + ↓ + ↓ + ↓ + s3 + ↓ + lambda → securityhub → lambda → slack + +- 인프라팀이 codedeploy 부분을 맡아주기로 해서 ecr 까지 만들었지만 분석도구 알림은 + 우리가 해야하는게 맞지않을까? + +## 디렉토리 구조 +terraform/ +├── main.tf +├── variables.tf +├── terraform.tfvars +├── outputs.tf +├── modules/ +│ ├── network/ # VPC, 서브넷, IGW 등 네트워크 구성 +│ ├── security_group/ # 공용 보안 그룹 (모든 EC2 공유 사용) +│ ├── jenkins/ # Jenkins EC2 인스턴스 +│ ├── sast/ # SonarQube 인스턴스 (SAST) +│ ├── sca/ # Dependency-Check 인스턴스 (SCA) +│ ├── dast/ # OWASP ZAP 인스턴스 (DAST) +│ └── ecr/ # Docker 이미지 저장소 + +20250719 1907부 수정 +- 기존 IaC용 iam과 keypair를 사용하던 방식에서 인스턴스 별 iam 생성 및 IaC용 keypair + 생성 후 사용하도록 변경 + +- IaC용 iam과 keypair는 테라폼 클라우드에서 갖고오므로 유지 .. + +나는 감쟈 뭐해야하지 + +20250720 1800부 lambda 만들기 시작 + +20250721 2300부 1단계 람다 및 s3 생성 완료. + +20250723 ecs, codedeploy, alb 생성 시작. subin 폴더 추가 + +20250723 1640부 +- aws상에서 route53 설정 완료 후 acm 권한도 For_jenkins에 넣어줌. iac코드 반영은 안함. +- (root/main.tf)alb_certificate_arn = var.alb_certificate_arn #이것도 자동 + 생성으로 박아야 함. \ No newline at end of file diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..0cc4092 --- /dev/null +++ b/main.tf @@ -0,0 +1,103 @@ +module "jenkins" { + source = "./modules/jenkins" + + ami_id = var.jenkins_ami_id + + subnet_id = module.network.public_subnet_ids[0] # ✅ AZ1에 생성 + vpc_id = module.network.vpc_id # 기존 사용 중이라면 유지 + + shared_key_name = module.shared_key.key_name + security_group_id = module.network.shared_security_group_id +} + +module "shared_key" { + source = "./modules/shared_key" +} + +module "network" { + source = "./modules/network" + name_prefix = "infra" +} + +module "ecr"{ + source = "./modules/ecr" + repository_name = var.ecr_repository_name +} + +module "sast" { + source = "./modules/sast" + ami_id = var.sast_ami_id + subnet_id = module.network.public_subnet_ids[0] + security_group_id = module.network.shared_security_group_id + shared_key_name = module.shared_key.key_name +} + +module "sca"{ + source = "./modules/sca" + ami_id = var.sca_ami_id + subnet_id = module.network.public_subnet_ids[0] + security_group_id = module.network.shared_security_group_id + shared_key_name = module.shared_key.key_name +} + +module "dast"{ + source = "./modules/dast" + ami_id = var.dast_ami_id + subnet_id = module.network.public_subnet_ids[0] + security_group_id = module.network.shared_security_group_id + shared_key_name = module.shared_key.key_name +} + +module "lambda_iam" { + source = "./modules/lambda/iam" + name = "lambda-common-role" + s3_bucket_arn = "arn:aws:s3:::webgoat-codedeploy-bucket" # 예시, 실제 값에 맞게 수정 +} + +module "s3" { + source = "./modules/s3" + + dast_s3_bucket_name = var.dast_s3_bucket_name + sast_s3_bucket_name = var.sast_s3_bucket_name +} + +module "lambda_sast" { + source = "./modules/lambda/sast" + function_name = "sast-s3-trigger-lambda" + iam_role_arn = module.lambda_iam.role_arn + slack_webhook_url = var.sast_slack_webhook_url + sast_s3_bucket_name = module.s3.sast_s3_bucket_name + sast_s3_filter_prefix = var.sast_s3_filter_prefix + sast_s3_bucket_arn = module.s3.sast_s3_bucket_arn +} + +module "lambda_dast" { + source = "./modules/lambda/dast" + function_name = "dast-s3-trigger-lambda" + iam_role_arn = module.lambda_iam.role_arn + slack_webhook_url = var.dast_slack_webhook_url + dast_s3_bucket_name = module.s3.dast_s3_bucket_name + dast_s3_filter_prefix = var.dast_s3_filter_prefix + dast_s3_bucket_arn = module.s3.dast_s3_bucket_arn +} + +module "alb" { + source = "./modules/alb" + vpc_id = module.network.vpc_id + public_subnet_ids = module.network.public_subnet_ids + alb_security_group_id = module.shared_security_group_id + alb_certificate_arn = var.alb_certificate_arn #이것도 자동 생성으로 박아야 함. + alb_name = var.alb_name +} + +module "codedeploy" { + source = "./modules/codedeploy" + project_name = var.project_name + + ecs_cluster_name = module.ecs.cluster_name + ecs_service = module.ecs.service_name + + listener_arn = module.alb.listener_arn + blue_target_group_name = module.alb.blue_target_group_name + green_target_group_name = module.alb.green_target_group_name +} \ No newline at end of file diff --git a/modules/alb/iam.tf b/modules/alb/iam.tf new file mode 100644 index 0000000..e69de29 diff --git a/modules/alb/main.tf b/modules/alb/main.tf new file mode 100644 index 0000000..6ad300a --- /dev/null +++ b/modules/alb/main.tf @@ -0,0 +1,88 @@ +# ALB +# 외부 사용자를 위한 로드 밸런서이므로 외부에 노출해야해서 tfsec 경고 무시 +resource "aws_lb" "main" { + name = var.alb_name + internal = false + load_balancer_type = "application" + security_groups = [var.alb_security_group_id] + subnets = var.public_subnet_ids + + #인프라와 상이함. 인프라는 true + enable_deletion_protection = false + + tabs = { + Name = val.alb_name + } +} + + +# Target Group +resource "aws_lb_target_group" "blue" { + name = "${var.alb_name}-blue" + port = 80 + protocol = "HTTP" + vpc_id = var.vpc_id + + health_check { + path = "/" + protocol = "HTTP" + matcher = "200-399" + 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.alb_name}-green" + port = 80 + protocol = "HTTP" + vpc_id = var.vpc_id + + # 우린 없음. target_type = "instance" + health_check { + path = "/" + protocol = "HTTP" + interval = 30 + timeout = 5 + healthy_threshold = 2 + unhealthy_threshold = 2 + } + tags = { + Name = "${var.alb_name}-green" + } +} + +# ALB 리스너 +resource "aws_lb_listener" "https" { + load_balancer_arn = aws_lb.main.arn + port = 443 + protocol = "HTTPS" + ssl_policy = "ELBSecurityPolicy2016-08" + certificate_arn = var.alb_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.main.arn + port = 80 + protocol = "HTTP" + + default_action { + type = "redirect" + + redirect { + port = "443" + protocol = "HTTPS" + status_code = "HTTP_301" + } + } +} \ No newline at end of file diff --git a/modules/alb/outputs.tf b/modules/alb/outputs.tf new file mode 100644 index 0000000..472c02f --- /dev/null +++ b/modules/alb/outputs.tf @@ -0,0 +1,29 @@ +output "alb_dns_name" { + description = "The DNS name of the ALB" + value = aws_lb.main.dns_name +} + +output "alb_https_listener_arn" { + description = "The ARN of the HTTPS listener" + value = aws_lb_listener.https.arn +} + +output "alb_http_listener_arn" { + description = "The ARN of the HTTP listener" + value = aws_lb_listener.http_redirect.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/modules/alb/variables.tf b/modules/alb/variables.tf new file mode 100644 index 0000000..b4b5bdf --- /dev/null +++ b/modules/alb/variables.tf @@ -0,0 +1,25 @@ +variable "vpc_id" { + description = "The VPC ID for the ALB" + type = string +} + +variable "public_subnet_ids" { + description = "List of public subnet IDs" + type = list(string) +} + +variable "alb_security_group_id" { + description = "Security group ID to associate with the ALB" + type = string +} + +variable "alb_name" { + description = "The name of the Application Load Balancer" + type = string + default = "cloudfence-alb" +} + +variable "alb_certificate_arn" { + description = "The ARN of the ACM certificate for HTTPS listener" + type = string +} diff --git a/modules/codedeploy/iam.tf b/modules/codedeploy/iam.tf new file mode 100644 index 0000000..cbff169 --- /dev/null +++ b/modules/codedeploy/iam.tf @@ -0,0 +1,21 @@ +resource "aws_iam_role" "codedeploy_role" { + name = "${var.project_name}-codedeploy-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Principal = { + Service = "codedeploy.amazonaws.com" + }, + Action = "sts:AssumeRole" + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "codedeploy_policy_attach" { + role = aws_iam_role.codedeploy_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForECS" +} diff --git a/modules/codedeploy/main.tf b/modules/codedeploy/main.tf new file mode 100644 index 0000000..a5f18f7 --- /dev/null +++ b/modules/codedeploy/main.tf @@ -0,0 +1,56 @@ +# CodeDeploy +resource "aws_codedeploy_app" "ecs_app" { + name = "${var.project_name}-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}-dg" + service_role_arn = aws_iam_role.codedeploy_role_arn + + # deployment_config_name = "CodeDeployDefault.ECSAllAtOnce" // 이거 뭐에요? + + deployment_style { + deployment_type = "BLUE_GREEN" + deployment_option = "WITH_TRAFFIC_CONTROL" + } + + blue_green_deployment_config { + terminate_blue_instances_on_deployment_success { + action = "TERMINATE" + termination_wait_time_in_minutes = 5 + } + + deployment_ready_option { + action_on_timeout = "CONTINUE_DEPLOYMENT" + } + } + + ecs_service { + cluster_name = var.ecs_cluster_name + service_name = var.ecs_service_name + } + + load_balancer_info { + target_group_pair_info { + prod_traffic_route { + listener_arns = [var.listener_arn] + } + + target_group { + name = var.blue_target_group_name + } + + target_group { + name =var.green_target_group_name + } + } + } + + auto_rollback_configuration { + enabled = true + events = ["DEPLOYMENT_FAILURE"] + } + +} \ No newline at end of file diff --git a/modules/codedeploy/outputs.tf b/modules/codedeploy/outputs.tf new file mode 100644 index 0000000..5478dee --- /dev/null +++ b/modules/codedeploy/outputs.tf @@ -0,0 +1,11 @@ +output "codedeploy_app_name" { + value = aws_codedeploy_app.ecs_app.name +} + +output "codedeploy_deploy_group" { + value = aws_codedeploy_deployment_group.ecs_deployment_group.deployment_group_name +} + +output "codedeploy_role_arn" { + value = aws_iam_role.codedeploy_role.arn +} diff --git a/modules/codedeploy/variables.tf b/modules/codedeploy/variables.tf new file mode 100644 index 0000000..48db07f --- /dev/null +++ b/modules/codedeploy/variables.tf @@ -0,0 +1,24 @@ +variable "project_name" { + +} + +variable "ecs_cluster_name" { + +} + +variable "ecs_service_name" { + +} + +variable "listener_arn" { + +} + +variable "blue_target_group_name" { + +} + +variable "green_target_group_name" { + +} + diff --git a/modules/dast/iam.tf b/modules/dast/iam.tf new file mode 100644 index 0000000..5aaa109 --- /dev/null +++ b/modules/dast/iam.tf @@ -0,0 +1,38 @@ +data "aws_iam_policy_document" "assume_ec2" { + statement { + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["ec2.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "dast" { + name = "dast-ec2-role" + assume_role_policy = data.aws_iam_policy_document.assume_ec2.json +} + +resource "aws_iam_role_policy" "dast_policy" { + name = "dast-logs" + role = aws_iam_role.dast.id + + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Action = [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + Resource = "*" + } + ] + }) +} + +resource "aws_iam_instance_profile" "dast" { + name = "dast-instance-profile" + role = aws_iam_role.dast.name +} diff --git a/modules/dast/main.tf b/modules/dast/main.tf new file mode 100644 index 0000000..c46bb7d --- /dev/null +++ b/modules/dast/main.tf @@ -0,0 +1,19 @@ +resource "aws_instance" "this" { + ami = var.ami_id + instance_type = var.instance_type + subnet_id = var.subnet_id + vpc_security_group_ids = [var.security_group_id] + key_name = var.shared_key_name + iam_instance_profile = aws_iam_instance_profile.dast.name + associate_public_ip_address = true + + root_block_device { + volume_size = 30 + volume_type = "gp2" + } + + tags = { + Name = "DAST-ZAP" + Role = "dast" + } +} \ No newline at end of file diff --git a/modules/dast/outputs.tf b/modules/dast/outputs.tf new file mode 100644 index 0000000..0db3ca4 --- /dev/null +++ b/modules/dast/outputs.tf @@ -0,0 +1,3 @@ +output "public_ip" { + value = aws_instance.this.public_ip +} diff --git a/modules/dast/variables.tf b/modules/dast/variables.tf new file mode 100644 index 0000000..e171198 --- /dev/null +++ b/modules/dast/variables.tf @@ -0,0 +1,22 @@ +variable "ami_id" { + description = "AMI ID for DAST EC2" + type = string +} + +variable "instance_type" { + description = "EC2 Instance Type" + type = string + default = "t3.large" +} + +variable "subnet_id" { + type = string +} + +variable "security_group_id" { + type = string +} + +variable "shared_key_name" { + type = string +} diff --git a/modules/ecr/main.tf b/modules/ecr/main.tf new file mode 100644 index 0000000..266f739 --- /dev/null +++ b/modules/ecr/main.tf @@ -0,0 +1,12 @@ +resource "aws_ecr_repository" "this" { + name = var.repository_name + image_tag_mutability = "MUTABLE" + + image_scanning_configuration{ + scan_on_push = true + } + + tags = { + Name = var.repository_name + } +} \ No newline at end of file diff --git a/modules/ecr/outputs.tf b/modules/ecr/outputs.tf new file mode 100644 index 0000000..a418774 --- /dev/null +++ b/modules/ecr/outputs.tf @@ -0,0 +1,4 @@ +output "repository_url"{ + description = "ECR Repository URL for Docker Push" + value = aws_ecr_repository.this.repository_url +} diff --git a/modules/ecr/variables.tf b/modules/ecr/variables.tf new file mode 100644 index 0000000..0acb2df --- /dev/null +++ b/modules/ecr/variables.tf @@ -0,0 +1,4 @@ +variable "repository_name"{ + description = "ECR 리포지토리 이름" + type = string +} \ No newline at end of file diff --git a/modules/ecs/main.tf b/modules/ecs/main.tf new file mode 100644 index 0000000..1af0126 --- /dev/null +++ b/modules/ecs/main.tf @@ -0,0 +1,141 @@ +# 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/modules/ecs/outputs.tf b/modules/ecs/outputs.tf new file mode 100644 index 0000000..c23b71b --- /dev/null +++ b/modules/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/modules/ecs/variables.tf b/modules/ecs/variables.tf new file mode 100644 index 0000000..c765a7b --- /dev/null +++ b/modules/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/modules/jenkins/iam.tf b/modules/jenkins/iam.tf new file mode 100644 index 0000000..962f9cd --- /dev/null +++ b/modules/jenkins/iam.tf @@ -0,0 +1,50 @@ +data "aws_iam_policy_document" "assume_ec2" { + statement { + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["ec2.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "jenkins" { + name = "jenkins-ec2-role" + assume_role_policy = data.aws_iam_policy_document.assume_ec2.json +} + +resource "aws_iam_role_policy" "jenkins_policy" { + name = "jenkins-minimal-policy" + role = aws_iam_role.jenkins.id + + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Action = [ + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:PutImage", + "ecr:InitiateLayerUpload", + "ecr:UploadLayerPart", + "ecr:CompleteLayerUpload" + ], + Resource = "*" + }, + { + Effect = "Allow", + Action = [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + Resource = "*" + } + ] + }) +} + +resource "aws_iam_instance_profile" "jenkins" { + name = "jenkins-instance-profile" + role = aws_iam_role.jenkins.name +} diff --git a/modules/jenkins/main.tf b/modules/jenkins/main.tf new file mode 100644 index 0000000..bab2d24 --- /dev/null +++ b/modules/jenkins/main.tf @@ -0,0 +1,13 @@ +resource "aws_instance" "jenkins" { + ami = var.ami_id + instance_type = var.instance_type + subnet_id = var.subnet_id + vpc_security_group_ids = [var.security_group_id] + key_name = var.shared_key_name + iam_instance_profile = aws_iam_instance_profile.jenkins.name + associate_public_ip_address = true + + tags = { + Name = "Jenkins-EC2" + } +} diff --git a/modules/jenkins/outputs.tf b/modules/jenkins/outputs.tf new file mode 100644 index 0000000..bb2fe50 --- /dev/null +++ b/modules/jenkins/outputs.tf @@ -0,0 +1,7 @@ +output "jenkins_ip" { + value = aws_instance.jenkins.public_ip +} + +output "jenkins_id" { + value = aws_instance.jenkins.id +} diff --git a/modules/jenkins/second.pem b/modules/jenkins/second.pem new file mode 100644 index 0000000..8dadc7d --- /dev/null +++ b/modules/jenkins/second.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAzRdgRwxqswmvTIRWsU/hwKY25jDHFW5YAx+r5BKnlvjmJKcx +5lvRSn5Bate4BLYIpJ6rM1t9QHEUTFi5IeJRAMA9Blt2/WT8cowAir/NT3Sr2t8t +WTS4q3aRw9UCYxITYVmRN5ddnTH6R+HJlJirso4zAkiR4jwojTY8Ce+Mi35BZS4P +I/w7qrFon7NakH2fh5Y1yczhEi3eFYdK6MeiVQRqI+Y+xxWamZ9VVxAfr3O0DnVH +15CFtZlFsubTqWLZQx+1sZCb3vDwSZU8NlYBlRpcnjM7f/eudN3JrybWCpjg2wfi +4uyubslPeGHKjUANuQXgIcGRu0bDNWX65YshRQIDAQABAoIBABndTdYH+djUatgi +hHBeG6FZFOlY03KfdNgIAXyE4b/HafbLYx6jC7TyUTuxMtLdRh/EpuCWNCiRWjb6 +pXZhfUez9r47gLkaKEAl6deVssXHd/jlVmm+nV96V42z6lYmLG2FhyvUmgi+CctQ +7UQhMN4W0tYB4uv+HDsa0N2L6VSaAHjd9ziWRbII68eWs0gDIkyinbQKWSmau+zu +r88nkqEWL8t4XmQs3gJr2YKVImYf7HaYPAQaK1VnehVcXXV17o6Lrg5twfRyySDg +oLR+9GETRQ+6qXCDyaLn0nsSG3twcm+UZ7mawNHVe3tXnbRr4DYLciAqyHypdJUp +hmZWdIECgYEA/hNWBCi6KYOe51lv9jWo9301VqRXXIDhqta7d7LTbA+P7axGRqGN +lIzL0uJm+RLiT3HyzPFDByxj+vK8nwH+nTyZROk/LoUi9SB6evve3JBiBZ+8RylC +IzAFY5xjaySRAEFRLmRz7TlywlWk/x6Z8U2UEGvTdAP+cvHzT5rDDyECgYEAzqUO +tZGu93l5l5dGvcEv1Yxo6hc9iqPSxActzcOl6pzGwbxZ4SWlsOTH/Uj/mYxL7l4b +QX6QqdHhBxPXukAE7U5I2HrjjPAZMvSEqvRJ9RnLEyk3o23e4715i16J2cyqTw2H +xSBzaf+WwCfAknBdjOOCp+vL9mVrhfv05babQaUCgYBkXvsiFXzFnauOtXRXjYc8 +jggePDoO3xNHTCEu/kQrclJnkCELEhM+VgjHPI11ZBJnVBqY/8587PpqTq5ZGo6d +Sy05XfOJyyquL7BzGUFHXPp8Qkg8zH3GLNhUK5nS39Uwhp0teJ2bX5CIWREff9VM +0FrnydD1CgbhHdgC4J6iQQKBgDdud1+lULX50/AiGEvWgqpaG9qPmWaTQ3pqIpNL +pDonC/n0OHf4zVWCSVNcPZRG2id6/vy2or+rGR346KmBetDdaxUHAftQfLH6fYwO +M7iXzq25JL+mPWAB95S3K2tNR3IlQwJSDiOk+B9bioC9u5qLfQTmb7QMyKcMS0lD +jqoxAoGBAKBO+yaTKoJxMIdjbFd5JpP89pkbM/GczxC3NvwmXdM3wGKRObUBPPCp +bofP8wIx4pOMmApl63IiK9NpN3fNvw+E7esu+9QwdlD7DkYxn5pi4wLpcT/jykZe +cRU46kTWmbSt7CkjRcJjU+I4MVyNargBZbD8wHHPUGqpK7FHenMZ +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/modules/jenkins/variables.tf b/modules/jenkins/variables.tf new file mode 100644 index 0000000..53a8347 --- /dev/null +++ b/modules/jenkins/variables.tf @@ -0,0 +1,28 @@ +variable "ami_id" { + description = "ami-043ce2e5b2d8eb73a" //jenkins 백업 ami파일. + type = string +} + +variable "instance_type" { + description = "EC2 instance type for jenkins" + type = string + default = "t3.medium" +} + +variable "subnet_id" { + description = "EC2를 배치할 Subnet ID" + type = string +} + +variable "security_group_id" { + type = string +} + +variable "vpc_id" { + type = string +} + +variable "shared_key_name" { + description = "Name of the shared EC2 key pair" + type = string +} diff --git a/modules/lambda/dast/index.zip b/modules/lambda/dast/index.zip new file mode 100644 index 0000000..3b08bbc Binary files /dev/null and b/modules/lambda/dast/index.zip differ diff --git a/modules/lambda/dast/main.tf b/modules/lambda/dast/main.tf new file mode 100644 index 0000000..118e48b --- /dev/null +++ b/modules/lambda/dast/main.tf @@ -0,0 +1,36 @@ +resource "aws_lambda_function" "dast_lambda" { + function_name = var.function_name + role = var.iam_role_arn + handler = "index.lambda_handler" + runtime = "python3.11" + timeout = 30 + + filename = "${path.module}/index.zip" + source_code_hash = filebase64sha256("${path.module}/index.zip") + + environment { + variables = { + SLACK_URL = var.slack_webhook_url + } + } +} + +resource "aws_lambda_permission" "allow_s3" { + statement_id = "AllowExecutionFromS3" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.dast_lambda.function_name + principal = "s3.amazonaws.com" + source_arn = var.dast_s3_bucket_arn +} + +resource "aws_s3_bucket_notification" "dast_notification" { + bucket = var.dast_s3_bucket_name + + lambda_function { + lambda_function_arn = aws_lambda_function.dast_lambda.arn + events = ["s3:ObjectCreated:*"] + filter_prefix = var.dast_s3_filter_prefix + } + + depends_on = [aws_lambda_permission.allow_s3] +} diff --git a/modules/lambda/dast/outputs.tf b/modules/lambda/dast/outputs.tf new file mode 100644 index 0000000..3c68ab8 --- /dev/null +++ b/modules/lambda/dast/outputs.tf @@ -0,0 +1,3 @@ +output "dast_lambda_arn" { + value = aws_lambda_function.dast_lambda.arn +} diff --git a/modules/lambda/dast/variables.tf b/modules/lambda/dast/variables.tf new file mode 100644 index 0000000..485dd89 --- /dev/null +++ b/modules/lambda/dast/variables.tf @@ -0,0 +1,29 @@ +variable "function_name" { + type = string + description = "DAST용 Lambda 함수 이름" +} + +variable "iam_role_arn" { + type = string + description = "Lambda에 연결할 IAM 역할 ARN" +} + +variable "slack_webhook_url" { + type = string + description = "DAST용 Slack Webhook URL" +} + +variable "dast_s3_bucket_name" { + type = string + description = "DAST 결과가 저장될 S3 버킷 이름" +} + +variable "dast_s3_filter_prefix" { + type = string + description = "DAST 결과가 저장될 S3 경로 prefix" +} + +variable "dast_s3_bucket_arn" { + type = string + description = "DAST 결과 S3의 ARN" +} diff --git a/modules/lambda/iam/main.tf b/modules/lambda/iam/main.tf new file mode 100644 index 0000000..0e76ffc --- /dev/null +++ b/modules/lambda/iam/main.tf @@ -0,0 +1,54 @@ +resource "aws_iam_role" "lambda_role" { + name = var.name + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [{ + Effect = "Allow", + Principal = { + Service = "lambda.amazonaws.com" + }, + Action = "sts:AssumeRole" + }] + }) +} + +resource "aws_iam_role_policy_attachment" "lambda_basic_execution" { + role = aws_iam_role.lambda_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" +} + +resource "aws_iam_policy" "custom_policy" { + name = "${var.name}-custom-policy" + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Action = [ + "s3:GetObject" + ], + Resource = "${var.s3_bucket_arn}/*" + }, + { + Effect = "Allow", + Action = [ + "securityhub:BatchImportFindings" + ], + Resource = "*" + }, + { + "Effect": "Allow", + "Action": [ + "logs:*" + ], + "Resource": "*" + } + ] + }) +} + +resource "aws_iam_policy_attachment" "custom_policy_attach" { + name = "${var.name}-custom-attach" + policy_arn = aws_iam_policy.custom_policy.arn + roles = [aws_iam_role.lambda_role.name] +} diff --git a/modules/lambda/iam/outputs.tf b/modules/lambda/iam/outputs.tf new file mode 100644 index 0000000..4af6e2c --- /dev/null +++ b/modules/lambda/iam/outputs.tf @@ -0,0 +1,4 @@ +output "role_arn" { + description = "람다에 연결할 IAM 역할의 ARN" + value = aws_iam_role.lambda_role.arn +} diff --git a/modules/lambda/iam/variables.tf b/modules/lambda/iam/variables.tf new file mode 100644 index 0000000..d822536 --- /dev/null +++ b/modules/lambda/iam/variables.tf @@ -0,0 +1,9 @@ +variable "name" { + description = "IAM Role 이름" + type = string +} + +variable "s3_bucket_arn" { + description = "S3 버킷 ARN" + type = string +} diff --git a/modules/lambda/sast/index.zip b/modules/lambda/sast/index.zip new file mode 100644 index 0000000..be7f647 Binary files /dev/null and b/modules/lambda/sast/index.zip differ diff --git a/modules/lambda/sast/main.tf b/modules/lambda/sast/main.tf new file mode 100644 index 0000000..e467ca3 --- /dev/null +++ b/modules/lambda/sast/main.tf @@ -0,0 +1,36 @@ +resource "aws_lambda_function" "sast_lambda" { + function_name = var.function_name + role = var.iam_role_arn + handler = "index.lambda_handler" + runtime = "python3.11" + timeout = 30 + + filename = "${path.module}/index.zip" + source_code_hash = filebase64sha256("${path.module}/index.zip") + + environment { + variables = { + SLACK_URL = var.slack_webhook_url + } + } +} + +resource "aws_lambda_permission" "allow_s3" { + statement_id = "AllowExecutionFromS3" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.sast_lambda.function_name + principal = "s3.amazonaws.com" + source_arn = var.sast_s3_bucket_arn +} + +resource "aws_s3_bucket_notification" "sast_notification" { + bucket = var.sast_s3_bucket_name + + lambda_function { + lambda_function_arn = aws_lambda_function.sast_lambda.arn + events = ["s3:ObjectCreated:*"] + filter_prefix = var.sast_s3_filter_prefix + } + + depends_on = [aws_lambda_permission.allow_s3] +} diff --git a/modules/lambda/sast/outputs.tf b/modules/lambda/sast/outputs.tf new file mode 100644 index 0000000..ca24ed4 --- /dev/null +++ b/modules/lambda/sast/outputs.tf @@ -0,0 +1,3 @@ +output "sast_lambda_arn" { + value = aws_lambda_function.sast_lambda.arn +} diff --git a/modules/lambda/sast/variables.tf b/modules/lambda/sast/variables.tf new file mode 100644 index 0000000..8123987 --- /dev/null +++ b/modules/lambda/sast/variables.tf @@ -0,0 +1,29 @@ +variable "function_name" { + type = string + description = "SAST용 Lambda 이름" +} + +variable "iam_role_arn" { + type = string + description = "IAM 역할 ARN" +} + +variable "slack_webhook_url" { + type = string + description = "Slack Webhook URL" +} + +variable "sast_s3_bucket_name" { + type = string + description = "S3 버킷 이름" +} + +variable "sast_s3_filter_prefix" { + type = string + description = "S3 키 접두어" +} + +variable "sast_s3_bucket_arn" { + type = string + description = "S3 버킷 ARN" +} diff --git a/modules/network/main.tf b/modules/network/main.tf new file mode 100644 index 0000000..90d1bcb --- /dev/null +++ b/modules/network/main.tf @@ -0,0 +1,71 @@ +# VPC 생성 +resource "aws_vpc" "main" { + cidr_block = "10.0.0.0/16" + enable_dns_support = true + enable_dns_hostnames = true + + tags = { + Name = "${var.name_prefix}-vpc" + } +} + +# 인터넷 게이트웨이 (IGW) +resource "aws_internet_gateway" "igw" { + vpc_id = aws_vpc.main.id + + tags = { + Name = "${var.name_prefix}-igw" + } +} + +# Public Subnet A (AZ 1) +resource "aws_subnet" "public_a" { + vpc_id = aws_vpc.main.id + cidr_block = "10.0.1.0/24" + availability_zone = data.aws_availability_zones.available.names[0] + map_public_ip_on_launch = true + + tags = { + Name = "${var.name_prefix}-subnet-a" + } +} + +# Public Subnet B (AZ 2) +resource "aws_subnet" "public_b" { + vpc_id = aws_vpc.main.id + cidr_block = "10.0.2.0/24" + availability_zone = data.aws_availability_zones.available.names[1] + map_public_ip_on_launch = true + + tags = { + Name = "${var.name_prefix}-subnet-b" + } +} + +# Route table (public 인터넷 라우팅용) +resource "aws_route_table" "public" { + vpc_id = aws_vpc.main.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.igw.id + } + + tags = { + Name = "${var.name_prefix}-public-rt" + } +} + +# Subnet → Route table 연결 +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_b" { + subnet_id = aws_subnet.public_b.id + route_table_id = aws_route_table.public.id +} + +# 가용영역 정보 +data "aws_availability_zones" "available" {} diff --git a/modules/network/outputs.tf b/modules/network/outputs.tf new file mode 100644 index 0000000..ced0840 --- /dev/null +++ b/modules/network/outputs.tf @@ -0,0 +1,15 @@ +output "vpc_id" { + value = aws_vpc.main.id +} + +output "public_subnet_ids" { + description = "PUblic subnet ID 목록" + value = [ + aws_subnet.public_a.id, + aws_subnet.public_b.id + ] +} + +output "shared_security_group_id" { + value = aws_security_group.shared_sg.id +} \ No newline at end of file diff --git a/modules/network/security_group.tf b/modules/network/security_group.tf new file mode 100644 index 0000000..9e33d53 --- /dev/null +++ b/modules/network/security_group.tf @@ -0,0 +1,56 @@ +resource "aws_security_group" "shared_sg" { + name = "${var.name_prefix}-shared-sg" + description = "Shared SG for Jenkins, ECS, ALB, etc" + vpc_id = aws_vpc.main.id + + ingress { + description = "SSH" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + description = "Jenkins Web" + from_port = 8080 + to_port = 8080 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + description = "HTTP" + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + description = "HTTPS" + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + description = "SCA" + from_port = 9000 + to_port = 9000 + 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"] + } + + tags = { + Name = "${var.name_prefix}-shared-sg" + } +} diff --git a/modules/network/variables.tf b/modules/network/variables.tf new file mode 100644 index 0000000..5187594 --- /dev/null +++ b/modules/network/variables.tf @@ -0,0 +1,4 @@ +variable "name_prefix" { + type = string + description = "jenkins" //vpc와 모든 리소스 앞에 붙는 이름 +} diff --git a/modules/s3/main.tf b/modules/s3/main.tf new file mode 100644 index 0000000..74532ab --- /dev/null +++ b/modules/s3/main.tf @@ -0,0 +1,9 @@ +resource "aws_s3_bucket" "dast_s3_bucket" { + bucket = var.dast_s3_bucket_name + force_destroy = true +} + +resource "aws_s3_bucket" "sast_s3_bucket" { + bucket = var.sast_s3_bucket_name + force_destroy = true +} diff --git a/modules/s3/outputs.tf b/modules/s3/outputs.tf new file mode 100644 index 0000000..5813b19 --- /dev/null +++ b/modules/s3/outputs.tf @@ -0,0 +1,15 @@ +output "dast_s3_bucket_name" { + value = aws_s3_bucket.dast_s3_bucket.id +} + +output "dast_s3_bucket_arn" { + value = aws_s3_bucket.dast_s3_bucket.arn +} + +output "sast_s3_bucket_name" { + value = aws_s3_bucket.sast_s3_bucket.id +} + +output "sast_s3_bucket_arn" { + value = aws_s3_bucket.sast_s3_bucket.arn +} diff --git a/modules/s3/variables.tf b/modules/s3/variables.tf new file mode 100644 index 0000000..a22f230 --- /dev/null +++ b/modules/s3/variables.tf @@ -0,0 +1,7 @@ +variable "dast_s3_bucket_name" { + type = string +} + +variable "sast_s3_bucket_name" { + type = string +} diff --git a/modules/sast/iam.tf b/modules/sast/iam.tf new file mode 100644 index 0000000..94dac34 --- /dev/null +++ b/modules/sast/iam.tf @@ -0,0 +1,38 @@ +data "aws_iam_policy_document" "assume_ec2" { + statement { + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["ec2.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "sast" { + name = "sast-ec2-role" + assume_role_policy = data.aws_iam_policy_document.assume_ec2.json +} + +resource "aws_iam_role_policy" "sast_policy" { + name = "sast-sonarqube-policy" + role = aws_iam_role.sast.id + + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Action = [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + Resource = "*" + } + ] + }) +} + +resource "aws_iam_instance_profile" "sast" { + name = "sast-instance-profile" + role = aws_iam_role.sast.name +} diff --git a/modules/sast/main.tf b/modules/sast/main.tf new file mode 100644 index 0000000..f66d0ec --- /dev/null +++ b/modules/sast/main.tf @@ -0,0 +1,18 @@ +resource "aws_instance" "sast" { + ami = var.ami_id + instance_type = var.instance_type + subnet_id = var.subnet_id + vpc_security_group_ids = [var.security_group_id] + key_name = var.shared_key_name + iam_instance_profile = aws_iam_instance_profile.sast.name + associate_public_ip_address = true + + root_block_device { + volume_size = 30 + volume_type = "gp2" + } + + tags = { + Name = "sast-ec2" + } +} diff --git a/modules/sast/outputs.tf b/modules/sast/outputs.tf new file mode 100644 index 0000000..3656afb --- /dev/null +++ b/modules/sast/outputs.tf @@ -0,0 +1,3 @@ +output "public_ip" { + value = aws_instance.sast.public_ip +} \ No newline at end of file diff --git a/modules/sast/variables.tf b/modules/sast/variables.tf new file mode 100644 index 0000000..35f7a8b --- /dev/null +++ b/modules/sast/variables.tf @@ -0,0 +1,25 @@ +variable "ami_id" { + description = "AMI ID for SAST EC2" + type = string +} + +variable "instance_type" { + description = "EC2 Instance Type" + type = string + default = "t3.medium" +} + +variable "subnet_id" { + description = "Subnet ID" + type = string +} + +variable "security_group_id" { + description = "Security Group ID" + type = string +} + +variable "shared_key_name" { + description = "Name of the shared key pair" + type = string +} diff --git a/modules/sca/iam.tf b/modules/sca/iam.tf new file mode 100644 index 0000000..bad2c56 --- /dev/null +++ b/modules/sca/iam.tf @@ -0,0 +1,38 @@ +data "aws_iam_policy_document" "assume_ec2" { + statement { + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["ec2.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "sca" { + name = "sca-ec2-role" + assume_role_policy = data.aws_iam_policy_document.assume_ec2.json +} + +resource "aws_iam_role_policy" "sca_policy" { + name = "sca-basic-logs" + role = aws_iam_role.sca.id + + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Action = [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + Resource = "*" + } + ] + }) +} + +resource "aws_iam_instance_profile" "sca" { + name = "sca-instance-profile" + role = aws_iam_role.sca.name +} diff --git a/modules/sca/main.tf b/modules/sca/main.tf new file mode 100644 index 0000000..8604795 --- /dev/null +++ b/modules/sca/main.tf @@ -0,0 +1,19 @@ +resource "aws_instance" "this" { + ami = var.ami_id + instance_type = var.instance_type + subnet_id = var.subnet_id + vpc_security_group_ids = [var.security_group_id] + key_name = var.shared_key_name + iam_instance_profile = aws_iam_instance_profile.sca.name + associate_public_ip_address = true + + root_block_device { + volume_size = 50 + volume_type = "gp2" + } + + tags = { + Name = "SCA-DependencyTrack" + Role = "sca" + } +} diff --git a/modules/sca/outputs.tf b/modules/sca/outputs.tf new file mode 100644 index 0000000..0db3ca4 --- /dev/null +++ b/modules/sca/outputs.tf @@ -0,0 +1,3 @@ +output "public_ip" { + value = aws_instance.this.public_ip +} diff --git a/modules/sca/variables.tf b/modules/sca/variables.tf new file mode 100644 index 0000000..f38fa20 --- /dev/null +++ b/modules/sca/variables.tf @@ -0,0 +1,25 @@ +variable "ami_id" { + description = "AMI ID for SCA EC2" + type = string +} + +variable "instance_type" { + description = "EC2 Instance Type" + type = string + default = "t3.large" +} + +variable "subnet_id" { + description = "Subnet ID" + type = string +} + +variable "security_group_id" { + description = "Security Group ID" + type = string +} + +variable "shared_key_name" { + description = "Shared key pair name" + type = string +} diff --git a/modules/shared_key/main.tf b/modules/shared_key/main.tf new file mode 100644 index 0000000..e767746 --- /dev/null +++ b/modules/shared_key/main.tf @@ -0,0 +1,12 @@ +# modules/shared_key/main.tf +resource "tls_private_key" "ec2" { + algorithm = "RSA" + rsa_bits = 4096 +} + +resource "aws_key_pair" "ec2" { + key_name = "shared-ec2-key" + public_key = tls_private_key.ec2.public_key_openssh +} + + diff --git a/modules/shared_key/outputs.tf b/modules/shared_key/outputs.tf new file mode 100644 index 0000000..856919b --- /dev/null +++ b/modules/shared_key/outputs.tf @@ -0,0 +1,8 @@ +output "key_name" { + value = aws_key_pair.ec2.key_name +} + +output "private_key_pem" { + value = tls_private_key.ec2.private_key_pem + sensitive = true +} \ No newline at end of file diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..47f058e --- /dev/null +++ b/outputs.tf @@ -0,0 +1,32 @@ +output "vpc_id" { + value = module.network.vpc_id +} + +output "public_subnet_ids" { + value = module.network.public_subnet_ids +} + +output "ecr_repository_url" { + description = "최종 생성된 ECR 리포지토리 URL" + value = module.ecr.repository_url +} + +output "sast_ip"{ + value = module.sast.public_ip +} + +output "sca_ip"{ + value = module.sca.public_ip +} + +output "dast_ip"{ + value = module.dast.public_ip +} + +output "jenkins_ip" { + value = module.jenkins.jenkins_ip +} + +output "key_name" { + value = module.shared_key.key_name +} \ No newline at end of file diff --git a/providers.tf b/providers.tf new file mode 100644 index 0000000..5ff54f0 --- /dev/null +++ b/providers.tf @@ -0,0 +1,3 @@ +provider "aws" { + region = var.region +} \ No newline at end of file diff --git a/soobin/gateway.tf b/soobin/gateway.tf new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/soobin/gateway.tf @@ -0,0 +1 @@ + diff --git a/soobin/main.tf b/soobin/main.tf new file mode 100644 index 0000000..e69de29 diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..bcb50d1 --- /dev/null +++ b/variables.tf @@ -0,0 +1,108 @@ +# ami 이미지 설정 +variable "jenkins_ami_id" { + description = "ami-043ce2e5b2d8eb73a" + type = string +} + +variable "sast_ami_id"{ + description = "AMI ID for SAST" + type = string +} + +variable "sca_ami_id"{ + description = "AMI ID for SCA" + type = string +} + +variable "dast_ami_id"{ + description = "AMI ID for DAST" + type = string +} + +variable "jenkins_iam_instance_profile" { + type = string +} + +# 네트워크 +variable "vpc_id" { + type = string +} + +variable "public_subnet_id" { + type = string +} + +# 지역 +variable "region" { + description = "AWS region" + type = string +} + +# ecr 저장소 +variable "ecr_repository_name"{ + description = "ECR에 생성할 리포지토리 이름" + type = string +} + +# 🔸 SAST 관련 변수 +variable "sast_slack_webhook_url" { + type = string +} + +variable "sast_s3_bucket_name" { + type = string +} + +variable "sast_s3_filter_prefix" { + type = string +} + +variable "sast_s3_bucket_arn" { + type = string +} + +# 🔸 DAST 관련 변수 +variable "dast_slack_webhook_url" { + type = string +} + +variable "dast_s3_bucket_name" { + type = string +} + +variable "dast_s3_filter_prefix" { + type = string +} + +variable "dast_s3_bucket_arn" { + type = string +} + +# alb 관련 변수 + +variable "public_subnet_ids" { + description = "List of public subnet IDs for ALB" + type = list(string) +} + +variable "alb_security_group_id" { + description = "Security Group ID for ALB" + type = string +} + +variable "alb_certificate_arn" { + description = "ARN of the ACM certificate for HTTPS listener" + type = string +} + +variable "alb_name" { + description = "Name of the ALB" + type = string + default = "cloudfence-alb" +} + +#codedeploy 설정 +variable "project_name" { + description = "The mane of the overall project, used for naming resources" + type = string +} \ No newline at end of file