diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 47ee844..a009549 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -1,41 +1,41 @@ -name: Validate IaC Scaffold - -on: - pull_request: - push: - branches: - - main - -jobs: - terraform: - runs-on: ubuntu-latest - defaults: - run: - working-directory: terraform/environments/dev - steps: - - uses: actions/checkout@v4 - - uses: hashicorp/setup-terraform@v3 - - name: Terraform fmt check - run: terraform fmt -check -recursive ../.. - - name: Terraform init - run: terraform init -backend=false - - name: Terraform validate - run: terraform validate - - ansible: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Install Ansible - run: | - python3 -m pip install --upgrade pip - python3 -m pip install ansible-core - ansible-galaxy collection install community.general - - name: Lint inventory graph - working-directory: ansible - run: ansible-inventory --graph - - name: Syntax check playbooks - working-directory: ansible - run: | - ansible-playbook -i inventories/dev/hosts.yml playbooks/bootstrap.yml --syntax-check - ansible-playbook -i inventories/dev/hosts.yml playbooks/site.yml --syntax-check +name: Validate IaC Scaffold + +on: + pull_request: + push: + branches: + - main + +jobs: + terraform: + runs-on: ubuntu-latest + defaults: + run: + working-directory: terraform/environments/dev + steps: + - uses: actions/checkout@v4 + - uses: hashicorp/setup-terraform@v3 + - name: Terraform fmt check + run: terraform fmt -check -recursive ../.. + - name: Terraform init + run: terraform init -backend=false + - name: Terraform validate + run: terraform validate + + ansible: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Ansible + run: | + python3 -m pip install --upgrade pip + python3 -m pip install ansible-core + ansible-galaxy collection install community.general community.postgresql + - name: Lint inventory graph + working-directory: ansible + run: ansible-inventory --graph + - name: Syntax check playbooks + working-directory: ansible + run: | + ansible-playbook -i inventories/dev/hosts.yml playbooks/bootstrap.yml --syntax-check + ansible-playbook -i inventories/dev/hosts.yml playbooks/site.yml --syntax-check diff --git a/ansible/README.md b/ansible/README.md index 0bd8ef0..56b2307 100644 --- a/ansible/README.md +++ b/ansible/README.md @@ -1,49 +1,66 @@ -# Ansible - -Ansible contains environment-specific inventories and reusable roles. - -## Structure - -- `inventories/dev`: development inventory and shared vars -- `playbooks/bootstrap.yml`: base server bootstrap -- `playbooks/site.yml`: app host runtime configuration -- `roles/common`: common baseline packages and configuration -- `roles/docker`: Docker Engine and Compose plugin installation -- `roles/redis`: Redis Docker Compose service for dev cache/session/verification use -- `roles/postgres`: RDS PostgreSQL client/extension setup - -## Redis dev runtime - -Redis is installed on the app EC2 through Docker Compose instead of ElastiCache for the current dev stage. - -Why: the BE app already reads `REDIS_HOST`/`REDIS_PORT`, and a localhost-only Redis keeps dev infra cheaper and simpler while preserving an easy later migration path to ElastiCache. - -Default shape: - -```text -BE on app EC2 -> 127.0.0.1:6379 -> Redis container -``` - -The role renders `/opt/moa/redis/compose.yml` and starts it with `docker compose up -d`. - -Security default: - -- Redis binds to `127.0.0.1:6379` only. -- No Redis password is required for localhost-only dev use. -- The role refuses `0.0.0.0` binding unless `redis_password` is set. - -Run only Redis: - -```bash -ansible-playbook ansible/playbooks/site.yml --tags redis -``` - -Override examples: - -```bash -ansible-playbook ansible/playbooks/site.yml \ - --tags redis \ - -e "redis_bind_address=127.0.0.1 redis_port=6379" -``` - -If Redis must be accessed from another host later, prefer ElastiCache. If temporarily exposing Redis beyond localhost, restrict the EC2 security group and set `redis_password`. +# Ansible + +Ansible contains environment-specific inventories and reusable roles. + +## Structure + +- `inventories/dev`: development inventory and shared vars +- `playbooks/bootstrap.yml`: base server bootstrap +- `playbooks/site.yml`: app host runtime configuration +- `roles/common`: common baseline packages and configuration +- `roles/docker`: Docker Engine and Compose plugin installation +- `roles/redis`: Redis Docker Compose service for dev cache/session/verification use +- `roles/cloudflared`: Cloudflare Tunnel container for reverse proxy ingress +- `roles/postgres`: RDS PostgreSQL client/extension setup + +## Redis dev runtime + +Redis is installed on the app EC2 through Docker Compose instead of ElastiCache for the current dev stage. + +Why: the BE app already reads `REDIS_HOST`/`REDIS_PORT`, and a localhost-only Redis keeps dev infra cheaper and simpler while preserving an easy later migration path to ElastiCache. + +Default shape: + +```text +BE on app EC2 -> 127.0.0.1:6379 -> Redis container +``` + +The role renders `/opt/moa/redis/compose.yml` and starts it with `docker compose up -d`. + +Security default: + +- Redis binds to `127.0.0.1:6379` only. +- No Redis password is required for localhost-only dev use. +- The role refuses `0.0.0.0` binding unless `redis_password` is set. + +Run only Redis: + +```bash +ansible-playbook ansible/playbooks/site.yml --tags redis +``` + +Override examples: + +```bash +ansible-playbook ansible/playbooks/site.yml \ + --tags redis \ + -e "redis_bind_address=127.0.0.1 redis_port=6379" +``` + +If Redis must be accessed from another host later, prefer ElastiCache. If temporarily exposing Redis beyond localhost, restrict the EC2 security group and set `redis_password`. + +## Cloudflare Tunnel + +`cloudflared` is managed as a Docker Compose service on the app host. + +Required runtime secret: + +- `cloudflared_tunnel_token`: Cloudflare Tunnel token. Inject via Ansible Vault, inventory secrets, or `-e`; never commit the real value. + +Run only this role: + +```bash +ansible-playbook ansible/playbooks/site.yml --tags cloudflared -e "cloudflared_tunnel_token=..." +``` + +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. diff --git a/ansible/playbooks/site.yml b/ansible/playbooks/site.yml index bbda0c7..455d222 100644 --- a/ansible/playbooks/site.yml +++ b/ansible/playbooks/site.yml @@ -12,5 +12,7 @@ - docker - role: redis tags: [cache, redis] + - role: cloudflared + tags: [cloudflared, tunnel] - role: postgres tags: [db, postgres] diff --git a/ansible/roles/cloudflared/defaults/main.yml b/ansible/roles/cloudflared/defaults/main.yml new file mode 100644 index 0000000..d986241 --- /dev/null +++ b/ansible/roles/cloudflared/defaults/main.yml @@ -0,0 +1,12 @@ +--- +# Cloudflare Tunnel role defaults. Secrets must be injected from inventory, vault, 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_network_mode: host + +# Required secret at runtime. Do not commit the real token. +cloudflared_tunnel_token: "" diff --git a/ansible/roles/cloudflared/tasks/main.yml b/ansible/roles/cloudflared/tasks/main.yml new file mode 100644 index 0000000..677613a --- /dev/null +++ b/ansible/roles/cloudflared/tasks/main.yml @@ -0,0 +1,41 @@ +--- +- name: Validate Cloudflare Tunnel token is provided + ansible.builtin.assert: + that: + - cloudflared_tunnel_token is defined + - cloudflared_tunnel_token | length > 0 + fail_msg: >- + cloudflared_tunnel_token is required. Inject it through Ansible Vault, + inventory secrets, or -e; do not commit it to git. + no_log: true + +- name: Create cloudflared project directory + ansible.builtin.file: + path: "{{ cloudflared_project_dir }}" + state: directory + owner: root + group: root + mode: "0755" + +- name: Render cloudflared compose file + ansible.builtin.template: + src: compose.yml.j2 + dest: "{{ cloudflared_project_dir }}/compose.yml" + owner: root + group: root + mode: "0600" + no_log: true + +- name: Pull cloudflared image + ansible.builtin.command: docker compose pull + args: + chdir: "{{ cloudflared_project_dir }}" + changed_when: true + no_log: true + +- name: Start cloudflared tunnel + ansible.builtin.command: docker compose up -d + args: + chdir: "{{ cloudflared_project_dir }}" + changed_when: true + no_log: true diff --git a/ansible/roles/cloudflared/templates/compose.yml.j2 b/ansible/roles/cloudflared/templates/compose.yml.j2 new file mode 100644 index 0000000..752ebd7 --- /dev/null +++ b/ansible/roles/cloudflared/templates/compose.yml.j2 @@ -0,0 +1,7 @@ +services: + cloudflared: + image: {{ cloudflared_image }} + container_name: {{ cloudflared_container_name }} + restart: {{ cloudflared_restart_policy }} + network_mode: {{ cloudflared_network_mode }} + command: tunnel --no-autoupdate --url {{ cloudflared_origin_url | quote }} --http-host-header {{ cloudflared_hostname | quote }} run --token {{ cloudflared_tunnel_token | quote }} diff --git a/terraform/modules/ec2/main.tf b/terraform/modules/ec2/main.tf index 9849d71..3d70734 100644 --- a/terraform/modules/ec2/main.tf +++ b/terraform/modules/ec2/main.tf @@ -45,33 +45,17 @@ resource "local_sensitive_file" "private_key" { file_permission = "0600" } -# EC2 보안그룹. SSH는 키 기반 인증만 사용하므로 IP 제한은 운영자 판단에 위임. +# EC2 보안그룹. Cloudflare Tunnel 기반 아웃바운드 연결만 사용하므로 퍼블릭 인바운드는 열지 않음. resource "aws_security_group" "this" { name = "${local.name_prefix}-ec2-sg" description = "Security group for ${local.name_prefix} app nodes" vpc_id = var.network_summary.vpc_id ingress { - description = "SSH (key-based auth only)" + description = "SSH access for operations" from_port = 22 to_port = 22 protocol = "tcp" - cidr_blocks = [var.ssh_allowed_cidr] - } - - ingress { - description = "HTTP" - from_port = 80 - to_port = 80 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - } - - ingress { - description = "HTTPS" - from_port = 443 - to_port = 443 - protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] }