Navigation Menu

Skip to content

Commit

Permalink
kube-aws: add option to create a DNS record automatically
Browse files Browse the repository at this point in the history
Currently it's on the user to create a record, via Route53 or otherwise,
in order to make the controller IP accessible via externalDNSName.  This
commit adds an option to automatically create a Route53 record in a given
hosted zone.

Related to: coreos#340, coreos#257
  • Loading branch information
cgag committed Apr 12, 2016
1 parent d12189f commit b22430f
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 7 deletions.
59 changes: 59 additions & 0 deletions multi-node/aws/pkg/cluster/cluster.go
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudformation"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/route53"

"github.com/coreos/coreos-kubernetes/multi-node/aws/pkg/config"
)
Expand Down Expand Up @@ -131,6 +132,11 @@ func (c *Cluster) validateExistingVPCState(ec2Svc ec2Service) error {
}

func (c *Cluster) Create(stackBody string) error {
r53Svc := route53.New(c.session)
if err := c.validateDNSConfig(r53Svc); err != nil {
return err
}

ec2Svc := ec2.New(c.session)

if err := c.validateKeyPair(ec2Svc); err != nil {
Expand Down Expand Up @@ -265,6 +271,59 @@ func (c *Cluster) validateKeyPair(ec2Svc ec2Service) error {
}
return err
}
return nil
}

type r53Service interface {
ListHostedZonesByName(*route53.ListHostedZonesByNameInput) (*route53.ListHostedZonesByNameOutput, error)
ListResourceRecordSets(*route53.ListResourceRecordSetsInput) (*route53.ListResourceRecordSetsOutput, error)
}

func (c *Cluster) validateDNSConfig(r53 r53Service) error {
if !c.CreateRecordSet {
return nil
}

if c.RecordSetTTL < 1 {
return fmt.Errorf("TTL must be at least 1 second")
}

if c.HostedZone == "" {
return fmt.Errorf("hostName cannot be blank when createRecordSet is true")
}

zonesResp, err := r53.ListHostedZonesByName(&route53.ListHostedZonesByNameInput{
DNSName: aws.String(c.HostedZone),
})
if err != nil {
return fmt.Errorf("Error validating HostedZone: %s", err)
}

zones := zonesResp.HostedZones
if len(zones) == 0 || (*zones[0].Name != c.HostedZone) {
return fmt.Errorf(
"HostedZone %s does not exist. You'll need to create it manually",
c.HostedZone,
)
}

recordSetsResp, err := r53.ListResourceRecordSets(
&route53.ListResourceRecordSetsInput{
HostedZoneId: zones[0].Id,
},
)

if len(recordSetsResp.ResourceRecordSets) > 0 {
for _, recordSet := range recordSetsResp.ResourceRecordSets {
if *recordSet.Name == c.ExternalDNSName {
return fmt.Errorf(
"RecordSet for \"%s\" already exists in Hosted Zone \"%s.\"",
c.ExternalDNSName,
c.HostedZone,
)
}
}
}

return nil
}
89 changes: 89 additions & 0 deletions multi-node/aws/pkg/cluster/cluster_test.go
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/route53"
"github.com/coreos/coreos-kubernetes/multi-node/aws/pkg/config"
)

Expand Down Expand Up @@ -203,3 +204,91 @@ func TestValidateKeyPair(t *testing.T) {
t.Errorf("failed to catch invalid key \"%s\"", c.KeyName)
}
}

type Zone struct {
Id string
DNS string
}

type dummyR53Service struct {
HostedZones []Zone
ResourceRecordSets map[string]string
}

func (r53 dummyR53Service) ListHostedZonesByName(input *route53.ListHostedZonesByNameInput) (*route53.ListHostedZonesByNameOutput, error) {
output := &route53.ListHostedZonesByNameOutput{}
for _, zone := range r53.HostedZones {
if zone.DNS == *input.DNSName {
output.HostedZones = append(output.HostedZones, &route53.HostedZone{
Name: aws.String(zone.DNS),
Id: aws.String(zone.Id),
})
}
}
return output, nil
}

func (r53 dummyR53Service) ListResourceRecordSets(input *route53.ListResourceRecordSetsInput) (*route53.ListResourceRecordSetsOutput, error) {
output := &route53.ListResourceRecordSetsOutput{}
if name, ok := r53.ResourceRecordSets[*input.HostedZoneId]; ok {
output.ResourceRecordSets = []*route53.ResourceRecordSet{
&route53.ResourceRecordSet{
Name: aws.String(name),
},
}
}
return output, nil
}

