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
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,16 @@ fabric.properties
hs_err_pid*
replay_pid*

### Terraform ###
**/.terraform/
*.tfstate
*.tfstate.*
*.tfplan
*.tfvars
!*.tfvars.example
infra/prod/backend.hcl
infra/prod/backend.tf

### JetBrains ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
Expand Down
88 changes: 88 additions & 0 deletions docs/infrastructure/prod-inventory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Production Infrastructure Inventory

## Purpose

This document records the existing production infrastructure before it is
adopted by Terraform. It is an inventory, not an authorization to change AWS
resources.

## Scope

| Area | Existing resource | Terraform approach |
| --- | --- | --- |
| EC2 | `i-0b4fe94824ca7d3c7` (`moru-server`) | Import and manage |
| Elastic IP | `eipalloc-0423074c40c068102` | Import and manage |
| EC2 security group | `sg-0ff632924dad2adb6` (`moru-ec2-sg`) | Import and manage |
| RDS MySQL | `moru-db` | Import and manage |
| RDS security group | `sg-0056408c9dc50cef4` (`moru-rds-sg`) | Import and manage |
| S3 assets | `moru-prod-assets-488230509502` | Import and manage |
| S3 TTS previews | `moru-prod-preview-assets-488230509502` | Import and manage |
| EC2 IAM role/profile | `moru-server-s3-role` | Import and manage |
| CloudWatch Logs | `/moru/prod/app` | Import and manage |
| VPC | `vpc-087c2d76939191694` | Reference with data source |
| EC2 subnet | `subnet-0a94acc68b254e59f` | Reference with data source |
| RDS subnet group | `default-vpc-087c2d76939191694` | Reference with data source |

The VPC and subnets are the account's default network and may be shared by
other workloads. They must not be imported or changed in this migration.

## Observed Production Configuration

### Compute and network

- EC2: `t3.micro`, Amazon Linux AMI `ami-0e4ab31f1847c850c`, in
`ap-northeast-2c`.
- The instance uses the `moru-key` key pair, the `moru-server-s3-role` instance
profile, and an IMDSv2-required metadata configuration.
- Elastic IP `43.202.84.114` is associated with the instance.
- The application container publishes only to `127.0.0.1:8080`; Nginx remains
host-managed and proxies the public HTTP(S) traffic.

### Database

- RDS MySQL `moru-db`: MySQL `8.4.9`, `db.t4g.micro`, 20 GiB `gp2`, encrypted
storage, one-day backup retention, and no Multi-AZ deployment.
- The database name is `moru`. Credentials are intentionally excluded from this
repository and Terraform configuration.

### Storage and logging

- `moru-prod-assets-488230509502` stores private application assets.
- `moru-prod-preview-assets-488230509502` allows public reads only below
`tts/previews/*` and `tts/common/*` for preview and common TTS audio.
- Both production buckets use AES256 default encryption. Versioning is not
currently enabled.
- Docker uses the `awslogs` driver and writes to `/moru/prod/app`, which retains
logs for 14 days. No CloudWatch metric alarms are currently configured.
- The EC2 role has inline policies for the production and development asset
buckets. The production policy allows only the `tts/*` and `profiles/*`
prefixes and CloudWatch log writes.

## Out of Scope for This Migration

- Docker Compose services, the Redis container, and the GitHub Actions deploy
workflow remain unchanged.
- Nginx, Certbot, DuckDNS, and files on the EC2 host are documented only. They
are not changed or managed by Terraform in this phase.
- Database credentials, application secrets, and GCP credentials remain in the
existing secret-management flow.

## Security Review Items

These items were observed during the read-only audit. They are intentionally
not changed by the Terraform adoption work and need separate approval.

1. The RDS instance is publicly accessible, and `moru-rds-sg` currently allows
all traffic from IPv4 and IPv6 addresses.
2. `moru-ec2-sg` allows SSH from every IPv4 address.
3. The production S3 buckets do not currently use versioning. This is distinct
from the planned Terraform state bucket, which must use versioning.

## Adoption Safety Rules

- Never run `terraform apply`, `terraform import`, or `terraform destroy`
without explicit approval.
- Do not accept a plan that replaces, creates, modifies, or destroys an
existing production resource during import.
- Keep Terraform state, plan files, credentials, and application secrets out of
Git.
128 changes: 128 additions & 0 deletions docs/infrastructure/terraform-import-runbook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Production Terraform Import Runbook

## Purpose

This runbook adopts existing Moru production AWS resources into Terraform
state. It must not create, update, replace, or delete an application resource.
The configuration uses declarative `import` blocks, so the import is reviewed
through the normal `plan` and `apply` workflow.

## Completion Record

The production import was completed on 2026-09-24 using the `moru-prod` AWS
profile in account `488230509502`. The reviewed plan contained 30 imports and
no additions, changes, or deletions. All 30 managed resources appeared in the
remote state afterward, and a fresh plan reported `No changes`.

The backend is the private `moru-prod-terraform-state-488230509502` bucket,
with state at `prod/terraform.tfstate`. Do not reapply the saved import plan or
repeat this procedure against the populated state. For future changes, follow
the approval workflow in [infra/README.md](../../infra/README.md).

## Historical Preconditions

1. The Terraform state bucket has been created from `infra/bootstrap` after a
separate approval.
2. The `moru_terraform` IAM user has production read access and the dedicated
state-access policy. This allowed the import-only apply to update state
without changing the imported AWS resources.
3. Future infrastructure changes require separately approved, scoped AWS
write permissions; the state-access policy alone is not sufficient.
4. No concurrent Terraform operation is running for prod.
5. The production service is healthy before beginning. Record the current
health result and do not restart Docker, Nginx, or EC2 as part of this work.

