add support for geolocation and latency records to aws route53 provider

This commit is contained in:
Adam Mielke 2015-08-12 11:03:47 -05:00 committed by clint shryock
parent f162d9173d
commit 97fbeaf59f
5 changed files with 459 additions and 43 deletions

View File

@ -27,9 +27,8 @@ func resourceAwsRoute53Record() *schema.Resource {
Update: resourceAwsRoute53RecordUpdate,
Delete: resourceAwsRoute53RecordDelete,
SchemaVersion: 1,
SchemaVersion: 2,
MigrateState: resourceAwsRoute53RecordMigrateState,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
@ -71,13 +70,10 @@ func resourceAwsRoute53Record() *schema.Resource {
ConflictsWith: []string{"alias"},
},
// Weight uses a special sentinel value to indicate its presence.
// Because 0 is a valid value for Weight, we default to -1 so that any
// inclusion of a weight (zero or not) will be a usable value
"weight": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: -1,
Removed: "Now implemented as weighted_routing_policy; see docs",
},
"set_identifier": &schema.Schema{
@ -114,6 +110,95 @@ func resourceAwsRoute53Record() *schema.Resource {
"failover": &schema.Schema{ // PRIMARY | SECONDARY
Type: schema.TypeString,
Optional: true,
Removed: "Now implemented as failover_routing_policy; see docs",
},
"failover_routing_policy": &schema.Schema{
Type: schema.TypeList,
Optional: true,
ConflictsWith: []string{
"geolocation_routing_policy",
"latency_routing_policy",
"weighted_routing_policy",
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"type": &schema.Schema{
Type: schema.TypeString,
Required: true,
ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
value := v.(string)
if value != "PRIMARY" && value != "SECONDARY" {
es = append(es, fmt.Errorf("Failover policy type must be PRIMARY or SECONDARY"))
}
return
},
},
},
},
},
"latency_routing_policy": &schema.Schema{
Type: schema.TypeList,
Optional: true,
ConflictsWith: []string{
"failover_routing_policy",
"geolocation_routing_policy",
"weighted_routing_policy",
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"region": &schema.Schema{
Type: schema.TypeString,
Required: true,
Optional: false,
},
},
},
},
"geolocation_routing_policy": &schema.Schema{ // AWS Geolocation
Type: schema.TypeList,
Optional: true,
ConflictsWith: []string{
"failover_routing_policy",
"latency_routing_policy",
"weighted_routing_policy",
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"continent": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"country": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"subdivision": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
},
},
"weighted_routing_policy": &schema.Schema{
Type: schema.TypeList,
Optional: true,
ConflictsWith: []string{
"failover_routing_policy",
"geolocation_routing_policy",
"latency_routing_policy",
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"weight": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
},
},
},
"health_check_id": &schema.Schema{ // ID of health check
@ -292,14 +377,50 @@ func resourceAwsRoute53RecordRead(d *schema.ResourceData, meta interface{}) erro
}
d.Set("ttl", record.TTL)
// Only set the weight if it's non-nil, otherwise we end up with a 0 weight
// which has actual contextual meaning with Route 53 records
// See http://docs.aws.amazon.com/fr_fr/Route53/latest/APIReference/API_ChangeResourceRecordSets_Examples.html
if record.Weight != nil {
d.Set("weight", record.Weight)
if record.Failover != nil {
v := []map[string]interface{}{{
"type": aws.StringValue(record.Failover),
}}
d.Set("failover_routing_policy", v)
if err != nil {
return fmt.Errorf("[DEBUG] Error setting records for: %s, error: %#v", d.Id(), err)
}
}
if record.GeoLocation != nil {
v := []map[string]interface{}{{
"continent": aws.StringValue(record.GeoLocation.ContinentCode),
"country": aws.StringValue(record.GeoLocation.CountryCode),
"subdivision": aws.StringValue(record.GeoLocation.SubdivisionCode),
}}
d.Set("geolocation_routing_policy", v)
if err != nil {
return fmt.Errorf("[DEBUG] Error setting records for: %s, error: %#v", d.Id(), err)
}
}
if record.Region != nil {
v := []map[string]interface{}{{
"region": aws.StringValue(record.Region),
}}
d.Set("latency_routing_policy", v)
if err != nil {
return fmt.Errorf("[DEBUG] Error setting records for: %s, error: %#v", d.Id(), err)
}
}
if record.Weight != nil {
v := []map[string]interface{}{{
"weight": aws.Int64Value((record.Weight)),
}}
d.Set("weighted_routing_policy", v)
if err != nil {
return fmt.Errorf("[DEBUG] Error setting records for: %s, error: %#v", d.Id(), err)
}
}
d.Set("set_identifier", record.SetIdentifier)
d.Set("failover", record.Failover)
d.Set("health_check_id", record.HealthCheckId)
return nil
@ -483,27 +604,69 @@ func resourceAwsRoute53RecordBuildSet(d *schema.ResourceData, zoneName string) (
}
}
if v, ok := d.GetOk("failover"); ok {
if v, ok := d.GetOk("failover_routing_policy"); ok {
if _, ok := d.GetOk("set_identifier"); !ok {
return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "set_identifier": required field is not set when "failover" is set`, d.Get("name").(string))
return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "set_identifier": required field is not set when "failover_routing_policy" is set`, d.Get("name").(string))
}
rec.Failover = aws.String(v.(string))
records := v.([]interface{})
if len(records) > 1 {
return nil, fmt.Errorf("You can only define a single failover_routing_policy per record")
}
failover := records[0].(map[string]interface{})
rec.Failover = aws.String(failover["type"].(string))
}
if v, ok := d.GetOk("health_check_id"); ok {
rec.HealthCheckId = aws.String(v.(string))
}
if v, ok := d.GetOk("weighted_routing_policy"); ok {
if _, ok := d.GetOk("set_identifier"); !ok {
return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "set_identifier": required field is not set when "weight_routing_policy" is set`, d.Get("name").(string))
}
records := v.([]interface{})
if len(records) > 1 {
return nil, fmt.Errorf("You can only define a single weighed_routing_policy per record")
}
weight := records[0].(map[string]interface{})
rec.Weight = aws.Int64(int64(weight["weight"].(int)))
}
if v, ok := d.GetOk("set_identifier"); ok {
rec.SetIdentifier = aws.String(v.(string))
}
w := d.Get("weight").(int)
if w > -1 {
if v, ok := d.GetOk("latency_routing_policy"); ok {
if _, ok := d.GetOk("set_identifier"); !ok {
return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "set_identifier": required field is not set when "weight" is set`, d.Get("name").(string))
return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "set_identifier": required field is not set when "latency_routing_policy" is set`, d.Get("name").(string))
}
rec.Weight = aws.Int64(int64(w))
records := v.([]interface{})
if len(records) > 1 {
return nil, fmt.Errorf("You can only define a single latency_routing_policy per record")
}
latency := records[0].(map[string]interface{})
rec.Region = aws.String(latency["region"].(string))
}
if v, ok := d.GetOk("geolocation_routing_policy"); ok {
if _, ok := d.GetOk("set_identifier"); !ok {
return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "set_identifier": required field is not set when "geolocation_routing_policy" is set`, d.Get("name").(string))
}
geolocations := v.([]interface{})
if len(geolocations) > 1 {
return nil, fmt.Errorf("You can only define a single geolocation_routing_policy per record")
}
geolocation := geolocations[0].(map[string]interface{})
rec.GeoLocation = &route53.GeoLocation{
ContinentCode: nilString(geolocation["continent"].(string)),
CountryCode: nilString(geolocation["country"].(string)),
SubdivisionCode: nilString(geolocation["subdivision"].(string)),
}
log.Printf("[DEBUG] Creating geolocation: %#v", geolocation)
}
return rec, nil
@ -551,3 +714,13 @@ func resourceAwsRoute53AliasRecordHash(v interface{}) int {
return hashcode.String(buf.String())
}
// nilString takes a string as an argument and returns a string
// pointer. The returned pointer is nil if the string argument is
// empty, otherwise it is a pointer to a copy of the string.
func nilString(s string) *string {
if s == "" {
return nil
}
return aws.String(s)
}

