Wondering what’s next for npm?Check out our public roadmap! »

    @joshwycuff/terrascript

    0.6.3 • Public • Published

    terrascript

    JavsScript/TypeScript wrapper for running Terraform commands in NodeJS

    npm version Actions Status

    What is Terrascript?

    There are two parts to Terrascript. One is a JavaScript/TypeScript wrapper for running Terraform commands in NodeJS. The other is a CLI which allows you to structure, organize, and automate Terraform configurations and tasks.

    Installation

    Using npm:

    npm install @joshwycuff/terrascript

    Using yarn:

    yarn add @joshwycuff/terrascript

    I also recommend using @jahed/terraform along with Terrascript.

    Usage

    CLI

    Terrascript command-line arguments take the form:

    terrascript TARGET_PATH COMMAND [ARGUMENTS...]

    You can run any command you want via terrascript but there is special support for Terraform commands.

    Terrascript needs to be run in the same directory as a Terrascript yaml configuration file (terrascript.yml).

    Configuration file

    A configuration file with every supported key and no values looks like the following:

    name:
    subprojects:
    config:
    groups:
    hooks:
    modules:
    scripts:
    targets:
    definitions:

    Note that nothing is stopping you from storing additional information in other keys. This can even be useful when using template expressions.

    Name

    The name key contains a string value. It is not required and will default to the name of the directory the yaml file is in.

    Targets

    A Terrascript command will not run if the TARGET_PATH does not resolve to a defined target in the configuration file.

    Here's a config file with a single target named dev:

    targets:
      dev:

    You can now run something like:

    terrascript dev echo hello world
    # hello world

    You can also specify multiple targets:

    targets:
      dev:
      prod:

    Now you can run commands for either target, or you can use glob patterns to run multiple targets:

    terrascript "*" pwd
    # /Users/somebody/project
    # /Users/somebody/project
    Groups

    You can also specify groups of targets:

    groups:
      agroup:
        - dev
        - prod
    targets:
      dev:
      prod:
    terrascript agroup pwd
    # /Users/somebody/project
    # /Users/somebody/project

    Yeah, I know. These commands are not terribly useful yet. Hold on...

    Config

    You can use the config key to set environment variables.

    config:
      env:
        A_VAR: A_VAL
    targets:
      dev:
    terrascript dev echo '$A_VAR'
    # A_VAL

    You can also override the top-level config with target-level configs.

    config:
      env:
        A_VAR: A_VAL
    targets:
      dev:
        config:
          env:
            A_VAR: overridden
    terrascript dev echo '$A_VAR'
    # overridden
    Aliasing

    If you have something in your config file that needs to be in several places, you can use aliasing. Alias definitions are found under the definitions key and must begin with "$".

    config:
      env:
        VAR1: $VAL
        VAR2: $VAL
    targets:
      dev:
    definitions:
      $VAL: stuffidontwanttorepeat
    terrascript dev echo '$VAR1'
    # stuffidontwanttorepeat

    Aliasing is a simple way to make DRY config files. However, it is limited. Subprojects cannot "see" alias definitions of parent projects. You also can't combine or perform any logic with aliases. For anything that aliasing can't accomplish, you probably need to go with template expressions.

    Scripts

    If you have a set of commands that you run often, you can create a script for them.

    targets:
      dev:
    scripts:
      things:
        - echo thing 1
        - echo thing 2
    terrascript dev things
    # thing 1
    # thing 2
    Template Expressions

    You can use template expressions to access various available variables within the current context or even to run arbitrary Javascript code.

    The main available variable is called context which looks like this:

    export interface IContext extends Hash<any> {
        tf?: Terraform;
        conf: IConfig;
        spec: ISpec;
        target?: ITarget;
    }

    tf is an instance of Terraform. This is more useful in module functions than templates.

    conf is the current config under which the given command/hook/script is being run.

    spec is the yaml configuration (or specification) under which the given command/hook/script is being run.

    target is the current target.

    tf and target may or may not be available depending on if you're accessing the context in a hook and which hook it is. They're always available in a normal command or script, though.

    Also, for convenience, conf, spec, and target are made available in templates.

    Template expressions are evaluated dynamically at runtime and can access inherited values.

    name: project
    target:
      dev:
    scripts:
      things:
        - echo {{ spec.name }}
        - echo {{ target.name }}
        - echo {{ 1 + 1 }}
    terrascript dev things
    # project
    # dev
    # 2
    Hooks

    There are a number of special hooks that allow you to run commands before or after certain events. Here they are in the order in which they run:

    • beforeEachSubproject
    • beforeEachTarget
    • beforeEachScript
    • beforeEachCommand
    • beforeEachTerraform
    • beforeEachTerraformApply
    • beforeEachTerraformDestroy
    • afterEachTerraformDestroy
    • afterEachTerraformApply
    • afterEachTerraform
    • afterEachCommand
    • afterEachScript
    • afterEachTarget
    • afterEachSubproject

    Here's an example:

    targets:
      dev:
      prod:
    hooks:
      beforeEachTarget:
        - echo do a thing beforeEachTarget
    terrascript "*" echo '{{ target.name }}'
    # do a thing beforeEachTarget
    # dev
    # do a thing beforeEachTarget
    # prod
    Modules

    With modules, you can also run Javascript functions within scripts. The function should take a single input which is the context object. Modules that you want to import should look like this:

    // scripts/mymodule.js
    module.exports = {
        func: (context) => {
            console.log('running func')
            console.log(`I'm in ${context.target.name}`)
        }
    }

    You can import Javascript modules and run them in scripts like so:

    targets:
      dev:
    modules:
      mymodule: scripts/mymodule.js
    scripts:
      doathing:
        - function: mymodule.func
    terrascript dev doathing
    # running func
    # I'm in dev

    You can also modify the context which then propagates to downstream targets.

    // scripts/mymodule.js
    module.exports = {
        func: (context) => {
            console.log('running func')
            console.log('adding an environment variable to the config')
            context.conf.env.A_VAR = 'A_VAL'
        }
    }
    targets:
      dev:
    modules:
      mymodule: scripts/mymodule.js
    scripts:
      doathing:
        - echo $A_VAR
        - function: mymodule.func
        - echo $A_VAR
    terrascript dev doathing
    #
    # running func
    # adding an environment variable to the config
    # A_VAL
    Special Terraform support

    Okay, so I want to do Terraform things...

    Any Terraform subcommand can be run by just specifying the subcommand and its arguments. You don't have to type terraform as part of the command.

    terrascript dev init
    terrascript dev plan
    terrascript dev apply

    This shorthand also extends to scripts.

    targets:
      dev:
    scripts:
      build:
        - init
        - plan
        - apply

    You can specify Terraform input variables in the config section.

    config:
      tfVars:
        environment: "{{ target.name }}"
    targets:
      dev:
    terrascript dev apply
    # `terraform apply -var=environment=dev`

    You can specify Terraform variable definitions (.tfvars) files in the config section as well.

    config:
      tfVarsFiles:
        - "tfvars/{{ target.name }}.tfvars"
    targets:
      dev:
    terrascript dev apply
    # `terraform apply -var-file=tfvars/dev.tfvars`

    Tired of typing -auto-approve?

    config:
      autoApprove: true
    targets:
      dev:
    terrascript dev apply
    # `terraform apply -auto-approve`

    Want to specify auto-approve for only apply or destroy commands?

    config:
      autoApproveApply: true
      autoApproveDestroy: true
    targets:
      dev:

    You can dynamically configure your remote backend in the config block.

    # main.tf
    terraform {
        backend "s3" {}
    }
    config:
      autoApprove: true
      backendConfig:
        profile: my-profile
        region: us-east-1
        bucket: my-remote-state-bucket
        key: "project/{{ target.name }}/terraform.tfstate"
        dynamodb_table: my-remote-state-lock-table
    targets:
      dev:
      prod:
    hooks:
      # This hook is useful when running multiple targets which have different backend configurations.
      beforeEachTarget: init -reconfigure
    terrascript "*" apply
    # `terraform init -reconfigure \
    #     -backend-config=profile=my-profile \
    #     -backend-config=region=us-east-1 \
    #     -backend-config=bucket=my-remote-state-bucket \
    #     -backend-config=key=project/dev/terraform.tfstate \
    #     -backend-config=dynamodb_table=my-remote-state-lock-table`
    # `terraform apply -auto-approve`
    # `terraform init -reconfigure \
    #     -backend-config=profile=my-profile \
    #     -backend-config=region=us-east-1 \
    #     -backend-config=bucket=my-remote-state-bucket \
    #     -backend-config=key=project/prod/terraform.tfstate \
    #     -backend-config=dynamodb_table=my-remote-state-lock-table`
    # `terraform apply -auto-approve`
    Subprojects

    What if I have a bunch of Terraform projects?

    Let's say we have a project structure like this:

    .
    ├── terrascript.yml
    └── infrastructure/
        ├── subproject1/
        │   ├── terrascript.yml
        │   ├── main.tf
        │   └── ...
        └── subproject2/
            ├── terrascript.yml
            ├── main.tf
            └── ...
    

    And the terrascript.yml files look like this:

    # ./terrascript.yml
    subprojects:
      subproject1: ./infrastructure/subproject1/
      subproject2: ./infrastructure/subproject2/
    # ./infrastructure/subproject1/terrascript.yml
    targets:
      dev:
      prod:
    # ./infrastructure/subproject2/terrascript.yml
    targets:
      dev:
      prod:

    To run the dev target for just subproject1:

    terrascript subproject1/dev apply

    To run the dev target for all subprojects:

    terrascript dev apply

    To run all targets for all subprojects:

    terrascript "*" apply

    Note that since the top-level terrascript.yml does not contain any targets, these commands are not run there. It simply passes the commands down to its subprojects.

    Also note that glob patterns apply to both subprojects and targets.

    Inheritance

    Subprojects inherit yaml configuration from parent projects. This applies to every supported key with the notable exceptions of subprojects and targets. This means that backendConfig (and pretty much anything else) set in the top-level terrascript.yml file is also found in lower-level subprojects at runtime. Values in subprojects override inherited values (just as target-level values will override everything else).

    Let's make a more complicated project.

    .
    ├── terrascript.yml
    └── infrastructure/
        ├── subproject1/
        │   ├── terrascript.yml
        │   ├── subproject1a/
        │   │   ├── terrascript.yml
        │   │   ├── main.tf
        │   │   └── ...
        │   └── subproject1b/
        │       ├── terrascript.yml
        │       ├── main.tf
        │       └── ...
        └── subproject2/
            ├── terrascript.yml
            ├── subproject2a/
            │   ├── terrascript.yml
            │   ├── main.tf
            │   └── ...
            └── subproject2b/
                ├── terrascript.yml
                ├── main.tf
                └── ...
    

    The terrascript files might look something like:

    # ./terrascript.yml
    # Note that the key below has no special significance to Terraform or Terrascript. It's simply a
    # stored value that gets passed down via inheritance and is useful for templating (as you can see
    # in the backend key below).
    projectName: project
    subprojects:
      subproject1: ./infrastructure/subproject1/
      subproject2: ./infrastructure/subproject2/
    config:
      autoApprove: true
      backendConfig:
        profile: my-profile
        region: us-east-1
        bucket: my-remote-state-bucket
        # this key would come out to something like: project/subproject1/subproject1a/dev/terraform.tfstate
        key: "{{ spec.projectName }}/{{ spec.subprojectName }}/{{ spec.name }}/{{ target.name }}/terraform.tfstate"
        dynamodb_table: my-remote-state-lock-table
      tfVars:
        environment: "{{ target.name }}" # This input variable would apply to every subproject.
    hooks:
      # This hook is useful when running multiple targets which have different backend configurations.
      beforeEachTarget: init -reconfigure
    # ./subproject1/terrascript.yml
    subprojectName: subproject1
    subprojects:
      subproject1a: ./subproject1a/
      subproject1b: ./subproject1b/
    config:
      tfVars:
        someName: someValue # this is an input variable that applies to all of subproject1 including its subprojects
    # ./subproject1/subproject1a/terrascript.yml
    config:
      tfVars:
        someNameFor1a: someValueFor1a # this is an input variable that applies only to subproject1a
    targets:
      dev:
      prod:

    You can see that this project has a more nested structure where different subprojects have different configuration needs but there are common elements that can be templated and passed down through inheritance.

    Now, let's say you want to run a command for just subproject1a:

    terrascript subproject1/subproject1a/dev apply # note that we don't need to init since we've got that hook

    What if, for some reason, I wanted to run a command for only subprojects ending in "b"?

    terrascript "*/*b/dev" apply  # note that the quotes are needed because of the asterisks

    Can I run dev for all subprojects? Yep.

    terrascript dev apply

    Can I run every target for every subproject? Yep.

    terrascript "*" apply
    Just so you know, the terraform commands that were automated with that last command look like this...
    cd ./infrastructure/subproject1/subproject1a/
    terraform init -reconfigure \
        -backend-config=profile=my-profile \
        -backend-config=region=us-east-1 \
        -backend-config=bucket=my-remote-state-bucket \
        -backend-config=key=project/subproject1/subproject1a/dev/terraform.tfstate \
        -backend-config=dynamodb_table=my-remote-state-lock-table
    terraform apply -auto-approve \
        -var=environment=dev \
        -var=someName=someValue \
        -var=someNameFor1a=someValueFor1a
    terraform init -reconfigure \
        -backend-config=profile=my-profile \
        -backend-config=region=us-east-1 \
        -backend-config=bucket=my-remote-state-bucket \
        -backend-config=key=project/subproject1/subproject1a/prod/terraform.tfstate \
        -backend-config=dynamodb_table=my-remote-state-lock-table
    terraform apply -auto-approve \
        -var=environment=prod \
        -var=someName=someValue \
        -var=someNameFor1a=someValueFor1a
    cd ../../infrastructure/subproject1/subproject1b/
    terraform init -reconfigure \
        -backend-config=profile=my-profile \
        -backend-config=region=us-east-1 \
        -backend-config=bucket=my-remote-state-bucket \
        -backend-config=key=project/subproject1/subproject1b/dev/terraform.tfstate \
        -backend-config=dynamodb_table=my-remote-state-lock-table
    terraform apply -auto-approve \
        -var=environment=dev \
        -var=someName=someValue
    terraform init -reconfigure \
        -backend-config=profile=my-profile \
        -backend-config=region=us-east-1 \
        -backend-config=bucket=my-remote-state-bucket \
        -backend-config=key=project/subproject1/subproject1b/prod/terraform.tfstate \
        -backend-config=dynamodb_table=my-remote-state-lock-table
    terraform apply -auto-approve \
        -var=environment=prod \
        -var=someName=someValue
    cd ../../infrastructure/subproject2/subproject2a/
    terraform init -reconfigure \
        -backend-config=profile=my-profile \
        -backend-config=region=us-east-1 \
        -backend-config=bucket=my-remote-state-bucket \
        -backend-config=key=project/subproject2/subproject2a/dev/terraform.tfstate \
        -backend-config=dynamodb_table=my-remote-state-lock-table
    terraform apply -auto-approve \
        -var=environment=dev
    terraform init -reconfigure \
        -backend-config=profile=my-profile \
        -backend-config=region=us-east-1 \
        -backend-config=bucket=my-remote-state-bucket \
        -backend-config=key=project/subproject2/subproject2a/prod/terraform.tfstate \
        -backend-config=dynamodb_table=my-remote-state-lock-table
    terraform apply -auto-approve \
        -var=environment=prod
    cd ../../infrastructure/subproject2/subproject2b/
    terraform init -reconfigure \
        -backend-config=profile=my-profile \
        -backend-config=region=us-east-1 \
        -backend-config=bucket=my-remote-state-bucket \
        -backend-config=key=project/subproject2/subproject2b/dev/terraform.tfstate \
        -backend-config=dynamodb_table=my-remote-state-lock-table
    terraform apply -auto-approve \
        -var=environment=dev
    terraform init -reconfigure \
        -backend-config=profile=my-profile \
        -backend-config=region=us-east-1 \
        -backend-config=bucket=my-remote-state-bucket \
        -backend-config=key=project/subproject2/subproject2b/prod/terraform.tfstate \
        -backend-config=dynamodb_table=my-remote-state-lock-table
    terraform apply -auto-approve \
        -var=environment=prod

    Javascript/Typescript wrapper

    TODO

    Install

    npm i @joshwycuff/terrascript

    DownloadsWeekly Downloads

    142

    Version

    0.6.3

    License

    MIT

    Unpacked Size

    216 kB

    Total Files

    88

    Last publish

    Collaborators

    • avatar