diff --git a/ansible/README.md b/ansible/README.md index 56b2307..8a661c9 100644 --- a/ansible/README.md +++ b/ansible/README.md @@ -53,14 +53,17 @@ If Redis must be accessed from another host later, prefer ElastiCache. If tempor `cloudflared` is managed as a Docker Compose service on the app host. -Required runtime secret: +Required runtime inputs: -- `cloudflared_tunnel_token`: Cloudflare Tunnel token. Inject via Ansible Vault, inventory secrets, or `-e`; never commit the real value. +- `cloudflared_tunnel_token`: Cloudflare Tunnel token. Inject via Ansible Vault, inventory secrets, environment variable, or `-e`; never commit the real value. +- `cloudflared_hostname`: Public hostname. Inject via `MOA_PUBLIC_HOSTNAME`, inventory, or `-e`; do not hardcode environment-specific domains in the role. Run only this role: ```bash -ansible-playbook ansible/playbooks/site.yml --tags cloudflared -e "cloudflared_tunnel_token=..." +export MOA_PUBLIC_HOSTNAME=... +export CLOUDFLARED_TUNNEL_TOKEN=... +ansible-playbook ansible/playbooks/site.yml --tags cloudflared ``` -Cloudflare DNS/ingress is expected to point public hostnames, such as temporary `*.yeoun.org` records, at the Cloudflare Tunnel and then reverse proxy to the EC2 origin. +Cloudflare DNS/ingress is expected to point the configured public hostname at the Cloudflare Tunnel and then reverse proxy to the EC2 origin. diff --git a/ansible/roles/cloudflared/defaults/main.yml b/ansible/roles/cloudflared/defaults/main.yml index d986241..b887b6a 100644 --- a/ansible/roles/cloudflared/defaults/main.yml +++ b/ansible/roles/cloudflared/defaults/main.yml @@ -1,12 +1,13 @@ --- -# Cloudflare Tunnel role defaults. Secrets must be injected from inventory, vault, or -e. +# Cloudflare Tunnel role defaults. Secrets and environment-specific hostnames +# must be injected from inventory, vault, environment, or -e. cloudflared_project_dir: /opt/moa/cloudflared cloudflared_container_name: moa-cloudflared cloudflared_image: cloudflare/cloudflared:latest cloudflared_restart_policy: unless-stopped cloudflared_origin_url: http://localhost:8080 -cloudflared_hostname: moa.yeoun.org +cloudflared_hostname: "{{ lookup('env', 'MOA_PUBLIC_HOSTNAME') }}" cloudflared_network_mode: host # Required secret at runtime. Do not commit the real token. -cloudflared_tunnel_token: "" +cloudflared_tunnel_token: "{{ lookup('env', 'CLOUDFLARED_TUNNEL_TOKEN') }}" diff --git a/ansible/roles/cloudflared/tasks/main.yml b/ansible/roles/cloudflared/tasks/main.yml index 677613a..bbde02c 100644 --- a/ansible/roles/cloudflared/tasks/main.yml +++ b/ansible/roles/cloudflared/tasks/main.yml @@ -1,12 +1,15 @@ --- -- name: Validate Cloudflare Tunnel token is provided +- name: Validate Cloudflare Tunnel runtime inputs are provided ansible.builtin.assert: that: - cloudflared_tunnel_token is defined - cloudflared_tunnel_token | length > 0 + - cloudflared_hostname is defined + - cloudflared_hostname | length > 0 fail_msg: >- - cloudflared_tunnel_token is required. Inject it through Ansible Vault, - inventory secrets, or -e; do not commit it to git. + cloudflared_tunnel_token and cloudflared_hostname are required. Inject + them through Ansible Vault, inventory, environment variables, or -e; do + not commit secrets to git. no_log: true - name: Create cloudflared project directory diff --git a/terraform/environments/dev/.terraform.lock.hcl b/terraform/environments/dev/.terraform.lock.hcl index f147702..39f6935 100644 --- a/terraform/environments/dev/.terraform.lock.hcl +++ b/terraform/environments/dev/.terraform.lock.hcl @@ -1,11 +1,35 @@ # This file is maintained automatically by "terraform init". # Manual edits may be lost in future updates. +provider "registry.terraform.io/cloudflare/cloudflare" { + version = "4.52.7" + constraints = "~> 4.52" + hashes = [ + "h1:y3rV8KF2q6GEMANNlf5EkKJurlfbKlIKpjGcdxoy7pQ=", + "zh:0c904ce31a4c6c4a5b3bf7ff1560e77c0cc7e2450c8553ded8e8c90398e1418b", + "zh:36183d310c36373fe4cb936b83c595c6fd3b0a94bc7827f28e5789ccbf59752e", + "zh:556a568a6f0235e8f41647de9e4d3a1e7b1d6502df8b19b54ec441f1c653ea10", + "zh:633ebbd5b0245e75e500ef9be4d9e62288f97e8da3baaa51323892a786d90285", + "zh:6acfe60cf52a65ba8f044f748548d2119e7f4fd7f8ebcb14698960d87c68f529", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:904acc31ebb9d6ef68c792074b30532ee61bf515f19e0a3c75b46f126cca1f13", + "zh:a1d0a81246afc8750286d3f6fe7a8fbe6460dd2662407b28dbfbabb612e5fa9d", + "zh:a41a36fe253fc365fe2b7ffc749624688b2693b4634862fda161179ab100029f", + "zh:a7ef269e77ffa8715c8945a2c14322c7ff159ea44c15f62505f3cbb2cae3b32d", + "zh:b01aa3bed30610633b762df64332b26f8844a68c3960cebcb30f04918efc67fe", + "zh:b069cc2cd18cae10757df3ae030508eac8d55de7e49eda7a5e3e11f2f7fe6455", + "zh:b2d2c6313729ebb7465dceece374049e2d08bda34473901be9ff46a8836d42b2", + "zh:db0e114edaf4bc2f3d4769958807c83022bfbc619a00bdf4c4bd17faa4ab2d8b", + "zh:ecc0aa8b9044f664fd2aaf8fa992d976578f78478980555b4b8f6148e8d1a5fe", + ] +} + provider "registry.terraform.io/hashicorp/aws" { version = "5.100.0" constraints = "~> 5.0" hashes = [ "h1:edXOJWE4ORX8Fm+dpVpICzMZJat4AX0VRCAy/xkcOc0=", + "h1:wOhTPz6apLBuF7/FYZuCoXRK/MLgrNprZ3vXmq83g5k=", "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", @@ -28,6 +52,7 @@ provider "registry.terraform.io/hashicorp/local" { version = "2.9.0" constraints = "~> 2.5" hashes = [ + "h1:19szYap0j+IaX3fdQkVlT/xXEAkyXZGoabKsK7RdLWU=", "h1:9rBZCMNpxKwMlRbWH2QpwD3kqUCAejdOZQ/aiiDObXQ=", "zh:0baa4566cf77f1ff52f4293d1c8536202dd23edc197c3196413a28343c3ac3a0", "zh:16b5559c3c07088ddad11a9bb9e9c0799999363c2958e9a5be2bcbbf2cd9ca64", @@ -50,6 +75,7 @@ provider "registry.terraform.io/hashicorp/random" { constraints = "~> 3.6" hashes = [ "h1:UlBuNVuCGJ39tTv2c5gz2NRZnQbXfbIWbTzWcth5o74=", + "h1:lVDv+0AjDjrLfpmaJbWqUmIw/k3/AHXLc3N4m55SNdo=", "zh:161ad0bd9a75768c82f53fb6e7172a9d8be2d4889b012645a34795031aaf1bf1", "zh:19dc9a5b17729725ccfc4f45b0500af0ee5bc6b6b160c7adb8f2bf617d2c80ea", "zh:269eda8fe42daa7974d5a34d166c3ba9defe80cde86c01e4dadcfdf2e1f05e5f", @@ -70,6 +96,7 @@ provider "registry.terraform.io/hashicorp/tls" { version = "4.3.0" constraints = "~> 4.0" hashes = [ + "h1:QO1mfwRWprT41JDGRUfpIYyFs8h63yeOsB8RfKOjNYU=", "h1:j/BqLS2N2AScZyotd9nZpHdieJ7e5S8y+A+ZfIu8kL8=", "zh:0ab58d6f8991d436c7d2dbd89ed814709b949b07ac5a54ee53b0aec1fa772a8b", "zh:60b347abcb56f45d97c56f14d895069cd15a83993f199777f571b79fea3642ee", diff --git a/terraform/environments/dev/CLOUDFLARE.md b/terraform/environments/dev/CLOUDFLARE.md new file mode 100644 index 0000000..a765361 --- /dev/null +++ b/terraform/environments/dev/CLOUDFLARE.md @@ -0,0 +1,76 @@ +# Cloudflare Terraform 운영 메모 + +MOA dev의 public hostname과 Cloudflare Tunnel은 Terraform에서 관리한다. + +## 인증/변수 주입 + +Cloudflare API token은 코드에 하드코딩하지 않는다. + +```bash +export CLOUDFLARE_API_TOKEN=... +``` + +도메인명/호스트명도 Terraform 변수 기본값에 박지 않고 환경변수로 주입한다. + +```bash +export TF_VAR_cloudflare_account_id=... +export TF_VAR_cloudflare_zone_name=... +export TF_VAR_cloudflare_hostname=... +``` + +나머지 환경별 값도 필요하면 같은 방식으로 바꿔 끼운다. + +```bash +export TF_VAR_cloudflare_tunnel_name=... +export TF_VAR_cloudflare_origin_service=... +``` + +`terraform.tfvars`를 쓰는 경우에도 실제 도메인 값은 커밋하지 말고 환경별 로컬/CI secret 파일로 분리한다. + +## 새로 만드는 경우 + +```bash +cd terraform/environments/dev +terraform init +terraform plan +terraform apply +terraform output -raw cloudflare_tunnel_token +``` + +출력된 tunnel token은 Ansible의 `cloudflared_tunnel_token`으로 주입한다. 토큰 값은 커밋하지 않는다. + +Ansible 쪽 runtime hostname도 환경변수 또는 extra-var로 주입한다. + +```bash +export MOA_PUBLIC_HOSTNAME=... +export CLOUDFLARED_TUNNEL_TOKEN=... +ansible-playbook ansible/playbooks/site.yml --tags cloudflared +``` + +또는: + +```bash +ansible-playbook ansible/playbooks/site.yml \ + --tags cloudflared \ + -e "cloudflared_hostname=$MOA_PUBLIC_HOSTNAME cloudflared_tunnel_token=$CLOUDFLARED_TUNNEL_TOKEN" +``` + +## 이미 Cloudflare에서 만든 리소스를 Terraform으로 가져오는 경우 + +기존 리소스를 새로 만들지 않으려면 먼저 import한다. + +```bash +terraform import cloudflare_zero_trust_tunnel_cloudflared.moa / +terraform import cloudflare_record.moa_hostname / +``` + +그 다음 `terraform plan`으로 drift를 확인한다. + +## 교체가 쉬운 지점 + +- 도메인 교체: `TF_VAR_cloudflare_zone_name`, `TF_VAR_cloudflare_hostname` +- Tunnel 이름 교체: `TF_VAR_cloudflare_tunnel_name` +- origin 교체: `TF_VAR_cloudflare_origin_service` +- 계정 교체: `TF_VAR_cloudflare_account_id` +- API token 교체: `CLOUDFLARE_API_TOKEN` +- Ansible hostname 교체: `MOA_PUBLIC_HOSTNAME` 또는 `cloudflared_hostname` diff --git a/terraform/environments/dev/cloudflare.tf b/terraform/environments/dev/cloudflare.tf new file mode 100644 index 0000000..e92951f --- /dev/null +++ b/terraform/environments/dev/cloudflare.tf @@ -0,0 +1,43 @@ +# Cloudflare DNS + Tunnel routing for MOA dev. +# +# 인증 토큰은 코드/vars에 박지 않는다. +# 로컬/CI에서 CLOUDFLARE_API_TOKEN 환경변수로 주입한다. + +data "cloudflare_zone" "moa" { + name = var.cloudflare_zone_name +} + +resource "random_id" "cloudflare_tunnel_secret" { + byte_length = 32 +} + +resource "cloudflare_zero_trust_tunnel_cloudflared" "moa" { + account_id = var.cloudflare_account_id + name = var.cloudflare_tunnel_name + secret = coalesce(var.cloudflare_tunnel_secret, random_id.cloudflare_tunnel_secret.b64_std) +} + +resource "cloudflare_zero_trust_tunnel_cloudflared_config" "moa" { + account_id = var.cloudflare_account_id + tunnel_id = cloudflare_zero_trust_tunnel_cloudflared.moa.id + + config { + ingress_rule { + hostname = var.cloudflare_hostname + service = var.cloudflare_origin_service + } + + ingress_rule { + service = "http_status:404" + } + } +} + +resource "cloudflare_record" "moa_hostname" { + zone_id = data.cloudflare_zone.moa.id + name = trimsuffix(var.cloudflare_hostname, ".${var.cloudflare_zone_name}") + type = "CNAME" + value = cloudflare_zero_trust_tunnel_cloudflared.moa.cname + proxied = true + ttl = 1 +} diff --git a/terraform/environments/dev/outputs.tf b/terraform/environments/dev/outputs.tf index b1aa460..6fd11eb 100644 --- a/terraform/environments/dev/outputs.tf +++ b/terraform/environments/dev/outputs.tf @@ -35,3 +35,19 @@ output "rds_tunnel_command" { description = "노트북에서 RDS 접속용 SSH 터널. 그대로 복붙해서 사용." value = "ssh -i ${module.ec2.private_key_path} -L 15432:${module.rds.address}:${module.rds.port} ubuntu@${module.ec2.public_ips[0]}" } + +output "cloudflare_tunnel_id" { + description = "Cloudflare Tunnel ID" + value = cloudflare_zero_trust_tunnel_cloudflared.moa.id +} + +output "cloudflare_hostname" { + description = "Cloudflare Tunnel로 공개되는 호스트명" + value = var.cloudflare_hostname +} + +output "cloudflare_tunnel_token" { + description = "EC2 cloudflared connector에 주입할 tunnel token. terraform output -raw cloudflare_tunnel_token 로 조회." + value = cloudflare_zero_trust_tunnel_cloudflared.moa.tunnel_token + sensitive = true +} diff --git a/terraform/environments/dev/providers.tf b/terraform/environments/dev/providers.tf index 3f9d92b..078683a 100644 --- a/terraform/environments/dev/providers.tf +++ b/terraform/environments/dev/providers.tf @@ -16,3 +16,8 @@ provider "aws" { } } } + +# Cloudflare provider 설정. +# API token은 CLOUDFLARE_API_TOKEN 환경변수로 주입한다. +# 계정/존/호스트명은 variables로 분리해서 도메인이나 터널 교체가 쉽도록 한다. +provider "cloudflare" {} diff --git a/terraform/environments/dev/terraform.tfvars.example b/terraform/environments/dev/terraform.tfvars.example index 51baaed..399256b 100644 --- a/terraform/environments/dev/terraform.tfvars.example +++ b/terraform/environments/dev/terraform.tfvars.example @@ -1,19 +1,21 @@ -project_name = "sw-hub" -environment = "dev" +# dev 환경 Terraform 변수 예시. +# 실제 값은 terraform.tfvars 또는 CI secret/env로 주입하고, 민감값은 커밋하지 않는다. -aws_region = "ap-northeast-2" -aws_profile = "default" +# Cloudflare API token은 여기에 쓰지 말고 환경변수로 주입: +# export CLOUDFLARE_API_TOKEN=... -# 네트워크 -vpc_cidr = "10.10.0.0/16" -public_subnet_cidrs = ["10.10.1.0/24", "10.10.2.0/24"] -private_subnet_cidrs = ["10.10.11.0/24", "10.10.12.0/24"] +# 도메인/호스트명도 환경변수로 주입하는 것을 기본으로 한다: +# export TF_VAR_cloudflare_account_id=... +# export TF_VAR_cloudflare_zone_name=... +# export TF_VAR_cloudflare_hostname=... -# EC2 -ec2_instance_count = 1 -ec2_instance_type = "t3.small" -private_key_output_path = "sw-hub-dev.pem" -ssh_allowed_cidr = "0.0.0.0/0" +# terraform.tfvars를 쓸 때도 실제 도메인을 하드코딩하지 말고 환경별 파일로 분리한다. +cloudflare_account_id = "replace-with-cloudflare-account-id" +cloudflare_zone_name = "replace-with-zone-name" +cloudflare_hostname = "replace-with-public-hostname" +cloudflare_tunnel_name = "moa-dev" +cloudflare_origin_service = "http://localhost:8080" -# RDS -rds_engine = "postgres" +# 선택: 기존 tunnel secret을 직접 관리해야 할 때만 사용. +# 기본값 null이면 Terraform이 random_id로 생성한다. +# cloudflare_tunnel_secret = "..." diff --git a/terraform/environments/dev/variables.tf b/terraform/environments/dev/variables.tf index 3d48c9c..3a39169 100644 --- a/terraform/environments/dev/variables.tf +++ b/terraform/environments/dev/variables.tf @@ -69,3 +69,37 @@ variable "rds_engine" { type = string default = "postgres" } + +variable "cloudflare_account_id" { + description = "Cloudflare account ID. 토큰과 분리해서 계정 교체가 쉽도록 변수로 받는다." + type = string +} + +variable "cloudflare_zone_name" { + description = "Cloudflare DNS zone name. TF_VAR_cloudflare_zone_name 환경변수로 주입한다." + type = string +} + +variable "cloudflare_hostname" { + description = "Cloudflare Tunnel로 노출할 전체 호스트명. TF_VAR_cloudflare_hostname 환경변수로 주입한다." + type = string +} + +variable "cloudflare_tunnel_name" { + description = "Cloudflare Tunnel 이름. 예: moa-dev" + type = string + default = "moa-dev" +} + +variable "cloudflare_origin_service" { + description = "Tunnel ingress origin service URL. EC2 connector가 host network면 localhost를 사용한다." + type = string + default = "http://localhost:8080" +} + +variable "cloudflare_tunnel_secret" { + description = "선택: Cloudflare Tunnel secret. null이면 Terraform이 random_id로 생성한다. 기존 tunnel import 시에는 기존 secret/상태 전략을 별도로 정한다." + type = string + default = null + sensitive = true +} diff --git a/terraform/environments/dev/versions.tf b/terraform/environments/dev/versions.tf index 1c4a226..d4bd3a9 100644 --- a/terraform/environments/dev/versions.tf +++ b/terraform/environments/dev/versions.tf @@ -18,5 +18,9 @@ terraform { source = "hashicorp/random" version = "~> 3.6" } + cloudflare = { + source = "cloudflare/cloudflare" + version = "~> 4.52" + } } }