Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions ansible/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
7 changes: 4 additions & 3 deletions ansible/roles/cloudflared/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -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') }}"
9 changes: 6 additions & 3 deletions ansible/roles/cloudflared/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
27 changes: 27 additions & 0 deletions terraform/environments/dev/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

76 changes: 76 additions & 0 deletions terraform/environments/dev/CLOUDFLARE.md
Original file line number Diff line number Diff line change
@@ -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 <account_id>/<tunnel_id>
terraform import cloudflare_record.moa_hostname <zone_id>/<dns_record_id>
```

그 다음 `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`
43 changes: 43 additions & 0 deletions terraform/environments/dev/cloudflare.tf
Original file line number Diff line number Diff line change
@@ -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
}
16 changes: 16 additions & 0 deletions terraform/environments/dev/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
5 changes: 5 additions & 0 deletions terraform/environments/dev/providers.tf
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,8 @@ provider "aws" {
}
}
}

# Cloudflare provider 설정.
# API token은 CLOUDFLARE_API_TOKEN 환경변수로 주입한다.
# 계정/존/호스트명은 variables로 분리해서 도메인이나 터널 교체가 쉽도록 한다.
provider "cloudflare" {}
32 changes: 17 additions & 15 deletions terraform/environments/dev/terraform.tfvars.example
Original file line number Diff line number Diff line change
@@ -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 = "..."
34 changes: 34 additions & 0 deletions terraform/environments/dev/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
4 changes: 4 additions & 0 deletions terraform/environments/dev/versions.tf
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,9 @@ terraform {
source = "hashicorp/random"
version = "~> 3.6"
}
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.52"
}
}
}
Loading