From c37a5531ed862ae30e08edbdad5569b87b6eb5de Mon Sep 17 00:00:00 2001 From: Ilias Bertsimas Date: Tue, 6 Jun 2017 17:28:13 +0100 Subject: [PATCH 1/6] Add support for Terraform 0.9.x and legacy mode. --- aws_helper/conn.go | 4 +++ main.go | 17 ++++++++++ tf_helper/state.go | 78 +++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 92 insertions(+), 7 deletions(-) diff --git a/aws_helper/conn.go b/aws_helper/conn.go index 2d0f408..701fbf9 100644 --- a/aws_helper/conn.go +++ b/aws_helper/conn.go @@ -8,6 +8,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/s3" "github.com/aws/aws-sdk-go/service/sts" ) @@ -15,6 +16,7 @@ import ( type AWSClient struct { stsconn *sts.STS S3conn *s3.S3 + Dynconn *dynamodb.DynamoDB region string } @@ -108,6 +110,7 @@ func (c *Config) Connect() interface{} { log.Println("[INFO] Initializing S3 Connection") client.S3conn = s3.New(sess) + client.Dynconn = dynamodb.New(sess) return &client @@ -127,6 +130,7 @@ func (c *Config) assumeConnect(sts *sts.AssumeRoleOutput) interface{} { log.Println("[INFO] Initializing S3 Connection") client.S3conn = s3.New(sess) + client.Dynconn = dynamodb.New(sess) return &client diff --git a/main.go b/main.go index 5f52fe4..dead8c3 100644 --- a/main.go +++ b/main.go @@ -134,7 +134,9 @@ func main() { state_config := &tf_helper.Config{Bucket_name: fmt.Sprintf("%s-%s-%s-tfstate", project_config.Project, project_config.account, project_config.environment), State_filename: fmt.Sprintf("%s-%s-%s.tfstate", project_config.Project, project_config.account, project_config.environment), + Lock_table: fmt.Sprintf("%s-%s-%s-locktable", project_config.Project, project_config.account, project_config.environment), Versioning: true, + Region: project_config.Region, Encrypt_s3_state: project_config.Encrypt_s3_state, TargetsTF: targetsTF, TFlegacy: tf_legacy, @@ -195,6 +197,21 @@ func main() { } + if !tf_legacy { + for i := 1; i <= retries; i++ { + + if !state_config.Create_locktable(client) { + log.Printf("[WARN] DynamoDB table %s failed to be created. Retrying.\n", state_config.Lock_table) + } else { + log.Printf("[INFO] DynamoDB table %s created.\n", state_config.Lock_table) + break + } + + time.Sleep(time.Duration(i) * time.Second) + + } + } + if bucket_created { state_config.Setup_remote_state() } else { diff --git a/tf_helper/state.go b/tf_helper/state.go index c27dae6..454da64 100644 --- a/tf_helper/state.go +++ b/tf_helper/state.go @@ -5,6 +5,7 @@ import ( "log" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/s3" "github.com/mhlias/tholos/aws_helper" ) @@ -12,10 +13,12 @@ import ( type Config struct { Bucket_name string State_filename string + Lock_table string Encrypt_s3_state bool Versioning bool TargetsTF []string TFlegacy bool + Region string } func (c *Config) Create_bucket(client interface{}) bool { @@ -99,7 +102,50 @@ func (c *Config) enable_versioning(client interface{}) bool { } -func (c *Config) setup_lock_DB() { +func (c *Config) Create_locktable(client interface{}) bool { + + params := &dynamodb.ListTablesInput{ + ExclusiveStartTableName: aws.String(c.Lock_table), + } + + resp, err := client.(*aws_helper.AWSClient).Dynconn.ListTables(params) + + if err != nil { + fmt.Println(err.Error()) + return false + } + + if len(resp.TableNames) > 0 { + return true + } + + params2 := &dynamodb.CreateTableInput{ + AttributeDefinitions: []*dynamodb.AttributeDefinition{ + { + AttributeName: aws.String("LockID"), + AttributeType: aws.String("S"), + }, + }, + KeySchema: []*dynamodb.KeySchemaElement{ + { + AttributeName: aws.String("LockID"), + KeyType: aws.String("HASH"), + }, + }, + ProvisionedThroughput: &dynamodb.ProvisionedThroughput{ + ReadCapacityUnits: aws.Int64(5), + WriteCapacityUnits: aws.Int64(5), + }, + TableName: aws.String(c.Lock_table), + } + _, err2 := client.(*aws_helper.AWSClient).Dynconn.CreateTable(params2) + + if err2 != nil { + fmt.Println(err2.Error()) + return false + } + + return true } @@ -109,12 +155,30 @@ func (c *Config) Setup_remote_state() { cmdName := "terraform" - args := []string{"remote", - "config", - "-backend=S3", - fmt.Sprintf("-backend-config=bucket=%s", c.Bucket_name), - fmt.Sprintf("-backend-config=key=%s", c.State_filename), - fmt.Sprintf("-backend-config=encrypt=%t", c.Encrypt_s3_state), + var args []string + + if c.TFlegacy { + + args = []string{"remote", + "config", + "-backend=S3", + fmt.Sprintf("-backend-config=bucket=%s", c.Bucket_name), + fmt.Sprintf("-backend-config=key=%s", c.State_filename), + fmt.Sprintf("-backend-config=encrypt=%t", c.Encrypt_s3_state), + } + + } else { + + args = []string{"init", + "-backend=true", + fmt.Sprintf("-backend-config='bucket=%s'", c.Bucket_name), + fmt.Sprintf("-backend-config='key=%s'", c.State_filename), + fmt.Sprintf("-backend-config='region=%s'", c.Region), + fmt.Sprintf("-backend-config='lock_table=%s'", c.Lock_table), + fmt.Sprintf("-backend-config=encrypt=%t", c.Encrypt_s3_state), + "-force-copy", + } + } if ExecCmd(cmdName, args) { From 7cd1987886d0acfa96f745c44c15cef6ba7174c1 Mon Sep 17 00:00:00 2001 From: Ilias Bertsimas Date: Wed, 7 Jun 2017 11:16:13 +0100 Subject: [PATCH 2/6] Make sure dynamoDB lock table is checked before operations that need it. --- main.go | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/main.go b/main.go index dead8c3..af35e33 100644 --- a/main.go +++ b/main.go @@ -179,6 +179,25 @@ func main() { } + if *syncPtr || *planPtr || *applyPtr { + + if !state_config.TFlegacy { + for j := 1; j <= retries; j++ { + + if !state_config.Create_locktable(client) { + log.Printf("[WARN] DynamoDB table %s failed to be created. Retrying.\n", state_config.Lock_table) + } else { + log.Printf("[INFO] DynamoDB table %s created.\n", state_config.Lock_table) + break + } + + time.Sleep(time.Duration(j) * time.Second) + + } + } + + } + if *syncPtr { bucket_created := false @@ -197,21 +216,6 @@ func main() { } - if !tf_legacy { - for i := 1; i <= retries; i++ { - - if !state_config.Create_locktable(client) { - log.Printf("[WARN] DynamoDB table %s failed to be created. Retrying.\n", state_config.Lock_table) - } else { - log.Printf("[INFO] DynamoDB table %s created.\n", state_config.Lock_table) - break - } - - time.Sleep(time.Duration(i) * time.Second) - - } - } - if bucket_created { state_config.Setup_remote_state() } else { From 9f94bc7ba678875fd96dcfa56f46a4071b40c6f1 Mon Sep 17 00:00:00 2001 From: Ilias Bertsimas Date: Mon, 12 Jun 2017 19:56:07 +0100 Subject: [PATCH 3/6] Add support for Terraform state environments. --- main.go | 12 +++++++++ tf_helper/state.go | 64 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index af35e33..380a938 100644 --- a/main.go +++ b/main.go @@ -57,6 +57,7 @@ func main() { modulesPtr := flag.Bool("u", false, "Fetch and update modules from remote repo") outputsPtr := flag.Bool("o", false, "Display Terraform outputs") configPtr := flag.Bool("c", false, "Force reconfiguration of Tholos") + envPtr := flag.String("e", "", "Terraform state environment to use") flag.Var(&targetsTF, "t", "Terraform resources to target only, (-t resourcetype.resource resourcetype2.resource2)") flag.Parse() @@ -128,6 +129,11 @@ func main() { if ver_int > 8 { tf_legacy = false + if len(*envPtr) > 0 { + log.Printf("[INFO] Will be working on STATE ENVIRONMENT: %s", *envPtr) + // Sleep for 5 seconds let the user stop execution if wrong state environment + time.Sleep(5 * time.Second) + } } else { log.Printf("[WARN] Running in legacy mode, current Terraform version: %s, install >=0.9.x for full features.\n", tf_version) } @@ -140,6 +146,7 @@ func main() { Encrypt_s3_state: project_config.Encrypt_s3_state, TargetsTF: targetsTF, TFlegacy: tf_legacy, + TFenv: *envPtr, } var tf_parallelism int16 = 10 @@ -182,6 +189,11 @@ func main() { if *syncPtr || *planPtr || *applyPtr { if !state_config.TFlegacy { + + if len(*envPtr) > 0 { + state_config.Switch_env() + } + for j := 1; j <= retries; j++ { if !state_config.Create_locktable(client) { diff --git a/tf_helper/state.go b/tf_helper/state.go index 454da64..2a381c5 100644 --- a/tf_helper/state.go +++ b/tf_helper/state.go @@ -1,8 +1,11 @@ package tf_helper import ( + "bytes" "fmt" "log" + "os/exec" + "strings" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/dynamodb" @@ -18,6 +21,7 @@ type Config struct { Versioning bool TargetsTF []string TFlegacy bool + TFenv string Region string } @@ -149,6 +153,64 @@ func (c *Config) Create_locktable(client interface{}) bool { } +func (c *Config) Switch_env() { + + var args []string + env_exists := false + + cmdList := exec.Command("terraform", "env", "list") + var out bytes.Buffer + cmdList.Stdout = &out + err := cmdList.Run() + if err != nil { + log.Fatal("Failed to get Terraform state environments list:", err) + } + + out_str := out.String() + + tfenvs := strings.Split(out_str, "\n") + + for _, e := range tfenvs { + if e == strings.Trim(c.TFenv, "* ") { + env_exists = true + break + } + } + + if !env_exists { + + cmdCreate := "terraform" + + args = []string{ + "env", + "new", + c.TFenv, + } + + if ExecCmd(cmdCreate, args) { + log.Printf("[INFO] Terraform state environment %s created.", c.TFenv) + } else { + log.Fatal("[ERROR] Failed create Terraform state environment. Aborting.\n") + } + + } + + cmdSelect := "terraform" + + args = []string{ + "env", + "select", + c.TFenv, + } + + if ExecCmd(cmdSelect, args) { + log.Printf("[INFO] Terraform state environment %s selected.", c.TFenv) + } else { + log.Fatal("[ERROR] Failed select Terraform state environment. Aborting.\n") + } + +} + func (c *Config) Setup_remote_state() { //log.Printf("[INFO] Environment variables: %s, %s, %s, %s", os.Getenv("AWS_ACCESS_KEY_ID"), os.Getenv("AWS_SECRET_ACCESS_KEY"), os.Getenv("AWS_SECURITY_TOKEN"), os.Getenv("AWS_DEFAULT_REGION") ) @@ -184,7 +246,7 @@ func (c *Config) Setup_remote_state() { if ExecCmd(cmdName, args) { log.Println("[INFO] Remote State was set up successfully.") } else { - log.Fatal("[INFO] Remote state failed to be set up. Aborting.\n") + log.Fatal("[ERROR] Remote state failed to be set up. Aborting.\n") } } From 9ad3d593cee683ac7b244c6e10f175d2512c7c6a Mon Sep 17 00:00:00 2001 From: Ilias Bertsimas Date: Mon, 19 Jun 2017 17:00:34 +0100 Subject: [PATCH 4/6] Fix locktable existance and env existance tests. --- tf_helper/state.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tf_helper/state.go b/tf_helper/state.go index 2a381c5..2e66f51 100644 --- a/tf_helper/state.go +++ b/tf_helper/state.go @@ -108,9 +108,7 @@ func (c *Config) enable_versioning(client interface{}) bool { func (c *Config) Create_locktable(client interface{}) bool { - params := &dynamodb.ListTablesInput{ - ExclusiveStartTableName: aws.String(c.Lock_table), - } + params := &dynamodb.ListTablesInput{} resp, err := client.(*aws_helper.AWSClient).Dynconn.ListTables(params) @@ -119,8 +117,10 @@ func (c *Config) Create_locktable(client interface{}) bool { return false } - if len(resp.TableNames) > 0 { - return true + for _, dt := range resp.TableNames { + if *dt == c.Lock_table { + return true + } } params2 := &dynamodb.CreateTableInput{ @@ -171,7 +171,7 @@ func (c *Config) Switch_env() { tfenvs := strings.Split(out_str, "\n") for _, e := range tfenvs { - if e == strings.Trim(c.TFenv, "* ") { + if c.TFenv == strings.Trim(e, "* ") { env_exists = true break } @@ -233,10 +233,10 @@ func (c *Config) Setup_remote_state() { args = []string{"init", "-backend=true", - fmt.Sprintf("-backend-config='bucket=%s'", c.Bucket_name), - fmt.Sprintf("-backend-config='key=%s'", c.State_filename), - fmt.Sprintf("-backend-config='region=%s'", c.Region), - fmt.Sprintf("-backend-config='lock_table=%s'", c.Lock_table), + fmt.Sprintf("-backend-config=bucket=%s", c.Bucket_name), + fmt.Sprintf("-backend-config=key=%s", c.State_filename), + fmt.Sprintf("-backend-config=region=%s", c.Region), + fmt.Sprintf("-backend-config=lock_table=%s", c.Lock_table), fmt.Sprintf("-backend-config=encrypt=%t", c.Encrypt_s3_state), "-force-copy", } From 1b652be1d8e10ce9cfa5411d21914487fa597351 Mon Sep 17 00:00:00 2001 From: Ilias Bertsimas Date: Tue, 20 Jun 2017 14:40:06 +0100 Subject: [PATCH 5/6] Update readmea to reflect support of 0.9.x --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index deade1d..19b007a 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ This tool wraps terraform execution forcing a specific structure while providing - Configures remote terraform state on the created S3 bucket. - Provides management of remote git/github terraform modules using the Terrafile concept. - Provides plan and apply functionality with resources target support and keeps the local & remote states in sync. + - Support for Terraform 0.9.x and legacy mode with version autodetection + - Support for Terraform state environments (created if not exist already) with Terraform 0.9.x versions ### Setup Requirements @@ -24,6 +26,15 @@ Configuration input required: - Name of your project config yaml file: This file will reside on the root directory of your project and needs to be `%name.yaml` which `%name` you specify in this stage. - Directory name of your terraform modules: This will be always created a level down of the root of your project and will be used by the Terrafile concept to store your project's modules. Will also be the modules source in your terraform templates. - Root profile, is your `$HOME/.aws/credentials` profile name that can assume roles on your AWS accounts + - With Terraform 0.9.x you need to include in a .tf file the following terraform block: + + ``` + terraform { + required_version = ">= 0.9.0" + backend "s3" {} + } + + ``` From the files mentioned above here are some examples of what their contents need to be: @@ -96,11 +107,12 @@ The tool accepts the following parameters: ``` -a Terraform Apply Plan -c Force reconfiguration of Tholos + -e Terraform state environment to use -o Display Terraform outputs -p Terraform Plan -s Sync remote S3 state + -t Terraform resources to target only, (-t resourcetype.resource resourcetype2.resource2) -u Fetch and update modules from remote repo - -t Terraform resources to target only, (-t resourcetype.resource resourcetype2.resource2) ``` From 0379eb7e2e1389ca97d3bda5b86445653c9ec6c8 Mon Sep 17 00:00:00 2001 From: Ilias Bertsimas Date: Tue, 20 Jun 2017 16:26:56 +0100 Subject: [PATCH 6/6] Update README. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 19b007a..b3e095e 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ This tool wraps terraform execution forcing a specific structure while providing - Gets STS tokens and uses them for the current account. - Creates an S3 bucket in the current account and enables versioning. - Configures remote terraform state on the created S3 bucket. + - Creates a DynamoDB lock table and uses it to lock remote S3 state (Terraform 0.9.x only) - Provides management of remote git/github terraform modules using the Terrafile concept. - Provides plan and apply functionality with resources target support and keeps the local & remote states in sync. - Support for Terraform 0.9.x and legacy mode with version autodetection