View File

@ -14,6 +14,9 @@ func resourceAwsRoute53RecordMigrateState(
case 0:
log.Println("[INFO] Found AWS Route53 Record State v0; migrating to v1")
return migrateRoute53RecordStateV0toV1(is)
case 1:
log.Println("[INFO] Found AWS Route53 Record State v0; migrating to v1")
return migrateRoute53RecordStateV1toV2(is)
default:
return is, fmt.Errorf("Unexpected schema version: %d", v)
}
@ -31,3 +34,25 @@ func migrateRoute53RecordStateV0toV1(is *terraform.InstanceState) (*terraform.In
log.Printf("[DEBUG] Attributes after migration: %#v, new name: %s", is.Attributes, newName)
return is, nil
}
func migrateRoute53RecordStateV1toV2(is *terraform.InstanceState) (*terraform.InstanceState, error) {
if is.Empty() {
log.Println("[DEBUG] Empty InstanceState; nothing to migrate.")
return is, nil
}
log.Printf("[DEBUG] Attributes before migration: %#v", is.Attributes)
if is.Attributes["weight"] != "" && is.Attributes["weight"] != "-1" {
is.Attributes["weighted_routing_policy.#"] = "1"
key := fmt.Sprintf("weighted_routing_policy.0.weight")
is.Attributes[key] = is.Attributes["weight"]
}
if is.Attributes["failover"] != "" {
is.Attributes["failover_routing_policy.#"] = "1"
key := fmt.Sprintf("failover_routing_policy.0.type")
is.Attributes[key] = is.Attributes["failover"]
}
delete(is.Attributes, "weight")
delete(is.Attributes, "failover")
log.Printf("[DEBUG] Attributes after migration: %#v", is.Attributes)
return is, nil
}

