Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

cdktf-injector

Package Overview
Dependencies
Maintainers
1
Versions
141
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

cdktf-injector

Dependency Injection for CDKTF(Cloud Development Kit for Terraform) powered by projen.

  • 1.0.1
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
1.8K
decreased by-14.51%
Maintainers
1
Weekly downloads
 
Created
Source

npm version Release License

Dependency Injection for CDKTF

cdktf-injector is a dependency injection library for CDKTF (AWS Cloud Development Kit for Terraform).

If you are not familiar with cdktf and terraform yet or haven't heard of them before, this library may not be that so useful. That's totally cool, but I recommend you to have a look what they (cdktf and terraform) are or at least what's the concept of IaC. Those are really amazing.

There are two prerequisites for cdktf-injector

  • Terraform - You need to install terraform on your dev env and it should be accessible on cli path. If you're using devcontainer you may paste following commands to your Dockerfile.

    ...
    
    # Pass terraform version as an argument
    ARG TERRAFORM_VERSION=1.1.7
    
    # Install Terraform
    RUN wget https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip && \
        unzip ./terraform_${TERRAFORM_VERSION}_linux_amd64.zip -d /usr/local/bin/ && \
        rm terraform_${TERRAFORM_VERSION}_linux_amd64.zip
    
    ...
    
  • cdktf - A library for defining Terraform resources using programming constructs. Click here to get started. Or, simply type following lines.

    mkdir your-project-name
    cd your-project-name
    npx cdktf-cli init --template="typescript" --local
    

Features

  • No decorator
  • No need to declare custom classes
  • No need to explicitly write dependencies

Installation

Using npm

npm install cdktf-injector --save

Using yarn

yarn add cdktf-injector

Note : cdktf supports multiple languages, such as TypeScript, Python, Java, C# and Go. Howerver according to Typescript restriction of jsii, which is I originally wanted to use to power my lib and what cdktf depends on, it does not allow using Parameterized Types(aka: Generics). And as far as I know, there is no other decent way to implement dependency injection without using generic or decorator. In brief, cdktf-injector is currently availiable only in Node.js env. I'm looking forward to making this lib supports multiple languages soon.


Necessity

I'm gonna show you a brief example of building a simple infrastructure on AWS comparing implementation method of using cdktf with cdktf-injector with pure terraform source(hcl) or just cdktf only. This is because AWS is probably the most commonly used cloud provider, I believe.

In case you are not a big fan of AWS, I'll make a list of what elements are used here and what they are like.

  • VPC - A virtual network dedicated to your AWS account.

    When you consider your aws account as a House, VPC is your Room.

  • Subnet - A range of IP addresses in your VPC.

    Then, this is your Router at home.

  • EC2 Instance - A virtual computing environment.

    This is your Computer.

  • Output - Exported data about your resource. In this example, output value is public ip of EC2 instance.

    And this is an Public IP address of your computer.

Using terraform only (Hashicorp Configuration Language)

resource "aws_vpc" "my-vpc" {
    cidr_block = "10.1.0.0/16"
}

resource "aws_subnet" "my-subnet" {
    vpc_id = aws_vpc.my-vpc.id
    cidr_block = "10.1.1.0/24"
}

resource "aws_instance" "my-ec2-instance" {
    subnet_id = aws_subnet.my-subnet.id
    ami = "ami-2757f631"
    instance_type = "t2.micro"
}

output "instance-ip" {
    value = aws_instance.my-ec2-instance.public_ip
}

As you can see, there are 4 terraform resources declared up above and their relationships are as follow.

  • instance-ip depends on my-ec2-instance
  • my-ec2-instance depends on my-subnet
  • And my-subnet depends on my-vpc

In general, if you used to normal programming languages, the first thing you can come up with might be...

"Ok, A depends on B, therefore I should declare B over A."

In hcl however, you don't have to. Declaration order is not important. Terraform will take care of it. All you need to do is to make sure that every resource is correctly configured.

This is a good thing. Imagine if you have to declare each and every single element in the right order. There could be hundreds or maybe thousands of different stuffs that depend on one another.

Still, hcl is a confusing, not very programmable language, and its intellisence is so slow.

cdktf could be an alternative. cdktf in a nutshell, is a tool used to generate hcl code with any one of your familiar programming languages such as TypeSript, Python, Java, C# or etc.

Let's take a look example below.

Using cdktf in TypeScript

import { ec2, vpc } from '@cdktf/provider-aws';
import { App, TerraformOutput, TerraformStack } from 'cdktf';
import { Construct } from 'constructs';

class MyStack extends TerraformStack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    const myVpc = new vpc.Vpc(this, 'my-vpc', {
      cidrBlock: '10.1.0.0/16',
    });

    const mySubnet = new vpc.Subnet(this, 'my-subnet', {
      vpcId: myVpc.id,
      cidrBlock: '10.1.1.0/24',
    });

    const myEc2Instance = new ec2.Instance(this, 'my-ec2-instance', {
      subnetId: mySubnet.id,
      ami: 'ami-2757f631',
      instanceType: 't2.micro',
    });

    new TerraformOutput(this, 'instance-ip', {
      value: myEc2Instance.publicIp,
    });
  }
}

const app = new App();
new MyStack(app, 'my-stack');
app.synth();

In cdktf, every resource is a class instance. Thoese are pretty straight forward. Constructing resources using new keyword, passing id string and its config, you can build real world infrastructure.

However unlike hcl, it is a sequencial language. You cannot create instance refering to another one that is not defined yet.

When you attempt to declare subnet before vpc, it'll say "Hey, you cannot use vpc before it's been initialized!"

And that's where the cdktf-injector comes in.

Using cdktf with cdktf-injector

import { ec2, vpc } from '@cdktf/provider-aws';
import { App, TerraformOutput } from 'cdktf';
import { TerraformInjectorStack } from 'cdktf-injector';
class MyStack extends TerraformInjectorStack {
  myEc2Instance = this.provide(ec2.Instance, 'my-ec2-instance', () => ({
    subnetId: this.mySubnet.element.id,
    ami: 'ami-2757f631',
    instanceType: 't2.micro',
  })).addOutput('instance-ip', (instance) => ({
    value: instance.publicIp,
  }));

  /* Or, you can provide TerraformOutput just as any other resources

  instanceIp = this.provide(TerraformOutput, 'instance-ip', () => ({
    value: this.myEc2Instance.element.publicIp,
  }));
  */

  mySubnet = this.provide(vpc.Subnet, 'my-subnet', () => ({
    vpcId: this.myVpc.element.id,
  }));

  myVpc = this.provide(vpc.Vpc, 'my-vpc', () => ({
    cidrBlock: '10.1.0.0/16',
  }));
}

const app = new App();
const myStack = new MyStack(app, 'my-stack');
myStack.inject(); // At this point, all the elements are instantiated
app.synth();

I intentionally cofigured all the resources completely upside down.

Still, it's working. Be aware that you should call inject method before sythesize your app.


Examples

Step 1. Load Injector

There are multiple ways to do that.

The most common method is

  TerraformInjector.scopesOn(scope : Construct)
// Use can load injector outside the stack with TerraformInjectorFactory
import { App, TerraformStack } from 'cdktf';
import { TerraformInjectorFactory } from 'cdktf-injector';

const app = new App();
const myStack = new TerraformStack(app, 'my-stack');
const myInjector = TerraformInjectorFactory.scopesOn(myStack); // Load Injector from scope
/*
  Add resources here
  const myBackend = myInjector.backend(SomeBackendClass, configurationCallback)
  const myResource =  myInjector.provide(SomeResourceClass, configurationCallback)
*/
myInjector.inject();
app.synth();
// Alternatively, declare your own class extending TerraformStack as usual but having injector property
import { App, TerraformStack } from 'cdktf';
import { TerraformInjectorFactory } from 'cdktf-injector';

class MyStack extends TerraformStack {
  injector = TerraformInjectorFactory.scopesOn(this);
  /*
    Add resource here
    myBackend = this.injector.backend(SomeBackendClass, configurationCallback)
    myResource = this.injector.provide(SomeResourceClass, id, configurationCallback)
  */
}

const app = new App();
const myStack = new MyStack(app, 'my-stack');
myStack.injector.inject();
app.synth();
// Or, you can just use TerraformInjectorStack
import { App } from 'cdktf';
import { TerraformInjectorStack } from 'cdktf-injector';

// It already extends TerraformStack and implemnts TerraformInjector
class MyStack extends TerraformInjectorStack {
  /*
    Add resource here
    myBackend = this.backend(SomeBackendClass, configurationCallback)
    myResource = this.provide(SomeResourceClass, id, configurationCallback)
  */
}

const app = new App();
const myStack = new MyStack(app, 'my-stack');
myStack.inject();
app.synth();

Step 2. Provide Backend

Providing backend is an option. It decides remote backend where your terraform state files are stored. In default, it would be in your local dir.

Sytax is as following

  yourInjector.backend(backendClass : ClassExtendsTerraformBackend, configure : () => BackendProps)

In my case, I use AWS S3 bucket as my remote store. Here is the example.

// Use S3Backend as remote backend
import { App, S3Backend } from 'cdktf';
import { TerraformInjectorStack } from 'cdktf-injector';

class MyStack extends TerraformInjectorStack {
  private myBackend = this.backend(S3Backend, () => ({
    bucket: 'your-tf-bucket-name',
    key: 'your/tf-state-file/path',
  }));
}

const app = new App();
const myStack = new MyStack(app, 'my-stack');
myStack.inject();
app.synth();

Step 3. Set Provider

Provider in this context, usually means cloud computing service such as AWS, GCP, or maybe Docker and etc.

You should set a provider for each stack. Syntax for provider is the same as any other resources.

  yourInjector.provide(terraformElementClass : ClassExtendsTerraformElement, id : string, configure : () => ElementConfig)
// Set AWS Provider
import { AwsProvider } from '@cdktf/provider-aws';
import { App, S3Backend } from 'cdktf';
import { TerraformInjectorStack } from 'cdktf-injector';
class MyStack extends TerraformInjectorStack {
  private myBackend = this.backend(S3Backend, () => ({
    bucket: 'your-tf-bucket-name',
    key: 'your/tf-state-file/path',
  }));
  private myProvider = this.provide(AwsProvider, 'myProvider', () => ({
    region: 'us-west-1',
    accessKey: 'your-access-key',
    secretKey: 'your-secret-key',
  }));
}

const app = new App();
const myStack = new MyStack(app, 'my-stack');
myStack.inject();
app.synth();

Step 4. Add resources

Let's add some resources.

  yourInjector.provide(terraformElementClass : ClassExtendsTerraformElement, id : string, configure : () => ElementConfig)
import { AwsProvider } from '@cdktf/provider-aws';
import { App, S3Backend } from 'cdktf';
import { TerraformInjectorStack } from 'cdktf-injector';
class MyStack extends TerraformInjectorStack {
  private myBackend = this.backend(S3Backend, () => ({
    bucket: 'your-tf-bucket-name',
    key: 'your/tf-state-file/path',
  }));
  private myProvider = this.provide(AwsProvider, 'myProvider', () => ({
    region: 'us-west-1',
    accessKey: 'your-access-key',
    secretKey: 'your-secret-key',
  }));

  mySubnet = this.provide(vpc.Subnet, 'my-subnet', () => ({
    vpcId: this.myVpc.element.id,
  }));

  myVpc = this.provide(vpc.Vpc, 'my-vpc', () => ({
    cidrBlock: '10.1.0.0/16',
  }));
}

const app = new App();
const myStack = new MyStack(app, 'my-stack');
myStack.inject();
app.synth();

Step 5. Inject all the dependencies

Call inject before synth

...
const app = new App();
const myStack = new MyStack(app, 'my-stack');

myStack.inject();

app.synth();

Multiple stacks cluster references

Sometimes, you may want to seperate your infrastructures into multiple stacks. And when the resources have cross-stack dependencis too complexly so that you cannot order which stack comes first, it would be a nightmare.

Basically, all injectors scopes on certain Construct instance. And when you call inject method of it, it'll load every injector below its scope path just like app.synth. Therefore, you do not have to manually commit injection for every injector, just call from the very root you want. Take the following example.

...
const app = new App()
const myBasicStack = new BasicStack(app, 'my-basic-stack');
const myVpcStack = new VpcStack(myBasicStack, 'my-vpc-stack');
const myEcsStack = new EcsStack(myVpcStack, 'my-ecs-stack')
myBasicStack.inject(); // Also commit injection for myVpcStack and myEcsStack
// Or, TerraformInjectorFactory.scopeOn(app).inject() <-- this will inject every resource below scope 'app'
app.synth();

Promise Support

There are 2 types of injector.

  • static injector
  • async injector.
staticasync
How to load injectorTerraformInjectorFactory
.scopesOn(scope)
TerraformInjectorFactory
.scopesOnAsync(scope)
Class Name of predefined stackTerraformInjectorStackTerraformInjectorStackAsync
Return type of
configuration callabck
Config or [Config, Shared]Config or [Config, Shared]
or Promise<Config or [Config, Shared]>
Injection Methodinject() : voidinject() : Promise<void>
Able to embedstatic injector onlystatic/async injector