## Import Order

The import blocks in `infra/prod/imports.tf` cover the following existing
resources:

1. EC2 security group and its seven rules
2. EC2 IAM role, two inline policies, and instance profile
3. EC2 instance and its Elastic IP
4. RDS security group and its three rules
5. RDS parameter group and MySQL instance
6. Private production assets S3 bucket and its security settings
7. Public TTS preview S3 bucket, its security settings, and its public-read
bucket policy
8. CloudWatch application log group

The default VPC, default subnets, default DB subnet group, Docker Compose,
Redis, GitHub Actions, Nginx, Certbot, DuckDNS, and EC2-hosted files are not
imported in this phase.

## Current Validation

The following commands validate the configuration and check for drift using
the `moru-prod` profile after login:

```bash
cd infra/prod
AWS_PROFILE=moru-prod terraform init -reconfigure -backend-config=backend.hcl
AWS_PROFILE=moru-prod terraform validate
AWS_PROFILE=moru-prod terraform plan -lock-timeout=5m
```

The expected result now is `No changes`. Any proposed create, update,
replacement, or deletion needs investigation and approval before an apply.

## Historical Import Procedure (Completed)

The steps below record the one-time migration. Do not run them again for this
production state.

1. Use the approved Terraform operator profile and configure
`infra/prod/backend.tf` and `infra/prod/backend.hcl` from the committed
examples.
2. Initialize the remote state backend:

```bash
terraform init -reconfigure -backend-config=backend.hcl
```

3. Produce a reviewable plan:

```bash
terraform plan -out=prod-import.tfplan -lock-timeout=5m
terraform show -no-color prod-import.tfplan
```

4. Confirm every action is an import and no existing resource has a planned
update, replacement, or deletion. Obtain explicit approval for the exact
plan.
5. Apply the reviewed plan once:

```bash
terraform apply prod-import.tfplan
```

6. Verify Terraform now reports no drift:

```bash
terraform plan -lock-timeout=5m
terraform state list
```

7. Keep the local `prod-import.tfplan` file out of Git and remove it when no
longer needed; saved plans can contain sensitive values.

## Stop Conditions

Stop and investigate before an apply when any of the following occurs:

- A plan proposes a create, change, replace, or destroy action for an existing
production resource.
- The AWS account check is not `488230509502`.
- The state bucket cannot be encrypted, versioned, or locked.
- A resource ID differs from `docs/infrastructure/prod-inventory.md`.
- The application health check fails before or during the activity.

## Post-Import Follow-Up

Keep an access-controlled backup of the local
`infra/bootstrap/terraform.tfstate`, which manages the state bucket itself.
The application deployment workflow does not run Terraform.

The open RDS and SSH network rules are documented security findings, not an
incidental Terraform migration change. Address them in a separate, reviewed
security issue with an explicit connectivity test and rollback procedure.
53 changes: 53 additions & 0 deletions infra/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Terraform Infrastructure

`bootstrap` created the dedicated S3 backend and retains local state. `prod`
manages the 30 existing Moru production AWS resources imported on 2026-09-24.
The post-import plan reported 0 to add, 0 to change, and 0 to destroy. See the
[import runbook](../docs/infrastructure/terraform-import-runbook.md) for the
original scope and verification record.

## State and Access

The production state is stored in the private
`moru-prod-terraform-state-488230509502` bucket at `prod/terraform.tfstate`.
The bucket uses AES256 encryption, versioning, public-access blocking, and S3
lockfiles. Keep state, plan files, and local backend configuration files out of
Git; state and plans may contain sensitive values. The local
`infra/bootstrap/terraform.tfstate` is also sensitive and must be backed up in
an access-controlled location outside the repository.

The `moru-prod` AWS CLI profile uses the `moru_terraform` IAM user. It has
production read access and the dedicated state-bucket policy, but not general
write access to EC2, RDS, or application S3 buckets. Grant only the specific
AWS write permissions required for an approved infrastructure change. Never
use the root account for Terraform operations.

On a new workstation, copy `infra/prod/backend.tf.example` to
`infra/prod/backend.tf` and `infra/prod/backend.hcl.example` to
`infra/prod/backend.hcl`. Set the approved bucket name in `backend.hcl`. Both
files are ignored by Git. Then initialize from `infra/prod` with
`AWS_PROFILE=moru-prod terraform init -reconfigure -backend-config=backend.hcl`.
Do not create another state bucket or repeat the production import.

## Production Changes

1. Change the Terraform configuration in a reviewed PR. Keep application
deployment and host configuration outside this change unless separately
approved.
2. From `infra/prod`, run `terraform fmt -check`, `terraform validate`, and
`AWS_PROFILE=moru-prod terraform plan -out=prod.tfplan -lock-timeout=5m`.
Review the exact plan, especially any replacement or deletion. Do not use
`-lock=false` for production changes.
3. After explicit approval and with scoped AWS write permissions, apply the
reviewed plan with `AWS_PROFILE=moru-prod terraform apply prod.tfplan`.
4. Run `AWS_PROFILE=moru-prod terraform plan -lock-timeout=5m` again and check
for `No changes`. Do not commit the saved plan.

The existing GitHub Actions workflow builds and deploys the application; it
does not run Terraform. Terraform applies remain manual and approval-gated.

## Outside Terraform

The shared VPC, subnets, and DB subnet group are referenced as data sources.
Docker Compose, Redis, GitHub Actions, Nginx, Certbot, DuckDNS, and EC2-hosted
files remain outside Terraform management in this phase.
26 changes: 26 additions & 0 deletions infra/bootstrap/.terraform.lock.hcl

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

Loading
Loading