View File

@ -57,3 +57,54 @@ func TestAWSRoute53RecordMigrateState(t *testing.T) {
}
}
}
func TestAWSRoute53RecordMigrateStateV1toV2(t *testing.T) {
cases := map[string]struct {
StateVersion int
Attributes map[string]string
Expected map[string]string
Meta interface{}
}{
"v0_1": {
StateVersion: 1,
Attributes: map[string]string{
"weight": "0",
"failover": "PRIMARY",
},
Expected: map[string]string{
"weighted_routing_policy.#": "1",
"weighted_routing_policy.0.weight": "0",
"failover_routing_policy.#": "1",
"failover_routing_policy.0.type": "PRIMARY",
},
},
"v0_2": {
StateVersion: 1,
Attributes: map[string]string{
"weight": "-1",
},
Expected: map[string]string{},
},
}
for tn, tc := range cases {
is := &terraform.InstanceState{
ID: "route53_record",
Attributes: tc.Attributes,
}
is, err := resourceAwsRoute53Record().MigrateState(
tc.StateVersion, is, tc.Meta)
if err != nil {
t.Fatalf("bad: %s, err: %#v", tn, err)
}
for k, v := range tc.Expected {
if is.Attributes[k] != v {
t.Fatalf(
"bad: %s\n\n expected: %#v -> %#v\n got: %#v -> %#v\n in: %#v",
tn, k, v, k, is.Attributes[k], is.Attributes)
}
}
}
}

View File

