# README
Terraform Core RPC API
This directory contains package rpcapi
, which is the main implementation code
for the Terraform Core RPC API.
What follows here is documentation aimed at those who are maintaining or otherwise contributing to this code. This is not end-user-oriented documentation; information on how to use the RPC API as an external caller belongs elsewhere.
NOTE WELL! The RPC API is currently experimental, existing primarily as a vehicle for the Terraform Stacks private preview. It is subject to arbitrary breaking changes -- even in patch releases -- until the Terraform Stacks features are considered stable.
What is the RPC API?
The RPC API is an integration point for making use of some Terraform Core functionality within external software.
It's primarily aimed at entirely separate programs using its gRPC server
configured using HashiCorp's go-plugin
library, although it does also have an
internal access point for use by the parts of this codebase that are
architecturally part of Terraform CLI rather than Terraform Core, to help
reinforce that architectural boundary despite them both currently living in the
same codebase.
The relationship between this package's implementation of the RPC API and external callers of that API is mechanically similar to the relationship between the Terraform Plugin Framework/SDK and Terraform Core: it's a client/server protocol using gRPC as the transport, over a local socket.
The protocol buffers definition for the API lives in
terraform1.proto
, which when included in
a tagged commit of this repository acts as the source of truth for a particular
version of the API.
RPC API services
The RPC API exposes a few different services that each wrap different parts of Terraform Core's functionality. These are broad thematic groupings, but they are all part of the same API and in particular values returned by one service are often accepted as input to another.
-
Setup
: This is a special service that's used only to prepare the other services for use by performing a negotiation handshake.Clients must always make exactly one call to
Setup.Handshake
before interacting with any other part of this API. That call acts as a capability negotiation which might therefore influence the behavior of other subequent calls as a measure of forward and backward compatibility. -
Dependencies
: Deals with some cross-cutting concerns related to dependencies such as remote source packages (e.g. external modules) and providers. -
Stacks
: Provides external access to the Terraform Stacks runtime, including planning and applying changes to the infrastructure described by a stack configuration.
API Object Handles
To allow passing live objects between different services and different
functions within the same service, the RPC API uses handles, which are
int64
values that each uniquely identify a live object of a particular
type.
Handles are typically (but not always) created by RPC functions whose names
start with the prefix Open
, and are later closed by functions whose names
start with Close
.
Handles persist between calls to the same RPC API process, but are automatically discarded when that process shuts down. Depending on the handle type, this automatic discarding may or may not be equivalent to explicitly closing the handle, and so callers should typically explicitly close handles for objects they no longer intend to use.
Objects represented by handles can sometimes depend on other objects. In that case, it might be necessary to close one handle before closing another.
Internally, handles are represented as values of the handle
generic type,
which is parameterized by the type of the underlying object the handle is
representing. This therefore allows a measure of type safety to help avoid
mistakes like using the wrong kind of handle when calling a function.
In the wire protocol the handle type information is erased, and so when accepting handles from a client the service implementation must check that the given handle is of the expected type.
Currently handles are unique across objects of all types, but that's an implementation detail that clients are not allowed to rely on. If designing a service which can accept handles of multiple different types, always design it to accept each handle type as a separate request field, and never rely on the system's internal state about what type each handle has, so that we can give the best possible feedback to clients when they have their own bugs that cause mixups between different handle types.
Handshake Dynamic Initialization
In order to allow clients to dynamically negotiate capabilities at runtime,
the server implementation of this API uses an extra indirection over the
real service implementations that's implemented in the subdirectory
dynrpcserver
.
The service implementations registered with the gRPC server are actually instances of the wrapper stubs in that package. Initially those stubs are all wrapping nothing at all, and so all calls to the service functions will return errors.
During a Setup.Handshake
call, the system finally instantiates the real
service implementations that are implemented within this package directory.
The exact details of what types are instantiated and how they are populated
can vary based on the negotiated capabilities, allowing some flexibility in
how we will handle requests based on those capabilities.
The dynrpcserver
stubs are automatically generated by a go:generate
directive in that package based on the protocol buffers definitions. Therefore
each time we change the set of service functions or the request and response
types for those functions we must first run make protobuf
to regenerate the
protocol buffers stubs, and then
go generate ./internal/rpcapi/...
to update the dynrpcserver
stubs to
match.
API Entry Points
The main entry point is rpcapi.CLICommandFactory
, which returns a factory
function intended for use with the github.com/mitchellh/cli
module that
Terraform CLI uses to route execution into its various subcommands.
Terraform CLI's package main
binds the subcommand rpcapi
directly to the
factory returned by that function, thereby providing the smallest possible
amount of Terraform CLI execution before reaching the RPC API. This is
intentional to help reinforce that rpcapi
is an alternative to using
Terraform CLI, rather than part of Terraform CLI itself, despite the
unavoidable use of some of its early entry-point code to get up and running.
When Terraform CLI itself needs to access Terraform Core functionality that's
exposed by the RPC API, an alternative entry point is
rpcapi.NewInternalClient
. This function returns an object which provides
access to gRPC clients just as would be used by an external caller accessing
the API when using go-plugin
, but arranges for its requests to be routed
via local buffers in-process rather than using a socket.
The intent of this "internal client" is to reinforce the architectural boundary
between Terraform CLI and Terraform Core despite them living in the same
codebase. Commands that interact with the internal client could potentially
be factored out into separate codebases in future with only minimal
modification to use go-plugin
to arrange access instead of using the
internal client.
At the time of writing this documentation there is plenty of surface area in Terraform CLI that predates the RPC API which accesses Terraform Core functionality which itself predates the RPC API, and therefore those calls are made directly via normal function calls. It's fine to continue maintaining those callers and callees until there's a strong reason to update them, but most new functionality should be mediated through the RPC API.
In particular, the RPC API is the only public interface to the Terraform Stacks
runtime, and so any Terraform CLI code which is orchestrating the Stacks
runtime must access it through the RPC API internal client, and must not
directly import anything under ./internal/stacks
.