Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

resource/aws_elasticsearch_domain: Add support for encrypt_at_rest #2632

Merged
merged 7 commits into from
Jan 22, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions aws/resource_aws_elasticsearch_domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"log"
"regexp"
"strings"
"time"

"github.com/aws/aws-sdk-go/aws"
Expand Down Expand Up @@ -94,6 +95,28 @@ func resourceAwsElasticSearchDomain() *schema.Resource {
},
},
},
"encrypt_at_rest": {
Type: schema.TypeList,
Optional: true,
Computed: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"enabled": {
Type: schema.TypeBool,
Required: true,
ForceNew: true,
},
"kms_key_id": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
DiffSuppressFunc: suppressEquivalentKmsKeyIds,
},
},
},
},
"cluster_config": {
Type: schema.TypeList,
Optional: true,
Expand Down Expand Up @@ -296,6 +319,16 @@ func resourceAwsElasticSearchDomainCreate(d *schema.ResourceData, meta interface
}
}

if v, ok := d.GetOk("encrypt_at_rest"); ok {
options := v.([]interface{})
if options[0] == nil {
return fmt.Errorf("At least one field is expected inside encrypt_at_rest")
}

s := options[0].(map[string]interface{})
input.EncryptionAtRestOptions = expandESEncryptAtRestOptions(s)
}

if v, ok := d.GetOk("cluster_config"); ok {
config := v.([]interface{})

Expand Down Expand Up @@ -466,6 +499,10 @@ func resourceAwsElasticSearchDomainRead(d *schema.ResourceData, meta interface{}
if err != nil {
return err
}
err = d.Set("encrypt_at_rest", flattenESEncryptAtRestOptions(ds.EncryptionAtRestOptions))
if err != nil {
return err
}
err = d.Set("cluster_config", flattenESClusterConfig(ds.ElasticsearchClusterConfig))
if err != nil {
return err
Expand Down Expand Up @@ -684,6 +721,13 @@ func resourceAwsElasticSearchDomainDelete(d *schema.ResourceData, meta interface
return err
}

func suppressEquivalentKmsKeyIds(k, old, new string, d *schema.ResourceData) bool {
// The Elasticsearch API accepts a short KMS key id but always returns the ARN of the key.
// The ARN is of the format 'arn:aws:kms:REGION:ACCOUNT_ID:key/KMS_KEY_ID'.
// These should be treated as equivalent.
return strings.Contains(old, new)
}

func getKibanaEndpoint(d *schema.ResourceData) string {
return d.Get("endpoint").(string) + "/_plugin/kibana/"
}
104 changes: 104 additions & 0 deletions aws/resource_aws_elasticsearch_domain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,44 @@ func TestAccAWSElasticSearchDomain_policy(t *testing.T) {
})
}

func TestAccAWSElasticSearchDomain_encrypt_at_rest_default_key(t *testing.T) {
var domain elasticsearch.ElasticsearchDomainStatus

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckESDomainDestroy,
Steps: []resource.TestStep{
{
Config: testAccESDomainConfigWithEncryptAtRestDefaultKey(acctest.RandInt()),
Check: resource.ComposeTestCheckFunc(
testAccCheckESDomainExists("aws_elasticsearch_domain.example", &domain),
testAccCheckESEncrypted(true, &domain),
),
},
},
})
}

func TestAccAWSElasticSearchDomain_encrypt_at_rest_specify_key(t *testing.T) {
var domain elasticsearch.ElasticsearchDomainStatus

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckESDomainDestroy,
Steps: []resource.TestStep{
{
Config: testAccESDomainConfigWithEncryptAtRestWithKey(acctest.RandInt()),
Check: resource.ComposeTestCheckFunc(
testAccCheckESDomainExists("aws_elasticsearch_domain.example", &domain),
testAccCheckESEncrypted(true, &domain),
),
},
},
})
}

func TestAccAWSElasticSearchDomain_tags(t *testing.T) {
var domain elasticsearch.ElasticsearchDomainStatus
var td elasticsearch.ListTagsOutput
Expand Down Expand Up @@ -340,6 +378,16 @@ func testAccCheckESNumberOfInstances(numberOfInstances int, status *elasticsearc
}
}

func testAccCheckESEncrypted(encrypted bool, status *elasticsearch.ElasticsearchDomainStatus) resource.TestCheckFunc {
return func(s *terraform.State) error {
conf := status.EncryptionAtRestOptions
if *conf.Enabled != encrypted {
return fmt.Errorf("Encrypt at rest not set properly. Given: %t, Expected: %t", *conf.Enabled, encrypted)
}
return nil
}
}

