Infrastructure as Code
The Origins of Dinghy
Dinghy was inspired by CDKTF (Cloud Development Kit for Terraform). Much like how React represents UI as a virtual DOM before rendering it to the browser, CDKTF builds an infrastructure model as a context tree, which then gets synthesized into Terraform configuration and ultimately applied to create real infrastructure.
Instead of following CDKTF's approach to building and using the infrastructure tree, I decided to leverage React component to construct the tree, and then transform it into Terraform configuration. By using this method, I could also render the same tree as a diagram, create tools that make both infrastructure development and operations more seamless.
Components
Dinghy Terraform components are structured as React components that extend from
the Shape base class. These components include special properties specific to
Infrastructure-as-Code (IaC), enabling automatic generation of Terraform
configuration. To learn more about their internal implementation and
capabilities, refer to the advanced guide:
Tf Components Definition.
Let's walk through a fully working
S3 Bucket example to explore the core
Dinghy components you'll encounter. With just a single app.tsx file, you have
everything you need to kickstart your Dinghy Infrastructure as Code project.
import { Shape } from '@dinghy/base-components'
import { AwsProvider, S3Backend } from '@dinghy/tf-aws'
import { S3Bucket } from '@dinghy/tf-aws/s3Bucket'
export default function Stack() {
return (
<Shape _title='S3 Bucket Composite Component Example'>
<AwsProvider region='eu-west-1'>
<S3Backend />
<S3Bucket
bucket='my-demo-bucket-with-versioning'
versioningEnabled
/>
</AwsProvider>
</Shape>
)
}
Fundational components
Stack
A Stack is conceptually similar to a terraform workspace — each Stack maintains
its own separate state. In Dinghy, the Stack is bound to the root of your React
application, and its title and name will be kept in sync if you specify them
via config file or component attributes. Within a single codebase, you can
define multiple stacks to represent different environments or layer of your
resources. You could have single app.tsx for multiple stack with different
configuration. Or have different tsx for different stack. Refer to
Stack Config for advanced
configuration.
Provider
The Provider component defines the Terraform provider block in your infrastructure code. For AWS, you can use the AwsProvider component like this:
import { AwsProvider } from '@dinghy/tf-aws'
<AwsProvider region='eu-west-1'>
Backend
The Backend component defines the Terraform backend block to store your state. For AWS, you can use the S3Backend component like this:
import { S3Backend } from '@dinghy/tf-aws'
<S3Backend />
Auto create backend
Traditionally, setting up the backend for Terraform can be a tedious manual
process that must be done before any Terraform operations can take place. With
the S3Backend component, Dinghy automates this for you: the backend will be
created on demand as part of your Infrastructure as Code workflow. You can
enable automatic backend bucket creation by either passing the
--auto-create-backend flag or by setting the environment variable
DINGHY_AUTO_CREATE_BACKEND=true. This will automatically create the backend
bucket during the init operation.
Componsite components
Composite components are analogous to Terraform modules. They allow you to configure and manage a group of related resources together using a higher-level, more convenient interface with simplified components and configuration. Composite components are constructed using one or more service components from below as building blocks.
Service componenets
Service compoenents are basic terraform data models such as resource or data. They are generated from official provider json data representation. There is one to one maping for AWS Provider elements e.g. Terraform resource aws_s3_bucket is available to use as AwsS3Bucket:
import { AwsS3Bucket } from '@dinghy/tf-aws/serviceS3'
<AwsS3Bucket />
Limitation of generated terraform schema
Because provider JSON data doesn't include all the details needed to accurately define the Dinghy schema, the generated Terraform code may occasionally be incomplete or not fully accurate.
Additionally, the generated code might lag behind updates to the latest provider versions you are using.
If you encounter these limitations, don't worry—Dinghy offers flexible ways to handle them. You can resolve such issues by using the advanced Tf Components Definition techniques to define component from ground up or provider raw value.
use* functions
The useCOMPONENT or useCOMPONENTs functions are
React hooks that let you access the
state of a component after Dinghy completes the react rendering process. The
hook returns a proxy object representing the component, but this object is not
available during the usual React render lifecycle. However, the object's
resolvable function can be used to extract the final evaluated output when
needed.
useCOMPONENT
The useCOMPONENT function returns a single value, named using the last two
words of the component’s name.
Following example show how we define a reusable component which use hooks to enable versioning for the nearest s3Bucket.
Notice how this implementation requires no parameters, making it straightforward and convenient to reuse wherever needed.
const BucketVersioning = () => {
const { s3Bucket } = useAwsS3Bucket()
const BucketVersioningComponent: any =
_components?.versioning as typeof AwsS3BucketVersioning ||
AwsS3BucketVersioning
return (
<BucketVersioningComponent
bucket={s3Bucket.bucket}
_id={() => `${deepResolve(s3Bucket._id)}_versioning`}
versioning_configuration={{ status: 'Enabled' }}
depends_on={() => [s3Bucket._terraformId]}
/>
)
}
_terraformId
As shown in the last line of the example above, you can access Terraform-style
resource references using the _terraformId property.
useCOMPONENTs
The useCOMPONENTs function returns an array of matched values. There are two
values returned:
- First none empty array match from any parent. Named using the last two words
of the component’s name with additional surfix
s. - All match from root. Named using the last two words of the component’s name
with prefix of
alland surfixs.
You can use the map function on the returned array to extract specific
attributes. For example, here’s how subnets are passed to an AWS Load Balancer:
const { awsSubnets } = useAwsSubnets();
return (
<AwsLb
subnets={() => awsSubnets.map((s) => s.id)}
...
use* function parameters
The use* functions accept up to three optional parameters that let you customize how the lookup is performed and which nodes are included.
idFilter?: string– Only include nodes whoseidcontains this substringbaseNode?: NodeTree– the node from which to begin searching downwards. If no match is found, the search continues upward through ancestor nodes.optional?: boolean– By default, if no matching node is found, an exception will be thrown. Setoptionaltotrueto allow the function to returnundefinedinstead of raising an error.
Tf commands
In Dinghy, the term tf refers to either Terraform or OpenTofu runtime,
depending on which tool you choose to use.
Dinghy provides several wrapper commands for tf. For a complete list of available commands, see the tf commands reference.
dinghy tf diff
The diff command executes a series of steps on the specified stack, or on all stacks if no stack is provided: