Skip to content

Commit

Permalink
Merge pull request #60 from gruntwork-io/spin
Browse files Browse the repository at this point in the history
Add spin-up and tear-down commands
  • Loading branch information
brikis98 authored Dec 7, 2016
2 parents 036baa3 + cca2c1a commit fb52ae7
Show file tree
Hide file tree
Showing 68 changed files with 3,558 additions and 349 deletions.
135 changes: 134 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,14 +315,17 @@ remote_state = {

Note how most of the content is copy/pasted, except for the `state_file_id` and `key` parameters, which match the path
of the `.terragrunt` file itself. How do you avoid having to manually maintain the contents of all of these
similar-looking `.terragrunt` files?
similar-looking `.terragrunt` files? Also, if you want to spin up an entire environment (e.g. `stage`, `prod`), how do
you do it without having to manually run `terragrunt apply` in each of the Terraform folders within that environment?

The solution is to use the following features of Terragrunt:

* Includes
* Find parent helper
* Relative path helper
* Overriding included settings
* The spin-up and tear-down commands
* Dependencies between modules

### Includes

Expand Down Expand Up @@ -456,6 +459,132 @@ remote_state = {
The result is that when you run `terragrunt` commands in the `qa/my-app` folder, you get the `lock` settings from the
parent, but the `remote_state` settings of the child.

### The spin-up and tear-down commands

Let's say you have a single environment (e.g. `stage` or `prod`) that has a number of Terraform modules within it:

```
my-terraform-repo
└ .terragrunt
└ stage
└ frontend-app
└ main.tf
└ .terragrunt
└ backend-app
└ main.tf
└ .terragrunt
└ search-app
└ main.tf
└ .terragrunt
└ mysql
└ main.tf
└ .terragrunt
└ redis
└ main.tf
└ .terragrunt
└ vpc
└ main.tf
└ .terragrunt
```

There is one module to deploy a frontend-app, another to deploy a backend-app, another for the MySQL database, and so
on. To deploy such an environment, you'd have to manually run `terragrunt apply` in each of the subfolders. How do you
avoid this tedious and time-consuming process?

The answer is that you can use the `spin-up` command:

```
cd my-terraform-repo/stage
terragrunt spin-up
```

When you run this command, Terragrunt will find all `.terragrunt` files in the subfolders of the current working
directory, and run `terragrunt apply` in each one concurrently.

Similarly, to undeploy all the Terraform modules, you can use the `tear-down` command:

```
cd my-terraform-repo/stage
terragrunt tear-down
```

Of course, if your modules have dependencies between them—for example, you can't deploy the backend-app until the MySQL
database is deployed—you'll need to express those dependencines in your `.terragrunt` config as explained in the next
section.

### Dependencies between modules

Consider the following file structure for the `stage` environment:

```
my-terraform-repo
└ .terragrunt
└ stage
└ frontend-app
└ main.tf
└ .terragrunt
└ backend-app
└ main.tf
└ .terragrunt
└ search-app
└ main.tf
└ .terragrunt
└ mysql
└ main.tf
└ .terragrunt
└ redis
└ main.tf
└ .terragrunt
└ vpc
└ main.tf
└ .terragrunt
```

Let's assume you have the following dependencies between Terraform modules:

* Every module depends on the VPC being deployed
* The backend-app depends on the MySQL database and Redis
* The frontend-app and search-app depend on the backend-app

You can express these dependencies in your `.terragrunt` config files using the `dependencies` block. For example, in
`stage/backend-app/.terragrunt` you would specify:

```hcl
include = {
path = "${find_in_parent_folders()}"
}
dependencies = {
paths = ["../vpc", "../mysql", "../redis"]
}
```

Similarly, in `stage/frontend-app/.terragrunt`, you would specify:

```hcl
include = {
path = "${find_in_parent_folders()}"
}
dependencies = {
paths = ["../vpc", "../backend-app"]
}
```

Once you've specified the depenedencies in each `.terragrunt` file, when you run the `terragrunt spin-up` and
`terragrunt tear-down`, Terragrunt will ensure that the dependencies are applied or destroyed, respectively, in the
correct order. For the example at the start of this section, the order for the `spin-up` command would be:

1. Deploy the VPC
1. Deploy MySQL and Redis in parallel
1. Deploy the backend-app
1. Deploy the frontend-app and search-app in parallel

If any of the modules fail to deploy, then Terragrunt will not attempt to deploy the modules that depend on them. Once
you've fixed the error, it's usually safe to re-run the `spin-up` or `tear-down` command again, since it'll be a noop
for the modules that already deployed successfully, and should only affect the ones that had an error the last time
around.

## CLI Options

Terragrunt forwards all arguments and options to Terraform. The only exceptions are the options that start with the
Expand All @@ -465,6 +594,10 @@ prefix `--terragrunt-`. The currently available options are:
environment variable. The default path is `.terragrunt` in the current directory.
* `--terragrunt-non-interactive`: Don't show interactive user prompts. This will default the answer for all prompts to
'yes'. Useful if you need to run Terragrunt in an automated setting (e.g. from a script).
* `--terragrunt-working-dir`: Set the directory where Terragrunt should execute the `terraform` command. Default is the
current working directory. Note that for the `spin-up` and `tear-down` directories, this parameter has a different
meaning: Terragrunt will apply or destroy all the Terraform modules in the subfolders of the
`terragrunt-working-dir`, running `terraform` in the root of each module it finds.

## Developing terragrunt

Expand Down
136 changes: 136 additions & 0 deletions cli/args.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package cli

import (
"fmt"
"os"
"github.com/urfave/cli"
"github.com/gruntwork-io/terragrunt/options"
"github.com/gruntwork-io/terragrunt/config"
"github.com/gruntwork-io/terragrunt/errors"
"github.com/gruntwork-io/terragrunt/util"
"strings"
)

// Parse command line options that are passed in for Terragrunt
func ParseTerragruntOptions(cliContext *cli.Context) (*options.TerragruntOptions, error) {
return parseTerragruntOptionsFromArgs(cliContext.Args())
}

// TODO: replace the urfave CLI library with something else.
//
// EXPLANATION: The normal way to parse flags with the urfave CLI library would be to define the flags in the
// CreateTerragruntCLI method and to read the values of those flags using cliContext.String(...),
// cliContext.Bool(...), etc. Unfortunately, this does not work here due to a limitation in the urfave
// CLI library: if the user passes in any "command" whatsoever, (e.g. the "apply" in "terragrunt apply"), then
// any flags that come after it are not parsed (e.g. the "--foo" is not parsed in "terragrunt apply --foo").
// Therefore, we have to parse options ourselves, which is infuriating. For more details on this limitation,
// see: https://github.com/urfave/cli/issues/533. For now, our workaround is to dumbly loop over the arguments
// and look for the ones we need, but in the future, we should change to a different CLI library to avoid this
// limitation.
func parseTerragruntOptionsFromArgs(args []string) (*options.TerragruntOptions, error) {
terragruntConfigPath, err := parseStringArg(args, OPT_TERRAGRUNT_CONFIG, os.Getenv("TERRAGRUNT_CONFIG"))
if err != nil {
return nil, err
}
if terragruntConfigPath == "" {
terragruntConfigPath = config.DefaultTerragruntConfigPath
}

currentDir, err := os.Getwd()
if err != nil {
return nil, errors.WithStackTrace(err)
}

workingDir, err := parseStringArg(args, OPT_WORKING_DIR, currentDir)
if err != nil {
return nil, err
}

return &options.TerragruntOptions{
TerragruntConfigPath: terragruntConfigPath,
NonInteractive: parseBooleanArg(args, OPT_NON_INTERACTIVE, false),
TerraformCliArgs: filterTerragruntArgs(args),
WorkingDir: workingDir,
Logger: util.CreateLogger(""),
RunTerragrunt: runTerragrunt,
}, nil
}

// Return a copy of the given args with all Terragrunt-specific args removed
func filterTerragruntArgs(args[]string) []string {
out := []string{}
for i := 0; i < len(args); i++ {
arg := args[i]
argWithoutPrefix := strings.TrimPrefix(arg, "--")

if util.ListContainsElement(MULTI_MODULE_COMMANDS, arg) {
// Skip multi-module commands entirely
continue
}

if util.ListContainsElement(ALL_TERRAGRUNT_STRING_OPTS, argWithoutPrefix) {
// String flags have the argument and the value, so skip both
i = i + 1
continue
}
if util.ListContainsElement(ALL_TERRAGRUNT_BOOLEAN_OPTS, argWithoutPrefix) {
// Just skip the boolean flag
continue
}

out = append(out, arg)
}
return out
}

// Find a boolean argument (e.g. --foo) of the given name in the given list of arguments. If it's present, return true.
// If it isn't, return defaultValue.
func parseBooleanArg(args []string, argName string, defaultValue bool) bool {
for _, arg := range args {
if arg == fmt.Sprintf("--%s", argName) {
return true
}
}
return defaultValue
}

// Find a string argument (e.g. --foo "VALUE") of the given name in the given list of arguments. If it's present,
// return its value. If it is present, but has no value, return an error. If it isn't present, return defaultValue.
func parseStringArg(args []string, argName string, defaultValue string) (string, error) {
for i, arg := range args {
if arg == fmt.Sprintf("--%s", argName) {
if (i + 1) < len(args) {
return args[i + 1], nil
} else {
return "", errors.WithStackTrace(ArgMissingValue(argName))
}
}
}
return defaultValue, nil
}


// A convenience method that returns the first item (0th index) in the given list or an empty string if this is an
// empty list
func firstArg(args []string) string {
if len(args) > 0 {
return args[0]
}
return ""
}

// A convenience method that returns the second item (1st index) in the given list or an empty string if this is a
// list that has less than 2 items in it
func secondArg(args []string) string {
if len(args) > 1 {
return args[1]
}
return ""
}

// Custom error types

type ArgMissingValue string
func (err ArgMissingValue) Error() string {
return fmt.Sprintf("You must specify a value for the --%s option", string(err))
}
Loading

0 comments on commit fb52ae7

Please sign in to comment.