Skip to content

Commit

Permalink
feat(acm): DaysToExpiry metric (#15424)
Browse files Browse the repository at this point in the history
Adds a convenient method to obtain the `DaysToExpiry` metric for an AWS
Certificates Manager Certificate, without having to craft it yourself.


----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
RomainMuller authored Jul 6, 2021
1 parent e61a5b8 commit ff044ed
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 9 deletions.
18 changes: 18 additions & 0 deletions packages/@aws-cdk/aws-certificatemanager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,21 @@ const certificate = Certificate.fromCertificateArn(this, 'Certificate', arn);

To share the certificate between stacks in the same CDK application, simply
pass the `Certificate` object between the stacks.

## Metrics

The `DaysToExpiry` metric is available via the `metricDaysToExpiry` method for
all certificates. This metric is emitted by AWS Certificates Manager once per
day until the certificate has effectively expired.

An alarm can be created to determine whether a certificate is soon due for
renewal ussing the following code:

```ts
const certificate = new Certificate(this, 'Certificate', { /* ... */ });
certificate.metricDaysToExpiry().createAlarm({
comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD,
evaluationPeriods: 1,
threshold: 45, // Automatic rotation happens between 60 and 45 days before expiry
});
```
32 changes: 32 additions & 0 deletions packages/@aws-cdk/aws-certificatemanager/lib/certificate-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
import { Statistic } from '@aws-cdk/aws-cloudwatch';
import { Duration, Resource } from '@aws-cdk/core';
import { ICertificate } from './certificate';

/**
* Shared implementation details of ICertificate implementations.
*
* @internal
*/
export abstract class CertificateBase extends Resource implements ICertificate {
public abstract readonly certificateArn: string;

/**
* If the certificate is provisionned in a different region than the
* containing stack, this should be the region in which the certificate lives
* so we can correctly create `Metric` instances.
*/
protected readonly region?: string;

public metricDaysToExpiry(props?: cloudwatch.MetricOptions): cloudwatch.Metric {
return new cloudwatch.Metric({
period: Duration.days(1),
...props,
dimensions: { CertificateArn: this.certificateArn },
metricName: 'DaysToExpiry',
namespace: 'AWS/CertificateManager',
region: this.region,
statistic: Statistic.MINIMUM,
});
}
}
21 changes: 16 additions & 5 deletions packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
import * as route53 from '@aws-cdk/aws-route53';
import { IResource, Resource, Token } from '@aws-cdk/core';
import { IResource, Token } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CertificateBase } from './certificate-base';
import { CfnCertificate } from './certificatemanager.generated';
import { apexDomain } from './util';

Expand All @@ -14,6 +16,16 @@ export interface ICertificate extends IResource {
* @attribute
*/
readonly certificateArn: string;

/**
* Return the DaysToExpiry metric for this AWS Certificate Manager
* Certificate. By default, this is the minimum value over 1 day.
*
* This metric is no longer emitted once the certificate has effectively
* expired, so alarms configured on this metric should probably treat missing
* data as "breaching".
*/
metricDaysToExpiry(props?: cloudwatch.MetricOptions): cloudwatch.Metric;
}

/**
Expand Down Expand Up @@ -169,14 +181,13 @@ export class CertificateValidation {
/**
* A certificate managed by AWS Certificate Manager
*/
export class Certificate extends Resource implements ICertificate {

export class Certificate extends CertificateBase implements ICertificate {
/**
* Import a certificate
*/
public static fromCertificateArn(scope: Construct, id: string, certificateArn: string): ICertificate {
class Import extends Resource implements ICertificate {
public certificateArn = certificateArn;
class Import extends CertificateBase {
public readonly certificateArn = certificateArn;
}

return new Import(scope, id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as route53 from '@aws-cdk/aws-route53';
import * as cdk from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CertificateProps, ICertificate } from './certificate';
import { CertificateBase } from './certificate-base';

/**
* Properties to create a DNS validated certificate managed by AWS Certificate Manager
Expand Down Expand Up @@ -54,7 +55,7 @@ export interface DnsValidatedCertificateProps extends CertificateProps {
*
* @resource AWS::CertificateManager::Certificate
*/
export class DnsValidatedCertificate extends cdk.Resource implements ICertificate, cdk.ITaggable {
export class DnsValidatedCertificate extends CertificateBase implements ICertificate, cdk.ITaggable {
public readonly certificateArn: string;

/**
Expand All @@ -63,13 +64,16 @@ export class DnsValidatedCertificate extends cdk.Resource implements ICertificat
*/

public readonly tags: cdk.TagManager;
protected readonly region?: string;
private normalizedZoneName: string;
private hostedZoneId: string;
private domainName: string;

constructor(scope: Construct, id: string, props: DnsValidatedCertificateProps) {
super(scope, id);

this.region = props.region;

this.domainName = props.domainName;
this.normalizedZoneName = props.hostedZone.zoneName;
// Remove trailing `.` from zone name
Expand Down
4 changes: 3 additions & 1 deletion packages/@aws-cdk/aws-certificatemanager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"@aws-cdk/assert-internal": "0.0.0"
},
"dependencies": {
"@aws-cdk/aws-cloudwatch": "0.0.0",
"@aws-cdk/aws-iam": "0.0.0",
"@aws-cdk/aws-lambda": "0.0.0",
"@aws-cdk/aws-route53": "0.0.0",
Expand All @@ -92,7 +93,8 @@
"@aws-cdk/aws-lambda": "0.0.0",
"@aws-cdk/aws-route53": "0.0.0",
"@aws-cdk/core": "0.0.0",
"constructs": "^3.3.69"
"constructs": "^3.3.69",
"@aws-cdk/aws-cloudwatch": "0.0.0"
},
"engines": {
"node": ">= 10.13.0 <13 || >=13.7.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import '@aws-cdk/assert-internal/jest';
import * as route53 from '@aws-cdk/aws-route53';
import { Lazy, Stack } from '@aws-cdk/core';
import { Duration, Lazy, Stack } from '@aws-cdk/core';
import { Certificate, CertificateValidation, ValidationMethod } from '../lib';

test('apex domain selection by default', () => {
Expand All @@ -19,6 +19,25 @@ test('apex domain selection by default', () => {
});
});

test('metricDaysToExpiry', () => {
const stack = new Stack();

const certificate = new Certificate(stack, 'Certificate', {
domainName: 'test.example.com',
});

expect(stack.resolve(certificate.metricDaysToExpiry().toMetricConfig())).toEqual({
metricStat: {
dimensions: [{ name: 'CertificateArn', value: stack.resolve(certificate.certificateArn) }],
metricName: 'DaysToExpiry',
namespace: 'AWS/CertificateManager',
period: Duration.days(1),
statistic: 'Minimum',
},
renderingProperties: expect.anything(),
});
});

test('validation domain can be overridden', () => {
const stack = new Stack();

Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1609,7 +1609,7 @@
dependencies:
"@types/istanbul-lib-report" "*"

"@types/jest@^26.0.23":
"@types/jest@^26.0.22", "@types/jest@^26.0.23":
version "26.0.23"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.23.tgz#a1b7eab3c503b80451d019efb588ec63522ee4e7"
integrity sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA==
Expand Down

0 comments on commit ff044ed

Please sign in to comment.