Here's an example of using async injector.

import { AwsProvider } from '@cdktf/provider-aws';
import { App, S3Backend } from 'cdktf';
// Import 'async' stack instead of normal 'static' stack
import { TerraformInjectorStackAsync } from 'cdktf-injector';
class MyStack extends TerraformInjectorStackAsync {
  private myBackend = this.backend(S3Backend, () => ({
    bucket: 'your-tf-bucket-name',
    key: 'your/tf-state-file/path',
  }));

  // You can pass both static/async function as configuration callback
  private myProvider = this.provide(AwsProvider, 'myProvider', async () => ({
    region: 'us-west-1',
    accessKey: 'your-access-key',
    secretKey: 'your-secret-key',
  }));

  mySubnet = this.provide(vpc.Subnet, 'my-subnet', () => ({
    vpcId: this.myVpc.element.id,
  }));

  myVpc = this.provide(vpc.Vpc, 'my-vpc', async () => ({
    cidrBlock: '10.1.0.0/16',
  }));
}

const process = async () => {
  const app = new App();
  const myStack = new MyStack(app, 'my-stack');
  // Injection method is now an async function
  await myStack.inject();
  app.synth();
};
process();

Dependency Problem

You don't have to explicitly write which depends on which.

cdktf-injector will automatically detect dependencies of each resource.

However, that very reason might cause potential problems.

To simplify example, I'll make a class mocking TerraformElement as follow.

export interface MockConfig extends TerraformMetaArguments {}
export class MockElement extends TerraformElement {
  static readonly tfResourceType: string = 'mock_element';
  constructor(scope: Construct, id: string, public config?: MockConfig) {
    super(scope, id);
  }
}

Case 1. Self-Dependence

class MyStack extends TerraformInjectorStack {
  res1 = this.provide(MockElement, 'res1', () => {
    console.log(this.res1.element);
    return {};
  });
}

In this case, res1 depends on res1, since it uses this.res1.element in its configuration callback.

This is not a possible structure of course. It's self-dependent.

I'll throw an error saying...

Error: <MockElement res1> is self-dependent. You cannot use its own element when you configure the container.

Case 2. Dependency Cycle

class MyStack extends TerraformInjectorStack {
  res1 = this.provide(MockElement, 'res1', () => {
    console.log(this.res2.element);
    return {};
  });

  res2 = this.provide(MockElement, 'res2', () => {
    console.log(this.res3.element);
    return {};
  });

  res3 = this.provide(MockElement, 'res3', () => {
    console.log(this.res4.element);
    return {};
  });

  res4 = this.provide(MockElement, 'res4', () => {
    console.log(this.res5.element);
    return {};
  });

  res5 = this.provide(MockElement, 'res5', () => {
    console.log(this.res3.element);
    return {};
  });
}

There are 5 resources.

  • res1 depends on res2
  • res2 depends on res3
  • res3 depends on res4
  • res4 depends on res5

Finally, res5 depends on res3. Oops! There is a problem.

Commiting injection later, cdktf-injector will require element of res3 to initialize res5.

But, because res3 depends on res4 and res4 depends res5, it's another impossible structure.

I'll throw an error saying...

Error: There are 3 elements trapped in dependency cycle.
➤ ➤ ➤
▲      ▼
▲      ▼
▲     <MockElement res3>
▲     Element Type : MockElement
▲     Scope path : my-stack/res3
▲     Created at /workspaces/cdktf-injector/test/index.ts:24:15
▲      ▼
▲      ▼
▲      ▼
▲      ▼
▲     <MockElement res4>
▲     Element Type : MockElement
▲     Scope path : my-stack/res4
▲     Created at /workspaces/cdktf-injector/test/index.ts:29:15
▲      ▼
▲      ▼
▲      ▼
▲      ▼
▲     <MockElement res5>
▲     Element Type : MockElement
▲     Scope path : my-stack/res5
▲     Created at /workspaces/cdktf-injector/test/index.ts:34:15
▲      ▼
▲      ▼
◀ ◀ ◀

In most case, the last resource of cycle is the cause of error.

Keep this in mind, existence of such errors is not normal. No matter what provider you're using there should not be any dependency cycles among resources.

Keywords

FAQs

Package last updated on 06 Mar 2022

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc