I received an error when I tried to publish two ss...
# help
g
I received an error when I tried to publish two sst.Api which share the same custom domain. My intent was to publish “users-api” and “books-api” in the same domain but with different path. The error I received is this
Copy code
api.dev.rt.parque.cloud already exists in stack arn:aws:cloudformation:us-east-1:834896107245:stack/dev-rt-test-api/47c3a270-8c5e-11eb-88a6-0ed5acf75591
The complete logs is inside this thread
Copy code
07:36:55 | UPDATE_IN_PROGRESS                           | AWS::CloudFormation::Stack    | dev-rt-test-api
07:37:31 | CREATE_IN_PROGRESS                           | AWS::ApiGatewayV2::DomainName | booksapiDomainNameC02658E8
07:37:32 | CREATE_IN_PROGRESS                           | AWS::ApiGatewayV2::Api        | booksapiApiD9E96849
07:37:32 | CREATE_FAILED                                | AWS::ApiGatewayV2::DomainName | booksapiDomainNameC02658E8 - api.dev.rt.parque.cloud already exists in stack arn:aws:cloudformation:us-east-1:834896107245:stack/dev-rt-test-api/47c3a270-8c5e-11eb-88a6-0ed5acf75591
07:37:32 | CREATE_IN_PROGRESS                           | AWS::IAM::Role                | booksapiLambdaDELETEuriServiceRole1C7BDA10
07:37:32 | CREATE_IN_PROGRESS                           | AWS::IAM::Role                | booksapiLambdaPUTuriServiceRole5C758468
07:37:32 | UPDATE_IN_PROGRESS                           | AWS::Lambda::Function         | usersapiLambdaPUTuri34BE95F6
07:37:32 | CREATE_IN_PROGRESS                           | AWS::IAM::Role                | booksapiLambdaGETServiceRole55FF2D79
07:37:32 | UPDATE_IN_PROGRESS                           | AWS::Lambda::Function         | usersapiLambdaDELETEuriE80EFA6D
07:37:32 | CREATE_IN_PROGRESS                           | AWS::Logs::LogGroup           | booksapiLogGroup865BFA94
07:37:32 | CREATE_IN_PROGRESS                           | AWS::IAM::Role                | booksapiLambdaGETuriServiceRole56D21611
07:37:32 | CREATE_FAILED                                | AWS::ApiGatewayV2::Api        | booksapiApiD9E96849 - Resource creation cancelled
07:37:32 | CREATE_FAILED                                | AWS::IAM::Role                | booksapiLambdaGETuriServiceRole56D21611 - Resource creation cancelled
07:37:32 | CREATE_FAILED                                | AWS::IAM::Role                | booksapiLambdaPUTuriServiceRole5C758468 - Resource creation cancelled
07:37:32 | CREATE_FAILED                                | AWS::IAM::Role                | booksapiLambdaDELETEuriServiceRole1C7BDA10 - Resource creation cancelled
07:37:32 | CREATE_FAILED                                | AWS::Logs::LogGroup           | booksapiLogGroup865BFA94 - Resource creation cancelled
07:37:32 | CREATE_FAILED                                | AWS::IAM::Role                | booksapiLambdaGETServiceRole55FF2D79 - Resource creation cancelled
07:37:33 | UPDATE_FAILED                                | AWS::Lambda::Function         | usersapiLambdaPUTuri34BE95F6 - Resource update cancelled
07:37:33 | UPDATE_FAILED                                | AWS::Lambda::Function         | usersapiLambdaDELETEuriE80EFA6D - Resource update cancelled
07:37:34 | UPDATE_ROLLBACK_IN_PROGRESS                  | AWS::CloudFormation::Stack    | dev-rt-test-api - The following resource(s) failed to create: [booksapiLambdaGETuriServiceRole56D21611, booksapiLambdaPUTuriServiceRole5C758468, booksapiLambdaGETServiceRole55FF2D79, booksapiLambdaDELETEuriServiceRole1C7BDA10, booksapiApiD9E96849, booksapiLogGroup865BFA94, booksapiDomainNameC02658E8]. The following resource(s) failed to update: [usersapiLambdaPUTuri34BE95F6, usersapiLambdaDELETEuriE80EFA6D]. 
07:37:57 | UPDATE_IN_PROGRESS                           | AWS::Lambda::Function         | usersapiLambdaPUTuri34BE95F6
07:37:57 | UPDATE_IN_PROGRESS                           | AWS::Lambda::Function         | usersapiLambdaDELETEuriE80EFA6D
07:37:58 | UPDATE_COMPLETE                              | AWS::Lambda::Function         | usersapiLambdaPUTuri34BE95F6
07:37:58 | UPDATE_COMPLETE                              | AWS::Lambda::Function         | usersapiLambdaDELETEuriE80EFA6D
07:38:00 | UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS | AWS::CloudFormation::Stack    | dev-rt-test-api
07:38:02 | DELETE_COMPLETE                              | AWS::IAM::Role                | booksapiLambdaGETuriServiceRole56D21611
07:38:02 | DELETE_IN_PROGRESS                           | AWS::IAM::Role                | booksapiLambdaDELETEuriServiceRole1C7BDA10
07:38:02 | DELETE_IN_PROGRESS                           | AWS::IAM::Role                | booksapiLambdaPUTuriServiceRole5C758468
07:38:03 | DELETE_COMPLETE                              | AWS::ApiGatewayV2::DomainName | booksapiDomainNameC02658E8
07:38:03 | DELETE_COMPLETE                              | AWS::IAM::Role                | booksapiLambdaGETServiceRole55FF2D79
07:38:03 | DELETE_COMPLETE                              | AWS::ApiGatewayV2::Api        | booksapiApiD9E96849
07:38:04 | DELETE_COMPLETE                              | AWS::IAM::Role                | booksapiLambdaPUTuriServiceRole5C758468
07:38:04 | DELETE_COMPLETE                              | AWS::IAM::Role                | booksapiLambdaDELETEuriServiceRole1C7BDA10
d
sst.Api creates an API in API Gateway. Custom domains map 1:1 with APIs
you'll need to attach those routes to one api if you want them to share the same custom domain - that's a limitation/feature of API GW, not of CDK or SST
g
I always attached the API in a specific base path
d
ah, you can map multiple apis to a domain, so I guess you should in theory be able to specify that through CDK/SST
g
Copy code
import * as sst from "@serverless-stack/resources";
import {Certificate, CertificateValidation} from "@aws-cdk/aws-certificatemanager";
import {HostedZone} from '@aws-cdk/aws-route53';
import {CfnOutput} from "@aws-cdk/core";
import {ApiAuthorizationType} from "@serverless-stack/resources";
import * as cdk from "@aws-cdk/core";

