diff --git a/.gitignore b/.gitignore index e986fe5..75e2a10 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,20 @@ # OS 파일 .DS_Store -Thumbs.db \ No newline at end of file +Thumbs.db + +# Terraform / OpenTofu +**/.terraform/* +*.tfstate +*.tfstate.* +crash.log +crash.*.log +*.tfvars +*.tfvars.json +override.tf +override.tf.json +*_override.tf +*_override.tf.json +.terraformrc +terraform.rc +.terraform.lock.hcl \ No newline at end of file diff --git a/terraform/README.md b/terraform/README.md new file mode 100644 index 0000000..320ee5e --- /dev/null +++ b/terraform/README.md @@ -0,0 +1,80 @@ +# Terraform (IaC) — 현재 인프라 스냅샷 + +운영 중인 AWS 인프라를 [terraformer](https://github.com/GoogleCloudPlatform/terraformer)로 떠낸 **초기 스냅샷**이다. +이 디렉터리는 brownfield IaC 전환의 시작점으로, 아직 모듈화/리팩터링 전의 자동 생성 코드다. + +> ⚠️ 이 코드는 **아직 실물과 state로 연결되지 않았다.** `tofu plan`이 "No changes"가 되도록 +> 정합성을 맞추는 작업은 후속 단계에서 진행한다. (issue 후속) + +## 도구 + +| 도구 | 버전 | 비고 | +| --- | --- | --- | +| OpenTofu | 1.12.3 | IaC 엔진 (`tofu`) | +| terraformer | 0.8.30 | 기존 인프라 export (upstream archived, deprecated) | +| AWS provider | hashicorp/aws ~> 5.0 | | +| Region | `ap-northeast-2` | 서울 | + +## 디렉터리 구조 + +``` +terraform/ +├── provider.tf # OpenTofu + AWS provider 선언 +└── generated/aws/ # terraformer 자동 생성 (서비스별 분리) + ├── vpc/ subnet/ route_table/ igw/ eip/ nacl/ # 네트워크 + ├── ec2_instance/ eni/ sg/ # 컴퓨트 + 보안그룹 + ├── route53/ # DNS + └── iam/ # LinClean 관련 IAM만 +``` + +## 스냅샷 범위 (50개 리소스) + +| 서비스 | 개수 | 서비스 | 개수 | +| --- | --- | --- | --- | +| iam | 8 (정리 후) | route_table | 4 | +| route53 | 9 | sg | 3 | +| ec2_instance | 2 (app·db) | eni | 2 | +| subnet | 5 | vpc/igw/eip/nacl | 각 1 | + +## 재현 방법 + +```bash +cd terraform +tofu init # AWS provider 다운로드 + +# terraformer가 provider를 ~/.terraform.d/plugins 에서 찾으므로 바이너리 연결 필요 +mkdir -p ~/.terraform.d/plugins/darwin_arm64 +cp .terraform/providers/registry.opentofu.org/hashicorp/aws/*/darwin_arm64/terraform-provider-aws \ + ~/.terraform.d/plugins/darwin_arm64/terraform-provider-aws_v5.100.0_x5 + +# EC2 계열은 AWS_REGION 환경변수가 있어야 aws-global 오해석을 피함 +AWS_REGION=ap-northeast-2 terraformer import aws \ + --resources=vpc,subnet,route_table,igw,eip,ec2_instance,eni,sg,nacl,route53 \ + --regions=ap-northeast-2 --connect=true + +# iam/route53 는 글로벌 서비스라 별도 실행 (AWS_REGION 불필요) +terraformer import aws --resources=iam,route53 --regions=ap-northeast-2 +``` + +## 알려진 한계 / 후속 작업 + +- [ ] **state 정합성**: 각 서비스별 분리된 tfstate를 통합하고 `tofu plan` "No changes" 검증 +- [ ] **모듈화**: `tfer--` prefix의 자동 생성 코드를 의미 있는 모듈(network / compute / iam ...)로 리팩터링 +- [ ] **OIDC provider 누락**: terraformer가 `aws_iam_openid_connect_provider`(GitHub Actions OIDC)를 + 리소스로 export하지 못함. `github-actions-deploy` 역할의 trust policy에 ARN으로만 참조됨 → 수동 import 필요 +- [ ] **SSM Parameter Store**: SecureString 값이 평문 노출되므로 스냅샷에서 의도적으로 제외함. + 값이 아닌 "껍데기/참조" 형태로 별도 관리 필요 +- [ ] **IAM 정리됨**: terraformer는 계정 전체 IAM을 export하므로, LinClean 무관 사용자/access key/ + 서비스 연결 역할은 제거하고 `github-actions-deploy`·`linclean-ec2-role` 관련만 남김 +- [ ] **레거시 provider 주소**: terraformer가 `required_providers`에 `source`를 넣지 않은 구형(TF 0.12) + 형식으로 생성 → 현재 상태로는 `tofu init`이 실패함. 모듈화 시 루트 provider로 통합하며 해결 +- [ ] **provider region/version 불일치**: 글로벌 서비스(iam/route53) provider.tf는 `us-east-1`, + 나머지는 `ap-northeast-2`. 버전 핀도 generated는 `~> 5.100.0`, 루트는 `~> 5.0`. 통합 시 일원화 + (글로벌 리소스라 동작엔 영향 없음 — 모듈화 때 정리) + +## 주의 + +- `*.tfstate`, `.terraform/`, `*.tfvars`, `.terraform.lock.hcl` 은 `.gitignore` 처리됨 (민감정보 보호). +- **계정 ID 비노출**: 공개 레포라 IAM 정책/역할 ARN의 계정 ID를 평문 대신 + `data.aws_caller_identity.current.account_id`로 동적 참조한다 (`iam/data.tf`). +- 자동 생성 코드의 리소스 이름(`tfer--...`)은 후속 리팩터링에서 정리한다. diff --git a/terraform/generated/aws/ec2_instance/ap-northeast-2/provider.tf b/terraform/generated/aws/ec2_instance/ap-northeast-2/provider.tf new file mode 100755 index 0000000..5c046f7 --- /dev/null +++ b/terraform/generated/aws/ec2_instance/ap-northeast-2/provider.tf @@ -0,0 +1,11 @@ +provider "aws" { + region = "ap-northeast-2" +} + +terraform { + required_providers { + aws = { + version = "~> 5.100.0" + } + } +} diff --git a/terraform/generated/aws/ec2_instance/ap-northeast-2/variables.tf b/terraform/generated/aws/ec2_instance/ap-northeast-2/variables.tf new file mode 100755 index 0000000..2ff7467 --- /dev/null +++ b/terraform/generated/aws/ec2_instance/ap-northeast-2/variables.tf @@ -0,0 +1,23 @@ +data "terraform_remote_state" "ebs" { + backend = "local" + + config = { + path = "../../../../generated/aws/ebs/ap-northeast-2/terraform.tfstate" + } +} + +data "terraform_remote_state" "sg" { + backend = "local" + + config = { + path = "../../../../generated/aws/sg/ap-northeast-2/terraform.tfstate" + } +} + +data "terraform_remote_state" "subnet" { + backend = "local" + + config = { + path = "../../../../generated/aws/subnet/ap-northeast-2/terraform.tfstate" + } +} diff --git a/terraform/generated/aws/ec2_instance/instance.tf b/terraform/generated/aws/ec2_instance/instance.tf new file mode 100755 index 0000000..6e5b2e8 --- /dev/null +++ b/terraform/generated/aws/ec2_instance/instance.tf @@ -0,0 +1,171 @@ +resource "aws_instance" "tfer--i-0175957e300c3ccd9_linclean-app" { + ami = "ami-0596f7562954deb8e" + associate_public_ip_address = "true" + availability_zone = "ap-northeast-2a" + + capacity_reservation_specification { + capacity_reservation_preference = "open" + } + + cpu_core_count = "1" + + cpu_options { + core_count = "1" + threads_per_core = "2" + } + + cpu_threads_per_core = "2" + + credit_specification { + cpu_credits = "unlimited" + } + + disable_api_stop = "false" + disable_api_termination = "false" + ebs_optimized = "true" + + enclave_options { + enabled = "false" + } + + get_password_data = "false" + hibernation = "false" + iam_instance_profile = "linclean-ec2-role" + instance_initiated_shutdown_behavior = "stop" + instance_type = "t3.small" + ipv6_address_count = "0" + key_name = "linclean-key" + + maintenance_options { + auto_recovery = "default" + } + + metadata_options { + http_endpoint = "enabled" + http_protocol_ipv6 = "disabled" + http_put_response_hop_limit = "1" + http_tokens = "optional" + instance_metadata_tags = "disabled" + } + + monitoring = "false" + placement_partition_number = "0" + + private_dns_name_options { + enable_resource_name_dns_a_record = "false" + enable_resource_name_dns_aaaa_record = "false" + hostname_type = "ip-name" + } + + private_ip = "172.31.10.100" + + root_block_device { + delete_on_termination = "true" + encrypted = "false" + iops = "3000" + throughput = "125" + volume_size = "30" + volume_type = "gp3" + } + + security_groups = ["app-sg"] + source_dest_check = "false" + subnet_id = "${data.terraform_remote_state.subnet.outputs.aws_subnet_tfer--subnet-00b08863bb393665d_id}" + + tags = { + Name = "linclean-app" + } + + tags_all = { + Name = "linclean-app" + } + + tenancy = "default" + vpc_security_group_ids = ["${data.terraform_remote_state.sg.outputs.aws_security_group_tfer--app-sg_sg-01eb4b595c6303b33_id}"] +} + +resource "aws_instance" "tfer--i-087e8229818de4182_linclean-db" { + ami = "ami-0596f7562954deb8e" + associate_public_ip_address = "false" + availability_zone = "ap-northeast-2a" + + capacity_reservation_specification { + capacity_reservation_preference = "open" + } + + cpu_core_count = "1" + + cpu_options { + core_count = "1" + threads_per_core = "2" + } + + cpu_threads_per_core = "2" + + credit_specification { + cpu_credits = "unlimited" + } + + disable_api_stop = "false" + disable_api_termination = "false" + ebs_optimized = "true" + + enclave_options { + enabled = "false" + } + + get_password_data = "false" + hibernation = "false" + iam_instance_profile = "linclean-ec2-role" + instance_initiated_shutdown_behavior = "stop" + instance_type = "t3.micro" + ipv6_address_count = "0" + key_name = "linclean-key" + + maintenance_options { + auto_recovery = "default" + } + + metadata_options { + http_endpoint = "enabled" + http_protocol_ipv6 = "disabled" + http_put_response_hop_limit = "1" + http_tokens = "optional" + instance_metadata_tags = "disabled" + } + + monitoring = "false" + placement_partition_number = "0" + + private_dns_name_options { + enable_resource_name_dns_a_record = "false" + enable_resource_name_dns_aaaa_record = "false" + hostname_type = "ip-name" + } + + private_ip = "172.31.67.93" + + root_block_device { + delete_on_termination = "true" + encrypted = "false" + iops = "3000" + throughput = "125" + volume_size = "30" + volume_type = "gp3" + } + + security_groups = ["db-sg"] + source_dest_check = "true" + subnet_id = "${data.terraform_remote_state.subnet.outputs.aws_subnet_tfer--subnet-0632d2f0a805011b3_id}" + + tags = { + Name = "linclean-db" + } + + tags_all = { + Name = "linclean-db" + } + + tenancy = "default" + vpc_security_group_ids = ["${data.terraform_remote_state.sg.outputs.aws_security_group_tfer--db-sg_sg-01d7376ba980bf2e6_id}"] +} diff --git a/terraform/generated/aws/ec2_instance/outputs.tf b/terraform/generated/aws/ec2_instance/outputs.tf new file mode 100755 index 0000000..972d8c7 --- /dev/null +++ b/terraform/generated/aws/ec2_instance/outputs.tf @@ -0,0 +1,7 @@ +output "aws_instance_tfer--i-0175957e300c3ccd9_linclean-app_id" { + value = "${aws_instance.tfer--i-0175957e300c3ccd9_linclean-app.id}" +} + +output "aws_instance_tfer--i-087e8229818de4182_linclean-db_id" { + value = "${aws_instance.tfer--i-087e8229818de4182_linclean-db.id}" +} diff --git a/terraform/generated/aws/ec2_instance/provider.tf b/terraform/generated/aws/ec2_instance/provider.tf new file mode 100755 index 0000000..5c046f7 --- /dev/null +++ b/terraform/generated/aws/ec2_instance/provider.tf @@ -0,0 +1,11 @@ +provider "aws" { + region = "ap-northeast-2" +} + +terraform { + required_providers { + aws = { + version = "~> 5.100.0" + } + } +} diff --git a/terraform/generated/aws/ec2_instance/variables.tf b/terraform/generated/aws/ec2_instance/variables.tf new file mode 100755 index 0000000..06a0d27 --- /dev/null +++ b/terraform/generated/aws/ec2_instance/variables.tf @@ -0,0 +1,23 @@ +data "terraform_remote_state" "ebs" { + backend = "local" + + config = { + path = "../../../generated/aws/ebs/terraform.tfstate" + } +} + +data "terraform_remote_state" "sg" { + backend = "local" + + config = { + path = "../../../generated/aws/sg/terraform.tfstate" + } +} + +data "terraform_remote_state" "subnet" { + backend = "local" + + config = { + path = "../../../generated/aws/subnet/terraform.tfstate" + } +} diff --git a/terraform/generated/aws/eip/ap-northeast-2/provider.tf b/terraform/generated/aws/eip/ap-northeast-2/provider.tf new file mode 100755 index 0000000..5c046f7 --- /dev/null +++ b/terraform/generated/aws/eip/ap-northeast-2/provider.tf @@ -0,0 +1,11 @@ +provider "aws" { + region = "ap-northeast-2" +} + +terraform { + required_providers { + aws = { + version = "~> 5.100.0" + } + } +} diff --git a/terraform/generated/aws/eip/eip.tf b/terraform/generated/aws/eip/eip.tf new file mode 100755 index 0000000..2d673e2 --- /dev/null +++ b/terraform/generated/aws/eip/eip.tf @@ -0,0 +1,17 @@ +resource "aws_eip" "tfer--eipalloc-02a4533b598a1859a" { + domain = "vpc" + instance = "i-0175957e300c3ccd9" + network_border_group = "ap-northeast-2" + network_interface = "eni-01e3526fd0a19ee2d" + public_ipv4_pool = "amazon" + + tags = { + Name = "linclean-app-eip" + } + + tags_all = { + Name = "linclean-app-eip" + } + + vpc = "true" +} diff --git a/terraform/generated/aws/eip/outputs.tf b/terraform/generated/aws/eip/outputs.tf new file mode 100755 index 0000000..2aa5fb5 --- /dev/null +++ b/terraform/generated/aws/eip/outputs.tf @@ -0,0 +1,3 @@ +output "aws_eip_tfer--eipalloc-02a4533b598a1859a_id" { + value = "${aws_eip.tfer--eipalloc-02a4533b598a1859a.id}" +} diff --git a/terraform/generated/aws/eip/provider.tf b/terraform/generated/aws/eip/provider.tf new file mode 100755 index 0000000..5c046f7 --- /dev/null +++ b/terraform/generated/aws/eip/provider.tf @@ -0,0 +1,11 @@ +provider "aws" { + region = "ap-northeast-2" +} + +terraform { + required_providers { + aws = { + version = "~> 5.100.0" + } + } +} diff --git a/terraform/generated/aws/eni/ap-northeast-2/provider.tf b/terraform/generated/aws/eni/ap-northeast-2/provider.tf new file mode 100755 index 0000000..5c046f7 --- /dev/null +++ b/terraform/generated/aws/eni/ap-northeast-2/provider.tf @@ -0,0 +1,11 @@ +provider "aws" { + region = "ap-northeast-2" +} + +terraform { + required_providers { + aws = { + version = "~> 5.100.0" + } + } +} diff --git a/terraform/generated/aws/eni/network_interface.tf b/terraform/generated/aws/eni/network_interface.tf new file mode 100755 index 0000000..320ceb3 --- /dev/null +++ b/terraform/generated/aws/eni/network_interface.tf @@ -0,0 +1,37 @@ +resource "aws_network_interface" "tfer--eni-004ca4335ea1baf2c" { + attachment { + device_index = "0" + instance = "i-087e8229818de4182" + } + + interface_type = "interface" + ipv4_prefix_count = "0" + ipv6_address_count = "0" + ipv6_prefix_count = "0" + private_ip = "172.31.67.93" + private_ip_list = ["172.31.67.93"] + private_ips = ["172.31.67.93"] + private_ips_count = "0" + security_groups = ["sg-01d7376ba980bf2e6"] + source_dest_check = "true" + subnet_id = "subnet-0632d2f0a805011b3" +} + +resource "aws_network_interface" "tfer--eni-01e3526fd0a19ee2d" { + attachment { + device_index = "0" + instance = "i-0175957e300c3ccd9" + } + + interface_type = "interface" + ipv4_prefix_count = "0" + ipv6_address_count = "0" + ipv6_prefix_count = "0" + private_ip = "172.31.10.100" + private_ip_list = ["172.31.10.100"] + private_ips = ["172.31.10.100"] + private_ips_count = "0" + security_groups = ["sg-01eb4b595c6303b33"] + source_dest_check = "false" + subnet_id = "subnet-00b08863bb393665d" +} diff --git a/terraform/generated/aws/eni/outputs.tf b/terraform/generated/aws/eni/outputs.tf new file mode 100755 index 0000000..7a418a5 --- /dev/null +++ b/terraform/generated/aws/eni/outputs.tf @@ -0,0 +1,7 @@ +output "aws_network_interface_tfer--eni-004ca4335ea1baf2c_id" { + value = "${aws_network_interface.tfer--eni-004ca4335ea1baf2c.id}" +} + +output "aws_network_interface_tfer--eni-01e3526fd0a19ee2d_id" { + value = "${aws_network_interface.tfer--eni-01e3526fd0a19ee2d.id}" +} diff --git a/terraform/generated/aws/eni/provider.tf b/terraform/generated/aws/eni/provider.tf new file mode 100755 index 0000000..5c046f7 --- /dev/null +++ b/terraform/generated/aws/eni/provider.tf @@ -0,0 +1,11 @@ +provider "aws" { + region = "ap-northeast-2" +} + +terraform { + required_providers { + aws = { + version = "~> 5.100.0" + } + } +} diff --git a/terraform/generated/aws/iam/data.tf b/terraform/generated/aws/iam/data.tf new file mode 100644 index 0000000..f9f298d --- /dev/null +++ b/terraform/generated/aws/iam/data.tf @@ -0,0 +1,2 @@ +# 공개 레포에 계정 ID 평문 노출을 피하기 위해 ARN의 account_id 부분을 동적으로 참조한다. +data "aws_caller_identity" "current" {} diff --git a/terraform/generated/aws/iam/iam_instance_profile.tf b/terraform/generated/aws/iam/iam_instance_profile.tf new file mode 100755 index 0000000..02be073 --- /dev/null +++ b/terraform/generated/aws/iam/iam_instance_profile.tf @@ -0,0 +1,5 @@ +resource "aws_iam_instance_profile" "tfer--linclean-ec2-role" { + name = "linclean-ec2-role" + path = "/" + role = "linclean-ec2-role" +} diff --git a/terraform/generated/aws/iam/iam_policy.tf b/terraform/generated/aws/iam/iam_policy.tf new file mode 100755 index 0000000..3c1d48e --- /dev/null +++ b/terraform/generated/aws/iam/iam_policy.tf @@ -0,0 +1,56 @@ +resource "aws_iam_policy" "tfer--linclean-deploy-ssm" { + name = "linclean-deploy-ssm" + path = "/" + + policy = <