provider/aws: New SSM Parameter resource (#15035)

* New SSM Parameter resource

Can be used for creating parameters in AWS' SSM Parameter Store that can then be used by other applications that have access to AWS and necessary IAM permissions.

* Add docs for new SSM Parameter resource

* Code Review and Bug Hunt and KMS Key
- Addressed all issues in #14043
- Added ForceNew directive to type
- Added the ability to specify a KMS key for encryption and decryption

* Add SSM Parameter Data Source

* Fix bad merge

* Fix SSM Parameter Integration Tests

* docs/aws: Fix typo in SSM sidebar link
This commit is contained in:
Paul Morton 2017-06-06 01:55:25 -07:00 committed by Radek Simko
parent aac43acce3
commit e4899de13e
10 changed files with 593 additions and 0 deletions

View File

@ -0,0 +1,63 @@
package aws
import (
"fmt"
"log"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ssm"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/schema"
)
func dataSourceAwsSsmParameter() *schema.Resource {
return &schema.Resource{
Read: dataAwsSsmParameterRead,
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
},
"type": {
Type: schema.TypeString,
Computed: true,
},
"value": {
Type: schema.TypeString,
Computed: true,
Sensitive: true,
},
},
}
}
func dataAwsSsmParameterRead(d *schema.ResourceData, meta interface{}) error {
ssmconn := meta.(*AWSClient).ssmconn
log.Printf("[DEBUG] Reading SSM Parameter: %s", d.Id())
paramInput := &ssm.GetParametersInput{
Names: []*string{
aws.String(d.Get("name").(string)),
},
WithDecryption: aws.Bool(true),
}
resp, err := ssmconn.GetParameters(paramInput)
if err != nil {
return errwrap.Wrapf("[ERROR] Error describing SSM parameter: {{err}}", err)
}
if len(resp.InvalidParameters) > 0 {
return fmt.Errorf("[ERROR] SSM Parameter %s is invalid", d.Get("name").(string))
}
param := resp.Parameters[0]
d.SetId(*param.Name)
d.Set("name", param.Name)
d.Set("type", param.Type)
d.Set("value", param.Value)
return nil
}

View File

@ -0,0 +1,42 @@
package aws
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
)
func TestAccAwsSsmParameterDataSource_basic(t *testing.T) {
name := "test.parameter"
resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
},
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccCheckAwsSsmParameterDataSourceConfig(name),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.aws_ssm_parameter.test", "name", name),
resource.TestCheckResourceAttr("data.aws_ssm_parameter.test", "type", "String"),
resource.TestCheckResourceAttr("data.aws_ssm_parameter.test", "value", "TestValue"),
),
},
},
})
}
func testAccCheckAwsSsmParameterDataSourceConfig(name string) string {
return fmt.Sprintf(`
resource "aws_ssm_parameter" "test" {
name = "%s"
type = "String"
value = "TestValue"
}
data "aws_ssm_parameter" "test" {
name = "${aws_ssm_parameter.test.name}"
}
`, name)
}

View File

