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\`\nShow 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\`\nShow 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\`\nShow 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\`\nShow 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\`\nShow 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\`\nShow 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\`\nShow 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\`\nShow 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\`\nShow 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\`\nShow 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