aws / chalice

Python Serverless Microframework for AWS
Apache License 2.0
10.64k stars 1.01k forks source link

Use CDK to setup Route 53 alias for custom domain #1728

Open jp-pix4d opened 3 years ago

jp-pix4d commented 3 years ago

First of all: Having a lot of fun using Chalice with CDK, very cool project :)

What I am trying to do

I basically followed https://aws.github.io/chalice/tutorials/cdk.html to setup my project.

I am trying to use a custom domain and let CDK configure an alias to it (basically following the solution outlined here: https://stackoverflow.com/questions/65986897/how-to-provide-a-custom-domain-name-for-a-lambda-based-apigateway-in-cdk).

So I am trying to do this:

from aws_cdk import (
    aws_route53 as route53,
    aws_route53_targets as route53_targets,
)

hosted_zone = route53.HostedZone.from_lookup(self, 'MyExistingHostedZone', domain_name="app.example.com")

rest_api = <How to get this?>

route53.ARecord(
    self, 'MyNewARecord',
    zone=hosted_zone,
    record_name="api",
    target=route53.RecordTarget.from_alias(route53_targets.ApiGateway(rest_api))
)

Problem

The problem is that I cannot get my hands on rest_api because it is created by Chalice and not by CDK. I tried a lot of ways starting with:

REST_API_RESOURCE_NAME = 'RestAPI'
cfn_api_gateway = self.chalice.get_resource(REST_API_RESOURCE_NAME)

Then for example:

from aws_cdk import (
    aws_apigateway as apigateway,
)
apigateway.RestApi.from_rest_api_id(self, REST_API_RESOURCE_NAME, ???)

But the classes don't match up ... so for example I would get this error:

jsii.errors.JavaScriptError:
  Error: Object of type @aws-cdk/aws-apigateway.RestApiBase is not convertible to @aws-cdk/aws-apigateway.RestApi
      at Object.deserialize (C:\Users\JONASP~1\AppData\Local\Temp\tmpmocjnbdz\lib\program.js:9107:35)
      at Kernel._toSandbox (C:\Users\JONASP~1\AppData\Local\Temp\tmpmocjnbdz\lib\program.js:8505:69)
      at C:\Users\JONASP~1\AppData\Local\Temp\tmpmocjnbdz\lib\program.js:8553:42
      at Array.map (<anonymous>)
      at Kernel._boxUnboxParameters (C:\Users\JONASP~1\AppData\Local\Temp\tmpmocjnbdz\lib\program.js:8553:27)
      at Kernel._toSandboxValues (C:\Users\JONASP~1\AppData\Local\Temp\tmpmocjnbdz\lib\program.js:8539:29)
      at C:\Users\JONASP~1\AppData\Local\Temp\tmpmocjnbdz\lib\program.js:8154:75
      at Kernel._wrapSandboxCode (C:\Users\JONASP~1\AppData\Local\Temp\tmpmocjnbdz\lib\program.js:8582:24)
      at Kernel._create (C:\Users\JONASP~1\AppData\Local\Temp\tmpmocjnbdz\lib\program.js:8154:34)
      at Kernel.create (C:\Users\JONASP~1\AppData\Local\Temp\tmpmocjnbdz\lib\program.js:7895:29)

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "app.py", line 13, in <module>
    ChaliceApp(
  File "[..]\venv\lib\site-packages\jsii\_runtime.py", line 83, in __call__
    inst = super().__call__(*args, **kwargs)
  File "[..]\infrastructure\stacks\chaliceapp.py", line 76, in __init__
    target=route53.RecordTarget.from_alias(route53_targets.ApiGateway(rest_api))
  File "[..]\venv\lib\site-packages\jsii\_runtime.py", line 83, in __call__
    inst = super().__call__(*args, **kwargs)
  File "[..]\venv\lib\site-packages\aws_cdk\aws_route53_targets\__init__.py", line 444, in __init__
    jsii.create(ApiGateway, self, [api])
  File "[..]\venv\lib\site-packages\jsii\_kernel\__init__.py", line 275, in create
    response = self.provider.create(
  File "[..]\venv\lib\site-packages\jsii\_kernel\providers\process.py", line 344, in create
    return self._process.send(request, CreateResponse)
  File "[..]\venv\lib\site-packages\jsii\_kernel\providers\process.py", line 326, in send
    raise JSIIError(resp.error) from JavaScriptError(resp.stack)
jsii.errors.JSIIError: Object of type @aws-cdk/aws-apigateway.RestApiBase is not convertible to @aws-cdk/aws-apigateway.RestApi

There also is another target called route53_targets.ApiGatewayDomain but I am not sure that is even the right thing to use (documentation is basically silent on this one) and also I am not having any more luck starting with the following either:

REST_API_RESOURCE_NAME = 'ApiGatewayCustomDomain'
cfn_api_gateway = self.chalice.get_resource(REST_API_RESOURCE_NAME)

Question

Is this possible somehow? Is this a user error? Could it be made possible? Is there a workaround? :)

Thanks a lot!

rrraikman commented 3 years ago

I'm not sure if this is how the Chalice team would recommend doing this, but this is how I got this working.

If you follow the 'non-cdk' custom domain setup instructions, you'll notice a final step to set up an Alias Record Configuration. Basically what I did was that, but in CDK code (using the CloudFormation classes).

Note: This assumes you are using REGIONAL api's (e.g. You have "api_gateway_endpoint_type": "REGIONAL" set in your Chalice stage_config)

self.custom_domain = self.chalice.get_resource("ApiGatewayCustomDomain")
self.a_record = route53.CfnRecordSet(self, "<some id>"
    hosted_zone_id="<id of the hosted zone you have configured in Route53>",
    name="<dns name, e.g. api.my-chalice-app.com>",
    type="A",
    alias_target=route53.CfnRecordSet.AliasTargetProperty(
        dns_name=self.custom_domain.get_att('RegionalDomainName').to_string(),
        hosted_zone_id=self.custom_domain.get_att('RegionalHostedZoneId').to_string(),
        evaluate_target_health=False,
    ),
)

My full code looks like:

self.domain = "api.my-chalice-app.com"
self.hosted_zone = route53.HostedZone.from_hosted_zone_id(self, "hosted-zone-id", 
    hosted_zone_id="<redacted>"
)

self.acm_cert = acm.Certificate(self, "acm-cert-id",
    domain_name=self.domain,
    validation=acm.CertificateValidation.from_dns(self.hosted_zone),
)

self.custom_domain = self.chalice.get_resource("ApiGatewayCustomDomain")
self.a_record = route53.CfnRecordSet(self, "a-record-id"
    hosted_zone_id=self.hosted_zone.hosted_zone_id,
    name=self.domain,
    type="A",
    alias_target=route53.CfnRecordSet.AliasTargetProperty(
        dns_name=self.custom_domain.get_att('RegionalDomainName').to_string(),
        hosted_zone_id=self.custom_domain.get_att('RegionalHostedZoneId').to_string(),
        evaluate_target_health=False,
    ),
)
brno32 commented 2 years ago

I'd also like to get my hands on rest_api for different purposes. I can't find any examples online on how to reference the apigw resource provisioned by chalice

harveypham commented 2 years ago

1) If you need to configure new custom domain for RestAPI, the easiest route is to pass the custom domain parameters as part of stage_config:

        self.chalice = Chalice(
            self, 'ChaliceApp', source_dir=RUNTIME_SOURCE_DIR,
            stage_config={
                "api_gateway_custom_domain": {
                    "domain_name": "api.example.com",
                    "certificate_arn": "arn:aws:acm:us-east-1:1234567:certificate/00000-00000-000000",
                    },
                'environment_variables': {
                    'APP_TABLE_NAME': self.dynamodb_table.table_name
                }
            }
        )

(Code taken from this . Note that the issue mentioned has been fixed in the latest release).

2) cdk.Fn.ref("RestAPI") returns a reference token which can be used in place of rest_api_id. So,