@ -203,6 +203,7 @@ func Provider() terraform.ResourceProvider {
"aws_route53_zone": dataSourceAwsRoute53Zone(), "aws_route53_zone": dataSourceAwsRoute53Zone(),
"aws_s3_bucket_object": dataSourceAwsS3BucketObject(), "aws_s3_bucket_object": dataSourceAwsS3BucketObject(),
"aws_sns_topic": dataSourceAwsSnsTopic(), "aws_sns_topic": dataSourceAwsSnsTopic(),
"aws_ssm_parameter": dataSourceAwsSsmParameter(),
"aws_subnet": dataSourceAwsSubnet(), "aws_subnet": dataSourceAwsSubnet(),
"aws_subnet_ids": dataSourceAwsSubnetIDs(), "aws_subnet_ids": dataSourceAwsSubnetIDs(),
"aws_security_group": dataSourceAwsSecurityGroup(), "aws_security_group": dataSourceAwsSecurityGroup(),
@ -433,6 +434,7 @@ func Provider() terraform.ResourceProvider {
"aws_ssm_maintenance_window_task": resourceAwsSsmMaintenanceWindowTask(), "aws_ssm_maintenance_window_task": resourceAwsSsmMaintenanceWindowTask(),
"aws_ssm_patch_baseline": resourceAwsSsmPatchBaseline(), "aws_ssm_patch_baseline": resourceAwsSsmPatchBaseline(),
"aws_ssm_patch_group": resourceAwsSsmPatchGroup(), "aws_ssm_patch_group": resourceAwsSsmPatchGroup(),
"aws_ssm_parameter": resourceAwsSsmParameter(),
"aws_spot_datafeed_subscription": resourceAwsSpotDataFeedSubscription(), "aws_spot_datafeed_subscription": resourceAwsSpotDataFeedSubscription(),
"aws_spot_instance_request": resourceAwsSpotInstanceRequest(), "aws_spot_instance_request": resourceAwsSpotInstanceRequest(),
"aws_spot_fleet_request": resourceAwsSpotFleetRequest(), "aws_spot_fleet_request": resourceAwsSpotFleetRequest(),

View File

@ -0,0 +1,128 @@
package aws
import (
"fmt"
"log"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ssm"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsSsmParameter() *schema.Resource {
return &schema.Resource{
Create: resourceAwsSsmParameterCreate,
Read: resourceAwsSsmParameterRead,
Update: resourceAwsSsmParameterUpdate,
Delete: resourceAwsSsmParameterDelete,
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
},
"type": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validateSsmParameterType,
},
"value": {
Type: schema.TypeString,
Required: true,
Sensitive: true,
},
"key_id": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
},
}
}
func resourceAwsSsmParameterCreate(d *schema.ResourceData, meta interface{}) error {
return putAwsSSMParameter(d, meta)
}
func resourceAwsSsmParameterRead(d *schema.ResourceData, meta interface{}) error {
ssmconn := meta.(*AWSClient).ssmconn
log.Printf("[DEBUG] Reading SSM Parameter: %s", d.Id())
paramInput := &ssm.GetParametersInput{
Names: []*string{
aws.String(d.Get("name").(string)),
},
WithDecryption: aws.Bool(true),
}
resp, err := ssmconn.GetParameters(paramInput)
if err != nil {
return errwrap.Wrapf("[ERROR] Error describing SSM parameter: {{err}}", err)
}
if len(resp.InvalidParameters) > 0 {
return fmt.Errorf("[ERROR] SSM Parameter %s is invalid", d.Id())
}
param := resp.Parameters[0]
d.Set("name", param.Name)
d.Set("type", param.Type)
d.Set("value", param.Value)
return nil
}
func resourceAwsSsmParameterUpdate(d *schema.ResourceData, meta interface{}) error {
return putAwsSSMParameter(d, meta)
}
func resourceAwsSsmParameterDelete(d *schema.ResourceData, meta interface{}) error {
ssmconn := meta.(*AWSClient).ssmconn
log.Printf("[INFO] Deleting SSM Parameter: %s", d.Id())
paramInput := &ssm.DeleteParameterInput{
Name: aws.String(d.Get("name").(string)),
}
_, err := ssmconn.DeleteParameter(paramInput)
if err != nil {
return err
}
d.SetId("")
return nil
}
func putAwsSSMParameter(d *schema.ResourceData, meta interface{}) error {
ssmconn := meta.(*AWSClient).ssmconn
log.Printf("[INFO] Creating SSM Parameter: %s", d.Get("name").(string))
paramInput := &ssm.PutParameterInput{
Name: aws.String(d.Get("name").(string)),
Type: aws.String(d.Get("type").(string)),
Value: aws.String(d.Get("value").(string)),
Overwrite: aws.Bool(!d.IsNewResource()),
}
if keyID, ok := d.GetOk("key_id"); ok {
log.Printf("[DEBUG] Setting key_id for SSM Parameter %s: %s", d.Get("name").(string), keyID.(string))
paramInput.SetKeyId(keyID.(string))
}
log.Printf("[DEBUG] Waiting for SSM Parameter %q to be updated", d.Get("name").(string))
_, err := ssmconn.PutParameter(paramInput)
if err != nil {
return errwrap.Wrapf("[ERROR] Error creating SSM parameter: {{err}}", err)
}
d.SetId(d.Get("name").(string))
return resourceAwsSsmParameterRead(d, meta)
}

View File

