From a3bf2b7c721e9e3d29dde03f76f91b9b94b8a5a8 Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Tue, 28 Apr 2026 15:27:42 +0900 Subject: [PATCH 01/18] =?UTF-8?q?fix:=20=EC=98=AC=EB=B0=94=EB=A5=B4?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=9D=80=20=EB=B2=84=ED=82=B7=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20=EB=9E=8C=EB=8B=A4=20=EC=8B=A4=ED=96=89=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EB=B6=80=EC=97=AC=20=EB=82=B4=EC=9A=A9=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/shared_resources/lambda.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/shared_resources/lambda.tf b/modules/shared_resources/lambda.tf index 2865d2f..e6248e3 100644 --- a/modules/shared_resources/lambda.tf +++ b/modules/shared_resources/lambda.tf @@ -44,7 +44,7 @@ resource "aws_lambda_permission" "allow_s3_resizing" { action = "lambda:InvokeFunction" function_name = aws_lambda_function.resizing_img_func.function_name principal = "s3.amazonaws.com" - source_arn = aws_s3_bucket.default.arn + source_arn = aws_s3_bucket.upload.arn } resource "aws_lambda_permission" "allow_s3_thumbnail" { @@ -52,12 +52,12 @@ resource "aws_lambda_permission" "allow_s3_thumbnail" { action = "lambda:InvokeFunction" function_name = aws_lambda_function.thumbnail_generating_func.function_name principal = "s3.amazonaws.com" - source_arn = aws_s3_bucket.default.arn + source_arn = aws_s3_bucket.upload.arn } # 4. S3 Trigger Setting resource "aws_s3_bucket_notification" "bucket_notification" { - bucket = aws_s3_bucket.default.id + bucket = aws_s3_bucket.upload.id lambda_function { lambda_function_arn = aws_lambda_function.resizing_img_func.arn From 29003a922144d0726890023590605a915ce6586f Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Tue, 28 Apr 2026 15:46:16 +0900 Subject: [PATCH 02/18] =?UTF-8?q?fix:=20=EA=B8=B0=EC=A1=B4=20AWS=20?= =?UTF-8?q?=EC=8B=A4=EC=A0=9C=20resource=EC=97=90=20=EB=A7=9E=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8A=94=20stage=20rds=20=EB=B6=80=EB=B6=84=EC=97=90?= =?UTF-8?q?=20=EB=8C=80=ED=95=9C=20=EC=A0=95=EC=9D=98=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=20-=20app=5Fstack=EC=97=90=EC=84=9C=20rds=20=EB=B6=80=EB=B6=84?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=9C=20enable=5Frds=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=84=A0=EC=96=B8=20-=20=EA=B7=B8=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20prod/stage=EC=97=90=20=EB=8C=80=ED=95=9C?= =?UTF-8?q?=20rds=20=EC=A1=B4=EC=9E=AC=20=EC=97=AC=EB=B6=80=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- environment/prod/provider.tf | 20 ++++++++++ environment/stage/main.tf | 30 ++++----------- environment/stage/variables.tf | 55 ---------------------------- modules/app_stack/provider.tf | 7 ---- modules/app_stack/rds.tf | 8 ++-- modules/app_stack/security_groups.tf | 1 + modules/app_stack/variables.tf | 14 +++++++ 7 files changed, 48 insertions(+), 87 deletions(-) diff --git a/environment/prod/provider.tf b/environment/prod/provider.tf index 087653c..ff0b71f 100644 --- a/environment/prod/provider.tf +++ b/environment/prod/provider.tf @@ -1,3 +1,16 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + mysql = { + source = "petoju/mysql" + version = ">= 3.0" + } + } +} + provider "aws" { region = "ap-northeast-2" default_tags { @@ -7,3 +20,10 @@ provider "aws" { } } } + +# MySQL Provider 설정 (SSH 터널링을 통해 로컬호스트로 접속) +provider "mysql" { + endpoint = "127.0.0.1:3306" + username = var.db_root_username + password = var.db_root_password +} diff --git a/environment/stage/main.tf b/environment/stage/main.tf index f5c999e..b71629d 100644 --- a/environment/stage/main.tf +++ b/environment/stage/main.tf @@ -13,40 +13,26 @@ module "stage_stack" { # 키페어 및 접속 허용 key_name = var.key_name - + # 인스턴스 스펙 - instance_type = var.server_instance_type - db_instance_class = var.db_instance_class + instance_type = var.server_instance_type + + # RDS 미사용 (Docker container로 대체) + enable_rds = false # 보안 그룹 규칙 api_ingress_rules = var.api_ingress_rules - db_ingress_rules = var.db_ingress_rules - - # RDS 식별자 설정 - rds_identifier = var.rds_identifier - - # DB 계정 정보 - db_username = var.db_root_username - db_password = var.db_root_password - - # DB 엔진 및 암호화 설정 - db_engine_version = var.db_engine_version # MySQL 버전 지정 - db_parameter_group_name = var.db_parameter_group_name # MySQL 파라미터 그룹 지정 - kms_key_arn = var.kms_key_arn # KMS ARN 변수 전달 - - # 추가 유저마다 다른 권한 부여 - additional_db_users = var.additional_db_users # Nginx 및 도메인 설정 - domain_name = var.domain_name - cert_email = var.cert_email + domain_name = var.domain_name + cert_email = var.cert_email nginx_conf_name = var.nginx_conf_name # ssh key 경로 전달 ssh_key_path = var.ssh_key_path # Side Infra 관련 변수 전달 - work_dir = var.work_dir + work_dir = var.work_dir alloy_env_name = var.alloy_env_name redis_version = var.redis_version diff --git a/environment/stage/variables.tf b/environment/stage/variables.tf index 5245959..19d4ae7 100644 --- a/environment/stage/variables.tf +++ b/environment/stage/variables.tf @@ -8,11 +8,6 @@ variable "server_instance_type" { type = string } -variable "db_instance_class" { - description = "DB instance class for the stage environment" - type = string -} - variable "api_ingress_rules" { description = "List of ingress rules for API Server" type = list(object({ @@ -24,61 +19,11 @@ variable "api_ingress_rules" { })) } -variable "db_ingress_rules" { - description = "List of ingress rules for DB Server" - type = list(object({ - from_port = number - to_port = number - protocol = string - description = string - })) -} - -variable "rds_identifier" { - description = "RDS identifier for the stage environment" - type = string -} - -variable "db_engine_version" { - description = "MySQL engine version for the stage environment" - type = string -} - -variable "db_parameter_group_name" { - description = "MySQL parameter group name for the stage environment" - type = string -} - -variable "db_root_username" { - description = "DB Username for stage" - type = string -} - -variable "db_root_password" { - description = "DB Password for stage" - type = string - sensitive = true -} - -variable "additional_db_users" { - description = "추가 DB 유저 및 권한 목록" - type = map(object({ - password = string - database = string - privileges = list(string) - })) -} - variable "key_name" { description = "Key pair name" type = string } -variable "kms_key_arn" { - description = "Existing KMS Key ARN for stage DB Encryption" - type = string -} - variable "domain_name" { description = "Domain name for the stage environment" type = string diff --git a/modules/app_stack/provider.tf b/modules/app_stack/provider.tf index b1a1d17..c756fdc 100644 --- a/modules/app_stack/provider.tf +++ b/modules/app_stack/provider.tf @@ -14,10 +14,3 @@ terraform { } } } - -# MySQL Provider 설정 (SSH 터널링을 통해 로컬호스트로 접속 가정) -provider "mysql" { - endpoint = "127.0.0.1:3306" - username = var.db_username - password = var.db_password -} diff --git a/modules/app_stack/rds.tf b/modules/app_stack/rds.tf index 8f484c1..47d4f0d 100644 --- a/modules/app_stack/rds.tf +++ b/modules/app_stack/rds.tf @@ -1,5 +1,7 @@ # 5. RDS resource "aws_db_instance" "default" { + count = var.enable_rds ? 1 : 0 + identifier = var.rds_identifier allocated_storage = 20 engine = "mysql" @@ -10,7 +12,7 @@ resource "aws_db_instance" "default" { parameter_group_name = var.db_parameter_group_name copy_tags_to_snapshot = true skip_final_snapshot = true - vpc_security_group_ids = [aws_security_group.db_sg.id] + vpc_security_group_ids = [aws_security_group.db_sg[count.index].id] storage_encrypted = true kms_key_id = var.kms_key_arn @@ -22,7 +24,7 @@ resource "aws_db_instance" "default" { # 6. MySQL 추가 유저 생성 resource "mysql_user" "users" { - for_each = var.additional_db_users + for_each = var.enable_rds ? var.additional_db_users : {} user = each.key host = "%" @@ -33,7 +35,7 @@ resource "mysql_user" "users" { # 7. MySQL 권한 부여 resource "mysql_grant" "user_grants" { - for_each = var.additional_db_users + for_each = var.enable_rds ? var.additional_db_users : {} user = each.key host = "%" diff --git a/modules/app_stack/security_groups.tf b/modules/app_stack/security_groups.tf index 5ec8b31..6607c6b 100644 --- a/modules/app_stack/security_groups.tf +++ b/modules/app_stack/security_groups.tf @@ -51,6 +51,7 @@ resource "aws_security_group" "api_sg" { # 2. RDS용 보안 그룹 (API Server만 믿음) resource "aws_security_group" "db_sg" { + count = var.enable_rds ? 1 : 0 name = "sc-${var.env_name}-db-sg" description = "Security Group for RDS" vpc_id = var.vpc_id diff --git a/modules/app_stack/variables.tf b/modules/app_stack/variables.tf index 68d1013..6623013 100644 --- a/modules/app_stack/variables.tf +++ b/modules/app_stack/variables.tf @@ -6,8 +6,15 @@ variable "instance_type" { description = "EC2 인스턴스 타입" } +variable "enable_rds" { + description = "RDS 사용 여부" + type = bool + default = true +} + variable "db_instance_class" { description = "RDS 인스턴스 타입" + default = null } variable "api_ingress_rules" { @@ -29,18 +36,21 @@ variable "db_ingress_rules" { protocol = string description = string })) + default = [] } # [DB 관련 추가 변수] variable "db_username" { description = "DB 마스터 사용자명" type = string + default = "" } variable "db_password" { description = "DB 마스터 비밀번호" type = string sensitive = true + default = "" } # 추가할 DB 유저 목록 @@ -57,21 +67,25 @@ variable "additional_db_users" { variable "db_engine_version" { description = "MySQL 엔진 버전" type = string + default = null } variable "db_parameter_group_name" { description = "MySQL 엔진 파라미터 그룹" type = string + default = null } variable "rds_identifier" { description = "RDS DB Identifier" type = string + default = null } variable "kms_key_arn" { description = "RDS 스토리지 암호화를 위한 KMS Key ARN" type = string + default = null } variable "vpc_id" { From 42a548452e220f45ed19a81e68973e0bb03d8455 Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Tue, 28 Apr 2026 17:21:16 +0900 Subject: [PATCH 03/18] =?UTF-8?q?feat:=20tfstate=20=EB=B2=84=ED=82=B7=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EB=B0=8F=20Github=20Action=EC=9A=A9=20s3/?= =?UTF-8?q?iam=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bootstrap/iam.tf | 138 ++++++++++++++++++++++++++++++++++++++++++ bootstrap/outputs.tf | 17 ++++++ bootstrap/provider.tf | 21 +++++++ bootstrap/s3.tf | 56 +++++++++++++++++ 4 files changed, 232 insertions(+) create mode 100644 bootstrap/iam.tf create mode 100644 bootstrap/outputs.tf create mode 100644 bootstrap/provider.tf create mode 100644 bootstrap/s3.tf diff --git a/bootstrap/iam.tf b/bootstrap/iam.tf new file mode 100644 index 0000000..af9c402 --- /dev/null +++ b/bootstrap/iam.tf @@ -0,0 +1,138 @@ +data "aws_caller_identity" "current" {} + +# ============================================= +# 개발자용 IAM Policy +# ============================================= + +# 로컬 terraform plan용: tfstate 읽기 + tflock 쓰기 +resource "aws_iam_policy" "developer_tfstate" { + name = "TerraformStateAccessPolicy" + description = "For local terraform plan: read tfstate + write/delete tflock" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = ["s3:ListBucket"] + Resource = aws_s3_bucket.tfstate.arn + }, + { + Effect = "Allow" + Action = ["s3:GetObject"] + Resource = "${aws_s3_bucket.tfstate.arn}/*" + }, + { + Effect = "Allow" + Action = ["s3:PutObject", "s3:DeleteObject"] + Resource = "${aws_s3_bucket.tfstate.arn}/*.tfstate.tflock" + } + ] + }) +} + +# ============================================= +# GitHub Actions OIDC +# ============================================= + +resource "aws_iam_openid_connect_provider" "github" { + url = "https://token.actions.githubusercontent.com" + client_id_list = ["sts.amazonaws.com"] + thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"] +} + +resource "aws_iam_role" "github_actions" { + name = "GitHubActionsTerraformRole" + description = "IAM Role for GitHub Actions terraform plan/apply via OIDC" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Principal = { Federated = aws_iam_openid_connect_provider.github.arn } + Action = "sts:AssumeRoleWithWebIdentity" + Condition = { + StringEquals = { + "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com" + } + StringLike = { + "token.actions.githubusercontent.com:sub" = "repo:solid-connection/solid-connection-infra:*" + } + } + }] + }) +} + +# GitHub Actions: tfstate 버킷 전체 접근 +resource "aws_iam_policy" "github_actions_tfstate" { + name = "GitHubActionsTfstatePolicy" + description = "For GitHub Actions terraform apply: full access to tfstate bucket" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Action = [ + "s3:ListBucket", + "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject" + ] + Resource = [ + aws_s3_bucket.tfstate.arn, + "${aws_s3_bucket.tfstate.arn}/*" + ] + }] + }) +} + +# GitHub Actions: AWS 인프라 관리 (terraform apply) +resource "aws_iam_policy" "github_actions_infra" { + name = "GitHubActionsTerraformInfraPolicy" + description = "For GitHub Actions terraform apply: AWS infrastructure management" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "ec2:*", + "rds:*", + "s3:*", + "cloudfront:*", + "lambda:*", + "acm:*", + "kms:DescribeKey", + "kms:GenerateDataKey", + "kms:Decrypt", + "kms:CreateGrant", + "iam:PassRole", + "iam:GetRole", + "iam:CreateRole", + "iam:DeleteRole", + "iam:AttachRolePolicy", + "iam:DetachRolePolicy", + "iam:ListRolePolicies", + "iam:ListAttachedRolePolicies", + "logs:CreateLogGroup", + "logs:DeleteLogGroup", + "logs:DescribeLogGroups", + "logs:ListTagsLogGroup", + "logs:PutRetentionPolicy" + ] + Resource = "*" + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "github_actions_tfstate" { + role = aws_iam_role.github_actions.name + policy_arn = aws_iam_policy.github_actions_tfstate.arn +} + +resource "aws_iam_role_policy_attachment" "github_actions_infra" { + role = aws_iam_role.github_actions.name + policy_arn = aws_iam_policy.github_actions_infra.arn +} diff --git a/bootstrap/outputs.tf b/bootstrap/outputs.tf new file mode 100644 index 0000000..1e2e891 --- /dev/null +++ b/bootstrap/outputs.tf @@ -0,0 +1,17 @@ +output "tfstate_bucket_arn" { + value = aws_s3_bucket.tfstate.arn +} + +output "tfstate_bucket_name" { + value = aws_s3_bucket.tfstate.bucket +} + +output "developer_tfstate_policy_arn" { + description = "개발자 IAM 유저에 attach할 tfstate 접근 Policy ARN" + value = aws_iam_policy.developer_tfstate.arn +} + +output "github_actions_role_arn" { + description = "GitHub Actions workflow에서 사용할 IAM Role ARN" + value = aws_iam_role.github_actions.arn +} diff --git a/bootstrap/provider.tf b/bootstrap/provider.tf new file mode 100644 index 0000000..b37dadc --- /dev/null +++ b/bootstrap/provider.tf @@ -0,0 +1,21 @@ +terraform { + required_version = ">= 1.10.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } +} + +provider "aws" { + region = "ap-northeast-2" + + default_tags { + tags = { + Project = "solid-connection" + ManagedBy = "terraform" + } + } +} diff --git a/bootstrap/s3.tf b/bootstrap/s3.tf new file mode 100644 index 0000000..09159fc --- /dev/null +++ b/bootstrap/s3.tf @@ -0,0 +1,56 @@ +resource "aws_s3_bucket" "tfstate" { + bucket = "solid-connection-tfstate" + + lifecycle { + prevent_destroy = true + } +} + +resource "aws_s3_bucket_versioning" "tfstate" { + bucket = aws_s3_bucket.tfstate.id + + versioning_configuration { + status = "Enabled" + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "tfstate" { + bucket = aws_s3_bucket.tfstate.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +resource "aws_s3_bucket_public_access_block" "tfstate" { + bucket = aws_s3_bucket.tfstate.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_policy" "tfstate" { + bucket = aws_s3_bucket.tfstate.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Deny" + Principal = "*" + Action = "s3:*" + Resource = [ + aws_s3_bucket.tfstate.arn, + "${aws_s3_bucket.tfstate.arn}/*" + ] + Condition = { + Bool = { "aws:SecureTransport" = "false" } + } + }] + }) + + depends_on = [aws_s3_bucket_public_access_block.tfstate] +} From 1e3ab68520302914420166f70885c78641dab6cb Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Tue, 28 Apr 2026 18:30:14 +0900 Subject: [PATCH 04/18] =?UTF-8?q?feat:=20tfstate=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EB=93=A4=EC=97=90=20=EB=8C=80=ED=95=9C=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EB=A5=BC=20S3=20=EB=B0=B1=EC=97=94=EB=93=9C=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bootstrap/provider.tf | 8 ++++++++ environment/global/provider.tf | 12 ++++++++++-- environment/monitoring/provider.tf | 23 +++++++++++++++++++++++ environment/prod/provider.tf | 10 ++++++++++ environment/stage/provider.tf | 19 +++++++++++++++++++ 5 files changed, 70 insertions(+), 2 deletions(-) diff --git a/bootstrap/provider.tf b/bootstrap/provider.tf index b37dadc..4e85df5 100644 --- a/bootstrap/provider.tf +++ b/bootstrap/provider.tf @@ -7,6 +7,14 @@ terraform { version = ">= 5.0" } } + + backend "s3" { + bucket = "solid-connection-tfstate" + key = "env/bootstrap/terraform.tfstate" + region = "ap-northeast-2" + use_lockfile = true + encrypt = true + } } provider "aws" { diff --git a/environment/global/provider.tf b/environment/global/provider.tf index 38fdf1e..fe58799 100644 --- a/environment/global/provider.tf +++ b/environment/global/provider.tf @@ -1,12 +1,20 @@ terraform { - required_version = ">= 1.0.0" + required_version = ">= 1.10.0" required_providers { aws = { source = "hashicorp/aws" - version = "~> 5.0" + version = ">= 5.0" } } + + backend "s3" { + bucket = "solid-connection-tfstate" + key = "env/global/terraform.tfstate" + region = "ap-northeast-2" + use_lockfile = true + encrypt = true + } } provider "aws" { diff --git a/environment/monitoring/provider.tf b/environment/monitoring/provider.tf index 3c04703..08141c4 100644 --- a/environment/monitoring/provider.tf +++ b/environment/monitoring/provider.tf @@ -1,3 +1,26 @@ +terraform { + required_version = ">= 1.10.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + cloudinit = { + source = "hashicorp/cloudinit" + version = "~> 2.3" + } + } + + backend "s3" { + bucket = "solid-connection-tfstate" + key = "env/monitoring/terraform.tfstate" + region = "ap-northeast-2" + use_lockfile = true + encrypt = true + } +} + provider "aws" { region = "ap-northeast-2" default_tags { diff --git a/environment/prod/provider.tf b/environment/prod/provider.tf index ff0b71f..52f4aec 100644 --- a/environment/prod/provider.tf +++ b/environment/prod/provider.tf @@ -1,4 +1,6 @@ terraform { + required_version = ">= 1.10.0" + required_providers { aws = { source = "hashicorp/aws" @@ -9,6 +11,14 @@ terraform { version = ">= 3.0" } } + + backend "s3" { + bucket = "solid-connection-tfstate" + key = "env/prod/terraform.tfstate" + region = "ap-northeast-2" + use_lockfile = true + encrypt = true + } } provider "aws" { diff --git a/environment/stage/provider.tf b/environment/stage/provider.tf index 87dc788..3f6b867 100644 --- a/environment/stage/provider.tf +++ b/environment/stage/provider.tf @@ -1,3 +1,22 @@ +terraform { + required_version = ">= 1.10.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } + + backend "s3" { + bucket = "solid-connection-tfstate" + key = "env/stage/terraform.tfstate" + region = "ap-northeast-2" + use_lockfile = true + encrypt = true + } +} + provider "aws" { region = "ap-northeast-2" default_tags { From 308b21d39641a180c83f30abdc41aa67e09df2f1 Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Tue, 28 Apr 2026 21:26:42 +0900 Subject: [PATCH 05/18] =?UTF-8?q?refactor:=20prod/stage=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20terraform=20=EC=BD=94=EB=93=9C=20=EC=B5=9C=EC=8B=A0?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bootstrap/iam.tf | 18 ++++++++++++++++++ config/secrets | 2 +- environment/prod/main.tf | 3 +++ environment/prod/variables.tf | 5 +++++ environment/stage/main.tf | 3 +++ environment/stage/variables.tf | 5 +++++ modules/app_stack/ec2.tf | 1 + modules/app_stack/variables.tf | 6 ++++++ 8 files changed, 42 insertions(+), 1 deletion(-) diff --git a/bootstrap/iam.tf b/bootstrap/iam.tf index af9c402..f1e2511 100644 --- a/bootstrap/iam.tf +++ b/bootstrap/iam.tf @@ -1,5 +1,18 @@ data "aws_caller_identity" "current" {} +# ============================================= +# EC2 공유 IAM Role에 SSM 정책 부착 +# ============================================= + +data "aws_iam_role" "ec2_shared" { + name = "SolidConnectionParameterStoreReadRole" +} + +resource "aws_iam_role_policy_attachment" "ec2_ssm" { + role = data.aws_iam_role.ec2_shared.name + policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" +} + # ============================================= # 개발자용 IAM Policy # ============================================= @@ -103,6 +116,11 @@ resource "aws_iam_policy" "github_actions_infra" { "cloudfront:*", "lambda:*", "acm:*", + "ssm:StartSession", + "ssm:TerminateSession", + "ssm:DescribeSessions", + "ssm:GetConnectionStatus", + "ssm:DescribeInstanceInformation", "kms:DescribeKey", "kms:GenerateDataKey", "kms:Decrypt", diff --git a/config/secrets b/config/secrets index 8e6a967..26908c3 160000 --- a/config/secrets +++ b/config/secrets @@ -1 +1 @@ -Subproject commit 8e6a96778a2977516ad0e7210fa7f24561b8f3e1 +Subproject commit 26908c36395083c99dbd1f20923d661ba8e0567e diff --git a/environment/prod/main.tf b/environment/prod/main.tf index 16d584d..320f5e4 100644 --- a/environment/prod/main.tf +++ b/environment/prod/main.tf @@ -11,6 +11,9 @@ module "prod_stack" { ami_id = var.ami_id + # IAM Instance Profile (SSM + Parameter Store 접근) + ec2_iam_instance_profile = var.ec2_iam_instance_profile + # 키페어 및 접속 허용 key_name = var.key_name diff --git a/environment/prod/variables.tf b/environment/prod/variables.tf index 324438a..0a74365 100644 --- a/environment/prod/variables.tf +++ b/environment/prod/variables.tf @@ -1,3 +1,8 @@ +variable "ec2_iam_instance_profile" { + description = "EC2에 연결할 IAM Instance Profile 이름" + type = string +} + variable "ami_id" { description = "AMI ID for the prod environment" type = string diff --git a/environment/stage/main.tf b/environment/stage/main.tf index b71629d..3f3e129 100644 --- a/environment/stage/main.tf +++ b/environment/stage/main.tf @@ -11,6 +11,9 @@ module "stage_stack" { ami_id = var.ami_id + # IAM Instance Profile (SSM + Parameter Store 접근) + ec2_iam_instance_profile = var.ec2_iam_instance_profile + # 키페어 및 접속 허용 key_name = var.key_name diff --git a/environment/stage/variables.tf b/environment/stage/variables.tf index 19d4ae7..e432b29 100644 --- a/environment/stage/variables.tf +++ b/environment/stage/variables.tf @@ -1,3 +1,8 @@ +variable "ec2_iam_instance_profile" { + description = "EC2에 연결할 IAM Instance Profile 이름" + type = string +} + variable "ami_id" { description = "AMI ID for the stage environment" type = string diff --git a/modules/app_stack/ec2.tf b/modules/app_stack/ec2.tf index 9b0d1c7..b49aa52 100644 --- a/modules/app_stack/ec2.tf +++ b/modules/app_stack/ec2.tf @@ -32,6 +32,7 @@ resource "aws_instance" "api_server" { key_name = var.key_name associate_public_ip_address = true + iam_instance_profile = var.ec2_iam_instance_profile user_data_base64 = data.cloudinit_config.app_init.rendered diff --git a/modules/app_stack/variables.tf b/modules/app_stack/variables.tf index 6623013..33f8b1a 100644 --- a/modules/app_stack/variables.tf +++ b/modules/app_stack/variables.tf @@ -12,6 +12,12 @@ variable "enable_rds" { default = true } +variable "ec2_iam_instance_profile" { + description = "EC2에 연결할 IAM Instance Profile 이름" + type = string + default = null +} + variable "db_instance_class" { description = "RDS 인스턴스 타입" default = null From bac30ef89eafcc7efa2008680a21387cd72b05ab Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Tue, 28 Apr 2026 21:27:53 +0900 Subject: [PATCH 06/18] =?UTF-8?q?feat:=20Github=20Action=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98=20-=20pr=EC=97=90=20=EB=8C=80=ED=95=9C=20terraform=20?= =?UTF-8?q?plan=20=EA=B2=B0=EA=B3=BC=20=EC=83=9D=EC=84=B1=20-=20pr=20?= =?UTF-8?q?=EB=A8=B8=EC=A7=80=EC=97=90=20=EB=8C=80=ED=95=9C=20terraform=20?= =?UTF-8?q?apply=20=EC=9E=A1=20=EC=83=9D=EC=84=B1=20-=20coderabbitai?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=9E=90=EB=8F=99=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=EB=B7=B0=20=EB=B9=84=ED=99=9C=EC=84=B1?= =?UTF-8?q?=ED=99=94=20=EB=B0=8F=20terraform=20plan=20=EC=9D=B4=ED=9B=84?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=EB=B7=B0=20=ED=8A=B8=EB=A6=AC?= =?UTF-8?q?=EA=B1=B0=20=EB=B0=9C=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .coderabbit.yaml | 18 ++ .github/workflows/terraform-apply.yml | 203 +++++++++++++++++ .github/workflows/terraform-plan.yml | 314 ++++++++++++++++++++++++++ 3 files changed, 535 insertions(+) create mode 100644 .coderabbit.yaml create mode 100644 .github/workflows/terraform-apply.yml create mode 100644 .github/workflows/terraform-plan.yml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..384a507 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,18 @@ +language: ko-KR + +reviews: + auto_review: + enabled: false + + path_instructions: + - path: "**/*.tf" + instructions: | + 이 PR의 Terraform 코드 변경을 리뷰합니다. + PR 댓글에 올라온 각 환경의 "Terraform Plan" 결과를 반드시 확인하고, 코드 변경과 plan 결과가 일치하는지 검토하세요. + + 다음 항목을 중점적으로 검토하세요: + - plan에 예상치 못한 resource destroy 또는 replace가 포함된 경우 + - 보안 그룹(Security Group) 인바운드/아웃바운드 규칙 변경 + - IAM 권한이 과도하게 부여된 경우 (최소 권한 원칙) + - 민감한 값(비밀번호, 키 등)이 코드에 하드코딩된 경우 + - `lifecycle.ignore_changes` 설정이 의도에 맞게 사용되었는지 diff --git a/.github/workflows/terraform-apply.yml b/.github/workflows/terraform-apply.yml new file mode 100644 index 0000000..c86e266 --- /dev/null +++ b/.github/workflows/terraform-apply.yml @@ -0,0 +1,203 @@ +name: Terraform Apply + +on: + push: + branches: [main] + +permissions: + id-token: write + contents: read + +env: + TF_VERSION: "1.10.5" + +jobs: + detect-changes: + runs-on: ubuntu-latest + outputs: + bootstrap: ${{ steps.filter.outputs.bootstrap }} + global: ${{ steps.filter.outputs.global }} + prod: ${{ steps.filter.outputs.prod }} + stage: ${{ steps.filter.outputs.stage }} + monitoring: ${{ steps.filter.outputs.monitoring }} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + bootstrap: + - 'bootstrap/**' + global: + - 'environment/global/**' + - 'modules/shared_resources/**' + prod: + - 'environment/prod/**' + - 'modules/app_stack/**' + - 'modules/common/**' + stage: + - 'environment/stage/**' + - 'modules/app_stack/**' + - 'modules/common/**' + monitoring: + - 'environment/monitoring/**' + - 'modules/monitoring_stack/**' + - 'modules/common/**' + + apply-bootstrap: + needs: detect-changes + if: needs.detect-changes.outputs.bootstrap == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ap-northeast-2 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + terraform_wrapper: false + - name: Terraform Init + working-directory: bootstrap + run: terraform init + - name: Terraform Apply + working-directory: bootstrap + run: terraform apply -auto-approve + + apply-global: + needs: detect-changes + if: needs.detect-changes.outputs.global == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ap-northeast-2 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + terraform_wrapper: false + - name: Terraform Init + working-directory: environment/global + run: terraform init + - name: Terraform Apply + working-directory: environment/global + run: | + terraform apply -auto-approve \ + -var-file="../../config/secrets/shared_resources.tfvars" + + apply-prod: + needs: detect-changes + if: needs.detect-changes.outputs.prod == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ap-northeast-2 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + terraform_wrapper: false + - name: Install Session Manager Plugin + run: | + curl -sL "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" -o session-manager-plugin.deb + sudo dpkg -i session-manager-plugin.deb + - name: Start SSM Tunnel to RDS + run: | + EC2_ID=$(aws ec2 describe-instances \ + --filters "Name=tag:Name,Values=solid-connection-server-prod" "Name=instance-state-name,Values=running" \ + --query 'Reservations[0].Instances[0].InstanceId' \ + --output text) + + RDS_HOST=$(aws rds describe-db-instances \ + --query 'DBInstances[?contains(DBInstanceIdentifier, `prod`)].Endpoint.Address | [0]' \ + --output text) + + echo "Tunneling via $EC2_ID -> $RDS_HOST:3306" + + aws ssm start-session \ + --target "$EC2_ID" \ + --document-name AWS-StartPortForwardingSessionToRemoteHost \ + --parameters "{\"host\":[\"$RDS_HOST\"],\"portNumber\":[\"3306\"],\"localPortNumber\":[\"3306\"]}" & + echo "SSM_PID=$!" >> $GITHUB_ENV + + timeout 30 bash -c 'until nc -z 127.0.0.1 3306 2>/dev/null; do sleep 1; done' + echo "SSM tunnel ready" + - name: Terraform Init + working-directory: environment/prod + run: terraform init + - name: Terraform Apply + working-directory: environment/prod + run: | + terraform apply -auto-approve \ + -var-file="../../config/secrets/prod.tfvars" \ + -var-file="../../config/secrets/app_stack.tfvars" + - name: Stop SSM Tunnel + if: always() + run: kill $SSM_PID 2>/dev/null || true + + apply-stage: + needs: detect-changes + if: needs.detect-changes.outputs.stage == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ap-northeast-2 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + terraform_wrapper: false + - name: Terraform Init + working-directory: environment/stage + run: terraform init + - name: Terraform Apply + working-directory: environment/stage + run: | + terraform apply -auto-approve \ + -var-file="../../config/secrets/stage.tfvars" \ + -var-file="../../config/secrets/app_stack.tfvars" + + apply-monitoring: + needs: detect-changes + if: needs.detect-changes.outputs.monitoring == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ap-northeast-2 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + terraform_wrapper: false + - name: Terraform Init + working-directory: environment/monitoring + run: terraform init + - name: Terraform Apply + working-directory: environment/monitoring + run: | + terraform apply -auto-approve \ + -var-file="../../config/secrets/monitoring.tfvars" diff --git a/.github/workflows/terraform-plan.yml b/.github/workflows/terraform-plan.yml new file mode 100644 index 0000000..7b9303d --- /dev/null +++ b/.github/workflows/terraform-plan.yml @@ -0,0 +1,314 @@ +name: Terraform Plan + +on: + pull_request: + branches: [main] + +permissions: + id-token: write + contents: read + pull-requests: write + +env: + TF_VERSION: "1.10.5" + +jobs: + detect-changes: + runs-on: ubuntu-latest + outputs: + bootstrap: ${{ steps.filter.outputs.bootstrap }} + global: ${{ steps.filter.outputs.global }} + prod: ${{ steps.filter.outputs.prod }} + stage: ${{ steps.filter.outputs.stage }} + monitoring: ${{ steps.filter.outputs.monitoring }} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + bootstrap: + - 'bootstrap/**' + global: + - 'environment/global/**' + - 'modules/shared_resources/**' + prod: + - 'environment/prod/**' + - 'modules/app_stack/**' + - 'modules/common/**' + stage: + - 'environment/stage/**' + - 'modules/app_stack/**' + - 'modules/common/**' + monitoring: + - 'environment/monitoring/**' + - 'modules/monitoring_stack/**' + - 'modules/common/**' + + plan-bootstrap: + needs: detect-changes + if: needs.detect-changes.outputs.bootstrap == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ap-northeast-2 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + terraform_wrapper: false + - name: Terraform Init + working-directory: bootstrap + run: terraform init + - name: Terraform Plan + id: plan + working-directory: bootstrap + run: | + terraform plan -no-color 2>&1 | tee plan_output.txt + echo "exitcode=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT + - name: Post Plan Comment + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const output = fs.readFileSync('bootstrap/plan_output.txt', 'utf8'); + const truncated = output.length > 60000 ? output.substring(0, 60000) + '\n...(truncated)' : output; + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `## Terraform Plan: \`bootstrap\`\n
Show Plan\n\n\`\`\`\n${truncated}\n\`\`\`\n
` + }); + - name: Plan Status Check + if: steps.plan.outputs.exitcode == '1' + run: exit 1 + + plan-global: + needs: detect-changes + if: needs.detect-changes.outputs.global == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ap-northeast-2 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + terraform_wrapper: false + - name: Terraform Init + working-directory: environment/global + run: terraform init + - name: Terraform Plan + id: plan + working-directory: environment/global + run: | + terraform plan -no-color \ + -var-file="../../config/secrets/shared_resources.tfvars" \ + 2>&1 | tee plan_output.txt + echo "exitcode=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT + - name: Post Plan Comment + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const output = fs.readFileSync('environment/global/plan_output.txt', 'utf8'); + const truncated = output.length > 60000 ? output.substring(0, 60000) + '\n...(truncated)' : output; + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `## Terraform Plan: \`global\`\n
Show Plan\n\n\`\`\`\n${truncated}\n\`\`\`\n
` + }); + - name: Plan Status Check + if: steps.plan.outputs.exitcode == '1' + run: exit 1 + + plan-prod: + needs: detect-changes + if: needs.detect-changes.outputs.prod == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ap-northeast-2 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + terraform_wrapper: false + - name: Install Session Manager Plugin + run: | + curl -sL "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" -o session-manager-plugin.deb + sudo dpkg -i session-manager-plugin.deb + - name: Start SSM Tunnel to RDS + run: | + EC2_ID=$(aws ec2 describe-instances \ + --filters "Name=tag:Name,Values=solid-connection-server-prod" "Name=instance-state-name,Values=running" \ + --query 'Reservations[0].Instances[0].InstanceId' \ + --output text) + + RDS_HOST=$(aws rds describe-db-instances \ + --query 'DBInstances[?contains(DBInstanceIdentifier, `prod`)].Endpoint.Address | [0]' \ + --output text) + + echo "Tunneling via $EC2_ID -> $RDS_HOST:3306" + + aws ssm start-session \ + --target "$EC2_ID" \ + --document-name AWS-StartPortForwardingSessionToRemoteHost \ + --parameters "{\"host\":[\"$RDS_HOST\"],\"portNumber\":[\"3306\"],\"localPortNumber\":[\"3306\"]}" & + echo "SSM_PID=$!" >> $GITHUB_ENV + + timeout 30 bash -c 'until nc -z 127.0.0.1 3306 2>/dev/null; do sleep 1; done' + echo "SSM tunnel ready" + - name: Terraform Init + working-directory: environment/prod + run: terraform init + - name: Terraform Plan + id: plan + working-directory: environment/prod + run: | + terraform plan -no-color \ + -var-file="../../config/secrets/prod.tfvars" \ + -var-file="../../config/secrets/app_stack.tfvars" \ + 2>&1 | tee plan_output.txt + echo "exitcode=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT + - name: Stop SSM Tunnel + if: always() + run: kill $SSM_PID 2>/dev/null || true + - name: Post Plan Comment + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const output = fs.readFileSync('environment/prod/plan_output.txt', 'utf8'); + const truncated = output.length > 60000 ? output.substring(0, 60000) + '\n...(truncated)' : output; + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `## Terraform Plan: \`prod\`\n
Show Plan\n\n\`\`\`\n${truncated}\n\`\`\`\n
` + }); + - name: Plan Status Check + if: steps.plan.outputs.exitcode == '1' + run: exit 1 + + plan-stage: + needs: detect-changes + if: needs.detect-changes.outputs.stage == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ap-northeast-2 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + terraform_wrapper: false + - name: Terraform Init + working-directory: environment/stage + run: terraform init + - name: Terraform Plan + id: plan + working-directory: environment/stage + run: | + terraform plan -no-color \ + -var-file="../../config/secrets/stage.tfvars" \ + -var-file="../../config/secrets/app_stack.tfvars" \ + 2>&1 | tee plan_output.txt + echo "exitcode=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT + - name: Post Plan Comment + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const output = fs.readFileSync('environment/stage/plan_output.txt', 'utf8'); + const truncated = output.length > 60000 ? output.substring(0, 60000) + '\n...(truncated)' : output; + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `## Terraform Plan: \`stage\`\n
Show Plan\n\n\`\`\`\n${truncated}\n\`\`\`\n
` + }); + - name: Plan Status Check + if: steps.plan.outputs.exitcode == '1' + run: exit 1 + + trigger-coderabbit: + needs: [plan-bootstrap, plan-global, plan-prod, plan-stage, plan-monitoring] + if: always() + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: '@coderabbitai review' + }); + + plan-monitoring: + needs: detect-changes + if: needs.detect-changes.outputs.monitoring == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ap-northeast-2 + - uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ env.TF_VERSION }} + terraform_wrapper: false + - name: Terraform Init + working-directory: environment/monitoring + run: terraform init + - name: Terraform Plan + id: plan + working-directory: environment/monitoring + run: | + terraform plan -no-color \ + -var-file="../../config/secrets/monitoring.tfvars" \ + 2>&1 | tee plan_output.txt + echo "exitcode=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT + - name: Post Plan Comment + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const output = fs.readFileSync('environment/monitoring/plan_output.txt', 'utf8'); + const truncated = output.length > 60000 ? output.substring(0, 60000) + '\n...(truncated)' : output; + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `## Terraform Plan: \`monitoring\`\n
Show Plan\n\n\`\`\`\n${truncated}\n\`\`\`\n
` + }); + - name: Plan Status Check + if: steps.plan.outputs.exitcode == '1' + run: exit 1 From 3544783ed5a0f0af20b706797cfb89078f2bf438 Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Tue, 28 Apr 2026 23:38:28 +0900 Subject: [PATCH 07/18] =?UTF-8?q?fix:=20terraform=20plan=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?-=20stage=20=ED=99=98=EA=B2=BD=EC=9D=98=20ingress=20rule?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=9C=20tfstate=20=EC=B5=9C=EC=8B=A0?= =?UTF-8?q?=ED=99=94=20-=20monitoring=20=ED=99=98=EA=B2=BD=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20=EC=84=9C=EB=B8=8C=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EC=B0=B8=EC=A1=B0=20=EB=B6=80=EB=B6=84=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?-=20bootstrap=20=ED=99=98=EA=B2=BD=EC=97=90=EC=84=9C=EC=9D=98?= =?UTF-8?q?=20iam=20=EC=A0=95=EC=B1=85=20=EC=84=A4=EC=A0=95=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20=ED=95=B4=EB=8B=B9?= =?UTF-8?q?=20=EB=B6=80=EB=B6=84=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=88=98?= =?UTF-8?q?=EB=8F=99=20=EA=B4=80=EB=A6=AC=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/terraform-apply.yml | 3 +- .github/workflows/terraform-plan.yml | 1 + bootstrap/iam.tf | 103 +++----------------------- bootstrap/outputs.tf | 2 +- bootstrap/provider.tf | 1 + modules/app_stack/security_groups.tf | 21 ------ 6 files changed, 15 insertions(+), 116 deletions(-) diff --git a/.github/workflows/terraform-apply.yml b/.github/workflows/terraform-apply.yml index c86e266..4d1d051 100644 --- a/.github/workflows/terraform-apply.yml +++ b/.github/workflows/terraform-apply.yml @@ -200,4 +200,5 @@ jobs: working-directory: environment/monitoring run: | terraform apply -auto-approve \ - -var-file="../../config/secrets/monitoring.tfvars" + -var-file="../../config/secrets/monitoring.tfvars" \ + -var-file="../../config/secrets/monitoring_stack.tfvars" diff --git a/.github/workflows/terraform-plan.yml b/.github/workflows/terraform-plan.yml index 7b9303d..58d749b 100644 --- a/.github/workflows/terraform-plan.yml +++ b/.github/workflows/terraform-plan.yml @@ -294,6 +294,7 @@ jobs: run: | terraform plan -no-color \ -var-file="../../config/secrets/monitoring.tfvars" \ + -var-file="../../config/secrets/monitoring_stack.tfvars" \ 2>&1 | tee plan_output.txt echo "exitcode=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT - name: Post Plan Comment diff --git a/bootstrap/iam.tf b/bootstrap/iam.tf index f1e2511..bf70b38 100644 --- a/bootstrap/iam.tf +++ b/bootstrap/iam.tf @@ -14,34 +14,11 @@ resource "aws_iam_role_policy_attachment" "ec2_ssm" { } # ============================================= -# 개발자용 IAM Policy +# 개발자용 IAM Policy (수동 관리, Terraform은 참조만) # ============================================= -# 로컬 terraform plan용: tfstate 읽기 + tflock 쓰기 -resource "aws_iam_policy" "developer_tfstate" { - name = "TerraformStateAccessPolicy" - description = "For local terraform plan: read tfstate + write/delete tflock" - - policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Effect = "Allow" - Action = ["s3:ListBucket"] - Resource = aws_s3_bucket.tfstate.arn - }, - { - Effect = "Allow" - Action = ["s3:GetObject"] - Resource = "${aws_s3_bucket.tfstate.arn}/*" - }, - { - Effect = "Allow" - Action = ["s3:PutObject", "s3:DeleteObject"] - Resource = "${aws_s3_bucket.tfstate.arn}/*.tfstate.tflock" - } - ] - }) +data "aws_iam_policy" "developer_tfstate" { + name = "TerraformStateAccessPolicy" } # ============================================= @@ -76,81 +53,21 @@ resource "aws_iam_role" "github_actions" { }) } -# GitHub Actions: tfstate 버킷 전체 접근 -resource "aws_iam_policy" "github_actions_tfstate" { - name = "GitHubActionsTfstatePolicy" - description = "For GitHub Actions terraform apply: full access to tfstate bucket" - - policy = jsonencode({ - Version = "2012-10-17" - Statement = [{ - Effect = "Allow" - Action = [ - "s3:ListBucket", - "s3:GetObject", - "s3:PutObject", - "s3:DeleteObject" - ] - Resource = [ - aws_s3_bucket.tfstate.arn, - "${aws_s3_bucket.tfstate.arn}/*" - ] - }] - }) +# GitHub Actions 정책 (수동 관리, Terraform은 참조만) +data "aws_iam_policy" "github_actions_tfstate" { + name = "GitHubActionsTfstatePolicy" } -# GitHub Actions: AWS 인프라 관리 (terraform apply) -resource "aws_iam_policy" "github_actions_infra" { - name = "GitHubActionsTerraformInfraPolicy" - description = "For GitHub Actions terraform apply: AWS infrastructure management" - - policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Effect = "Allow" - Action = [ - "ec2:*", - "rds:*", - "s3:*", - "cloudfront:*", - "lambda:*", - "acm:*", - "ssm:StartSession", - "ssm:TerminateSession", - "ssm:DescribeSessions", - "ssm:GetConnectionStatus", - "ssm:DescribeInstanceInformation", - "kms:DescribeKey", - "kms:GenerateDataKey", - "kms:Decrypt", - "kms:CreateGrant", - "iam:PassRole", - "iam:GetRole", - "iam:CreateRole", - "iam:DeleteRole", - "iam:AttachRolePolicy", - "iam:DetachRolePolicy", - "iam:ListRolePolicies", - "iam:ListAttachedRolePolicies", - "logs:CreateLogGroup", - "logs:DeleteLogGroup", - "logs:DescribeLogGroups", - "logs:ListTagsLogGroup", - "logs:PutRetentionPolicy" - ] - Resource = "*" - } - ] - }) +data "aws_iam_policy" "github_actions_infra" { + name = "GitHubActionsTerraformInfraPolicy" } resource "aws_iam_role_policy_attachment" "github_actions_tfstate" { role = aws_iam_role.github_actions.name - policy_arn = aws_iam_policy.github_actions_tfstate.arn + policy_arn = data.aws_iam_policy.github_actions_tfstate.arn } resource "aws_iam_role_policy_attachment" "github_actions_infra" { role = aws_iam_role.github_actions.name - policy_arn = aws_iam_policy.github_actions_infra.arn + policy_arn = data.aws_iam_policy.github_actions_infra.arn } diff --git a/bootstrap/outputs.tf b/bootstrap/outputs.tf index 1e2e891..f59a70c 100644 --- a/bootstrap/outputs.tf +++ b/bootstrap/outputs.tf @@ -8,7 +8,7 @@ output "tfstate_bucket_name" { output "developer_tfstate_policy_arn" { description = "개발자 IAM 유저에 attach할 tfstate 접근 Policy ARN" - value = aws_iam_policy.developer_tfstate.arn + value = data.aws_iam_policy.developer_tfstate.arn } output "github_actions_role_arn" { diff --git a/bootstrap/provider.tf b/bootstrap/provider.tf index 4e85df5..eec93f5 100644 --- a/bootstrap/provider.tf +++ b/bootstrap/provider.tf @@ -22,6 +22,7 @@ provider "aws" { default_tags { tags = { + Env = "bootstrap" Project = "solid-connection" ManagedBy = "terraform" } diff --git a/modules/app_stack/security_groups.tf b/modules/app_stack/security_groups.tf index 6607c6b..1a10fbd 100644 --- a/modules/app_stack/security_groups.tf +++ b/modules/app_stack/security_groups.tf @@ -1,15 +1,3 @@ -data "aws_instance" "monitoring_ec2" { - filter { - name = "tag:Name" - values = ["solid-connection-monitoring"] - } - - filter { - name = "instance-state-name" - values = ["running"] - } -} - # 1. API Server용 보안 그룹 (SSH 연결 허용) resource "aws_security_group" "api_sg" { name = "sc-${var.env_name}-api-sg" @@ -27,15 +15,6 @@ resource "aws_security_group" "api_sg" { } } - ingress { - description = "Allow 8081 from EC2: (${data.aws_instance.monitoring_ec2.tags.Name})" - from_port = 8081 - to_port = 8081 - protocol = "tcp" - - cidr_blocks = ["${data.aws_instance.monitoring_ec2.private_ip}/32"] - } - # [Outbound] 모든 트래픽 허용 egress { from_port = 0 From d62a9ea566ae989ca56ae8eaccee2ddde14cdf3f Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Wed, 29 Apr 2026 00:08:05 +0900 Subject: [PATCH 08/18] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EB=B0=98=EC=98=81=20-=20=EA=B8=B0=EC=A1=B4=20terra?= =?UTF-8?q?form-plan=20=EC=9B=8C=ED=81=AC=ED=94=8C=EB=A1=9C=EC=9A=B0?= =?UTF-8?q?=EA=B0=80=20=EC=83=88=EB=A1=9C=EC=9A=B4=20=EC=BB=A4=EB=B0=8B?= =?UTF-8?q?=EC=9D=B4=20=EC=B6=94=EA=B0=80=EB=90=98=EC=97=88=EC=9D=84=20?= =?UTF-8?q?=EB=95=8C=20=EA=B8=B0=EC=A1=B4=20=EB=8C=93=EA=B8=80=EC=9D=84=20?= =?UTF-8?q?=EB=8D=AE=EC=96=B4=EC=94=8C=EC=9A=B0=EB=8A=94=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20-=20?= =?UTF-8?q?=EB=B0=A9=EC=96=B4=EC=A0=81=20=EC=BD=94=EB=94=A9=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20aws=20oidc=EC=97=90=20=EB=8C=80=ED=95=9C=20thumbpri?= =?UTF-8?q?nt=20=EC=B6=94=EA=B0=80=20-=20terraform=20plan=20=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=ED=94=8C=EB=A1=9C=EC=9A=B0=EC=9D=98=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=EC=A0=84=EB=AC=B8=EC=9D=B4=20pr=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=EB=A1=9C=20=EB=B3=B4=EC=9D=B4=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD=20-=20terraform=20apply?= =?UTF-8?q?=20=EC=9B=8C=ED=81=AC=ED=94=8C=EB=A1=9C=EC=9A=B0=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20bootstrap=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=88=9C=EC=84=9C=20=EC=84=A0=EC=96=B8=20-=20SSM=20=ED=84=B0?= =?UTF-8?q?=EB=84=90=EB=A7=81=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=ED=95=B8=EB=93=A4?= =?UTF-8?q?=EB=A7=81=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/terraform-apply.yml | 63 ++++++++-- .github/workflows/terraform-plan.yml | 175 +++++++++++++++++++++----- bootstrap/iam.tf | 2 +- 3 files changed, 195 insertions(+), 45 deletions(-) diff --git a/.github/workflows/terraform-apply.yml b/.github/workflows/terraform-apply.yml index 4d1d051..beddc35 100644 --- a/.github/workflows/terraform-apply.yml +++ b/.github/workflows/terraform-apply.yml @@ -69,8 +69,11 @@ jobs: run: terraform apply -auto-approve apply-global: - needs: detect-changes - if: needs.detect-changes.outputs.global == 'true' + needs: [detect-changes, apply-bootstrap] + if: | + always() && + needs.detect-changes.outputs.global == 'true' && + (needs.apply-bootstrap.result == 'success' || needs.apply-bootstrap.result == 'skipped') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -95,8 +98,11 @@ jobs: -var-file="../../config/secrets/shared_resources.tfvars" apply-prod: - needs: detect-changes - if: needs.detect-changes.outputs.prod == 'true' + needs: [detect-changes, apply-bootstrap] + if: | + always() && + needs.detect-changes.outputs.prod == 'true' && + (needs.apply-bootstrap.result == 'success' || needs.apply-bootstrap.result == 'skipped') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -126,16 +132,45 @@ jobs: --query 'DBInstances[?contains(DBInstanceIdentifier, `prod`)].Endpoint.Address | [0]' \ --output text) + if [ -z "$EC2_ID" ] || [ "$EC2_ID" = "None" ]; then + echo "::error::prod EC2 인스턴스를 찾을 수 없습니다" + exit 1 + fi + if [ -z "$RDS_HOST" ] || [ "$RDS_HOST" = "None" ]; then + echo "::error::prod RDS 엔드포인트를 찾을 수 없습니다" + exit 1 + fi + echo "Tunneling via $EC2_ID -> $RDS_HOST:3306" aws ssm start-session \ --target "$EC2_ID" \ --document-name AWS-StartPortForwardingSessionToRemoteHost \ --parameters "{\"host\":[\"$RDS_HOST\"],\"portNumber\":[\"3306\"],\"localPortNumber\":[\"3306\"]}" & - echo "SSM_PID=$!" >> $GITHUB_ENV + SSM_PID=$! + echo "SSM_PID=$SSM_PID" >> $GITHUB_ENV - timeout 30 bash -c 'until nc -z 127.0.0.1 3306 2>/dev/null; do sleep 1; done' - echo "SSM tunnel ready" + for i in $(seq 1 30); do + if ! kill -0 $SSM_PID 2>/dev/null; then + echo "::error::SSM 세션이 터널 준비 전에 종료되었습니다" + exit 1 + fi + if nc -z 127.0.0.1 3306 2>/dev/null; then + echo "SSM tunnel ready (${i}s)" + break + fi + sleep 1 + done + + if ! nc -z 127.0.0.1 3306 2>/dev/null; then + echo "::error::30초 내에 터널이 준비되지 않았습니다" + kill $SSM_PID 2>/dev/null || true + exit 1 + fi + if ! kill -0 $SSM_PID 2>/dev/null; then + echo "::error::포트는 열렸으나 SSM 세션이 이미 종료되었습니다" + exit 1 + fi - name: Terraform Init working-directory: environment/prod run: terraform init @@ -150,8 +185,11 @@ jobs: run: kill $SSM_PID 2>/dev/null || true apply-stage: - needs: detect-changes - if: needs.detect-changes.outputs.stage == 'true' + needs: [detect-changes, apply-bootstrap] + if: | + always() && + needs.detect-changes.outputs.stage == 'true' && + (needs.apply-bootstrap.result == 'success' || needs.apply-bootstrap.result == 'skipped') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -177,8 +215,11 @@ jobs: -var-file="../../config/secrets/app_stack.tfvars" apply-monitoring: - needs: detect-changes - if: needs.detect-changes.outputs.monitoring == 'true' + needs: [detect-changes, apply-bootstrap] + if: | + always() && + needs.detect-changes.outputs.monitoring == 'true' && + (needs.apply-bootstrap.result == 'success' || needs.apply-bootstrap.result == 'skipped') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/terraform-plan.yml b/.github/workflows/terraform-plan.yml index 58d749b..cbd2886 100644 --- a/.github/workflows/terraform-plan.yml +++ b/.github/workflows/terraform-plan.yml @@ -71,19 +71,35 @@ jobs: run: | terraform plan -no-color 2>&1 | tee plan_output.txt echo "exitcode=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT + - name: Upload Plan Artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: terraform-plan-bootstrap + path: bootstrap/plan_output.txt - name: Post Plan Comment + if: always() uses: actions/github-script@v7 with: script: | const fs = require('fs'); + const marker = ''; const output = fs.readFileSync('bootstrap/plan_output.txt', 'utf8'); - const truncated = output.length > 60000 ? output.substring(0, 60000) + '\n...(truncated)' : output; - github.rest.issues.createComment({ + const summary = output.split('\n').find(l => /^Plan:/.test(l)) || output.split('\n').find(l => /No changes/.test(l)) || '(결과 파싱 불가)'; + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const body = `${marker}\n## Terraform Plan: \`bootstrap\`\n\n${summary}\n\n> 전체 plan 결과는 보안을 위해 댓글에 포함되지 않습니다. [워크플로우 실행 아티팩트](${runUrl})를 확인하세요.`; + + const { data: comments } = await github.rest.issues.listComments({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, - body: `## Terraform Plan: \`bootstrap\`\n
Show Plan\n\n\`\`\`\n${truncated}\n\`\`\`\n
` }); + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ comment_id: existing.id, owner: context.repo.owner, repo: context.repo.repo, body }); + } else { + await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body }); + } - name: Plan Status Check if: steps.plan.outputs.exitcode == '1' run: exit 1 @@ -116,19 +132,35 @@ jobs: -var-file="../../config/secrets/shared_resources.tfvars" \ 2>&1 | tee plan_output.txt echo "exitcode=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT + - name: Upload Plan Artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: terraform-plan-global + path: environment/global/plan_output.txt - name: Post Plan Comment + if: always() uses: actions/github-script@v7 with: script: | const fs = require('fs'); + const marker = ''; const output = fs.readFileSync('environment/global/plan_output.txt', 'utf8'); - const truncated = output.length > 60000 ? output.substring(0, 60000) + '\n...(truncated)' : output; - github.rest.issues.createComment({ + const summary = output.split('\n').find(l => /^Plan:/.test(l)) || output.split('\n').find(l => /No changes/.test(l)) || '(결과 파싱 불가)'; + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const body = `${marker}\n## Terraform Plan: \`global\`\n\n${summary}\n\n> 전체 plan 결과는 보안을 위해 댓글에 포함되지 않습니다. [워크플로우 실행 아티팩트](${runUrl})를 확인하세요.`; + + const { data: comments } = await github.rest.issues.listComments({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, - body: `## Terraform Plan: \`global\`\n
Show Plan\n\n\`\`\`\n${truncated}\n\`\`\`\n
` }); + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ comment_id: existing.id, owner: context.repo.owner, repo: context.repo.repo, body }); + } else { + await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body }); + } - name: Plan Status Check if: steps.plan.outputs.exitcode == '1' run: exit 1 @@ -165,16 +197,45 @@ jobs: --query 'DBInstances[?contains(DBInstanceIdentifier, `prod`)].Endpoint.Address | [0]' \ --output text) + if [ -z "$EC2_ID" ] || [ "$EC2_ID" = "None" ]; then + echo "::error::prod EC2 인스턴스를 찾을 수 없습니다" + exit 1 + fi + if [ -z "$RDS_HOST" ] || [ "$RDS_HOST" = "None" ]; then + echo "::error::prod RDS 엔드포인트를 찾을 수 없습니다" + exit 1 + fi + echo "Tunneling via $EC2_ID -> $RDS_HOST:3306" aws ssm start-session \ --target "$EC2_ID" \ --document-name AWS-StartPortForwardingSessionToRemoteHost \ --parameters "{\"host\":[\"$RDS_HOST\"],\"portNumber\":[\"3306\"],\"localPortNumber\":[\"3306\"]}" & - echo "SSM_PID=$!" >> $GITHUB_ENV + SSM_PID=$! + echo "SSM_PID=$SSM_PID" >> $GITHUB_ENV + + for i in $(seq 1 30); do + if ! kill -0 $SSM_PID 2>/dev/null; then + echo "::error::SSM 세션이 터널 준비 전에 종료되었습니다" + exit 1 + fi + if nc -z 127.0.0.1 3306 2>/dev/null; then + echo "SSM tunnel ready (${i}s)" + break + fi + sleep 1 + done - timeout 30 bash -c 'until nc -z 127.0.0.1 3306 2>/dev/null; do sleep 1; done' - echo "SSM tunnel ready" + if ! nc -z 127.0.0.1 3306 2>/dev/null; then + echo "::error::30초 내에 터널이 준비되지 않았습니다" + kill $SSM_PID 2>/dev/null || true + exit 1 + fi + if ! kill -0 $SSM_PID 2>/dev/null; then + echo "::error::포트는 열렸으나 SSM 세션이 이미 종료되었습니다" + exit 1 + fi - name: Terraform Init working-directory: environment/prod run: terraform init @@ -190,19 +251,35 @@ jobs: - name: Stop SSM Tunnel if: always() run: kill $SSM_PID 2>/dev/null || true + - name: Upload Plan Artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: terraform-plan-prod + path: environment/prod/plan_output.txt - name: Post Plan Comment + if: always() uses: actions/github-script@v7 with: script: | const fs = require('fs'); + const marker = ''; const output = fs.readFileSync('environment/prod/plan_output.txt', 'utf8'); - const truncated = output.length > 60000 ? output.substring(0, 60000) + '\n...(truncated)' : output; - github.rest.issues.createComment({ + const summary = output.split('\n').find(l => /^Plan:/.test(l)) || output.split('\n').find(l => /No changes/.test(l)) || '(결과 파싱 불가)'; + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const body = `${marker}\n## Terraform Plan: \`prod\`\n\n${summary}\n\n> 전체 plan 결과는 보안을 위해 댓글에 포함되지 않습니다. [워크플로우 실행 아티팩트](${runUrl})를 확인하세요.`; + + const { data: comments } = await github.rest.issues.listComments({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, - body: `## Terraform Plan: \`prod\`\n
Show Plan\n\n\`\`\`\n${truncated}\n\`\`\`\n
` }); + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ comment_id: existing.id, owner: context.repo.owner, repo: context.repo.repo, body }); + } else { + await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body }); + } - name: Plan Status Check if: steps.plan.outputs.exitcode == '1' run: exit 1 @@ -236,38 +313,39 @@ jobs: -var-file="../../config/secrets/app_stack.tfvars" \ 2>&1 | tee plan_output.txt echo "exitcode=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT + - name: Upload Plan Artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: terraform-plan-stage + path: environment/stage/plan_output.txt - name: Post Plan Comment + if: always() uses: actions/github-script@v7 with: script: | const fs = require('fs'); + const marker = ''; const output = fs.readFileSync('environment/stage/plan_output.txt', 'utf8'); - const truncated = output.length > 60000 ? output.substring(0, 60000) + '\n...(truncated)' : output; - github.rest.issues.createComment({ + const summary = output.split('\n').find(l => /^Plan:/.test(l)) || output.split('\n').find(l => /No changes/.test(l)) || '(결과 파싱 불가)'; + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const body = `${marker}\n## Terraform Plan: \`stage\`\n\n${summary}\n\n> 전체 plan 결과는 보안을 위해 댓글에 포함되지 않습니다. [워크플로우 실행 아티팩트](${runUrl})를 확인하세요.`; + + const { data: comments } = await github.rest.issues.listComments({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, - body: `## Terraform Plan: \`stage\`\n
Show Plan\n\n\`\`\`\n${truncated}\n\`\`\`\n
` }); + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ comment_id: existing.id, owner: context.repo.owner, repo: context.repo.repo, body }); + } else { + await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body }); + } - name: Plan Status Check if: steps.plan.outputs.exitcode == '1' run: exit 1 - trigger-coderabbit: - needs: [plan-bootstrap, plan-global, plan-prod, plan-stage, plan-monitoring] - if: always() - runs-on: ubuntu-latest - steps: - - uses: actions/github-script@v7 - with: - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: '@coderabbitai review' - }); - plan-monitoring: needs: detect-changes if: needs.detect-changes.outputs.monitoring == 'true' @@ -297,19 +375,50 @@ jobs: -var-file="../../config/secrets/monitoring_stack.tfvars" \ 2>&1 | tee plan_output.txt echo "exitcode=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT + - name: Upload Plan Artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: terraform-plan-monitoring + path: environment/monitoring/plan_output.txt - name: Post Plan Comment + if: always() uses: actions/github-script@v7 with: script: | const fs = require('fs'); + const marker = ''; const output = fs.readFileSync('environment/monitoring/plan_output.txt', 'utf8'); - const truncated = output.length > 60000 ? output.substring(0, 60000) + '\n...(truncated)' : output; - github.rest.issues.createComment({ + const summary = output.split('\n').find(l => /^Plan:/.test(l)) || output.split('\n').find(l => /No changes/.test(l)) || '(결과 파싱 불가)'; + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const body = `${marker}\n## Terraform Plan: \`monitoring\`\n\n${summary}\n\n> 전체 plan 결과는 보안을 위해 댓글에 포함되지 않습니다. [워크플로우 실행 아티팩트](${runUrl})를 확인하세요.`; + + const { data: comments } = await github.rest.issues.listComments({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, - body: `## Terraform Plan: \`monitoring\`\n
Show Plan\n\n\`\`\`\n${truncated}\n\`\`\`\n
` }); + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ comment_id: existing.id, owner: context.repo.owner, repo: context.repo.repo, body }); + } else { + await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body }); + } - name: Plan Status Check if: steps.plan.outputs.exitcode == '1' run: exit 1 + + trigger-coderabbit: + needs: [plan-bootstrap, plan-global, plan-prod, plan-stage, plan-monitoring] + if: always() + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: '@coderabbitai review' + }); diff --git a/bootstrap/iam.tf b/bootstrap/iam.tf index bf70b38..4b6c660 100644 --- a/bootstrap/iam.tf +++ b/bootstrap/iam.tf @@ -28,7 +28,7 @@ data "aws_iam_policy" "developer_tfstate" { resource "aws_iam_openid_connect_provider" "github" { url = "https://token.actions.githubusercontent.com" client_id_list = ["sts.amazonaws.com"] - thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"] + thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1", "1c58a3a8518e8759bf075b76b750d4f2df264fcd"] } resource "aws_iam_role" "github_actions" { From a10ea7063d88bb110d3576f07c39e66f9e7ca57a Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Wed, 29 Apr 2026 00:16:00 +0900 Subject: [PATCH 09/18] =?UTF-8?q?fix:=20prod=ED=99=98=EA=B2=BD=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20ssm=20=ED=84=B0=EB=84=90=EB=A7=81=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C(=ED=94=8C=EB=9F=AC=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=84=A4=EC=B9=98=20=EB=B0=A9=EC=8B=9D=20=EC=98=A4=EB=A5=98)?= =?UTF-8?q?=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/terraform-apply.yml | 6 ++++-- .github/workflows/terraform-plan.yml | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/terraform-apply.yml b/.github/workflows/terraform-apply.yml index beddc35..e70dded 100644 --- a/.github/workflows/terraform-apply.yml +++ b/.github/workflows/terraform-apply.yml @@ -119,8 +119,10 @@ jobs: terraform_wrapper: false - name: Install Session Manager Plugin run: | - curl -sL "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" -o session-manager-plugin.deb - sudo dpkg -i session-manager-plugin.deb + curl -sL "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/linux_64bit/session-manager-plugin" \ + -o /tmp/session-manager-plugin + sudo install -m 755 /tmp/session-manager-plugin /usr/local/bin/session-manager-plugin + session-manager-plugin --version - name: Start SSM Tunnel to RDS run: | EC2_ID=$(aws ec2 describe-instances \ diff --git a/.github/workflows/terraform-plan.yml b/.github/workflows/terraform-plan.yml index cbd2886..e07b9d2 100644 --- a/.github/workflows/terraform-plan.yml +++ b/.github/workflows/terraform-plan.yml @@ -184,8 +184,10 @@ jobs: terraform_wrapper: false - name: Install Session Manager Plugin run: | - curl -sL "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" -o session-manager-plugin.deb - sudo dpkg -i session-manager-plugin.deb + curl -sL "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/linux_64bit/session-manager-plugin" \ + -o /tmp/session-manager-plugin + sudo install -m 755 /tmp/session-manager-plugin /usr/local/bin/session-manager-plugin + session-manager-plugin --version - name: Start SSM Tunnel to RDS run: | EC2_ID=$(aws ec2 describe-instances \ From b9e3fd9e89c0e496b8538e67a7dd9440d3f348a1 Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Wed, 29 Apr 2026 00:24:56 +0900 Subject: [PATCH 10/18] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81=20=EB=B0=8F=20prod=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EC=97=90=20=EB=8C=80=ED=95=9C=20terraform=20=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/terraform-apply.yml | 7 ++++--- .github/workflows/terraform-plan.yml | 7 ++++--- bootstrap/iam.tf | 14 +++++++++++++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/.github/workflows/terraform-apply.yml b/.github/workflows/terraform-apply.yml index e70dded..e91cbf9 100644 --- a/.github/workflows/terraform-apply.yml +++ b/.github/workflows/terraform-apply.yml @@ -119,9 +119,10 @@ jobs: terraform_wrapper: false - name: Install Session Manager Plugin run: | - curl -sL "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/linux_64bit/session-manager-plugin" \ - -o /tmp/session-manager-plugin - sudo install -m 755 /tmp/session-manager-plugin /usr/local/bin/session-manager-plugin + curl -sL "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" \ + -o /tmp/session-manager-plugin.deb + sudo dpkg -i /tmp/session-manager-plugin.deb + sudo ln -sf /usr/local/sessionmanagerplugin/bin/session-manager-plugin /usr/bin/session-manager-plugin session-manager-plugin --version - name: Start SSM Tunnel to RDS run: | diff --git a/.github/workflows/terraform-plan.yml b/.github/workflows/terraform-plan.yml index e07b9d2..1db6ba7 100644 --- a/.github/workflows/terraform-plan.yml +++ b/.github/workflows/terraform-plan.yml @@ -184,9 +184,10 @@ jobs: terraform_wrapper: false - name: Install Session Manager Plugin run: | - curl -sL "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/linux_64bit/session-manager-plugin" \ - -o /tmp/session-manager-plugin - sudo install -m 755 /tmp/session-manager-plugin /usr/local/bin/session-manager-plugin + curl -sL "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" \ + -o /tmp/session-manager-plugin.deb + sudo dpkg -i /tmp/session-manager-plugin.deb + sudo ln -sf /usr/local/sessionmanagerplugin/bin/session-manager-plugin /usr/bin/session-manager-plugin session-manager-plugin --version - name: Start SSM Tunnel to RDS run: | diff --git a/bootstrap/iam.tf b/bootstrap/iam.tf index 4b6c660..82c38de 100644 --- a/bootstrap/iam.tf +++ b/bootstrap/iam.tf @@ -29,11 +29,20 @@ resource "aws_iam_openid_connect_provider" "github" { url = "https://token.actions.githubusercontent.com" client_id_list = ["sts.amazonaws.com"] thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1", "1c58a3a8518e8759bf075b76b750d4f2df264fcd"] + tags = { + Project = "solid-connection" + Env = "bootstrap" + } } resource "aws_iam_role" "github_actions" { name = "GitHubActionsTerraformRole" description = "IAM Role for GitHub Actions terraform plan/apply via OIDC" + tags = { + Project = "solid-connection" + Env = "bootstrap" + } + assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -46,7 +55,10 @@ resource "aws_iam_role" "github_actions" { "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com" } StringLike = { - "token.actions.githubusercontent.com:sub" = "repo:solid-connection/solid-connection-infra:*" + "token.actions.githubusercontent.com:sub" = [ + "repo:solid-connection/solid-connection-infra:ref:refs/heads/main", + "repo:solid-connection/solid-connection-infra:pull_request" + ] } } }] From 66eb913ce1d65bdfbd7efd239c4bd84205e302a9 Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Wed, 29 Apr 2026 00:35:47 +0900 Subject: [PATCH 11/18] =?UTF-8?q?fix:=20prod=20=ED=99=98=EA=B2=BD=EC=97=90?= =?UTF-8?q?=20=EB=8C=80=ED=95=9C=20terraform=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/terraform-apply.yml | 4 ++-- .github/workflows/terraform-plan.yml | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/terraform-apply.yml b/.github/workflows/terraform-apply.yml index e91cbf9..9bb040f 100644 --- a/.github/workflows/terraform-apply.yml +++ b/.github/workflows/terraform-apply.yml @@ -122,8 +122,8 @@ jobs: curl -sL "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" \ -o /tmp/session-manager-plugin.deb sudo dpkg -i /tmp/session-manager-plugin.deb - sudo ln -sf /usr/local/sessionmanagerplugin/bin/session-manager-plugin /usr/bin/session-manager-plugin - session-manager-plugin --version + echo "/usr/local/sessionmanagerplugin/bin" >> $GITHUB_PATH + /usr/local/sessionmanagerplugin/bin/session-manager-plugin --version - name: Start SSM Tunnel to RDS run: | EC2_ID=$(aws ec2 describe-instances \ diff --git a/.github/workflows/terraform-plan.yml b/.github/workflows/terraform-plan.yml index 1db6ba7..dbf20c1 100644 --- a/.github/workflows/terraform-plan.yml +++ b/.github/workflows/terraform-plan.yml @@ -187,8 +187,9 @@ jobs: curl -sL "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" \ -o /tmp/session-manager-plugin.deb sudo dpkg -i /tmp/session-manager-plugin.deb - sudo ln -sf /usr/local/sessionmanagerplugin/bin/session-manager-plugin /usr/bin/session-manager-plugin - session-manager-plugin --version + # 이후 모든 스텝의 PATH 맨 앞에 신규 설치 경로 추가 + echo "/usr/local/sessionmanagerplugin/bin" >> $GITHUB_PATH + /usr/local/sessionmanagerplugin/bin/session-manager-plugin --version - name: Start SSM Tunnel to RDS run: | EC2_ID=$(aws ec2 describe-instances \ From dccf84f6f3765f7d6694dc8e40d2bbf48b226483 Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Wed, 29 Apr 2026 00:49:48 +0900 Subject: [PATCH 12/18] =?UTF-8?q?fix:=20prod=20=ED=99=98=EA=B2=BD=EC=97=90?= =?UTF-8?q?=20=EB=8C=80=ED=95=9C=20terraform=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/terraform-plan.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/terraform-plan.yml b/.github/workflows/terraform-plan.yml index dbf20c1..1bd0eed 100644 --- a/.github/workflows/terraform-plan.yml +++ b/.github/workflows/terraform-plan.yml @@ -192,6 +192,11 @@ jobs: /usr/local/sessionmanagerplugin/bin/session-manager-plugin --version - name: Start SSM Tunnel to RDS run: | + echo "=== session-manager-plugin 진단 ===" + which session-manager-plugin || echo "NOT IN PATH" + session-manager-plugin --version || echo "VERSION CHECK FAILED" + echo "====================================" + EC2_ID=$(aws ec2 describe-instances \ --filters "Name=tag:Name,Values=solid-connection-server-prod" "Name=instance-state-name,Values=running" \ --query 'Reservations[0].Instances[0].InstanceId' \ From 5ec981dc307158ced8cf7355acbe48cb3a415887 Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Wed, 29 Apr 2026 00:49:54 +0900 Subject: [PATCH 13/18] =?UTF-8?q?fix:=20prod=20=ED=99=98=EA=B2=BD=EC=97=90?= =?UTF-8?q?=20=EB=8C=80=ED=95=9C=20terraform=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/terraform-apply.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/terraform-apply.yml b/.github/workflows/terraform-apply.yml index 9bb040f..d49b41e 100644 --- a/.github/workflows/terraform-apply.yml +++ b/.github/workflows/terraform-apply.yml @@ -126,6 +126,11 @@ jobs: /usr/local/sessionmanagerplugin/bin/session-manager-plugin --version - name: Start SSM Tunnel to RDS run: | + echo "=== session-manager-plugin 진단 ===" + which session-manager-plugin || echo "NOT IN PATH" + session-manager-plugin --version || echo "VERSION CHECK FAILED" + echo "====================================" + EC2_ID=$(aws ec2 describe-instances \ --filters "Name=tag:Name,Values=solid-connection-server-prod" "Name=instance-state-name,Values=running" \ --query 'Reservations[0].Instances[0].InstanceId' \ From 1e636ab6f4f918b0e9c1a3de9789ed75b635d4c8 Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Wed, 29 Apr 2026 01:59:53 +0900 Subject: [PATCH 14/18] =?UTF-8?q?fix:=20prod=20=ED=99=98=EA=B2=BD=EC=97=90?= =?UTF-8?q?=20=EB=8C=80=ED=95=9C=20terraform=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EC=88=98=EC=A0=95=20&=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EB=9E=98=EB=B9=97=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/terraform-apply.yml | 10 ++++++++-- .github/workflows/terraform-plan.yml | 9 +++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/terraform-apply.yml b/.github/workflows/terraform-apply.yml index d49b41e..4097a05 100644 --- a/.github/workflows/terraform-apply.yml +++ b/.github/workflows/terraform-apply.yml @@ -10,6 +10,7 @@ permissions: env: TF_VERSION: "1.10.5" + SSM_TUNNEL_TIMEOUT: "60" jobs: detect-changes: @@ -53,6 +54,7 @@ jobs: with: submodules: recursive token: ${{ secrets.GH_PAT }} + persist-credentials: false - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} @@ -80,6 +82,7 @@ jobs: with: submodules: recursive token: ${{ secrets.GH_PAT }} + persist-credentials: false - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} @@ -109,6 +112,7 @@ jobs: with: submodules: recursive token: ${{ secrets.GH_PAT }} + persist-credentials: false - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} @@ -158,7 +162,7 @@ jobs: SSM_PID=$! echo "SSM_PID=$SSM_PID" >> $GITHUB_ENV - for i in $(seq 1 30); do + for i in $(seq 1 $SSM_TUNNEL_TIMEOUT); do if ! kill -0 $SSM_PID 2>/dev/null; then echo "::error::SSM 세션이 터널 준비 전에 종료되었습니다" exit 1 @@ -171,7 +175,7 @@ jobs: done if ! nc -z 127.0.0.1 3306 2>/dev/null; then - echo "::error::30초 내에 터널이 준비되지 않았습니다" + echo "::error::${SSM_TUNNEL_TIMEOUT}초 내에 터널이 준비되지 않았습니다" kill $SSM_PID 2>/dev/null || true exit 1 fi @@ -204,6 +208,7 @@ jobs: with: submodules: recursive token: ${{ secrets.GH_PAT }} + persist-credentials: false - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} @@ -234,6 +239,7 @@ jobs: with: submodules: recursive token: ${{ secrets.GH_PAT }} + persist-credentials: false - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} diff --git a/.github/workflows/terraform-plan.yml b/.github/workflows/terraform-plan.yml index 1bd0eed..53cbd9a 100644 --- a/.github/workflows/terraform-plan.yml +++ b/.github/workflows/terraform-plan.yml @@ -11,6 +11,7 @@ permissions: env: TF_VERSION: "1.10.5" + SSM_TUNNEL_TIMEOUT: "60" jobs: detect-changes: @@ -113,6 +114,7 @@ jobs: with: submodules: recursive token: ${{ secrets.GH_PAT }} + persist-credentials: false - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} @@ -174,6 +176,7 @@ jobs: with: submodules: recursive token: ${{ secrets.GH_PAT }} + persist-credentials: false - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} @@ -224,7 +227,7 @@ jobs: SSM_PID=$! echo "SSM_PID=$SSM_PID" >> $GITHUB_ENV - for i in $(seq 1 30); do + for i in $(seq 1 $SSM_TUNNEL_TIMEOUT); do if ! kill -0 $SSM_PID 2>/dev/null; then echo "::error::SSM 세션이 터널 준비 전에 종료되었습니다" exit 1 @@ -237,7 +240,7 @@ jobs: done if ! nc -z 127.0.0.1 3306 2>/dev/null; then - echo "::error::30초 내에 터널이 준비되지 않았습니다" + echo "::error::${SSM_TUNNEL_TIMEOUT}초 내에 터널이 준비되지 않았습니다" kill $SSM_PID 2>/dev/null || true exit 1 fi @@ -302,6 +305,7 @@ jobs: with: submodules: recursive token: ${{ secrets.GH_PAT }} + persist-credentials: false - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} @@ -364,6 +368,7 @@ jobs: with: submodules: recursive token: ${{ secrets.GH_PAT }} + persist-credentials: false - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} From 7f078d05c4413dade63345cde63ab00e4f3dd2f8 Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Wed, 29 Apr 2026 02:02:27 +0900 Subject: [PATCH 15/18] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/terraform-plan.yml | 57 +++++++++++++++++++++------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/.github/workflows/terraform-plan.yml b/.github/workflows/terraform-plan.yml index 53cbd9a..c85d50a 100644 --- a/.github/workflows/terraform-plan.yml +++ b/.github/workflows/terraform-plan.yml @@ -85,8 +85,9 @@ jobs: script: | const fs = require('fs'); const marker = ''; - const output = fs.readFileSync('bootstrap/plan_output.txt', 'utf8'); - const summary = output.split('\n').find(l => /^Plan:/.test(l)) || output.split('\n').find(l => /No changes/.test(l)) || '(결과 파싱 불가)'; + const planFile = 'bootstrap/plan_output.txt'; + const output = fs.existsSync(planFile) ? fs.readFileSync(planFile, 'utf8') : ''; + const summary = output.split('\n').find(l => /^Plan:/.test(l)) || output.split('\n').find(l => /No changes/.test(l)) || (output ? '(결과 파싱 불가)' : '⚠️ plan 실행 전 단계에서 실패했습니다. 워크플로우 로그를 확인하세요.'); const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; const body = `${marker}\n## Terraform Plan: \`bootstrap\`\n\n${summary}\n\n> 전체 plan 결과는 보안을 위해 댓글에 포함되지 않습니다. [워크플로우 실행 아티팩트](${runUrl})를 확인하세요.`; @@ -147,8 +148,9 @@ jobs: script: | const fs = require('fs'); const marker = ''; - const output = fs.readFileSync('environment/global/plan_output.txt', 'utf8'); - const summary = output.split('\n').find(l => /^Plan:/.test(l)) || output.split('\n').find(l => /No changes/.test(l)) || '(결과 파싱 불가)'; + const planFile = 'environment/global/plan_output.txt'; + const output = fs.existsSync(planFile) ? fs.readFileSync(planFile, 'utf8') : ''; + const summary = output.split('\n').find(l => /^Plan:/.test(l)) || output.split('\n').find(l => /No changes/.test(l)) || (output ? '(결과 파싱 불가)' : '⚠️ plan 실행 전 단계에서 실패했습니다. 워크플로우 로그를 확인하세요.'); const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; const body = `${marker}\n## Terraform Plan: \`global\`\n\n${summary}\n\n> 전체 plan 결과는 보안을 위해 댓글에 포함되지 않습니다. [워크플로우 실행 아티팩트](${runUrl})를 확인하세요.`; @@ -276,8 +278,9 @@ jobs: script: | const fs = require('fs'); const marker = ''; - const output = fs.readFileSync('environment/prod/plan_output.txt', 'utf8'); - const summary = output.split('\n').find(l => /^Plan:/.test(l)) || output.split('\n').find(l => /No changes/.test(l)) || '(결과 파싱 불가)'; + const planFile = 'environment/prod/plan_output.txt'; + const output = fs.existsSync(planFile) ? fs.readFileSync(planFile, 'utf8') : ''; + const summary = output.split('\n').find(l => /^Plan:/.test(l)) || output.split('\n').find(l => /No changes/.test(l)) || (output ? '(결과 파싱 불가)' : '⚠️ plan 실행 전 단계에서 실패했습니다. 워크플로우 로그를 확인하세요.'); const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; const body = `${marker}\n## Terraform Plan: \`prod\`\n\n${summary}\n\n> 전체 plan 결과는 보안을 위해 댓글에 포함되지 않습니다. [워크플로우 실행 아티팩트](${runUrl})를 확인하세요.`; @@ -339,8 +342,9 @@ jobs: script: | const fs = require('fs'); const marker = ''; - const output = fs.readFileSync('environment/stage/plan_output.txt', 'utf8'); - const summary = output.split('\n').find(l => /^Plan:/.test(l)) || output.split('\n').find(l => /No changes/.test(l)) || '(결과 파싱 불가)'; + const planFile = 'environment/stage/plan_output.txt'; + const output = fs.existsSync(planFile) ? fs.readFileSync(planFile, 'utf8') : ''; + const summary = output.split('\n').find(l => /^Plan:/.test(l)) || output.split('\n').find(l => /No changes/.test(l)) || (output ? '(결과 파싱 불가)' : '⚠️ plan 실행 전 단계에서 실패했습니다. 워크플로우 로그를 확인하세요.'); const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; const body = `${marker}\n## Terraform Plan: \`stage\`\n\n${summary}\n\n> 전체 plan 결과는 보안을 위해 댓글에 포함되지 않습니다. [워크플로우 실행 아티팩트](${runUrl})를 확인하세요.`; @@ -402,8 +406,9 @@ jobs: script: | const fs = require('fs'); const marker = ''; - const output = fs.readFileSync('environment/monitoring/plan_output.txt', 'utf8'); - const summary = output.split('\n').find(l => /^Plan:/.test(l)) || output.split('\n').find(l => /No changes/.test(l)) || '(결과 파싱 불가)'; + const planFile = 'environment/monitoring/plan_output.txt'; + const output = fs.existsSync(planFile) ? fs.readFileSync(planFile, 'utf8') : ''; + const summary = output.split('\n').find(l => /^Plan:/.test(l)) || output.split('\n').find(l => /No changes/.test(l)) || (output ? '(결과 파싱 불가)' : '⚠️ plan 실행 전 단계에서 실패했습니다. 워크플로우 로그를 확인하세요.'); const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; const body = `${marker}\n## Terraform Plan: \`monitoring\`\n\n${summary}\n\n> 전체 plan 결과는 보안을 위해 댓글에 포함되지 않습니다. [워크플로우 실행 아티팩트](${runUrl})를 확인하세요.`; @@ -424,15 +429,41 @@ jobs: trigger-coderabbit: needs: [plan-bootstrap, plan-global, plan-prod, plan-stage, plan-monitoring] - if: always() + if: | + always() && + ( + needs.plan-bootstrap.result == 'success' || needs.plan-bootstrap.result == 'failure' || + needs.plan-global.result == 'success' || needs.plan-global.result == 'failure' || + needs.plan-prod.result == 'success' || needs.plan-prod.result == 'failure' || + needs.plan-stage.result == 'success' || needs.plan-stage.result == 'failure' || + needs.plan-monitoring.result == 'success' || needs.plan-monitoring.result == 'failure' + ) runs-on: ubuntu-latest steps: - uses: actions/github-script@v7 with: script: | - github.rest.issues.createComment({ + const marker = ''; + const body = `${marker}\n@coderabbitai review`; + + const { data: comments } = await github.rest.issues.listComments({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, - body: '@coderabbitai review' }); + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + await github.rest.issues.updateComment({ + comment_id: existing.id, + owner: context.repo.owner, + repo: context.repo.repo, + body, + }); + } else { + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body, + }); + } From d45a117923f7274e2f2c21635fa5351c6d7012ca Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Sun, 3 May 2026 18:54:32 +0900 Subject: [PATCH 16/18] =?UTF-8?q?fix:=20pr=EC=97=90=EC=84=9C=20aws=20?= =?UTF-8?q?=EB=A6=AC=EC=86=8C=EC=8A=A4=EC=97=90=20=EC=A0=91=EA=B7=BC?= =?UTF-8?q?=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94=20=EA=B6=8C=ED=95=9C?= =?UTF-8?q?=EC=9D=84=20read=20=EC=9A=A9=EB=8F=84=EB=A1=9C=20=EC=B6=95?= =?UTF-8?q?=EC=86=8C=20=EB=B0=8F=20apply=EC=99=80=EC=9D=98=20=EC=97=AD?= =?UTF-8?q?=ED=95=A0=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/terraform-plan.yml | 10 +++--- bootstrap/iam.tf | 50 +++++++++++++++++++++++++--- bootstrap/outputs.tf | 7 +++- 3 files changed, 56 insertions(+), 11 deletions(-) diff --git a/.github/workflows/terraform-plan.yml b/.github/workflows/terraform-plan.yml index c85d50a..3eb5e10 100644 --- a/.github/workflows/terraform-plan.yml +++ b/.github/workflows/terraform-plan.yml @@ -57,7 +57,7 @@ jobs: token: ${{ secrets.GH_PAT }} - uses: aws-actions/configure-aws-credentials@v4 with: - role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + role-to-assume: ${{ secrets.AWS_PLAN_ROLE_ARN }} aws-region: ap-northeast-2 - uses: hashicorp/setup-terraform@v3 with: @@ -118,7 +118,7 @@ jobs: persist-credentials: false - uses: aws-actions/configure-aws-credentials@v4 with: - role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + role-to-assume: ${{ secrets.AWS_PLAN_ROLE_ARN }} aws-region: ap-northeast-2 - uses: hashicorp/setup-terraform@v3 with: @@ -181,7 +181,7 @@ jobs: persist-credentials: false - uses: aws-actions/configure-aws-credentials@v4 with: - role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + role-to-assume: ${{ secrets.AWS_PLAN_ROLE_ARN }} aws-region: ap-northeast-2 - uses: hashicorp/setup-terraform@v3 with: @@ -311,7 +311,7 @@ jobs: persist-credentials: false - uses: aws-actions/configure-aws-credentials@v4 with: - role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + role-to-assume: ${{ secrets.AWS_PLAN_ROLE_ARN }} aws-region: ap-northeast-2 - uses: hashicorp/setup-terraform@v3 with: @@ -375,7 +375,7 @@ jobs: persist-credentials: false - uses: aws-actions/configure-aws-credentials@v4 with: - role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + role-to-assume: ${{ secrets.AWS_PLAN_ROLE_ARN }} aws-region: ap-northeast-2 - uses: hashicorp/setup-terraform@v3 with: diff --git a/bootstrap/iam.tf b/bootstrap/iam.tf index 82c38de..5b8901f 100644 --- a/bootstrap/iam.tf +++ b/bootstrap/iam.tf @@ -35,14 +35,41 @@ resource "aws_iam_openid_connect_provider" "github" { } } +# Apply Role: main 브랜치 push 전용 (terraform apply) resource "aws_iam_role" "github_actions" { name = "GitHubActionsTerraformRole" - description = "IAM Role for GitHub Actions terraform plan/apply via OIDC" + description = "IAM Role for GitHub Actions terraform apply via OIDC (main only)" tags = { Project = "solid-connection" Env = "bootstrap" } + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Principal = { Federated = aws_iam_openid_connect_provider.github.arn } + Action = "sts:AssumeRoleWithWebIdentity" + Condition = { + StringEquals = { + "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com" + } + StringLike = { + "token.actions.githubusercontent.com:sub" = "repo:solid-connection/solid-connection-infra:ref:refs/heads/main" + } + } + }] + }) +} + +# Plan Role: PR 전용 (terraform plan, 읽기 전용) +resource "aws_iam_role" "github_actions_plan" { + name = "GitHubActionsTerraformPlanRole" + description = "IAM Role for GitHub Actions terraform plan via OIDC (pull_request only)" + tags = { + Project = "solid-connection" + Env = "bootstrap" + } assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -55,10 +82,7 @@ resource "aws_iam_role" "github_actions" { "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com" } StringLike = { - "token.actions.githubusercontent.com:sub" = [ - "repo:solid-connection/solid-connection-infra:ref:refs/heads/main", - "repo:solid-connection/solid-connection-infra:pull_request" - ] + "token.actions.githubusercontent.com:sub" = "repo:solid-connection/solid-connection-infra:pull_request" } } }] @@ -74,6 +98,11 @@ data "aws_iam_policy" "github_actions_infra" { name = "GitHubActionsTerraformInfraPolicy" } +data "aws_iam_policy" "github_actions_infra_read" { + name = "GithubActionsTerraformInfraReadPolicy" +} + +# Apply Role 정책 연결 resource "aws_iam_role_policy_attachment" "github_actions_tfstate" { role = aws_iam_role.github_actions.name policy_arn = data.aws_iam_policy.github_actions_tfstate.arn @@ -83,3 +112,14 @@ resource "aws_iam_role_policy_attachment" "github_actions_infra" { role = aws_iam_role.github_actions.name policy_arn = data.aws_iam_policy.github_actions_infra.arn } + +# Plan Role 정책 연결 (tfstate 읽기 전용 + 인프라 읽기 전용) +resource "aws_iam_role_policy_attachment" "github_actions_plan_tfstate" { + role = aws_iam_role.github_actions_plan.name + policy_arn = data.aws_iam_policy.developer_tfstate.arn +} + +resource "aws_iam_role_policy_attachment" "github_actions_plan_infra_read" { + role = aws_iam_role.github_actions_plan.name + policy_arn = data.aws_iam_policy.github_actions_infra_read.arn +} diff --git a/bootstrap/outputs.tf b/bootstrap/outputs.tf index f59a70c..538299d 100644 --- a/bootstrap/outputs.tf +++ b/bootstrap/outputs.tf @@ -12,6 +12,11 @@ output "developer_tfstate_policy_arn" { } output "github_actions_role_arn" { - description = "GitHub Actions workflow에서 사용할 IAM Role ARN" + description = "GitHub Actions apply workflow에서 사용할 IAM Role ARN (main 전용)" value = aws_iam_role.github_actions.arn } + +output "github_actions_plan_role_arn" { + description = "GitHub Actions plan workflow에서 사용할 IAM Role ARN (PR 전용)" + value = aws_iam_role.github_actions_plan.arn +} From 0c7b91bae4451f93d91cdb193a49a80dd122b12f Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Mon, 4 May 2026 17:05:17 +0900 Subject: [PATCH 17/18] =?UTF-8?q?fix:=20=EC=8B=9C=ED=81=AC=EB=A6=BF?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EB=93=A4=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=8C=80=ED=95=B4=20=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=ED=94=8C=EB=A1=9C=EC=9A=B0=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=EA=B0=80=20=EC=A0=95=EC=83=81=20=EC=9E=91=EB=8F=99=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/terraform-apply.yml | 10 ++++++++++ .github/workflows/terraform-plan.yml | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/.github/workflows/terraform-apply.yml b/.github/workflows/terraform-apply.yml index 4097a05..c197776 100644 --- a/.github/workflows/terraform-apply.yml +++ b/.github/workflows/terraform-apply.yml @@ -23,6 +23,9 @@ jobs: monitoring: ${{ steps.filter.outputs.monitoring }} steps: - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} - uses: dorny/paths-filter@v3 id: filter with: @@ -32,18 +35,25 @@ jobs: global: - 'environment/global/**' - 'modules/shared_resources/**' + - 'config/secrets/shared_resources.tfvars' prod: - 'environment/prod/**' - 'modules/app_stack/**' - 'modules/common/**' + - 'config/secrets/prod.tfvars' + - 'config/secrets/app_stack.tfvars' stage: - 'environment/stage/**' - 'modules/app_stack/**' - 'modules/common/**' + - 'config/secrets/stage.tfvars' + - 'config/secrets/app_stack.tfvars' monitoring: - 'environment/monitoring/**' - 'modules/monitoring_stack/**' - 'modules/common/**' + - 'config/secrets/monitoring.tfvars' + - 'config/secrets/monitoring_stack.tfvars' apply-bootstrap: needs: detect-changes diff --git a/.github/workflows/terraform-plan.yml b/.github/workflows/terraform-plan.yml index 3eb5e10..870f6e3 100644 --- a/.github/workflows/terraform-plan.yml +++ b/.github/workflows/terraform-plan.yml @@ -24,6 +24,9 @@ jobs: monitoring: ${{ steps.filter.outputs.monitoring }} steps: - uses: actions/checkout@v4 + with: + submodules: recursive + token: ${{ secrets.GH_PAT }} - uses: dorny/paths-filter@v3 id: filter with: @@ -33,18 +36,25 @@ jobs: global: - 'environment/global/**' - 'modules/shared_resources/**' + - 'config/secrets/shared_resources.tfvars' prod: - 'environment/prod/**' - 'modules/app_stack/**' - 'modules/common/**' + - 'config/secrets/prod.tfvars' + - 'config/secrets/app_stack.tfvars' stage: - 'environment/stage/**' - 'modules/app_stack/**' - 'modules/common/**' + - 'config/secrets/stage.tfvars' + - 'config/secrets/app_stack.tfvars' monitoring: - 'environment/monitoring/**' - 'modules/monitoring_stack/**' - 'modules/common/**' + - 'config/secrets/monitoring.tfvars' + - 'config/secrets/monitoring_stack.tfvars' plan-bootstrap: needs: detect-changes From 09cc257ada444c3109400d597f8c85beedc3f1f0 Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Mon, 4 May 2026 17:24:15 +0900 Subject: [PATCH 18/18] =?UTF-8?q?fix:=20global=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EC=97=90=EC=84=9C=EC=9D=98=20s3=20=EB=B2=84=ED=82=B7=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9=20=EC=88=98=EC=A0=95=20=EC=9D=B4=EB=A0=A5?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=9C=20terraform=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=B5=9C=EC=8B=A0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/secrets | 2 +- environment/global/main.tf | 4 +- environment/global/variables.tf | 10 ---- modules/shared_resources/acm.tf | 14 ------ modules/shared_resources/cloudfront.tf | 68 +------------------------- modules/shared_resources/output.tf | 6 --- modules/shared_resources/s3.tf | 11 ----- modules/shared_resources/variables.tf | 10 ---- 8 files changed, 3 insertions(+), 122 deletions(-) diff --git a/config/secrets b/config/secrets index 26908c3..f88a84c 160000 --- a/config/secrets +++ b/config/secrets @@ -1 +1 @@ -Subproject commit 26908c36395083c99dbd1f20923d661ba8e0567e +Subproject commit f88a84cdab72136d294614fd1e2c855c4a026c43 diff --git a/environment/global/main.tf b/environment/global/main.tf index ea785f0..5701eb6 100644 --- a/environment/global/main.tf +++ b/environment/global/main.tf @@ -5,7 +5,6 @@ module "shared_resources" { aws = aws } - s3_default_bucket_name = var.s3_default_bucket_name s3_upload_bucket_name = var.s3_upload_bucket_name resizing_img_func_name = var.resizing_img_func_name @@ -20,6 +19,5 @@ module "shared_resources" { thumbnail_generating_func_runtime = var.thumbnail_generating_func_runtime thumbnail_generating_func_layers = var.thumbnail_generating_func_layers - default_cdn_web_acl_id = var.default_cdn_web_acl_id upload_cdn_web_acl_id = var.upload_cdn_web_acl_id -} \ No newline at end of file +} diff --git a/environment/global/variables.tf b/environment/global/variables.tf index 58d89e5..b994d24 100644 --- a/environment/global/variables.tf +++ b/environment/global/variables.tf @@ -1,9 +1,4 @@ # [S3 버킷 관련 변수] -variable "s3_default_bucket_name" { - description = "Name of the default S3 bucket" - type = string -} - variable "s3_upload_bucket_name" { description = "Name of the upload S3 bucket" type = string @@ -60,11 +55,6 @@ variable "thumbnail_generating_func_layers" { type = list(string) } -variable "default_cdn_web_acl_id" { - description = "WAF Web ACL Id for Default Cloudfront CDN" - type = string -} - variable "upload_cdn_web_acl_id" { description = "WAF Web ACL Id for Upload Cloudfront CDN" type = string diff --git a/modules/shared_resources/acm.tf b/modules/shared_resources/acm.tf index 9dc9b66..af78dfd 100644 --- a/modules/shared_resources/acm.tf +++ b/modules/shared_resources/acm.tf @@ -1,17 +1,3 @@ -resource "aws_acm_certificate" "default_cdn_cert" { - provider = aws.virginia - domain_name = "cdn.default.solid-connection.com" - validation_method = "DNS" - - tags = { - Name = "cdn-default-solid-connection-cert" - } - - lifecycle { - create_before_destroy = true - } -} - resource "aws_acm_certificate" "upload_cdn_cert" { provider = aws.virginia domain_name = "cdn.upload.solid-connection.com" diff --git a/modules/shared_resources/cloudfront.tf b/modules/shared_resources/cloudfront.tf index a6a75a0..0fb1d95 100644 --- a/modules/shared_resources/cloudfront.tf +++ b/modules/shared_resources/cloudfront.tf @@ -1,22 +1,9 @@ # 0. S3 bucket Information read (Data Source) -data "aws_s3_bucket" "default" { - bucket = var.s3_default_bucket_name -} - data "aws_s3_bucket" "upload" { bucket = var.s3_upload_bucket_name } # 1. OAC (Origin Access Control) 리소스 정의 -# 하드코딩된 ID 대신, 테라폼 리소스로 관리하여 ID를 동적으로 참조합니다. -resource "aws_cloudfront_origin_access_control" "default_oac" { - name = "default-oac-${var.s3_default_bucket_name}" - description = "OAC for Default Bucket" - origin_access_control_origin_type = "s3" - signing_behavior = "always" - signing_protocol = "sigv4" -} - resource "aws_cloudfront_origin_access_control" "upload_oac" { name = "upload-oac-${var.s3_upload_bucket_name}" description = "OAC for Upload Bucket" @@ -25,60 +12,7 @@ resource "aws_cloudfront_origin_access_control" "upload_oac" { signing_protocol = "sigv4" } -# 2. CDN for Default Bucket -resource "aws_cloudfront_distribution" "default_cdn" { - enabled = true - is_ipv6_enabled = true - comment = "solid-connection s3 default cloudfront" - price_class = "PriceClass_All" - http_version = "http2" - - web_acl_id = var.default_cdn_web_acl_id - - tags = { - "Name" = "solid-connection s3 default cloudfront" - } - - aliases = [aws_acm_certificate.default_cdn_cert.domain_name] - - origin { - domain_name = data.aws_s3_bucket.default.bucket_regional_domain_name - origin_id = "S3-${var.s3_default_bucket_name}" - origin_access_control_id = aws_cloudfront_origin_access_control.default_oac.id - - connection_attempts = 3 - connection_timeout = 10 - } - - default_cache_behavior { - target_origin_id = "S3-${var.s3_default_bucket_name}" # 위 origin_id와 같아야 함 - viewer_protocol_policy = "redirect-to-https" - compress = true - - allowed_methods = ["GET", "HEAD"] - cached_methods = ["GET", "HEAD"] - - cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6" - - smooth_streaming = false - } - - restrictions { - geo_restriction { - restriction_type = "none" - locations = [] - } - } - - viewer_certificate { - cloudfront_default_certificate = false - acm_certificate_arn = aws_acm_certificate.default_cdn_cert.arn - minimum_protocol_version = "TLSv1.2_2021" - ssl_support_method = "sni-only" - } -} - -# 3. CDN for Upload Bucket +# 2. CDN for Upload Bucket resource "aws_cloudfront_distribution" "upload_cdn" { enabled = true is_ipv6_enabled = true diff --git a/modules/shared_resources/output.tf b/modules/shared_resources/output.tf index 44c0c10..b771123 100644 --- a/modules/shared_resources/output.tf +++ b/modules/shared_resources/output.tf @@ -1,12 +1,6 @@ output "acm_validation_records" { description = "Cloudflare에 등록해야 할 인증서 검증용 CNAME 값들 (Proxy Off 필수!)" value = { - default_cdn = { - for dvo in aws_acm_certificate.default_cdn_cert.domain_validation_options : dvo.domain_name => { - name = dvo.resource_record_name - record = dvo.resource_record_value - } - } upload_cdn = { for dvo in aws_acm_certificate.upload_cdn_cert.domain_validation_options : dvo.domain_name => { name = dvo.resource_record_name diff --git a/modules/shared_resources/s3.tf b/modules/shared_resources/s3.tf index 9a158ab..79b9e89 100644 --- a/modules/shared_resources/s3.tf +++ b/modules/shared_resources/s3.tf @@ -1,15 +1,4 @@ # 8. S3 Buckets -resource "aws_s3_bucket" "default" { - bucket = var.s3_default_bucket_name - - force_destroy = false - - lifecycle { - prevent_destroy = true - ignore_changes = [tags_all] - } -} - resource "aws_s3_bucket" "upload" { bucket = var.s3_upload_bucket_name diff --git a/modules/shared_resources/variables.tf b/modules/shared_resources/variables.tf index 7d78475..8cb35ab 100644 --- a/modules/shared_resources/variables.tf +++ b/modules/shared_resources/variables.tf @@ -1,9 +1,4 @@ # [S3 버킷 관련 변수] -variable "s3_default_bucket_name" { - description = "Name of the default S3 bucket" - type = string -} - variable "s3_upload_bucket_name" { description = "Name of the upload S3 bucket" type = string @@ -60,11 +55,6 @@ variable "thumbnail_generating_func_layers" { type = list(string) } -variable "default_cdn_web_acl_id" { - description = "WAF Web ACL Id for Default Cloudfront CDN" - type = string -} - variable "upload_cdn_web_acl_id" { description = "WAF Web ACL Id for Upload Cloudfront CDN" type = string