function createCertificate(scope: sst.Stack, app: <http://sst.App|sst.App>, id: string, props?: sst.StackProps): Certificate {
    const domain_name = `${app.stage}.rt.parque.cloud`;

    const hosted_zone = HostedZone.fromLookup(scope, 'HostedZone', {
        domainName: domain_name,
    });

    const certificate: Certificate = new Certificate(scope, "CustomDomainCertificate", {
        domainName: `*.${domain_name}`,
        validation: CertificateValidation.fromDns(hosted_zone),
    });

    new CfnOutput(scope, "CertificateArn", {
        value: certificate.certificateArn,
    });

    return certificate;
}

function createBooksApi(stack: sst.Stack, app: <http://sst.App|sst.App>, id: string, props?: sst.StackProps): sst.Api {
    const books_api: sst.Api = new sst.Api(stack, id, {
        accessLog:
            "$context.identity.sourceIp,$context.requestTime,$context.httpMethod,$context.routeKey,$context.protocol,$context.status,$context.responseLength,$context.requestId",
        customDomain: {
            domainName: "api.dev.rt.parque.cloud",
            hostedZone: "dev.rt.parque.cloud",
            path: "books",
            certificate: Certificate.fromCertificateArn(
                stack,
                "books-api-certificate",
                'arn:aws:acm:us-east-1:834896107245:certificate/d9f2ee08-fc7b-4b09-819e-d0289f904ac4'
            ),
        },
        defaultAuthorizationType: ApiAuthorizationType.AWS_IAM,
        defaultFunctionProps: {
            timeout: 30,
            environment: {
                STAGE: app.stage
            }
        },
        routes: {
            "POST   /": "sample-app/books-api/books-api.createBook",
            "GET    /{uri}": "sample-app/books-api/books-api.readBook",
            "PUT    /{uri}": "sample-app/books-api/books-api.updateBook",
            "DELETE /{uri}": "sample-app/books-api/books-api.deleteBook",
            "GET    /": "sample-app/books-api/books-api.findBook",
        }
    });

    books_api.attachPermissions(["dynamodb"]);

    return books_api;
}

