Skip to content

Commit 4c9c1af

Browse files
feat(awstf): add custom domains for terraform APIs (#785)
1 parent 73961ae commit 4c9c1af

File tree

31 files changed

+239
-99
lines changed

31 files changed

+239
-99
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
locals {
2+
domain_list = tolist(var.domain_names)
3+
base_names = { for domain in local.domain_list : domain => split(".", domain)[0] }
4+
}
5+
6+
resource "aws_acm_certificate" "website-cert" {
7+
for_each = var.domain_names
8+
9+
domain_name = each.value
10+
validation_method = "DNS"
11+
}
12+
13+
locals {
14+
domain_validation_options = {
15+
for domain in var.domain_names :
16+
domain => one(aws_acm_certificate.website-cert[domain].domain_validation_options)
17+
}
18+
}
19+
20+
resource "aws_route53_record" "cert-validation-dns" {
21+
for_each = var.domain_names
22+
23+
zone_id = var.zone_ids[each.value]
24+
ttl = "600"
25+
name = local.domain_validation_options[each.value].resource_record_name
26+
type = local.domain_validation_options[each.value].resource_record_type
27+
records = [local.domain_validation_options[each.value].resource_record_value]
28+
29+
depends_on = [aws_acm_certificate.website-cert]
30+
}
31+
32+
resource "aws_acm_certificate_validation" "cert-validation" {
33+
for_each = var.domain_names
34+
35+
certificate_arn = aws_acm_certificate.website-cert[each.value].arn
36+
validation_record_fqdns = [aws_route53_record.cert-validation-dns[each.value].fqdn]
37+
}
38+
39+
resource "aws_apigatewayv2_domain_name" "api_domain_name" {
40+
for_each = var.domain_names
41+
42+
domain_name = each.value
43+
44+
domain_name_configuration {
45+
certificate_arn = aws_acm_certificate_validation.cert-validation[each.value].certificate_arn
46+
endpoint_type = "REGIONAL"
47+
security_policy = "TLS_1_2"
48+
}
49+
}
50+
51+
resource "aws_apigatewayv2_api_mapping" "api_mapping" {
52+
for_each = var.domain_names
53+
54+
api_id = aws_apigatewayv2_api.api_gateway.id
55+
stage = aws_apigatewayv2_stage.stage.id
56+
domain_name = aws_apigatewayv2_domain_name.api_domain_name[each.value].domain_name
57+
}
58+
59+
resource "aws_route53_record" "api-dnsrecord" {
60+
for_each = var.domain_names
61+
62+
zone_id = var.zone_ids[each.value]
63+
// The name is prepended onto the target domain name alias. If theres a subdomain will use it, otherwise just use the target domain name
64+
name = length(split(".", each.value)) > 2 ? local.base_names[each.value] : ""
65+
type = "A"
66+
67+
alias {
68+
name = aws_apigatewayv2_domain_name.api_domain_name[each.value].domain_name_configuration[0].target_domain_name
69+
zone_id = aws_apigatewayv2_domain_name.api_domain_name[each.value].domain_name_configuration[0].hosted_zone_id
70+
evaluate_target_health = false
71+
}
72+
}

cloud/aws/deploytf/.nitric/modules/api/main.tf

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,4 @@ resource "aws_lambda_permission" "apigw_lambda" {
2323
function_name = each.value
2424
principal = "apigateway.amazonaws.com"
2525
source_arn = "${aws_apigatewayv2_api.api_gateway.execution_arn}/*/*/*"
26-
}
27-
28-
# look up existing certificate for domains
29-
data "aws_acm_certificate" "cert" {
30-
for_each = var.domains
31-
domain = each.value
32-
}
33-
34-
# deploy custom domain names
35-
resource "aws_apigatewayv2_domain_name" "domain" {
36-
for_each = var.domains
37-
domain_name = each.value
38-
domain_name_configuration {
39-
certificate_arn = data.aws_acm_certificate.cert[each.key].arn
40-
endpoint_type = "REGIONAL"
41-
security_policy = "TLS_1_2"
42-
}
4326
}

cloud/aws/deploytf/.nitric/modules/api/variables.tf

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ variable "target_lambda_functions" {
1818
type = map(string)
1919
}
2020

21-
variable "domains" {
22-
description = "The domains to associate with the API Gateway"
23-
type = set(string)
21+
variable "domain_names" {
22+
description = "A set of each domain name."
23+
type = set(string)
24+
}
25+
26+
variable "zone_ids" {
27+
description = "The id of the hosted zone mapped to the domain name."
28+
type = map(string)
2429
}

cloud/aws/deploytf/api.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ func (n *NitricAwsTerraformProvider) Api(stack cdktf.TerraformStack, name string
6565
return fmt.Errorf("aws provider can only deploy OpenAPI specs")
6666
}
6767

68+
additionalApiConfig := n.AwsConfig.Apis[name]
69+
6870
openapiDoc := &openapi3.T{}
6971
err := openapiDoc.UnmarshalJSON([]byte(config.GetOpenapi()))
7072
if err != nil {
@@ -172,16 +174,20 @@ func (n *NitricAwsTerraformProvider) Api(stack cdktf.TerraformStack, name string
172174
templateFile := cdktf.Fn_Templatefile(asset.Path(), nameArnPairs)
173175

174176
domains := []string{}
175-
if n.AwsConfig != nil && n.AwsConfig.Apis != nil && n.AwsConfig.Apis[name] != nil {
176-
domains = n.AwsConfig.Apis[name].Domains
177+
zoneIds := make(map[string]*string)
178+
179+
if additionalApiConfig != nil {
180+
domains = additionalApiConfig.Domains
181+
zoneIds = getZoneIds(additionalApiConfig.Domains)
177182
}
178183

179184
n.Apis[name] = api.NewApi(stack, jsii.Sprintf("api_%s", name), &api.ApiConfig{
180185
Name: jsii.String(name),
181186
Spec: cdktf.Token_AsString(templateFile, &cdktf.EncodingOptions{}),
182187
TargetLambdaFunctions: &targetNames,
183-
Domains: jsii.Strings(domains...),
184188
StackId: n.Stack.StackIdOutput(),
189+
DomainNames: jsii.Strings(domains...),
190+
ZoneIds: &zoneIds,
185191
})
186192

187193
return nil

cloud/aws/deploytf/domain.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright 2021 Nitric Technologies Pty Ltd.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package deploytf
16+
17+
import (
18+
"context"
19+
"strings"
20+
21+
"github.com/aws/aws-sdk-go-v2/config"
22+
"github.com/aws/aws-sdk-go-v2/service/route53"
23+
"github.com/aws/aws-sdk-go/aws"
24+
)
25+
26+
func getZoneIds(domainNames []string) map[string]*string {
27+
ctx := context.TODO()
28+
29+
cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("us-west-2"))
30+
if err != nil {
31+
return nil
32+
}
33+
34+
client := route53.NewFromConfig(cfg)
35+
36+
zoneMap := make(map[string]*string)
37+
38+
normalizedDomains := make(map[string]string)
39+
for _, d := range domainNames {
40+
d = strings.ToLower(strings.TrimSuffix(d, "."))
41+
normalizedDomains[d] = d + "."
42+
}
43+
44+
paginator := route53.NewListHostedZonesPaginator(client, &route53.ListHostedZonesInput{})
45+
hostedZones := make(map[string]string) // map of zone name -> zone ID
46+
47+
for paginator.HasMorePages() {
48+
page, err := paginator.NextPage(ctx)
49+
if err != nil {
50+
return nil
51+
}
52+
53+
for _, hz := range page.HostedZones {
54+
name := strings.ToLower(strings.TrimSuffix(*hz.Name, "."))
55+
hostedZones[name] = strings.TrimPrefix(*hz.Id, "/hostedzone/")
56+
}
57+
}
58+
59+
// Resolve each domain name
60+
for domain, normalized := range normalizedDomains {
61+
// Check full domain
62+
if id, ok := hostedZones[strings.TrimSuffix(normalized, ".")]; ok {
63+
zoneMap[domain] = aws.String(id)
64+
continue
65+
}
66+
67+
// Try parent/root domain
68+
parts := strings.Split(domain, ".")
69+
if len(parts) > 2 {
70+
root := strings.Join(parts[len(parts)-2:], ".")
71+
if id, ok := hostedZones[root]; ok {
72+
zoneMap[domain] = aws.String(id)
73+
continue
74+
}
75+
}
76+
77+
// No match
78+
zoneMap[domain] = nil
79+
}
80+
81+
return zoneMap
82+
}

cloud/aws/deploytf/generated/api/Api.go

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ type Api interface {
2323
DependsOn() *[]*string
2424
// Experimental.
2525
SetDependsOn(val *[]*string)
26-
Domains() *[]*string
27-
SetDomains(val *[]*string)
26+
DomainNames() *[]*string
27+
SetDomainNames(val *[]*string)
2828
EndpointOutput() *string
2929
// Experimental.
3030
ForEach() cdktf.ITerraformIterator
@@ -56,6 +56,8 @@ type Api interface {
5656
SetTargetLambdaFunctions(val *map[string]*string)
5757
// Experimental.
5858
Version() *string
59+
ZoneIds() *map[string]*string
60+
SetZoneIds(val *map[string]*string)
5961
// Experimental.
6062
AddOverride(path *string, value interface{})
6163
// Experimental.
@@ -127,11 +129,11 @@ func (j *jsiiProxy_Api) DependsOn() *[]*string {
127129
return returns
128130
}
129131

130-
func (j *jsiiProxy_Api) Domains() *[]*string {
132+
func (j *jsiiProxy_Api) DomainNames() *[]*string {
131133
var returns *[]*string
132134
_jsii_.Get(
133135
j,
134-
"domains",
136+
"domainNames",
135137
&returns,
136138
)
137139
return returns
@@ -297,6 +299,16 @@ func (j *jsiiProxy_Api) Version() *string {
297299
return returns
298300
}
299301

302+
func (j *jsiiProxy_Api) ZoneIds() *map[string]*string {
303+
var returns *map[string]*string
304+
_jsii_.Get(
305+
j,
306+
"zoneIds",
307+
&returns,
308+
)
309+
return returns
310+
}
311+
300312

301313
func NewApi(scope constructs.Construct, id *string, config *ApiConfig) Api {
302314
_init_.Initialize()
@@ -333,13 +345,13 @@ func (j *jsiiProxy_Api)SetDependsOn(val *[]*string) {
333345
)
334346
}
335347

336-
func (j *jsiiProxy_Api)SetDomains(val *[]*string) {
337-
if err := j.validateSetDomainsParameters(val); err != nil {
348+
func (j *jsiiProxy_Api)SetDomainNames(val *[]*string) {
349+
if err := j.validateSetDomainNamesParameters(val); err != nil {
338350
panic(err)
339351
}
340352
_jsii_.Set(
341353
j,
342-
"domains",
354+
"domainNames",
343355
val,
344356
)
345357
}
@@ -396,6 +408,17 @@ func (j *jsiiProxy_Api)SetTargetLambdaFunctions(val *map[string]*string) {
396408
)
397409
}
398410

411+
func (j *jsiiProxy_Api)SetZoneIds(val *map[string]*string) {
412+
if err := j.validateSetZoneIdsParameters(val); err != nil {
413+
panic(err)
414+
}
415+
_jsii_.Set(
416+
j,
417+
"zoneIds",
418+
val,
419+
)
420+
}
421+
399422
// Checks if `x` is a construct.
400423
//
401424
// Use this method instead of `instanceof` to properly detect `Construct`

cloud/aws/deploytf/generated/api/ApiConfig.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ type ApiConfig struct {
1313
Providers *[]interface{} `field:"optional" json:"providers" yaml:"providers"`
1414
// Experimental.
1515
SkipAssetCreationFromLocalModules *bool `field:"optional" json:"skipAssetCreationFromLocalModules" yaml:"skipAssetCreationFromLocalModules"`
16-
// The domains to associate with the API Gateway.
17-
Domains *[]*string `field:"required" json:"domains" yaml:"domains"`
16+
// A set of each domain name.
17+
DomainNames *[]*string `field:"required" json:"domainNames" yaml:"domainNames"`
1818
// The name of the API Gateway.
1919
Name *string `field:"required" json:"name" yaml:"name"`
2020
// Open API spec.
@@ -23,5 +23,9 @@ type ApiConfig struct {
2323
StackId *string `field:"required" json:"stackId" yaml:"stackId"`
2424
// The names of the target lambda functions The property type contains a map, they have special handling, please see {@link cdk.tf /module-map-inputs the docs}.
2525
TargetLambdaFunctions *map[string]*string `field:"required" json:"targetLambdaFunctions" yaml:"targetLambdaFunctions"`
26+
// The id of the hosted zone mapped to the domain name.
27+
//
28+
// The property type contains a map, they have special handling, please see {@link cdk.tf /module-map-inputs the docs}
29+
ZoneIds *map[string]*string `field:"required" json:"zoneIds" yaml:"zoneIds"`
2630
}
2731

cloud/aws/deploytf/generated/api/Api__checks.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func validateApi_IsTerraformElementParameters(x interface{}) error {
9090
return nil
9191
}
9292

93-
func (j *jsiiProxy_Api) validateSetDomainsParameters(val *[]*string) error {
93+
func (j *jsiiProxy_Api) validateSetDomainNamesParameters(val *[]*string) error {
9494
if val == nil {
9595
return fmt.Errorf("parameter val is required, but nil was provided")
9696
}
@@ -130,6 +130,14 @@ func (j *jsiiProxy_Api) validateSetTargetLambdaFunctionsParameters(val *map[stri
130130
return nil
131131
}
132132

133+
func (j *jsiiProxy_Api) validateSetZoneIdsParameters(val *map[string]*string) error {
134+
if val == nil {
135+
return fmt.Errorf("parameter val is required, but nil was provided")
136+
}
137+
138+
return nil
139+
}
140+
133141
func validateNewApiParameters(scope constructs.Construct, id *string, config *ApiConfig) error {
134142
if scope == nil {
135143
return fmt.Errorf("parameter scope is required, but nil was provided")

cloud/aws/deploytf/generated/api/Api__no_checks.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func validateApi_IsTerraformElementParameters(x interface{}) error {
3232
return nil
3333
}
3434

35-
func (j *jsiiProxy_Api) validateSetDomainsParameters(val *[]*string) error {
35+
func (j *jsiiProxy_Api) validateSetDomainNamesParameters(val *[]*string) error {
3636
return nil
3737
}
3838

@@ -52,6 +52,10 @@ func (j *jsiiProxy_Api) validateSetTargetLambdaFunctionsParameters(val *map[stri
5252
return nil
5353
}
5454

55+
func (j *jsiiProxy_Api) validateSetZoneIdsParameters(val *map[string]*string) error {
56+
return nil
57+
}
58+
5559
func validateNewApiParameters(scope constructs.Construct, id *string, config *ApiConfig) error {
5660
return nil
5761
}
474 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)