Skip to content
Open
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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# tfz53 (previously knows as bzfttr53rdutil)
A conversion utility for creating [Terraform](https://terraform.io) resource definitions for AWS Route53 from BIND zonefiles.
A conversion utility for creating [Terraform](https://terraform.io) or [Cloudformation](https://aws.amazon.com/cloudformation/) resource definitions for AWS Route53 from BIND zonefiles.

## Installation
Download the [latest release](https://github.com/carlpett/tfz53/releases/latest).

## Usage
`tfz53 -domain <domain-name> [flags] > route53-domain.tf`

`tfz53 -cloudformation -domain <domain-name> [flags] > route53-domain.cfn.yaml`


## Flags
| Name | Description | Default |
|------------|----------------------------------------------------|-----------------|
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module github.com/carlpett/tfz53

require (
github.com/google/go-cmp v0.3.0
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334
github.com/miekg/dns v1.0.8
golang.org/x/crypto v0.0.0-20180718160520-a2144134853f // indirect
golang.org/x/net v0.0.0-20180719001425-81d44fd177a9
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 h1:VHgatEHNcBFEB7inlalqfNqw65aNkM1lGX2yt3NmbS8=
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
github.com/miekg/dns v1.0.8 h1:Zi8HNpze3NeRWH1PQV6O71YcvJRQ6j0lORO6DAEmAAI=
github.com/miekg/dns v1.0.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
golang.org/x/crypto v0.0.0-20180718160520-a2144134853f h1:lRy+hhwk7YT7MsKejxuz0C5Q1gk6p/QoPQYEmKmGFb8=
Expand Down
58 changes: 52 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"
"text/template"

"github.com/iancoleman/strcase"
"github.com/miekg/dns"
"golang.org/x/net/idna"
)
Expand All @@ -24,11 +25,11 @@ var (
)

const (
zoneTemplateStr = `resource "aws_route53_zone" "{{ .ID }}" {
tfZoneTemplateStr = `resource "aws_route53_zone" "{{ .ID }}" {
name = "{{ .Domain }}"
}
`
recordTemplateStr = `{{- range .Record.Comments }}
tfRecordTemplateStr = `{{- range .Record.Comments }}
# {{ . }}{{ end }}
resource "aws_route53_record" "{{ .ResourceID }}" {
zone_id = {{ zoneReference .ZoneID }}
Expand All @@ -37,6 +38,25 @@ resource "aws_route53_record" "{{ .ResourceID }}" {
ttl = "{{ .Record.TTL }}"
records = [{{ range $idx, $elem := .Record.Data }}{{ if $idx }}, {{ end }}{{ ensureQuoted $elem }}{{ end }}]
}
`

cfnZoneTemplateStr = `Resources:
{{ .ID }}:
Type: AWS::Route53::HostedZone
Properties:
Name: "{{ .Domain }}"
`
cfnRecordTemplateStr = `{{- range .Record.Comments }}
# {{ . }}{{ end }}
{{ .ResourceID }}:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref {{ .ZoneID }}
Name: "{{ .Record.Name }}"
Type: "{{ .Record.Type }}"
TTL: "{{ .Record.TTL }}"
ResourceRecords:{{ range $idx, $elem := .Record.Data }}
- {{ ensureQuoted $elem }}{{ end }}
`
)

Expand All @@ -48,6 +68,8 @@ func (m syntaxMode) String() string {
return "modern"
case Legacy:
return "legacy"
case Cloudformation:
return "cloudformation"
default:
panic("Unknown syntax")
}
Expand All @@ -56,6 +78,7 @@ func (m syntaxMode) String() string {
const (
Modern syntaxMode = iota
Legacy
Cloudformation
)

type configGenerator struct {
Expand All @@ -66,6 +89,16 @@ type configGenerator struct {
}

func newConfigGenerator(syntax syntaxMode) *configGenerator {
var zoneTemplateStr string
var recordTemplateStr string
if syntax == Cloudformation {
zoneTemplateStr = cfnZoneTemplateStr
recordTemplateStr = cfnRecordTemplateStr
} else {
zoneTemplateStr = tfZoneTemplateStr
recordTemplateStr = tfRecordTemplateStr
}

g := &configGenerator{syntax: syntax}
g.zoneTemplate = template.Must(template.New("zone").Parse(zoneTemplateStr))
g.recordTemplate = template.Must(template.New("record").Funcs(template.FuncMap{
Expand Down Expand Up @@ -118,6 +151,7 @@ var (
zoneFile = flag.String("zone-file", "", "Path to zone file. Defaults to <domain>.zone in working dir")
showVersion = flag.Bool("version", false, "Show version")
legacySyntax = flag.Bool("legacy-syntax", false, "Generate legacy terraform syntax (versions older than 0.12)")
cloudformation = flag.Bool("cloudformation", false, "Generate cloudformation syntax")
)

func main() {
Expand All @@ -142,16 +176,18 @@ func main() {
}

var syntax syntaxMode
if !*legacySyntax {
if *cloudformation {
syntax = Cloudformation
} else if !*legacySyntax {
syntax = Modern
} else {
syntax = Legacy
}
g := newConfigGenerator(syntax)
g.generateTerraformForZone(*domain, excludedTypes, fileReader, os.Stdout)
g.generateTemplateForZone(*domain, excludedTypes, fileReader, os.Stdout)
}

func (g *configGenerator) generateTerraformForZone(domain string, excludedTypes map[uint16]bool, zoneReader io.Reader, output io.Writer) {
func (g *configGenerator) generateTemplateForZone(domain string, excludedTypes map[uint16]bool, zoneReader io.Reader, output io.Writer) {
records := readZoneRecords(zoneReader, excludedTypes)

zoneID, err := g.generateZoneResource(domain, output)
Expand Down Expand Up @@ -203,8 +239,15 @@ func readZoneRecords(zoneReader io.Reader, excludedTypes map[uint16]bool) map[re

func (g *configGenerator) generateZoneResource(domain string, w io.Writer) (string, error) {
zoneName := strings.TrimRight(domain, ".")
var zoneID string

if g.syntax == Cloudformation {
zoneID = strcase.ToCamel(zoneName)
} else {
zoneID = strings.Replace(zoneName, ".", "-", -1)
}
data := zoneTemplateData{
ID: strings.Replace(zoneName, ".", "-", -1),
ID: zoneID,
Domain: zoneName,
}

Expand All @@ -215,6 +258,9 @@ func (g *configGenerator) generateZoneResource(domain string, w io.Writer) (stri
func (g *configGenerator) generateRecordResource(record dnsRecord, zoneID string, w io.Writer) error {
sanitizedName := sanitizeRecordName(record.Name)
id := fmt.Sprintf("%s-%s", sanitizedName, record.Type)
if g.syntax == Cloudformation {
id = strcase.ToCamel(id)
}

data := recordTemplateData{
ResourceID: id,
Expand Down
15 changes: 13 additions & 2 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ resource "aws_route53_record" "foo-bar-A" {
ttl = "3600"
records = ["127.0.0.1"]
}`,
Cloudformation: `# This is a test
Resources:
FooBarA:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref TestZone
Name: "foo.bar"
Type: "A"
TTL: ""3600"
ResourceRecords:
- "127.0.0.1"`,
},
},
}
Expand Down Expand Up @@ -108,7 +119,7 @@ func TestAcceptance(t *testing.T) {
}

for _, n := range fileNames {
for _, syntax := range []syntaxMode{Modern, Legacy} {
for _, syntax := range []syntaxMode{Modern, Legacy, Cloudformation} {
t.Run(caseName(n, syntax), func(t *testing.T) {
file, err := os.Open(n)
if err != nil {
Expand All @@ -123,7 +134,7 @@ func TestAcceptance(t *testing.T) {
var buf bytes.Buffer
domain := strings.Replace(filepath.Base(n), ".zone", "", 1)
excludedTypes := excludedTypesFromString("SOA,NS")
g.generateTerraformForZone(domain, excludedTypes, file, &buf)
g.generateTemplateForZone(domain, excludedTypes, file, &buf)

if diff := cmp.Diff(string(expected), buf.String(), diffOpts); diff != "" {
t.Errorf("Unexpected result from full Terraform output (-want +got):\n%s", diff)
Expand Down
119 changes: 119 additions & 0 deletions testdata/example.com.expected-cloudformation
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
Resources:
ExampleCom:
Type: AWS::Route53::HostedZone
Properties:
Name: "example.com"

# wwwtest.example.com is another alias for www.example.com
WwwtestExampleComCNAME:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref ExampleCom
Name: "wwwtest.example.com."
Type: "CNAME"
TTL: "3600"
ResourceRecords:
- "www.example.com."

# www.example.com is an alias for example.com
WwwExampleComCNAME:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref ExampleCom
Name: "www.example.com."
Type: "CNAME"
TTL: "3600"
ResourceRecords:
- "example.com."

# IPv6 address for ns.example.com
NsExampleComAAAA:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref ExampleCom
Name: "ns.example.com."
Type: "AAAA"
TTL: "3600"
ResourceRecords:
- "2001:db8:10::2"

# IPv4 address for ns.example.com
NsExampleComA:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref ExampleCom
Name: "ns.example.com."
Type: "A"
TTL: "3600"
ResourceRecords:
- "192.0.2.2"

# IPv4 address for mail3.example.com
Mail3ExampleComA:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref ExampleCom
Name: "mail3.example.com."
Type: "A"
TTL: "3600"
ResourceRecords:
- "192.0.2.5"

# IPv4 address for mail2.example.com
Mail2ExampleComA:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref ExampleCom
Name: "mail2.example.com."
Type: "A"
TTL: "3600"
ResourceRecords:
- "192.0.2.4"

# IPv4 address for mail.example.com
MailExampleComA:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref ExampleCom
Name: "mail.example.com."
Type: "A"
TTL: "3600"
ResourceRecords:
- "192.0.2.3"

# mail.example.com is the mailserver for example.com
# equivalent to above line, "@" represents zone origin
# equivalent to above line, but using a relative host name
ExampleComMX:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref ExampleCom
Name: "example.com."
Type: "MX"
TTL: "3600"
ResourceRecords:
- "10 mail.example.com."
- "20 mail2.example.com."
- "50 mail3.example.com."

# IPv6 address for example.com
ExampleComAAAA:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref ExampleCom
Name: "example.com."
Type: "AAAA"
TTL: "3600"
ResourceRecords:
- "2001:db8:10::1"

# IPv4 address for example.com
ExampleComA:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref ExampleCom
Name: "example.com."
Type: "A"
TTL: "3600"
ResourceRecords:
- "192.0.2.1"