Repository: Miserlou/Zappa

Issue:
Allow certify without any downtime (45min)
## Context
zappa certify needs to update the certificate -- previously this wasn't possible by boto/amazon apis, but now it is -- so let's take advantage

## Expected Behavior
up to 45min of downtime stinks!  let's make it better now that there are apis for this in amazon

## Actual Behavior
`zappa certify` does a delete/create cycle just to update the certificate

## Possible Fix
Use this:
https://boto3.readthedocs.io/en/latest/reference/services/apigateway.html#APIGateway.Client.update_domain_name

## Steps to Reproduce
(this is documented in README.md)

## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in -->
* Zappa version used: master
* Operating System and Python version: 2.7



Discussion:

Resolving patch:
diff --git a/zappa/cli.py b/zappa/cli.py
--- a/zappa/cli.py
+++ b/zappa/cli.py
@@ -1502,9 +1502,6 @@ def certify(self, no_cleanup=False, no_confirm=True, manual=False):
             raise ClickException("Can't certify a domain without " + click.style("domain", fg="red", bold=True) + " configured!")
 
         if not no_confirm: # pragma: no cover
-            if not manual and self.zappa.get_domain_name(self.domain):
-                click.echo(click.style("Warning!", fg="red", bold=True) + " If you have already certified this domain and are calling certify again, you may incur downtime.")
-                click.echo("You can avoid this downtime by calling certify with " + click.style("--manual", bold=True) + " and rotating your certificate yourself through the AWS console.")
             confirm = input("Are you sure you want to certify? [y/n] ")
             if confirm != 'y':
                 return
@@ -2372,4 +2369,4 @@ def handle(): # pragma: no cover
         sys.exit(-1)
 
 if __name__ == '__main__': # pragma: no cover
-    handle()
\ No newline at end of file
+    handle()
diff --git a/zappa/core.py b/zappa/core.py
--- a/zappa/core.py
+++ b/zappa/core.py
@@ -251,6 +251,9 @@ def __init__(self,
         self.lambda_client = self.boto_session.client('lambda', config=long_config)
         self.events_client = self.boto_session.client('events')
         self.apigateway_client = self.boto_session.client('apigateway')
+        # acm certificates need to be created from us-east-1 to be used by API gateway
+        east_config = botocore.client.Config(region_name='us-east-1')
+        self.acm_client = self.boto_session.client('acm', config=east_config)
         self.logs_client = self.boto_session.client('logs')
         self.iam_client = self.boto_session.client('iam')
         self.iam = self.boto_session.resource('iam')
@@ -1733,23 +1736,20 @@ def update_domain_name(self,
                            stage=None,
                            route53=True):
         """
+        This updates your certificate information for an existing domain,
+        with similar arguments to boto's update_domain_name API Gateway api.
 
-        This doesn't quite do what it seems like it should do.
+        It returns the resulting new domain information including the new certificate's ARN
+        if created during this process.
 
-        Unfortunately, there is currently no way to programatically rotate the
-        certificate for a currently deployed domain.
-
-        So, what we can do instead is delete the record of it and then recreate it.
-
-        The problem is that this causes a period of downtime. This could take up to 40 minutes,
-        in theory, but in practice this seems to only take (way) less than a minute, making it
-        at least somewhat acceptable.
+        Previously, this method involved downtime that could take up to 40 minutes
+        because the API Gateway api only allowed this by deleting, and then creating it.
 
         Related issues:     https://github.com/Miserlou/Zappa/issues/590
                             https://github.com/Miserlou/Zappa/issues/588
                             https://github.com/Miserlou/Zappa/pull/458
-
-
+                            https://github.com/Miserlou/Zappa/issues/882
+                            https://github.com/Miserlou/Zappa/pull/883
         """
 
         print("Updating domain name!")
@@ -1757,19 +1757,22 @@ def update_domain_name(self,
         certificate_name = certificate_name + str(time.time())
 
         api_gateway_domain = self.apigateway_client.get_domain_name(domainName=domain_name)
-        self.apigateway_client.delete_domain_name(domainName=domain_name)
-        dns_name = self.create_domain_name(domain_name,
-                           certificate_name,
-                           certificate_body,
-                           certificate_private_key,
-                           certificate_chain,
-                           certificate_arn,
-                           lambda_name,
-                           stage)
-        if route53:
-            self.update_route53_records(domain_name, dns_name)
-
-        return
+        if not certificate_arn\
+           and certificate_body and certificate_private_key and certificate_chain:
+            acm_certificate = self.acm_client.import_certificate(Certificate=certificate_body,
+                                                                 PrivateKey=certificate_private_key,
+                                                                 CertificateChain=certificate_chain)
+            certificate_arn = acm_certificate['CertificateArn']
+
+        return self.apigateway_client.update_domain_name(domainName=domain_name,
+                                                         patchOperations=[
+                                                             {"op" : "replace",
+                                                              "path" : "/certificateName",
+                                                              "value" : certificate_name},
+                                                             {"op" : "replace",
+                                                              "path" : "/certificateArn",
+                                                              "value" : certificate_arn}
+                                                         ])
 
     def get_domain_name(self, domain_name):
         """


Test patch: