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
82 changes: 41 additions & 41 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -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
115 changes: 66 additions & 49 deletions ansible/README.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions ansible/playbooks/site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@
- docker
- role: redis
tags: [cache, redis]
- role: cloudflared
tags: [cloudflared, tunnel]
- role: postgres
tags: [db, postgres]
12 changes: 12 additions & 0 deletions ansible/roles/cloudflared/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -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: ""
41 changes: 41 additions & 0 deletions ansible/roles/cloudflared/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions ansible/roles/cloudflared/templates/compose.yml.j2
Original file line number Diff line number Diff line change
@@ -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 }}
20 changes: 2 additions & 18 deletions terraform/modules/ec2/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}

Expand Down
Loading