1.2.0 • Published 3 years ago

@mdawar/terraformjs v1.2.0

Weekly downloads
4
License
MIT
Repository
github
Last release
3 years ago

TerraformJS

npm

TerraformJS is a wrapper for Terraform that allows you to write your infrastructure configurations in JavaScript.

Requirements

  • Node.js (With ES modules support) (Tested on 13.2.0+)
  • Terraform 0.12+

Note: This package was only tested on Linux, it should work just fine on macOS.

Overview

You can write your Terraform configuration using JavaScript modules named with a .tf.js suffix, TerraformJS imports these modules and generates Terraform JSON configuration files named with a .tf.json suffix.

Terraform loads these .tf.json files along with your .tf files, so you can use TerraformJS in your current Terraform project without changing anything at all, you can just add new .tf.js files for your configurations that need some programming features that don't exist in the native Terraform language syntax.

Why?

  • Write your infrastructure configuration files in a language you already know
  • Use programming features that are not available in Terraform's language syntax (HCL)
  • Import your configuration data from databases, files or any other source

This project was inspired by pretf, a similar tool but for Python.

Features

  • Drop in replacement command for the terraform CLI
  • Small API and easy to learn (Only top-level blocks are pre-defined everything else is dynamic)
  • Can be used in an existing Terraform project without any changes, you can use both JavaScript configuration files .tf.js and Terraform's configuration language files .tf in the same project

How it works?

When you use the terraformjs command instead of terraform:

  1. The previously generated .tf.json files are removed if any
  2. The .tf.js files are imported and the exports are used to generate the .tf.json files
  3. Terraform is executed with any passed command line arguments
  4. After Terraform's execution is complete all the generated .tf.json files are removed

All the command line arguments that you pass to terraformjs will be passed to terraform, with the exception of one command specific to terraformjs used to generate the JSON files without executing terraform:

# Generate the JSON files (*.tf.json) without executing Terraform
terraformjs generate

The configuration code is not checked for correctness, to validate your code use terraformjs validate.

Getting Started

  1. Install TerraformJS globally to be able to execute it from any directory:
npm i -g @mdawar/terraformjs

Optional: You can alias terraform to terraformjs by adding this line alias terraform="terraformjs" to ~/.bash_aliases or ~/.bashrc.

  1. In a new directory or in your current Terraform project's directory, create a package.json file:
npm init -y
  1. Install terraformjs to be able to import it in your modules:
npm i @mdawar/terraformjs
  1. Open the package.json file and add a type field with the value of module:
{
  "type": "module"
}

This is required for your .js files to be treated as ES modules by Node.js, because TerraformJS supports ES modules only and does not support CommonJS.

  1. Start writing your Terraform configuration files with JavaScript in .tf.js files instead of the standard .tf files, then you can use terraformjs instead of terraform to manage your infrastructure

Write your infrastructure configuration in JavaScript

Top-level Terraform blocks

All the top-level Terraform blocks are predefined and can imported from the terraformjs package:

  • terraform
  • provider
  • resource
  • variable
  • module
  • data
  • locals
  • output
// Import the needed top-level blocks
import {
  provider,
  resource,
  variable,
  data,
  output
} from '@mdawar/terraformjs';

Define the block labels

To define the block labels, you can chain as many properties as needed:

// Chain an "aws" property to define the provider label
provider.aws;

// You can chain any number of properties as they are dynamically handled
resource.aws_instance.web;
data.aws_ami.debian;

// If the label is dynamic you can use the square brackets []
output[`dynamic-value-${n}`];

Define the block body

To define the block body you can call the last chained property with an object or a function that returns an object:

import {
  terraform,
  provider,
  variable,
  data,
  resource,
  output
} from '@mdawar/terraformjs';

// You must export the defined blocks so terraformjs can import them and generate the JSON file
// The export names do not matter, they can be used as values in the body of other blocks

// The terraform top-level block does not have any labels so it can be called directly to define the body
export const tf = terraform({
  // Define any supported arguments
  required_version: '>= 0.12',

  // When a nested block requires a label
  // The label can be nested in an object
  backend: {
    // Defines the "backend s3 {}" block
    s3: {
      profile: 'default',
      bucket: 'BUCKET_NAME',
      region: 'us-east-1'
    }
  }
});

// To define a block with an empty body you must call the last property
// Set the variable value in *.tfvars file
// or using -var="aws_instance_type=..." CLI option
export const instance_type = variable.aws_instance_type(); // An empty object {} is the default body

export const aws = provider.aws({
  region: 'us-west-1',
  profile: 'default'
});

// Create a data source for the Ubuntu AMI that will be used to create an EC2 instance
export const ubuntu = data.aws_ami.ubuntu({
  most_recent: true,

  // When multiple nested blocks of the same type can be given
  // an array can be used to define these nested blocks
  filter: [
    {
      name: 'name',
      values: ['ubuntu/images/hvm-ssd/ubuntu-trusty-14.04-amd64-server-*']
    },
    {
      name: 'virtualization-type',
      values: ['hvm']
    }
  ],

  owners: ['099720109477']
});

// Define a resource
export const instance = resource.aws_instance.web({
  // Example of referencing another defined block
  provider: aws,

  // We can use the data source that we have defined earlier
  // Results in ${data.aws_ami.ubuntu.id}
  ami: ubuntu.id,

  // Use the value of the variable defined above
  // Results in ${var.aws_instance_type}
  instance_type,

  tags: {
    Name: 'web'
  },

  // When the nested block type requires one or more labels
  // an array of objects can be used
  provisioner: [
    {
      // provisioner "local-exec" {}
      'local-exec': {
        command: 'echo "Hello World" > example.txt'
      }
    },
    {
      // provisioner "remote-exec" {}
      'remote-exec': {
        inline: ['sudo install-something -f /tmp/example.txt']
      }
    }
  ]
});

// Create an output value for the instance's public IP address
export const publicIP = output.instance_ip_address({
  // Results in ${aws_instance.web.public_ip}
  value: instance.public_ip
});

Exporting multiple blocks using an Array

Multiple blocks may be exported using arrays:

import { provider, variable, resource, output } from '@mdawar/terraformjs';

// Define a variable
// Set the variable value in *.tfvars file
// or using -var="do_token=..." CLI option
export const token = variable.do_token();

// Configure the DigitalOcean Provider
export const doProvider = provider.digitalocean({
  // Translates to ${var.do_token}
  // Same as token: token
  token,
  version: '~> 1.9'
});

// List of droplets
export const droplets = [];
// Droplets IPv4 addresses
export const ipAddresses = [];

// This is just an example using an array of numbers
// You can pull the data from anywhere, databases, files...
for (const n of [1, 2, 3, 4]) {
  const droplet = resource.digitalocean_droplet[`web-${n}`]({
    provider: doProvider,
    image: 'ubuntu-18-04-x64',
    name: `web-${n}`,
    region: 'nyc2',
    size: 's-1vcpu-1gb'
  });

  droplets.push(droplet);

  // Define an output block with a dynamic label using square brakctes []
  const ip = output[`web-${n}`]({
    value: droplet.ipv4_address
  });

  ipAddresses.push(ip);
}

Nested arrays of any number of levels deep are also supported, so for the above example you can also export 1 array:

// List of droplets
const droplets = [];
// Droplets IPv4 addresses
const ipAddresses = [];

export const all = [
  // Nested array
  droplets,
  // You can nest arrays of any levels deep
  [ipAddresses]
];

Using a function as the block body

Example of using an async function as the block body:

// In this example we configure an **S3** bucket to respond to requests coming from CloudFlare proxy IP addresses only
// This is used when hosting a static website using CloudFlare and AWS S3
import axios from 'axios';
import { provider, resource } from '@mdawar/terraformjs';

export const aws = provider.aws({
  version: '~> 2.45',
  profile: 'default',
  region: 'us-east-1'
});

// Static site name, to be used as the S3 bucket name
const siteName = 'example.com';

// Using an async function as the block body instead of an object
// A function that returns a Promise that resolves to an object can be used instead
export const bucket = resource.aws_s3_bucket.website(async () => {
  // Get the Cloudflare proxy IP addresses
  // See: https://www.cloudflare.com/ips/
  let [ipv4, ipv6] = await Promise.all([
    axios.get('https://www.cloudflare.com/ips-v4'),
    axios.get('https://www.cloudflare.com/ips-v6')
  ]);

  ipv4 = ipv4.data.trim().split('\n');
  ipv6 = ipv6.data.trim().split('\n');

  const ips = [...ipv4, ...ipv6];

  // Returning an object to be used as the block body
  return {
    bucket: siteName,
    region: 'us-east-1',
    acl: 'public-read',

    website: {
      index_document: 'index.html',
      error_document: '404.html'
    },

    // We want to configure the bucket policy to allow Cloudflare proxy IP addresses only
    policy: `{
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "PublicReadGetObject",
                "Effect": "Allow",
                "Principal": "*",
                "Action": "s3:GetObject",
                "Resource": "arn:aws:s3:::${siteName}/*",
                "Condition": {
                  "IpAddress": {
                      "aws:SourceIp": ${JSON.stringify(ips)}
                  }
                }
            }
        ]
    }`,

    tags: {
      Name: siteName,
      Environment: 'Production'
    }
  };
});