Skip to content

Commit

Permalink
Merge pull request #2 from NickolasHKraus/wait-until-issued
Browse files Browse the repository at this point in the history
Wait until issued
  • Loading branch information
Nickolas Kraus committed Sep 18, 2019
2 parents daf3e27 + b6939bc commit 43c5378
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 5 deletions.
6 changes: 6 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# History

## v0.3.0 (2019-09-18)
---

* The CertificateValidator custom resource will now wait until the ACM
Certificate is issued before returning a SUCCESS response.

## v0.2.0 (2019-09-16)
---

Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ clean: ## remove deployment and Python artifacts
deploy: package ## deploy AWS Lambda function via Serverless
serverless deploy -v --stage ${STAGE}

remove: ## remove AWS Lambda function via Serverless
serverless remove -v --stage ${STAGE}

package: clean ## create AWS Lambda function deployment package
mkdir -p ${PACKAGE_DIR}
pip3 install -r certificate_validator/requirements.txt -t ${PACKAGE_DIR}
Expand Down
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ From the [`AWS::CertificateManager::Certificate`](https://docs.aws.amazon.com/AW
>
>When you use the `AWS::CertificateManager::Certificate` resource in an AWS CloudFormation stack, the stack will remain in the `CREATE_IN_PROGRESS` state. Further stack operations will be delayed until you validate the certificate request, either by acting upon the instructions in the validation email, or by adding a CNAME record to your DNS configuration.
## Getting Started

Check out the [Getting Started](https://github.com/Dwolla/certificate-validator/blob/master/docs/getting-started.md) documentation to start using Certificate Validator.

## Validating a certificate with DNS

When you use the `AWS::CertificateManager::Certificate` resource in an AWS CloudFormation stack, the stack will remain in the `CREATE_IN_PROGRESS` state and any further stack operations will be delayed until you validate the certificate request. Certificate validation can be completed either by acting upon the instructions in the certificate validation email or by adding a CNAME record to your DNS configuration.
Expand Down Expand Up @@ -111,12 +115,28 @@ Install requirements:
pip install -r certificate_validator/requirements_dev.txt
```

### Deployment
## Deployment

Deploy Certificate Validator:

```bash
serverless deploy -v
make deploy
```

**Note**: An optional `STAGE` variable can be used to specify the *stage*. Defaults to `dev`.

**Example**

```bash
make deploy STAGE=prod
```

**Note**: An optional `--stage` flag can be used to specify the *stage*. Defaults to `dev`.
To remove Certificate Validator, run `make remove`.

**Note**: An optional `STAGE` variable can be used to specify the *stage*. Defaults to `dev`.

**Example**

```bash
make remove STAGE=prod
```
2 changes: 1 addition & 1 deletion certificate_validator/certificate_validator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@

__author__ = """Nickolas Kraus"""
__email__ = 'NickHKraus@gmail.com'
__version__ = '0.1.0'
__version__ = '0.3.0'
23 changes: 23 additions & 0 deletions certificate_validator/certificate_validator/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def __init__(self, *args, **kwargs) -> None:
:return: None
"""
super(ACM, self).__init__(*args, **kwargs)
self.waiter = self.client.get_waiter('certificate_validated')

def request_certificate(
self, domain_name: str, subject_alternative_names: list
Expand Down Expand Up @@ -133,6 +134,28 @@ def describe_certificate(self, certificate_arn: str) -> dict:
CertificateArn=certificate_arn,
)

def wait(self, certificate_arn: str) -> None:
"""
Wait for the specified ACM certificate to be issued.
Poll the DescribeCertificate API endpoint every 5 seconds until a
successful state is reached. An error is returned after 60 failed
checks (5 minutes).
:type certificate_arn: str
:param certificate_arn: ARN of the ACM certificate
:rtype: None
:return: None
"""
return self.waiter.wait(
CertificateArn=certificate_arn,
WaiterConfig={
'Delay': 5,
'MaxAttempts': 60
}
)


class Route53(AWS):
"""
Expand Down
4 changes: 4 additions & 0 deletions certificate_validator/certificate_validator/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,13 +198,17 @@ def create(self) -> None:
"""
Create AWS::Route53::RecordSetGroup resources.
Wait until the AWS::CertificateManager::Certificate resource is issued
before proceeding.
:rtype: None
:return: None
"""
self.response.set_physical_resource_id(str(uuid.uuid4()))
self.change_resource_record_sets(
self.request.resource_properties['CertificateArn'], Action.UPSERT
)
self.acm.wait(self.request.resource_properties['CertificateArn'])

def update(self) -> None:
"""
Expand Down
2 changes: 1 addition & 1 deletion certificate_validator/setup.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.1.0
current_version = 0.3.0
commit = True
tag = True

Expand Down
13 changes: 13 additions & 0 deletions certificate_validator/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def setUp(self):
super(ACMTestCase, self).setUp()
self.acm = ACM()
self.acm.client = Mock()
self.acm.waiter = Mock()

def test_request_certificate(self):
expected = {'CertificateArn': 'string'}
Expand Down Expand Up @@ -86,6 +87,18 @@ def test_describe_certificate(self):
)
self.assertEqual(expected, actual)

def test_wait(self):
certificate_arn = \
'arn:aws:acm:region:account-id:certificate/certificate-id'
self.acm.wait(certificate_arn)
self.acm.waiter.wait.assert_called_with(
CertificateArn=certificate_arn,
WaiterConfig={
'Delay': 5,
'MaxAttempts': 60
}
)


class Route53TestCase(AWSBaseTestCase):
def setUp(self):
Expand Down
4 changes: 4 additions & 0 deletions certificate_validator/tests/test_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ def test_change_resource_record_sets_delete_failed(self):
self.mock_response.set_reason.assert_called_with(reason=reason)

def test_create(self):
mock_wait = patch.object(resources.ACM, 'wait').start()
mock_change_resource_record_sets = \
patch.object(resources.CertificateValidator,
'change_resource_record_sets').start()
Expand All @@ -313,6 +314,9 @@ def test_create(self):
mock_change_resource_record_sets.assert_called_with(
'arn:aws:acm:us-east-1:123:certificate/1', Action.UPSERT
)
mock_wait.assert_called_once_with(
'arn:aws:acm:us-east-1:123:certificate/1'
)

def test_update(self):
mock_change_resource_record_sets = \
Expand Down
91 changes: 91 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Getting Started

It is extremely easy to get started with Certificate Validator.

1. Clone the [`certificate-validator`](https://github.com/Dwolla/certificate-validator) repository or download the latest [release](https://github.com/Dwolla/certificate-validator/releases).

2. Install Node.js and NPM:

```bash
brew install node
```

3. Install the Serverless Framework open-source CLI:

```bash
npm install -g serverless
```

4. Deploy Certificate Validator:

```bash
make deploy
```

**Note**: An optional `STAGE` variable can be used to specify the *stage*. Defaults to `dev`.

**Example**

```bash
make deploy STAGE=prod
```

To remove Certificate Validator, run `make remove`.

**Note**: An optional `STAGE` variable can be used to specify the *stage*. Defaults to `dev`.

**Example**

```bash
make remove STAGE=prod
```

5. Retrieve the Amazon Resource Name (ARN) of your newly created AWS Lambda function.

**Example**

```
arn:aws:lambda:<region>:<account-id>:function:<function-name>
```

The ARN of the AWS Lambda function serves as the service token ([`ServiceToken`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cfn-customresource.html#cfn-customresource-servicetoken)) for your `Custom::Certificate` and `Custom::CertificateValidator` custom resources.

**Note**: The service token must be in the same region as the CloudFormation stack.

6. Add the `Custom::Certificate` and `Custom::CertificateValidator` custom resources to your CloudFormation template:

**Example**

```yaml
Certificate:
Type: Custom::Certificate
Properties:
ServiceToken: !Ref ServiceToken
DomainName: !Ref DomainName
SubjectAlternativeNames:
- !Sub 'www.${DomainName}'

CertificateValidator:
Type: Custom::CertificateValidator
Properties:
ServiceToken: !Ref ServiceToken
CertificateArn: !GetAtt Certificate.CertificateArn
```

The `Custom::Certificate` custom resource can now be used anywhere a `AWS::CertificateManager::Certificate` resource would be used by calling `!GetAtt Certificate.CertificateArn`.

**Warning**: Since the ARN of a `AWS::CertificateManager::Certificate` resource is returned when you pass the logical ID of this resource to the intrinsic `Ref` function, an implicit dependency is created when it is referenced by other resources in your CloudFormation template. This ensures that the resource that references the `AWS::CertificateManager::Certificate` resource is created only after the certificate has been created. This is not the case for a `Custom::Certificate` custom resource, since the ARN is retrieved using the intrinsic `GetAtt` function, which does not create an implicit dependency. Therefore, you must explicitly create the dependency using the [`DependsOn`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-dependson.html) attribute for the `Custom::CertificateValidator` custom resource.

**Example**

```yaml
CloudFrontDistribution:
DependsOn: CertificateValidator
Type: AWS::CloudFront::Distribution
Properties:
...
```

The `Custom::CertificateValidator` uses a [waiter](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/acm.html?highlight=waiter#waiters), which polls for the status of the `AWS::CertificateManager::Certificate` resource created by the `Custom::Certificate` custom resource and only allows execution to proceed after the certificate has been issued.

For an example CloudFormation stack, see [`certificate-validator/example`](https://github.com/Dwolla/certificate-validator/tree/master/example).

0 comments on commit 43c5378

Please sign in to comment.