func testAccLoadESTags(conf *elasticsearch.ElasticsearchDomainStatus, td *elasticsearch.ListTagsOutput) resource.TestCheckFunc {
return func(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).esconn
Expand Down Expand Up @@ -412,6 +460,7 @@ func testAccESDomainConfig(randInt int) string {
return fmt.Sprintf(`
resource "aws_elasticsearch_domain" "example" {
domain_name = "tf-test-%d"

ebs_options {
ebs_enabled = true
volume_size = 10
Expand Down Expand Up @@ -505,6 +554,61 @@ data "aws_iam_policy_document" "instance-assume-role-policy" {
`, randESId, randRoleId)
}

func testAccESDomainConfigWithEncryptAtRestDefaultKey(randESId int) string {
return fmt.Sprintf(`

resource "aws_elasticsearch_domain" "example" {
domain_name = "tf-test-%d"

elasticsearch_version = "6.0"

# Encrypt at rest requires m4/c4/r4/i2 instances. See http://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/aes-supported-instance-types.html
cluster_config {
instance_type = "m4.large.elasticsearch"
}

ebs_options {
ebs_enabled = true
volume_size = 10
}

encrypt_at_rest {
enabled = true
}
}
`, randESId)
}

func testAccESDomainConfigWithEncryptAtRestWithKey(randESId int) string {
return fmt.Sprintf(`
resource "aws_kms_key" "es" {
description = "kms-key-for-tf-test-%d"
deletion_window_in_days = 7
}

resource "aws_elasticsearch_domain" "example" {
domain_name = "tf-test-%d"

elasticsearch_version = "6.0"

# Encrypt at rest requires m4/c4/r4/i2 instances. See http://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/aes-supported-instance-types.html
cluster_config {
instance_type = "m4.large.elasticsearch"
}

ebs_options {
ebs_enabled = true
volume_size = 10
}

encrypt_at_rest {
enabled = true
kms_key_id = "${aws_kms_key.es.key_id}"
}
}
`, randESId, randESId)
}

func testAccESDomainConfig_complex(randInt int) string {
return fmt.Sprintf(`
resource "aws_elasticsearch_domain" "example" {
Expand Down
30 changes: 30 additions & 0 deletions aws/structure.go
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,36 @@ func expandESEBSOptions(m map[string]interface{}) *elasticsearch.EBSOptions {
return &options
}

func flattenESEncryptAtRestOptions(o *elasticsearch.EncryptionAtRestOptions) []map[string]interface{} {
if o == nil {
return []map[string]interface{}{}
}

m := map[string]interface{}{}

if o.Enabled != nil {
m["enabled"] = *o.Enabled
}
if o.KmsKeyId != nil {
m["kms_key_id"] = *o.KmsKeyId
}

return []map[string]interface{}{m}
}

func expandESEncryptAtRestOptions(m map[string]interface{}) *elasticsearch.EncryptionAtRestOptions {
options := elasticsearch.EncryptionAtRestOptions{}

if v, ok := m["enabled"]; ok {
options.Enabled = aws.Bool(v.(bool))
}
if v, ok := m["kms_key_id"]; ok && v.(string) != "" {
options.KmsKeyId = aws.String(v.(string))
}

return &options
}

func flattenESVPCDerivedInfo(o *elasticsearch.VPCDerivedInfo) []map[string]interface{} {
m := map[string]interface{}{}

Expand Down
6 changes: 6 additions & 0 deletions website/docs/r/elasticsearch_domain.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ The following arguments are supported:
* `access_policies` - (Optional) IAM policy document specifying the access policies for the domain
* `advanced_options` - (Optional) Key-value string pairs to specify advanced configuration options.
* `ebs_options` - (Optional) EBS related options, may be required based on chosen [instance size](https://aws.amazon.com/elasticsearch-service/pricing/). See below.
* `encrypt_at_rest` - (Optional) Encrypt at rest options. Only available for [certain instance types](http://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/aes-supported-instance-types.html). See below.
* `cluster_config` - (Optional) Cluster configuration of the domain, see below.
* `snapshot_options` - (Optional) Snapshot related options, see below.
* `vpc_options` - (Optional) VPC related options, see below. Adding or removing this configuration forces a new resource ([documentation](https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-vpc.html#es-vpc-limitations)).
Expand All @@ -73,6 +74,11 @@ The following arguments are supported:
* `iops` - (Optional) The baseline input/output (I/O) performance of EBS volumes
attached to data nodes. Applicable only for the Provisioned IOPS EBS volume type.

**encrypt_at_rest** supports the following attributes:

* `enabled` - (Required) Whether to enable encryption at rest. If the `encrypt_at_rest` block is not provided then this defaults to `false`.
* `kms_key_id` - (Optional) The KMS key id to encrypt the Elasticsearch domain with. If not specified then it defaults to using the `aws/es` service KMS key.

**cluster_config** supports the following attributes:

* `instance_type` - (Optional) Instance type of data nodes in the cluster.
Expand Down