func TestValidateDNSConfig(t *testing.T) {
dnsConfig := `
createRecordSet: true
recordSetTTL: 60
hostedZone: staging.core-os.net
`

configBody := minimalConfigYaml + dnsConfig
clusterConfig, err := config.ClusterFromBytes([]byte(configBody))
if err != nil {
t.Errorf("could not get valid cluster config: %v", err)
}
c := &Cluster{Cluster: *clusterConfig}

r53 := dummyR53Service{
HostedZones: []Zone{
Zone{
Id: "staging_id",
DNS: "staging.core-os.net",
},
},
ResourceRecordSets: map[string]string{
"staging_id": "existing-record.staging.core-os.net",
},
}

if err := c.validateDNSConfig(r53); err != nil {
t.Errorf("returned error for valid config: %v", err)
}

c.RecordSetTTL = 0
if err := c.validateDNSConfig(r53); err == nil {
t.Errorf("failed to reject invalid TTL")
}

c.RecordSetTTL = 300
c.HostedZone = ""
if err := c.validateDNSConfig(r53); err == nil {
t.Errorf("failed to reject empty HostName")
}

c.HostedZone = "non-existant-zone"
if err := c.validateDNSConfig(r53); err == nil {
t.Errorf("failed to catch non-existent hosted zone")
}

c.HostedZone = "staging.core-os.net"
c.ExternalDNSName = "existing-record.staging.core-os.net"
if err := c.validateDNSConfig(r53); err == nil {
t.Errorf("failed to catch already existing ExternalDNSName")
}
}
21 changes: 21 additions & 0 deletions multi-node/aws/pkg/config/config.go
Expand Up @@ -12,6 +12,7 @@ import (
"net"
"strings"
"text/template"
"unicode/utf8"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
Expand Down Expand Up @@ -43,6 +44,7 @@ func newDefaultCluster() *Cluster {
WorkerCount: 1,
WorkerInstanceType: "m3.medium",
WorkerRootVolumeSize: 30,
CreateRecordSet: false,
}
}

Expand All @@ -57,6 +59,13 @@ func ClusterFromFile(filename string) (*Cluster, error) {
return nil, fmt.Errorf("file %s: %v", filename, err)
}

// HostedZone needs to end with a '.'
c.HostedZone = withTrailingDot(c.HostedZone)

// ExternalDNSName doesn't require '.' to be created,
// but adding it here makes validations easier
c.ExternalDNSName = withTrailingDot(c.ExternalDNSName)

return c, nil
}

Expand Down Expand Up @@ -96,6 +105,10 @@ type Cluster struct {
K8sVer string `yaml:"kubernetesVersion"`
HyperkubeImageRepo string `yaml:"hyperkubeImageRepo"`
KMSKeyARN string `yaml:"kmsKeyArn"`

CreateRecordSet bool `yaml:"createRecordSet"`
RecordSetTTL int `yaml:"recordSetTTL"`
HostedZone string `yaml:"hostedZone"`
}

const (
Expand Down Expand Up @@ -476,3 +489,11 @@ func incrementIP(netIP net.IP) net.IP {
func cidrOverlap(a, b *net.IPNet) bool {
return a.Contains(b.IP) || b.Contains(a.IP)
}

func withTrailingDot(s string) string {
lastRune, _ := utf8.DecodeLastRuneInString(s)
if lastRune != rune('.') {
return s + "."
}
return s
}
16 changes: 14 additions & 2 deletions multi-node/aws/pkg/config/templates/cluster.yaml
Expand Up @@ -5,10 +5,22 @@
clusterName: {{.ClusterName}}

# DNS name routable to the Kubernetes controller nodes
# from worker nodes and external clients. The deployer
# is responsible for making this name routable
# from worker nodes and external clients. Configure the options
# below if you'd like kube-aws to create a Route53 record sets/hosted zones
# for you. Otherwise the deployer is responsible for making this name routable
externalDNSName: {{.ExternalDNSName}}

# Set to true if you want kube-aws to create a Route53 A Record for you.
#createRecordSet: false

# TTL in seconds for the Route53 RecordSet created if createRecordSet is set to true.
#recordSetTTL: 300

# The name of the hosted zone to add the externalDNSName to,
# E.g: "google.com". This needs to already exist, kube-aws will not create
# it for you.
#hostedZone: ""

# Name of the SSH keypair already loaded into the AWS
# account being used to deploy this cluster.
keyName: {{.KeyName}}
Expand Down
22 changes: 17 additions & 5 deletions multi-node/aws/pkg/config/templates/stack-template.json
Expand Up @@ -91,6 +91,18 @@
},
"Type": "AWS::EC2::EIP"
},
{{ if .CreateRecordSet }}
"ExternalDNS": {
"Type": "AWS::Route53::RecordSet",
"Properties": {
"HostedZoneName": "{{.HostedZone}}",
"Name": "{{.ExternalDNSName}}",
"TTL": {{.RecordSetTTL}},
"ResourceRecords": [{ "Ref": "EIPController"}],
"Type": "A"
}
},
{{ end }}
"IAMInstanceProfileController": {
"Properties": {
"Path": "/",
Expand Down Expand Up @@ -198,11 +210,11 @@
"Effect": "Allow",
"Resource": "*"
},
{
"Action" : "kms:Decrypt",
"Effect" : "Allow",
"Resource" : "{{.KMSKeyARN}}"
}
{
"Action" : "kms:Decrypt",
"Effect" : "Allow",
"Resource" : "{{.KMSKeyARN}}"
}
],
"Version": "2012-10-17"
},
Expand Down

0 comments on commit b22430f

Please sign in to comment.