function createAuth(stack: sst.Stack, app: <http://sst.App|sst.App>, id: string, props?: sst.StackProps): sst.Auth {
    // Create auth provider
    const auth = new sst.Auth(stack, id, {
        // Create a Cognito User Pool to manage user's authentication info.
        cognito: {
            // Users will login using their email and password
            signInAliases: {email: true},
        },
    });

    new cdk.CfnOutput(stack, "UserPoolId", {
        value: auth.cognitoUserPool!.userPoolId,
    });
    new cdk.CfnOutput(stack, "UserPoolClientId", {
        value: auth.cognitoUserPoolClient!.userPoolClientId,
    });
    new cdk.CfnOutput(stack, "IdentityPoolId", {
        value: auth.cognitoCfnIdentityPool.ref,
    });

    return auth;
}

function createUsersApi(stack: sst.Stack, app: <http://sst.App|sst.App>, id: string, props?: sst.StackProps): sst.Api {
    const users_api: sst.Api = new sst.Api(stack, id, {
        accessLog:
            "$context.identity.sourceIp,$context.requestTime,$context.httpMethod,$context.routeKey,$context.protocol,$context.status,$context.responseLength,$context.requestId",
        customDomain: {
            domainName: "api.dev.rt.parque.cloud",
            hostedZone: "dev.rt.parque.cloud",
            path: "users",
            certificate: Certificate.fromCertificateArn(
                stack,
                "users-api-certificate",
                'arn:aws:acm:us-east-1:834896107245:certificate/d9f2ee08-fc7b-4b09-819e-d0289f904ac4'
            ),
        },
        defaultAuthorizationType: ApiAuthorizationType.AWS_IAM,
        defaultFunctionProps: {
            timeout: 30,
            environment: {
                STAGE: app.stage
            }
        },
        routes: {
            "POST   /": "sample-app/users-api/users-api.createUser",
            "GET    /{uri}": "sample-app/users-api/users-api.readUser",
            "PUT    /{uri}": "sample-app/users-api/users-api.updateUser",
            "DELETE /{uri}": "sample-app/users-api/users-api.deleteUser",
            "GET    /": "sample-app/users-api/users-api.findUser",
        }
    });

    users_api.attachPermissions(["dynamodb"]);

    return users_api;
}

export default class TestApiStack extends sst.Stack {
    constructor(app: <http://sst.App|sst.App>, id: string, props?: sst.StackProps) {
        super(app, id, props);

        createCertificate(this, app, 'certificate');
        const auth: sst.Auth = createAuth(this, app, 'users-auth');
        const users_api: sst.Api = createUsersApi(this, app, 'users-api');
        const books_api: sst.Api = createBooksApi(this, app, 'books-api');

        auth.attachPermissionsForAuthUsers([users_api, books_api]);
    }
}
@Dmitry Pavluk as you can see I specified a different “path” property on custom domain
d
Took a look at
Api.ts
in
sst/resources
and at CDK docs - didn't see any obvious reason this should fail. Maybe it's a dangling resource left behind from previous deploys? Have you tried manually removing that resource from the stack and/or removing the whole stack and redeploying?
I think this is actually related to an issue that I came across when attempting to use a custom domain, although it manifested differently. In my case the custom domain deployed fine the first time but failed on every subsequent try. I think some of the required resources, like certificate, Route53 records, etc are getting recreated on
sst start
and/or
sst deploy
State/idempotency of deploys is getting messed up somewhere along the way when a custom domain is specified
If you search "custom domain" in this group, the errors people have encountered point to a single origin somewhere in the course of creating the route 53 records and/or cert
g
do you suggest a fresh deploy? this couldn’t be a solution, it must be necessary to expand the infrastructure later
i tried also to remove certificate creation but the error still be the same
d
not fresh deploy - literally deleting the CloudFormation stack in AWS, making sure all its resources delete, going to Route53, making sure all records and certificates created by SST/CDK are gone, and then doing a fresh deploy
g
yes I got it but is it the only way to solve? I need to create new apis in future, i can’t do this everytime
d
I do think there's a bug that needs to be solved at the source code level, this suggestion is just to gather more evidence and hopefully unblock development for you
g
Copy code
customDomain: {
    domainName: apigatewayv2.DomainName.fromDomainNameAttributes(
        stack,
        "books-api-domain",
        {
            name: 'api.dev.rt.parque.cloud',
            regionalDomainName: 'api.dev.rt.parque.cloud',
            regionalHostedZoneId: 'dev.rt.parque.cloud',
        }
    ),
    path: "books"
},
SOLVED importing an existing domain through CDK as described is SST docs.
f
@gio Importing the domain isn’t a reliable solution. Imagine the domain has not been created in your account and importing the domain
apigatewayv2.DomainName.fromDomainNameAttributes
might fail if CloudFormation configured the domain for books_api before users_api.
I just created a new release v0.10.6 to expose the api gateway domain prop for an api. You should do something like this instead:
Copy code
// Create users api normally
const usersApi = new sst.Api(this, "UsersAPI", {
  customDomain: ...what u had before...
});

