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.
110 changes: 110 additions & 0 deletions docs/infrastructure/terraform-import-runbook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# 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.

## Preconditions

1. The Terraform state bucket has been created from `infra/bootstrap` after a
separate approval.
2. The approved Terraform operator has the state-access policy created by the
bootstrap configuration, plus only the AWS permissions needed to manage the
imported resources.
3. The `moru_terraform` discovery user remains read-only. Do not use it for
import or apply.
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.

## Read-Only Validation

The following commands do not write Terraform state and are safe to run with
the `moru-prod` read-only profile after login (`moru_terraform` is the
read-only discovery user, not the profile name):

```bash
cd infra/prod
AWS_PROFILE=moru-prod terraform init -reconfigure
AWS_PROFILE=moru-prod terraform validate
AWS_PROFILE=moru-prod terraform plan -lock=false
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```

The expected result is a plan containing imports only. Stop immediately when
the plan includes an action other than `import` for an existing resource.

## Approved Import Procedure

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. Remove the local `prod-import.tfplan` file. It is ignored by Git and must
never be committed.

## 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

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.
76 changes: 76 additions & 0 deletions infra/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Terraform Infrastructure

`bootstrap` creates the dedicated S3 backend resources. It intentionally uses
local state for this one-time setup. `prod` manages the existing Moru
production infrastructure after its backend is configured.

## Safety Boundary

The current `moru-prod` AWS profile is read-only. It can run discovery,
`terraform fmt`, `terraform validate`, and a read-only import plan. It cannot
create the state bucket or import resources into remote state.

Do not run `terraform apply`, `terraform import`, or `terraform destroy` until
the plan is reviewed and explicit approval is given.

## State Backend

The backend is deliberately separate from the existing application buckets.
The bootstrap configuration creates a private bucket with:

- AES256 server-side encryption
- S3 bucket versioning for state recovery
- S3 lockfiles through `use_lockfile = true`
- public-access blocking and bucket-owner-enforced ownership
- a transport-security bucket policy
- an unattached least-privilege policy for the future Terraform operator

HashiCorp recommends bucket versioning for S3 state recovery and S3 lockfiles
for state locking. DynamoDB locking is not used because it is deprecated by the
S3 backend.

### Bootstrap after approval

1. Sign in with a separately approved AWS identity that can create the backend
bucket and IAM policy. Do not use the root account.
2. Choose a globally unique state bucket name. The recommended candidate is
`moru-prod-terraform-state-488230509502`, subject to availability.
3. Run the following from `infra/bootstrap`:

```bash
terraform init
terraform plan -var='state_bucket_name=CHOSEN_BUCKET_NAME'
```

4. Review that the plan only creates the state bucket resources and the
unattached operator policy. After explicit approval, run `terraform apply`
with the same variable.
5. Attach the created state-access policy only to the approved Terraform
operator identity. Keep the `moru_terraform` discovery user read-only.

### Configure the production backend

1. Copy `infra/prod/backend.tf.example` to `infra/prod/backend.tf` and copy
`infra/prod/backend.hcl.example` to `infra/prod/backend.hcl`.
2. Replace the bucket name with the approved state bucket name. Do not commit
either file.
3. Run the following from `infra/prod`:

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

The first production plan should show imports only. Any planned create, update,
replacement, or destroy for an existing production resource is a stop signal.

Before the state bucket exists, leave `backend.tf` absent and use Terraform's
local backend only for read-only validation. Local state and plan files are
ignored by Git and must never be committed.

## Non-Terraform Host Configuration

Docker Compose, Redis, GitHub Actions, Nginx, Certbot, DuckDNS, and EC2-hosted
files remain outside the Terraform scope in this phase. Their existing setup is
documented in the repository and on the production host; it must not be changed
as part of adoption.
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