@ -0,0 +1,210 @@
package aws
import (
"fmt"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ssm"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSSSMParameter_basic(t *testing.T) {
name := acctest.RandString(10)
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSSMParameterDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSSSMParameterBasicConfig(name, "bar"),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSSSMParameterHasValue("aws_ssm_parameter.foo", "bar"),
testAccCheckAWSSSMParameterType("aws_ssm_parameter.foo", "String"),
),
},
},
})
}
func TestAccAWSSSMParameter_update(t *testing.T) {
name := acctest.RandString(10)
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSSMParameterDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSSSMParameterBasicConfig(name, "bar"),
},
{
Config: testAccAWSSSMParameterBasicConfig(name, "baz"),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSSSMParameterHasValue("aws_ssm_parameter.foo", "baz"),
testAccCheckAWSSSMParameterType("aws_ssm_parameter.foo", "String"),
),
},
},
})
}
func TestAccAWSSSMParameter_secure(t *testing.T) {
name := acctest.RandString(10)
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSSMParameterDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSSSMParameterSecureConfig(name, "secret"),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSSSMParameterHasValue("aws_ssm_parameter.secret_foo", "secret"),
testAccCheckAWSSSMParameterType("aws_ssm_parameter.secret_foo", "SecureString"),
),
},
},
})
}
func TestAccAWSSSMParameter_secure_with_key(t *testing.T) {
name := acctest.RandString(10)
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSSMParameterDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSSSMParameterSecureConfigWithKey(name, "secret"),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSSSMParameterHasValue("aws_ssm_parameter.secret_foo", "secret"),
testAccCheckAWSSSMParameterType("aws_ssm_parameter.secret_foo", "SecureString"),
),
},
},
})
}
func testAccCheckAWSSSMGetParameter(s *terraform.State, n string) ([]*ssm.Parameter, error) {
rs, ok := s.RootModule().Resources[n]
if !ok {
return []*ssm.Parameter{}, fmt.Errorf("Not found: %s", n)
}
if rs.Primary.ID == "" {
return []*ssm.Parameter{}, fmt.Errorf("No SSM Parameter ID is set")
}
conn := testAccProvider.Meta().(*AWSClient).ssmconn
paramInput := &ssm.GetParametersInput{
Names: []*string{
aws.String(rs.Primary.Attributes["name"]),
},
WithDecryption: aws.Bool(true),
}
resp, _ := conn.GetParameters(paramInput)
if len(resp.Parameters) == 0 {
return resp.Parameters, fmt.Errorf("Expected AWS SSM Parameter to be created, but wasn't found")
}
return resp.Parameters, nil
}
func testAccCheckAWSSSMParameterHasValue(n string, v string) resource.TestCheckFunc {
return func(s *terraform.State) error {
parameters, err := testAccCheckAWSSSMGetParameter(s, n)
if err != nil {
return err
}
parameterValue := parameters[0].Value
if *parameterValue != v {
return fmt.Errorf("Expected AWS SSM Parameter to have value %s but had %s", v, *parameterValue)
}
return nil
}
}
func testAccCheckAWSSSMParameterType(n string, v string) resource.TestCheckFunc {
return func(s *terraform.State) error {
parameters, err := testAccCheckAWSSSMGetParameter(s, n)
if err != nil {
return err
}
parameterValue := parameters[0].Type
if *parameterValue != v {
return fmt.Errorf("Expected AWS SSM Parameter to have type %s but had %s", v, *parameterValue)
}
return nil
}
}
func testAccCheckAWSSSMParameterDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ssmconn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_ssm_parameter" {
continue
}
paramInput := &ssm.GetParametersInput{
Names: []*string{
aws.String(rs.Primary.Attributes["name"]),
},
}
resp, _ := conn.GetParameters(paramInput)
if len(resp.Parameters) > 0 {
return fmt.Errorf("Expected AWS SSM Parameter to be gone, but was still found")
}
return nil
}
return fmt.Errorf("Default error in SSM Parameter Test")
}
func testAccAWSSSMParameterBasicConfig(rName string, value string) string {
return fmt.Sprintf(`
resource "aws_ssm_parameter" "foo" {
name = "test_parameter-%s"
type = "String"
value = "%s"
}
`, rName, value)
}
func testAccAWSSSMParameterSecureConfig(rName string, value string) string {
return fmt.Sprintf(`
resource "aws_ssm_parameter" "secret_foo" {
name = "test_secure_parameter-%s"
type = "SecureString"
value = "%s"
}
`, rName, value)
}
func testAccAWSSSMParameterSecureConfigWithKey(rName string, value string) string {
return fmt.Sprintf(`
resource "aws_ssm_parameter" "secret_foo" {
name = "test_secure_parameter-%s"
type = "SecureString"
value = "%s"
key_id = "${aws_kms_key.test_key.id}"
}
resource "aws_kms_key" "test_key" {
description = "KMS key 1"
deletion_window_in_days = 7
}
`, rName, value)
}

View File

@ -1336,3 +1336,17 @@ func validateIamRoleDescription(v interface{}, k string) (ws []string, errors []
} }
return return
} }
func validateSsmParameterType(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
types := map[string]bool{
"String": true,
"StringList": true,
"SecureString": true,
}
if !types[value] {
errors = append(errors, fmt.Errorf("Parameter type %s is invalid. Valid types are String, StringList or SecureString", value))
}
return
}

View File

