Turn the AWS provider into an EC2 instance API

Generate an OpenAPI contract for EC2 instances from the AWS provider's schemas with tofu-api gen, then restrict instance types with a rule. No AWS account needed.

tofu-api Beginner 15 min

tofu-api builds its whole API from a provider’s schemas. In this tutorial you write the file that exposes EC2 instances from the hashicorp/aws provider, generate the API’s contract, and shape it with a rule — without an AWS account: tofu-api gen only needs the provider binary, no credentials.

You will need: Go 1.26 or later, git, jq, network access, and OpenTofu or Terraform in your PATH (the repository’s script uses it to download and verify the provider).

Note

tofu-api is early stage, and the AWS provider is not yet among the providers it has been tested live with. Generating the contract, as here, calls nothing in AWS; see Testing for what has been tested.

Get tofu-api#

git clone https://github.com/infrapi/tofu-api.git
cd tofu-api
go run . -h

-h lists the commands; gen writes a contract, serve runs the API.

Download the provider#

tofu-api runs the same provider binary Terraform does. The repository’s script fetches it into ./providers and prints where it put it:

scripts/fetch-provider.sh hashicorp/aws 6.0.0

Keep the printed path: it goes into provider.binary_path below.

Describe the API#

Create aws.yaml, with the path the script printed:

provider:
  binary_path: providers/registry.opentofu.org/hashicorp/aws/6.0.0/linux_amd64/terraform-provider-aws_v6.0.0_x5
  config:                        # the provider block, as in Terraform
    region: ${AWS_REGION}        # read from the environment when serving, not by gen
resources:
  aws_instance: {}               # exposed as the provider has it

resources names every resource type to expose; anything not listed stays out of the API.

Generate the contract#

go run . gen -config aws.yaml -o aws-v1.json

gen reads the schema of aws_instance from the provider, applies the file’s rules, and writes an OpenAPI 3 contract. It does not read the environment and needs no credentials. It fails exactly as the service would on a wrong file, which makes it a good check before a deployment.

Read it#

jq '.paths | keys' aws-v1.json
jq '.components.schemas | keys' aws-v1.json

The instances are under /aws/v1/namespaces/{namespace}/instance — the provider’s name, then the resource type without its aws_ prefix. Their request schema is built from the provider’s schema:

  • required attributes are required;
  • computed-only ones, such as id, arn or private_ip, are absent from the request: sending one is a 400, and they come back in the response;
  • sensitive ones are format: password.

The full mapping is in Mapping.

Shape it with rules#

As is, a caller may ask for any instance type. Rules lay over the provider’s schemas what a Terraform module adds around a bare resource — here, the sizes your platform allows, and an attribute callers should not set:

resources:
  aws_instance:
    attributes:
      instance_type:
        enum: [t3.micro, t3.small, t3.medium]
      user_data:
        hidden: true             # not the caller's choice: sending it is a 400

Generate again and compare:

go run . gen -config aws.yaml -o aws-v1-rules.json
diff <(jq -S . aws-v1.json) <(jq -S . aws-v1-rules.json)

The enum is now in the contract, and user_data is gone from it. Once served, a request for a m5.24xlarge is a 400 and the provider is never called. A misspelt key, such as Enum, stops gen with an error.

Next steps#

  • Serve it for real — it creates real, billed instances: Getting started and Configuration.
  • Give each tenant its own AWS credentials from Vault: Namespaces.
  • Require a token and RBAC through Kayra: Authentication.
  • Defaults, values set by the service, error mapping and CEL expressions: Rules.

Something unclear or missing?

Tutorials are open source too: open an issue or a pull request on GitHub.