@ -274,6 +274,43 @@ func TestAccAWSRoute53Record_weighted_alias(t *testing.T) {
})
}
func TestAccAWSRoute53Record_geolocation_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckRoute53RecordDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccRoute53GeolocationCNAMERecord,
Check: resource.ComposeTestCheckFunc(
testAccCheckRoute53RecordExists("aws_route53_record.default"),
testAccCheckRoute53RecordExists("aws_route53_record.california"),
testAccCheckRoute53RecordExists("aws_route53_record.oceania"),
testAccCheckRoute53RecordExists("aws_route53_record.denmark"),
),
},
},
})
}
func TestAccAWSRoute53Record_latency_basic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckRoute53RecordDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccRoute53LatencyCNAMERecord,
Check: resource.ComposeTestCheckFunc(
testAccCheckRoute53RecordExists("aws_route53_record.us-east-1"),
testAccCheckRoute53RecordExists("aws_route53_record.eu-west-1"),
testAccCheckRoute53RecordExists("aws_route53_record.ap-northeast-1"),
),
},
},
})
}
func TestAccAWSRoute53Record_TypeChange(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
@ -595,7 +632,9 @@ resource "aws_route53_record" "www-primary" {
name = "www"
type = "CNAME"
ttl = "5"
failover = "PRIMARY"
failover_routing_policy {
type = "PRIMARY"
}
health_check_id = "${aws_route53_health_check.foo.id}"
set_identifier = "www-primary"
records = ["primary.notexample.com"]
@ -606,7 +645,9 @@ resource "aws_route53_record" "www-secondary" {
name = "www"
type = "CNAME"
ttl = "5"
failover = "SECONDARY"
failover_routing_policy {
type = "SECONDARY"
}
set_identifier = "www-secondary"
records = ["secondary.notexample.com"]
}
@ -622,7 +663,9 @@ resource "aws_route53_record" "www-dev" {
name = "www"
type = "CNAME"
ttl = "5"
weight = 10
weighted_routing_policy {
weight = 10
}
set_identifier = "dev"
records = ["dev.notexample.com"]
}
@ -632,7 +675,9 @@ resource "aws_route53_record" "www-live" {
name = "www"
type = "CNAME"
ttl = "5"
weight = 90
weighted_routing_policy {
weight = 90
}
set_identifier = "live"
records = ["dev.notexample.com"]
}
@ -642,12 +687,111 @@ resource "aws_route53_record" "www-off" {
name = "www"
type = "CNAME"
ttl = "5"
weight = 0
weighted_routing_policy = {
weight = 0
}
set_identifier = "off"
records = ["dev.notexample.com"]
}
`
const testAccRoute53GeolocationCNAMERecord = `
resource "aws_route53_zone" "main" {
name = "notexample.com"
}
resource "aws_route53_record" "default" {
zone_id = "${aws_route53_zone.main.zone_id}"
name = "www"
type = "CNAME"
ttl = "5"
geolocation_routing_policy {
country = "*"
}
set_identifier = "Default"
records = ["dev.notexample.com"]
}
resource "aws_route53_record" "california" {
zone_id = "${aws_route53_zone.main.zone_id}"
name = "www"
type = "CNAME"
ttl = "5"
geolocation_routing_policy {
country = "US"
subdivision = "CA"
}
set_identifier = "California"
records = ["dev.notexample.com"]
}
resource "aws_route53_record" "oceania" {
zone_id = "${aws_route53_zone.main.zone_id}"
name = "www"
type = "CNAME"
ttl = "5"
geolocation_routing_policy {
continent = "OC"
}
set_identifier = "Oceania"
records = ["dev.notexample.com"]
}
resource "aws_route53_record" "denmark" {
zone_id = "${aws_route53_zone.main.zone_id}"
name = "www"
type = "CNAME"
ttl = "5"
geolocation_routing_policy {
country = "DK"
}
set_identifier = "Denmark"
records = ["dev.notexample.com"]
}
`
const testAccRoute53LatencyCNAMERecord = `
resource "aws_route53_zone" "main" {
name = "notexample.com"
}
resource "aws_route53_record" "us-east-1" {
zone_id = "${aws_route53_zone.main.zone_id}"
name = "www"
type = "CNAME"
ttl = "5"
latency_routing_policy {
region = "us-east-1"
}
set_identifier = "us-east-1"
records = ["dev.notexample.com"]
}
resource "aws_route53_record" "eu-west-1" {
zone_id = "${aws_route53_zone.main.zone_id}"
name = "www"
type = "CNAME"
ttl = "5"
latency_routing_policy {
region = "eu-west-1"
}
set_identifier = "eu-west-1"
records = ["dev.notexample.com"]
}
resource "aws_route53_record" "ap-northeast-1" {
zone_id = "${aws_route53_zone.main.zone_id}"
name = "www"
type = "CNAME"
ttl = "5"
latency_routing_policy {
region = "ap-northeast-1"
}
set_identifier = "ap-northeast-1"
records = ["dev.notexample.com"]
}
`
const testAccRoute53ElbAliasRecord = `
resource "aws_route53_zone" "main" {
name = "notexample.com"
@ -752,7 +896,9 @@ resource "aws_route53_record" "elb_weighted_alias_live" {
name = "www"
type = "A"
weight = 90
weighted_routing_policy {
weight = 90
}
set_identifier = "live"
alias {
@ -779,7 +925,9 @@ resource "aws_route53_record" "elb_weighted_alias_dev" {
name = "www"
type = "A"
weight = 10
weighted_routing_policy {
weight = 10
}
set_identifier = "dev"
alias {
@ -808,7 +956,9 @@ resource "aws_route53_record" "r53_weighted_alias_live" {
name = "www"
type = "CNAME"
weight = 90
weighted_routing_policy {
weight = 90
}
set_identifier = "blue"
alias {
@ -831,7 +981,9 @@ resource "aws_route53_record" "r53_weighted_alias_dev" {
name = "www"
type = "CNAME"
weight = 10
weighted_routing_policy {
weight = 10
}
set_identifier = "green"
alias {

View File

@ -25,7 +25,7 @@ resource "aws_route53_record" "www" {
```
### Weighted routing policy
See [AWS Route53 Developer Guide](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy.html#routing-policy-weighted) for details.
Other routing policies are configured similarly. See [AWS Route53 Developer Guide](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy.html) for details.
```
resource "aws_route53_record" "www-dev" {
@ -33,7 +33,9 @@ resource "aws_route53_record" "www-dev" {
name = "www"
type = "CNAME"
ttl = "5"
weight = 10
weighted_routing_policy {
weight = 10
}
set_identifier = "dev"
records = ["dev.example.com"]
}
@ -43,7 +45,9 @@ resource "aws_route53_record" "www-live" {
name = "www"
type = "CNAME"
ttl = "5"
weight = 90
weighted_routing_policy {
weight = 90
}
set_identifier = "live"
records = ["live.example.com"]
}
@ -91,20 +95,14 @@ The following arguments are supported:
* `type` - (Required) The record type.
* `ttl` - (Required for non-alias records) The TTL of the record.
* `records` - (Required for non-alias records) A string list of records.
* `weight` - (Optional) The weight of weighted record (0-255).
* `set_identifier` - (Optional) Unique identifier to differentiate weighted
and failover records from one another. Required if using `weighted` or
`failover` attributes
* `failover` - (Optional) The routing behavior when associated health check fails. Must be PRIMARY or SECONDARY.
* `set_identifier` - (Optional) Unique identifier to differentiate records with routing policies from one another. Required if using `failover`, `geolocation`, `latency`, or `weighted` routing policies documented below.
* `health_check_id` - (Optional) The health check the record should be associated with.
* `alias` - (Optional) An alias block. Conflicts with `ttl` & `records`.
Alias record documented below.
~> **Note:** The `weight` attribute uses a special sentinel value of `-1` for a
default in Terraform. This allows Terraform to distinquish between a `0` value
and an empty value in the configuration (none specified). As a result, a
`weight` of `-1` will be present in the statefile if `weight` is omitted in the
configuration.
* `failover_routing_policy` - (Optional) A block indicating the routing behavior when associated health check fails. Conflicts with any other routing policy. Documented below.
* `geolocation_routing_policy` - (Optional) A block indicating a routing policy based on the geolocation of the requestor. Conflicts with any other routing policy. Documented below.
* `latency_routing_policy` - (Optional) A block indicating a routing policy based on the latency between the requestor and an AWS region. Conflicts with any other routing policy. Documented below.
* `weighted_routing_policy` - (Optional) A block indicating a weighted routing policy. Conflicts with any other routing policy. Documented below.
Exactly one of `records` or `alias` must be specified: this determines whether it's an alias record.
@ -114,7 +112,24 @@ Alias records support the following:
* `zone_id` - (Required) Hosted zone ID for a CloudFront distribution, S3 bucket, ELB, or Route 53 hosted zone. See [`resource_elb.zone_id`](/docs/providers/aws/r/elb.html#zone_id) for example.
* `evaluate_target_health` - (Required) Set to `true` if you want Route 53 to determine whether to respond to DNS queries using this resource record set by checking the health of the resource record set. Some resources have special requirements, see [related part of documentation](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resource-record-sets-values.html#rrsets-values-alias-evaluate-target-health).
Failover routing policies support the following:
* `type` - (Required) `PRIMARY` or `SECONDARY`. A `PRIMARY` record will be served if its healthcheck is passing, otherwise the `SECONDARY` will be served. See http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-failover-configuring-options.html#dns-failover-failover-rrsets
Geolocation routing policies support the following:
* `continent` - A two-letter continent code. See http://docs.aws.amazon.com/Route53/latest/APIReference/API_GetGeoLocation.html for code details. Either `continent` or `country` must be specified.
* `country` - A two-character country code or `*` to indicate a default resource record set.
* `subdivision` - (Optional) A subdivision code for a country.
Latency routing policies support the following:
* `region` - (Required) An AWS region from which to measure latency. See http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy.html#routing-policy-latency
Weighted routing policies support the following:
* `weight` - (Required) A numeric value indicating the relative weight of the record. See http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-policy.html#routing-policy-weighted.
## Attributes Reference
* `fqdn` - [FQDN](https://en.wikipedia.org/wiki/Fully_qualified_domain_name) built using the zone domain and `name`