@jaggr2/cdk-cf-dns
v0.3.1
Published
Manage Cloudflare DNS records as CloudFormation resources from AWS CDK, so Route 53 hosted zones are no longer needed.
Maintainers
Readme
@jaggr2/cdk-cf-dns
Manage Cloudflare DNS records as CloudFormation resources from AWS CDK, so that Route 53 hosted zones (and their $0.50/zone/month charge) are no longer needed when Cloudflare is already authoritative for the domain.
The developer experience is modelled on aws-cdk-lib/aws-route53: reference a
zone, add records, and let the stack manage them. A single shared Lambda custom
resource performs the Cloudflare API calls — one Lambda per stack, no matter how
many records you define.
const zone = CloudflareZone.fromZoneId(this, 'Zone', {
zoneId: 'abc123...',
apiToken: secretsmanager.Secret.fromSecretNameV2(this, 'CfToken', 'cloudflare/dns-token'),
zoneName: 'example.com',
});
new CloudflareRecord(this, 'AppCname', {
zone,
recordName: 'app', // relative -> app.example.com
type: CloudflareRecordType.CNAME,
content: distribution.distributionDomainName,
proxied: true,
});Install
npm install @jaggr2/cdk-cf-dnsaws-cdk-lib and constructs are peer dependencies — you must have them
installed in your own project (any CDK v2 app already does):
npm install aws-cdk-lib constructsYou also need esbuild as a dev dependency, because the library's Lambda
handler is bundled with NodejsFunction at synthesis time:
npm install --save-dev esbuildThe handler itself has zero runtime npm dependencies — it uses the AWS SDK v3
that is built into the Lambda runtime and Cloudflare's API over fetch.
Cloudflare API token setup
- Log in to the Cloudflare dashboard.
- Go to My Profile → API Tokens → Create Token.
- Use the Edit zone DNS template.
- Scope the token to the single zone you intend to manage (do not select "All zones" unless you really need it).
- Create the token, then store it in Secrets Manager so only its ARN ever reaches CloudFormation:
aws secretsmanager create-secret --name cloudflare/dns-token --secret-string 'cf_token_here'The token grants DNS write access to that zone; scope it as narrowly as possible. The token itself never appears in your CDK template, an environment variable, or a log line — the Lambda resolves it from Secrets Manager at runtime.
Finding your Zone ID
Cloudflare dashboard → select the domain → Overview → scroll to the right rail → API section shows the Zone ID (a 32-character hex string).
Usage
CNAME to a CloudFront distribution
const zone = CloudflareZone.fromZoneId(this, 'Zone', {
zoneId: 'abc123...',
zoneName: 'example.com',
apiToken: secretsmanager.Secret.fromSecretNameV2(this, 'CfToken', 'cloudflare/dns-token'),
});
new CloudflareCnameRecord(this, 'AppCname', {
zone,
recordName: 'app', // -> app.example.com
content: distribution.distributionDomainName,
proxied: true, // Cloudflare terminates TLS and caches
});Apex A record
new CloudflareARecord(this, 'Apex', {
zone, // recordName omitted -> zone apex
content: '1.2.3.4',
});TXT record for domain verification
new CloudflareTxtRecord(this, 'Verify', {
zone,
recordName: '_amazonses.example.com',
content: 'LONG_VERIFICATION_STRING_...', // >255 chars is chunked automatically
});Managed-by comment
Every record gets a Cloudflare comment identifying it as managed by this library, including the CloudFormation stack name and account id, so records can be traced back to their source stack:
managed by cdk-cf-dns (stack: MyStack, account: 123456789012)This uses the comment field because Cloudflare only supports record tags on
paid (Pro/Business/Enterprise) plans — comments are available on all plans.
Pass your own comment to override it, or set managedByCdkComment: false to
disable it.
MX records
new CloudflareMxRecord(this, 'Mx', {
zone,
content: 'mail.example.com',
priority: 10,
});Every record type also works through the generic CloudflareRecord with
CloudflareRecordType, including SRV, CAA and URI (which take a data
object instead of a plain content string). Thin subclasses
(CloudflareARecord, CloudflareAaaaRecord, CloudflareCnameRecord,
CloudflareTxtRecord, CloudflareMxRecord, CloudflareCaaRecord) narrow the
type and validate their own required fields.
Proxied records and ACM
If you use ACM certificate DNS validation (for example with CloudFront), the
_acme-challenge CNAMEs generated by CertificateValidation.fromDns() must be
proxied: false — DNS-01 validation requires the raw CNAME to resolve, and a
proxied record will never pass. Use a plain CNAME record for validation names:
new CloudflareCnameRecord(this, 'AcmeValidation', {
zone,
recordName: record.targetName, // e.g. _acme-challenge.example.com
content: record.domainName, // the validation value
proxied: false, // CRITICAL
ttl: cdk.Duration.seconds(60),
});Automated ACM DNS validation
CloudflareValidatedCertificate removes the manual copy-paste step during ACM
DNS validation. It creates the certificate and, via a second shared custom
resource, polls acm:DescribeCertificate until the validation CNAMEs are
populated (they are absent for a few seconds after creation) and writes each one
into Cloudflare with proxied: false and ttl: 60. A wildcard SAN and its apex
share a single validation record.
const cert = new CloudflareValidatedCertificate(this, 'Cert', {
domainName: 'example.com',
subjectAlternativeNames: ['*.example.com'],
zone,
});
new cloudfront.Distribution(this, 'Dist', {
defaultBehavior: {
origin: origin,
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
domainNames: ['example.com'],
certificate: cert.certificate,
});The validation records are left in Cloudflare after issuance — they are harmless. If the certificate is attached to CloudFront it must live in us-east-1.
Limitations
- No zone creation. Zones must already exist in Cloudflare; this library only references them by Zone ID.
- No zone-level settings. Record-only. SSL/TLS modes, page rules, workers, and other zone configuration are out of scope.
- No page rules and no bulk import.
- Out-of-band edits to records in the Cloudflare dashboard are only reconciled on the next stack update (an update re-applies the desired state; a delete of a record that only exists out-of-band is ignored).
- Deleting a managed record requires
adoptExisting: trueif it was created outside the stack, otherwise the deployment fails with a helpful error.
API
See API.md for the generated reference.
License
MIT