a) If you want to add a base path to existing custom domain:
```
    apigateway.CfnBasePathMapping(
        self, "MyAppBasePath",
        domain_name="api.example.com",
        base_path="my_app",
        rest_api_id=cdk.Fn.ref("RestAPI"),
        stage=cdk.Fn.Ref("RestAPIapiStage")
```

b) If you want to grab the RestAPI instance:

```
    rest_api = apigateway.RestApi.from_rest_api_id(
        self, "ChaliceRestAPI", cdk.Fn.ref("RestAPI"))
```
davidpricedev commented 2 years ago

I was trying to get this working, and I think I've got it.

I tried a few things:

I tried using this to configure the alias, but no luck.

        rest_api = apigateway.RestApi.from_rest_api_id(
            self, "ChaliceRestAPI", cdk.Fn.ref("RestAPI")
        )
        route53.ARecord(
            self,
            "MyNewARecord",
            zone=dns_zone,
            record_name="api",
            target=route53.RecordTarget.from_alias(
                route53_targets.ApiGateway(rest_api)
            ),
        )

Error: API does not define a default domain name

Next, I tried rrraikman's route53 setup too, but got errors:

Attribute 'RegionalHostedZoneId' does not exist

However, I found this which seems to suggest replacing "Regional" with "Distribution", and that did work.

My working cdk looks like:

        self.chalice = Chalice(
            self,
            "ChaliceApp",
            source_dir=RUNTIME_SOURCE_DIR,
            stage_config={
                "api_gateway_custom_domain": {
                    "domain_name": api_domain_name,
                    "certificate_arn": certificate_arn,
                }
            },
        )

        dns_zone = route53.HostedZone.from_hosted_zone_id(...)

        custom_domain = self.chalice.get_resource("ApiGatewayCustomDomain")
        route53.CfnRecordSet(
            self,
            "api-subdomain",
            hosted_zone_id=dns_zone.hosted_zone_id,
            name=api_domain_name,
            type="A",
            alias_target=route53.CfnRecordSet.AliasTargetProperty(
                dns_name=custom_domain.get_att("DistributionDomainName").to_string(),
                hosted_zone_id=custom_domain.get_att(
                    "DistributionHostedZoneId"
                ).to_string(),
                evaluate_target_health=False,
            ),
        )