@ -2291,3 +2291,29 @@ func TestValidateIamRoleDescription(t *testing.T) {
} }
} }
} }
func TestValidateSsmParameterType(t *testing.T) {
validTypes := []string{
"String",
"StringList",
"SecureString",
}
for _, v := range validTypes {
_, errors := validateSsmParameterType(v, "name")
if len(errors) != 0 {
t.Fatalf("%q should be a valid SSM parameter type: %q", v, errors)
}
}
invalidTypes := []string{
"foo",
"string",
"Securestring",
}
for _, v := range invalidTypes {
_, errors := validateSsmParameterType(v, "name")
if len(errors) == 0 {
t.Fatalf("%q should be an invalid SSM parameter type", v)
}
}
}

View File

@ -0,0 +1,37 @@
---
layout: "aws"
page_title: "AWS: aws_ssm_parameter"
sidebar_current: "docs-aws-datasource-ssm-parameter"
description: |-
Provides a SSM Parameter datasource
---
# aws\_ssm\_parameter
Provides an SSM Parameter data source.
## Example Usage
To store a basic string parameter:
```hcl
data "aws_ssm_parameter" "foo" {
name = "foo"
}
```
~> **Note:** The unencrypted value of a SecureString will be stored in the raw state as plain-text.
[Read more about sensitive data in state](/docs/state/sensitive-data.html).
## Argument Reference
The following arguments are supported:
* `name` - (Required) The name of the parameter.
The following attributes are exported:
* `name` - (Required) The name of the parameter.
* `type` - (Required) The type of the parameter. Valid types are `String`, `StringList` and `SecureString`.
* `value` - (Required) The value of the parameter.

View File

@ -0,0 +1,65 @@
---
layout: "aws"
page_title: "AWS: aws_ssm_parameter"
sidebar_current: "docs-aws-resource-ssm-parameter"
description: |-
Provides a SSM Parameter resource
---
# aws\_ssm\_parameter
Provides an SSM Parameter resource.
## Example Usage
To store a basic string parameter:
```hcl
resource "aws_ssm_parameter" "foo" {
name = "foo"
type = "String"
value = "bar"
}
```
To store an encrypted string using the default SSM KMS key:
```hcl
resource "aws_db_instance" "default" {
allocated_storage = 10
storage_type = "gp2"
engine = "mysql"
engine_version = "5.7.16"
instance_class = "db.t2.micro"
name = "mydb"
username = "foo"
password = "${var.database_master_password}"
db_subnet_group_name = "my_database_subnet_group"
parameter_group_name = "default.mysql5.7"
}
resource "aws_ssm_parameter" "secret" {
name = "${var.environment}/database/password/master"
type = "SecureString"
value = "${var.database_master_password}"
}
```
~> **Note:** The unencrypted value of a SecureString will be stored in the raw state as plain-text.
[Read more about sensitive data in state](/docs/state/sensitive-data.html).
## Argument Reference
The following arguments are supported:
* `name` - (Required) The name of the parameter.
* `type` - (Required) The type of the parameter. Valid types are `String`, `StringList` and `SecureString`.
* `value` - (Required) The value of the parameter.
* `key_id` - (Optional) The KMS key id or arn for encrypting a SecureString.
## Attributes Reference
The following attributes are exported:
* `name` - (Required) The name of the parameter.
* `type` - (Required) The type of the parameter. Valid types are `String`, `StringList` and `SecureString`.
* `value` - (Required) The value of the parameter.

View File

@ -149,6 +149,9 @@
<li<%= sidebar_current("docs-aws-datasource-sns-topic") %>> <li<%= sidebar_current("docs-aws-datasource-sns-topic") %>>
<a href="/docs/providers/aws/d/sns_topic.html">aws_sns_topic</a> <a href="/docs/providers/aws/d/sns_topic.html">aws_sns_topic</a>
</li> </li>
<li<%= sidebar_current("docs-aws-datasource-ssm-parameter") %>>
<a href="/docs/providers/aws/d/ssm_parameter.html">aws_ssm_parameter</a>
</li>
<li<%= sidebar_current("docs-aws-datasource-subnet-x") %>> <li<%= sidebar_current("docs-aws-datasource-subnet-x") %>>
<a href="/docs/providers/aws/d/subnet.html">aws_subnet</a> <a href="/docs/providers/aws/d/subnet.html">aws_subnet</a>
</li> </li>
@ -1323,6 +1326,9 @@
<li<%= sidebar_current("docs-aws-resource-ssm-patch-group") %>> <li<%= sidebar_current("docs-aws-resource-ssm-patch-group") %>>
<a href="/docs/providers/aws/r/ssm_patch_group.html">aws_ssm_patch_group</a> <a href="/docs/providers/aws/r/ssm_patch_group.html">aws_ssm_patch_group</a>
</li> </li>
<li<%= sidebar_current("docs-aws-resource-ssm-parameter") %>>
<a href="/docs/providers/aws/r/ssm_parameter.html">aws_ssm_parameter</a>
</li>
</ul> </ul>
</li> </li>