diff --git a/README.md b/README.md index ecbf6d2..bcb1b36 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ public route. The provider owns how those VMs and networks exist: - macOS + Lima uses two local Lima VMs and a Lima user-v2 private network. -- AWS EC2 uses two EC2 instances, a dedicated VPC, and Terraform-managed +- AWS EC2 uses two EC2 instances, a dedicated VPC, and OpenTofu-managed networking. The two VMs have the same roles in either provider: @@ -116,7 +116,7 @@ On macOS with Homebrew: brew tap oven-sh/bun brew install bun brew install lima # for the macOS + Lima provider -brew install terraform # for the AWS EC2 provider +brew install opentofu # for the AWS EC2 provider chmod +x ./rootcell bun install --frozen-lockfile @@ -152,8 +152,9 @@ Initialize the instance `.env` before the first run: ./rootcell -i aws-dev ``` -See [AWS EC2 provider](src/rootcell/providers/aws-ec2/README.md) for Terraform -layout, AWS resource ownership, AMI selection, and IAM isolation details. +See [AWS EC2 provider](src/rootcell/providers/aws-ec2/README.md) for the +OpenTofu/Terraform layout, AWS resource ownership, AMI selection, and IAM +isolation details. First run creates the provider resources and provisions both VMs. Provisioning uses Nix inside the VMs, but you do not need Nix installed on the host unless @@ -170,9 +171,13 @@ environment variable: ```bash ROOTCELL_LIMACTL=/path/to/limactl # Lima provider -ROOTCELL_TERRAFORM=/path/to/terraform # AWS EC2 provider +ROOTCELL_TERRAFORM=/path/to/tofu # AWS EC2 provider ``` +The AWS EC2 provider uses OpenTofu's `tofu` command by default. Set +`ROOTCELL_TERRAFORM=/path/to/terraform` if you want to use a Terraform binary +you installed yourself. + Per-instance state defaults to `instances/` under the current repo. Set `ROOTCELL_STATE_DIR=/path/to/rootcell-instances` to use a different persistent state root. @@ -365,7 +370,7 @@ Provider state is intentionally provider-specific: - macOS + Lima writes generated Lima YAML and VM state under `v/a/` and `v/f/` and keeps Lima's own VM state under normal `LIMA_HOME`. -- AWS EC2 writes a generated Terraform module and Terraform state under +- AWS EC2 writes a generated Terraform-compatible module and state under `v/aws-ec2/`. Use `./rootcell list` to show known VMs and their state. `./rootcell stop` diff --git a/flake.nix b/flake.nix index 67b9f7c..465f986 100644 --- a/flake.nix +++ b/flake.nix @@ -55,6 +55,7 @@ paths = [ p.bun p.lima + p.opentofu ]; }; }); diff --git a/src/rootcell/host-tools.ts b/src/rootcell/host-tools.ts index 2b63373..dff2cbe 100644 --- a/src/rootcell/host-tools.ts +++ b/src/rootcell/host-tools.ts @@ -34,7 +34,7 @@ function hostToolMissingMessage(spec: HostToolSpec): string { ...(envVars.length === 0 ? [] : [`Set ${envVars.join(" or ")}=/path/to/${spec.name} to use a non-PATH binary.`]), "Install host tools with Homebrew:", " brew tap oven-sh/bun", - " brew install bun lima", + " brew install bun lima opentofu", "Or run with Nix-provided host tools:", " nix shell .#hostTools --command ./rootcell", ].join("\n"); diff --git a/src/rootcell/integration/providers/aws-ec2/provider.ts b/src/rootcell/integration/providers/aws-ec2/provider.ts index a07e6c7..3214193 100644 --- a/src/rootcell/integration/providers/aws-ec2/provider.ts +++ b/src/rootcell/integration/providers/aws-ec2/provider.ts @@ -7,6 +7,7 @@ import { } from "../../common/fixtures.ts"; import { instancePaths } from "../../../instance.ts"; import { commandExists, runInherited } from "../../../process.ts"; +import { resolveHostTool } from "../../../host-tools.ts"; import type { RootcellConfig } from "../../../types.ts"; import { AwsEc2VmProvider } from "../../../providers/aws-ec2.ts"; import { @@ -47,7 +48,12 @@ export function preflightAwsEc2Integration(): Promise { requireEnv("ROOTCELL_VM_PROVIDER", "aws-ec2"); requireEnv("ROOTCELL_AWS_PROFILE"); requireEnv("ROOTCELL_AWS_REGION"); - for (const tool of ["terraform", "ssh", "scp", "ssh-keygen", "curl"]) { + resolveHostTool({ + name: "tofu", + envVar: "ROOTCELL_TERRAFORM", + purpose: "for AWS EC2 integration tests", + }); + for (const tool of ["ssh", "scp", "ssh-keygen", "curl"]) { if (!commandExists(tool)) { throw new Error(`aws-ec2 integration tests require '${tool}' on PATH`); } diff --git a/src/rootcell/providers/aws-ec2-terraform.ts b/src/rootcell/providers/aws-ec2-terraform.ts index e3069fc..a7b9d01 100644 --- a/src/rootcell/providers/aws-ec2-terraform.ts +++ b/src/rootcell/providers/aws-ec2-terraform.ts @@ -105,7 +105,7 @@ export class AwsEc2TerraformProject { return; } - this.log(`applying AWS EC2 Terraform for instance '${this.config.instanceName}'...`); + this.log(`applying AWS EC2 infrastructure for instance '${this.config.instanceName}'...`); this.writeTerraformFiles(metadata, "running", controlCidr); this.runner.init(this.terraformDir(), this.terraformEnv()); this.runner.apply(this.terraformDir(), this.terraformEnv()); @@ -354,7 +354,7 @@ export class AwsEc2TerraformProject { private ensureTerraform(): string { if (this.terraformBin.length === 0) { this.terraformBin = resolveHostTool({ - name: "terraform", + name: "tofu", envVar: "ROOTCELL_TERRAFORM", purpose: "to manage rootcell AWS EC2 resources", }); diff --git a/src/rootcell/providers/aws-ec2/README.md b/src/rootcell/providers/aws-ec2/README.md index af0183c..00037fc 100644 --- a/src/rootcell/providers/aws-ec2/README.md +++ b/src/rootcell/providers/aws-ec2/README.md @@ -2,7 +2,10 @@ The `aws-ec2` provider runs rootcell's agent and firewall VMs as EC2 instances. Rootcell creates a dedicated VPC per rootcell instance and manages -AWS infrastructure with a generated Terraform module. +AWS infrastructure with a generated Terraform-compatible module. It runs +OpenTofu's `tofu` command by default; set +`ROOTCELL_TERRAFORM=/path/to/terraform` to use a Terraform binary you installed +yourself. ## Required Instance Environment @@ -26,13 +29,13 @@ ROOTCELL_AWS_CONTROL_CIDR=auto not fall back to `AWS_PROFILE` or `AWS_REGION` for provider selection. `ROOTCELL_AWS_CONTROL_CIDR=auto` resolves your current public IPv4 address to a -single `/32` when Terraform is applied. If that address changes, normal +single `/32` when OpenTofu is applied. If that address changes, normal `rootcell` entry fails with instructions to run `rootcell provision` so the firewall SSH ingress rule is updated intentionally. -## Terraform Layout +## OpenTofu / Terraform Layout -Rootcell writes one Terraform module per instance: +Rootcell writes one Terraform-compatible module per instance: ```text /v/aws-ec2/ @@ -47,12 +50,12 @@ Rootcell writes one Terraform module per instance: ``` Terraform state is the ownership record for AWS resources. Normal VM entry does -not run `terraform init` or `terraform apply`; it reads cached Terraform -outputs, checks EC2 status, syncs allowlists, injects explicitly configured +not run `tofu init` or `tofu apply`; it reads cached infrastructure outputs, +checks EC2 status, syncs allowlists, injects explicitly configured session secrets, and opens SSH through the firewall. -Terraform runs for first create, explicit `rootcell provision`, Terraform-backed -start/stop transitions, and `rootcell remove`. +OpenTofu runs for first create, explicit `rootcell provision`, +state-backed start/stop transitions, and `rootcell remove`. ## Upstream NixOS AMI @@ -60,7 +63,7 @@ AWS EC2 instances boot from the official upstream NixOS ARM64 AMI. Rootcell does not use rootcell-owned release image manifests, VM Import/Export, imported snapshots, generated AMIs, or S3 image staging. -Terraform resolves the AMI at apply time: +OpenTofu resolves the AMI at apply time: ```hcl data "aws_ami" "nixos_arm64" { @@ -112,8 +115,8 @@ values. ## IAM And Credential Isolation Rootcell does not attach an IAM instance profile to the agent or firewall. The -generated Terraform module must not create IAM roles, IAM instance profiles, or -instance-profile associations for those instances. +generated Terraform-compatible module must not create IAM roles, IAM instance +profiles, or instance-profile associations for those instances. Rootcell never copies host `~/.aws` files into either VM and never injects AWS credentials unless the user explicitly maps them in `secrets.env`. diff --git a/src/rootcell/rootcell.test.ts b/src/rootcell/rootcell.test.ts index da13433..3a023ba 100644 --- a/src/rootcell/rootcell.test.ts +++ b/src/rootcell/rootcell.test.ts @@ -438,6 +438,22 @@ describe("host tool resolution", () => { })).toThrow("nix shell .#hostTools --command ./rootcell"); }); + test("AWS EC2 uses OpenTofu by default but accepts Terraform override", () => { + const tofuSpec = { + name: "tofu", + envVar: "ROOTCELL_TERRAFORM", + purpose: "to manage rootcell AWS EC2 resources", + }; + expect(resolveHostTool(tofuSpec, { + env: {}, + commandExists: (command) => command === "tofu", + })).toBe("tofu"); + expect(resolveHostTool(tofuSpec, { + env: { ROOTCELL_TERRAFORM: "/usr/local/bin/terraform" }, + commandExists: () => false, + })).toBe("/usr/local/bin/terraform"); + }); + test("runtime host tools do not fall back to host-side nix builds", () => { for (const file of [ "src/rootcell/images.ts", @@ -1038,8 +1054,10 @@ describe("VM and network providers", () => { } }); - test("AWS EC2 README documents Terraform layout, upstream AMI, tags, and credential isolation", () => { + test("AWS EC2 README documents OpenTofu layout, upstream AMI, tags, and credential isolation", () => { const readme = readFileSync("src/rootcell/providers/aws-ec2/README.md", "utf8"); + expect(readme).toContain("OpenTofu"); + expect(readme).toContain("ROOTCELL_TERRAFORM=/path/to/terraform"); expect(readme).toContain("/v/aws-ec2/"); expect(readme).toContain("official upstream NixOS ARM64 AMI"); expect(readme).toContain("427812963091");