// Create books api and re-use users api's domain
const booksApi = new sst.Api(this, "BooksAPI", {
  customDomain: {
    domainName: usersApi.apiGatewayDomain,
    path: "books"
  }
});
Added an example to the doc - https://docs.serverless-stack.com/constructs/Api#mapping-multiple-apis-to-the-same-domain
Let me know if this makes sense
g
Well done @Frank! I will test it asap
p
I'm trying to import both an existing domain and an existing certificate like so:
Copy code
this.apiV3 = new sst.Api(this, `${id}_apiV3`, {
      customDomain: {
        domainName: apigatewayv2.DomainName.fromDomainNameAttributes(
          this,
          'PeasyCustomDomain',
          {
            name: domainName,
            regionalDomainName: domainName,
            regionalHostedZoneId: '<http://peasy.nu|peasy.nu>'
          }
        ),
        certificate: certificatemanager.Certificate.fromCertificateArn(
          this,
          'PeasyCustomCertificate',
          certArn[scope.stage]
        )
      },
....
But running this yields
Error: Cannot configure the "certificate" when the "domainName" is a construct
Any ideas on how to do this smarter @Frank? I'm open to deleting the domain and/or certificate to create from scratch, but need different domains for my various environments and these are all in different accounts, so I'll need to add NS records for the new domains to the root domain (which is why I created my domains manually)
f
Hey @Pål Brattberg, the certificate is tied to the domain. So if you are importing an existing domain, it should already has a certificate tied to it.
Or am I mistaken? 🤔
p
No, you're quite right, I was just confused! 🙈 Got it running by simply removing the cert of course!
Thanks @Frank!
Actually, still not quite there... I got the domainname imported correctly, but when I connect it to ApiI get an error:
Copy code
test-peasy-domain | CREATE_FAILED | AWS::ApiGatewayV2::ApiMapping | domainapiV2p1ApiDefaultStagetestpeasydomainPeasyCustomDomainundefinedD2651C5C Invalid domain name identifier specified (Service: AmazonApiGatewayV2; Status Code: 404; Error Code: NotFoundException; Request ID: xxxx; Proxy: null)
If I only create the domain and just output a dump of the object, that runs fine:
Copy code
this.domain = apigatewayv2.DomainName.fromDomainNameAttributes(
      this, 'PeasyCustomDomain',
      {
        name: this.domainName,
        regionalDomainName: this.domainName,
        regionalHostedZoneId: '<http://api.peasy.nu|api.peasy.nu>'
      }
    )
Any ideas on what I'm doing wrong? I checked the dump and it's in the correct region
f
are you setting a
path
? (ie. basemap)
p
No, is that required? Will try!
f
It’s not required. But without a basemap, you can only map 1 api to a domain.
Are you sharing the domain name with another API?
p
No, should only be this one API. Created a new domain for this and all services share the same Api
Same error with path defined btw
f
hmm can you send me the generated CloudFormation template that’s inside
.build/cdk.out
?
p
Will do!
For future references: my main problem was that I was importing a Route53 domain and not an APIGateway DomainName. I also tried to import both the domain name and the certificate, but after I simply made sure the domainname was present in each aws account (with an NS record on the root account) and let SST create the certificate for me it worked! I had been importing my cert, since for RESTAPI it needs to be located at us-east-1, and my region is eu-west-1. This is not required got HTTPAPI which is default in SST.