Skip to content

Commit

Permalink
aws_instance: allow providing user data as base64
Browse files Browse the repository at this point in the history
Providing base64 directly is convenient when it must be set to something
that isn't valid UTF-8, such as the gzipped payloads often passed to
cloud-init for more complex setups.

Passing binary data directly via Terraform attributes is not safe because
only UTF-8 strings can be stored in the state file. This is therefore
intended for use in conjunction with the base64 encoding mode of
template_cloudinit_config to enable gzip data to be passed safely to the
EC2 data without corruption as it passes through Terraform.
  • Loading branch information
apparentlymart committed Aug 8, 2017
1 parent 695be9b commit df158da
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 7 deletions.
43 changes: 37 additions & 6 deletions aws/resource_aws_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,10 @@ func resourceAwsInstance() *schema.Resource {
},

"user_data": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ConflictsWith: []string{"user_data_base64"},
StateFunc: func(v interface{}) string {
switch v.(type) {
case string:
Expand All @@ -117,6 +118,22 @@ func resourceAwsInstance() *schema.Resource {
},
},

"user_data_base64": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ConflictsWith: []string{"user_data"},
ValidateFunc: func(v interface{}, name string) (warns []string, errs []error) {
s := v.(string)
if !isBase64Encoded([]byte(s)) {
errs = append(errs, fmt.Errorf(
"%s: must be base64-encoded", name,
))
}
return
},
},

"security_groups": {
Type: schema.TypeSet,
Optional: true,
Expand Down Expand Up @@ -719,7 +736,16 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
return err
}
if attr.UserData != nil && attr.UserData.Value != nil {
d.Set("user_data", userDataHashSum(*attr.UserData.Value))
// Since user_data and user_data_base64 conflict with each other,
// we'll only set one or the other here to avoid a perma-diff.
// Since user_data_base64 was added later, we'll prefer to set
// user_data.
_, b64 := d.GetOk("user_data_base64")
if b64 {
d.Set("user_data_base64", attr.UserData.Value)
} else {
d.Set("user_data", userDataHashSum(*attr.UserData.Value))
}
}
}

Expand Down Expand Up @@ -1518,9 +1544,14 @@ func buildAwsInstanceOpts(
Name: aws.String(d.Get("iam_instance_profile").(string)),
}

user_data := d.Get("user_data").(string)
userData := d.Get("user_data").(string)
userDataBase64 := d.Get("user_data_base64").(string)

opts.UserData64 = aws.String(base64Encode([]byte(user_data)))
if userData != "" {
opts.UserData64 = aws.String(base64Encode([]byte(userData)))
} else if userDataBase64 != "" {
opts.UserData64 = aws.String(userDataBase64)
}

// check for non-default Subnet, and cast it to a String
subnet, hasSubnet := d.GetOk("subnet_id")
Expand Down
54 changes: 54 additions & 0 deletions aws/resource_aws_instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,36 @@ func TestAccAWSInstance_basic(t *testing.T) {
})
}

func TestAccAWSInstance_userDataBase64(t *testing.T) {
var v ec2.Instance

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },

// We ignore security groups because even with EC2 classic
// we'll import as VPC security groups, which is fine. We verify
// VPC security group import in other tests
IDRefreshName: "aws_instance.foo",
IDRefreshIgnore: []string{"security_groups", "vpc_security_group_ids"},

Providers: testAccProviders,
CheckDestroy: testAccCheckInstanceDestroy,
Steps: []resource.TestStep{
{
Config: testAccInstanceConfigWithUserDataBase64,
Check: resource.ComposeTestCheckFunc(
testAccCheckInstanceExists(
"aws_instance.foo", &v),
resource.TestCheckResourceAttr(
"aws_instance.foo",
"user_data_base64",
"aGVsbG8gd29ybGQ="),
),
},
},
})
}

func TestAccAWSInstance_GP2IopsDevice(t *testing.T) {
var v ec2.Instance

Expand Down Expand Up @@ -1260,6 +1290,30 @@ resource "aws_instance" "foo" {
}
`

const testAccInstanceConfigWithUserDataBase64 = `
resource "aws_security_group" "tf_test_foo" {
name = "tf_test_foo"
description = "foo"
ingress {
protocol = "icmp"
from_port = -1
to_port = -1
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "foo" {
# us-west-2
ami = "ami-4fccb37f"
availability_zone = "us-west-2a"
instance_type = "m1.small"
security_groups = ["${aws_security_group.tf_test_foo.name}"]
user_data_base64 = "${base64encode("hello world")}"
}
`

const testAccInstanceConfigWithSmallInstanceType = `
resource "aws_instance" "foo" {
# us-west-2
Expand Down
3 changes: 2 additions & 1 deletion website/docs/r/instance.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ instances. See [Shutdown Behavior](https://docs.aws.amazon.com/AWSEC2/latest/Use
instance in a VPC.
* `source_dest_check` - (Optional) Controls if traffic is routed to the instance when
the destination address does not match the instance. Used for NAT or VPNs. Defaults true.
* `user_data` - (Optional) The user data to provide when launching the instance.
* `user_data` - (Optional) The user data to provide when launching the instance. Do not pass gzip-compressed data via this argument; see `user_data_base64` instead.
* `user_data_base64` - (Optional) Can be used instead of `user_data` to pass base64-encoded binary data directly. Use this instead of `user_data` whenever the value is not a valid UTF-8 string. For example, gzip-encoded user data must be base64-encoded and passed via this argument to avoid corruption.
* `iam_instance_profile` - (Optional) The IAM Instance Profile to
launch the instance with. Specified as the name of the Instance Profile.
* `ipv6_address_count`- (Optional) A number of IPv6 addresses to associate with the primary network interface. Amazon EC2 chooses the IPv6 addresses from the range of your subnet.
Expand Down

0 comments on commit df158da

Please sign in to comment.