Skip to content

Commit

Permalink
Inline acm cert creation into cdk
Browse files Browse the repository at this point in the history
Signed-off-by: Andre Kurait <[email protected]>
  • Loading branch information
AndreKurait committed Jun 11, 2024
1 parent 50cedc3 commit 6298d7b
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 139 deletions.
135 changes: 0 additions & 135 deletions deployment/cdk/opensearch-service-migration/create-acm-cert.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import {
Context,
CloudFormationCustomResourceEvent,
CloudFormationCustomResourceResponse,
} from 'aws-lambda';
import { ACMClient, ImportCertificateCommand, DeleteCertificateCommand } from '@aws-sdk/client-acm';
import * as https from 'https';
import * as forge from 'node-forge';

export const handler = async (event: CloudFormationCustomResourceEvent, context: Context): Promise<CloudFormationCustomResourceResponse> => {
console.log('Received event:', JSON.stringify(event, null, 2));
console.log('Received context:', JSON.stringify(context, null, 2));
let responseData: { [key: string]: any } = {};
let physicalResourceId: string = '';

try {
switch (event.RequestType) {
case 'Create':
const { certificate, privateKey, certificateChain } = await generateSelfSignedCertificate();
const certificateArn = await importCertificate(certificate, privateKey, certificateChain);
console.log(`Certificate imported with ARN: ${certificateArn}`);
responseData = { CertificateArn: certificateArn };
physicalResourceId = certificateArn;
break;
case 'Update':
// No update logic needed, return existing physical resource id
physicalResourceId = event.PhysicalResourceId;
break;
case 'Delete':
const arn = event.PhysicalResourceId;
await deleteCertificate(arn);
responseData = { CertificateArn: arn };
physicalResourceId = arn;
break;
}

return await sendResponse(event, context, 'SUCCESS', responseData, physicalResourceId);
} catch (error) {
console.error(error);
return await sendResponse(event, context, 'FAILED', { Error: (error as Error).message }, physicalResourceId);
}
};

async function generateSelfSignedCertificate(): Promise<{ certificate: string, privateKey: string, certificateChain: string }> {
return new Promise((resolve, reject) => {
const keys = forge.pki.rsa.generateKeyPair(2048);
const cert = forge.pki.createCertificate();

cert.publicKey = keys.publicKey;
cert.serialNumber = '01';
cert.validity.notBefore = new Date(Date.UTC(1970, 0, 1, 0, 0, 0));
cert.validity.notAfter = new Date(Date.UTC(9999, 11, 31, 23, 59, 59, 999));
const attrs = [{
name: 'commonName',
value: 'localhost'
}];

cert.setSubject(attrs);
cert.setIssuer(attrs);

cert.setExtensions([{
name: 'basicConstraints',
cA: true
}, {
name: 'subjectAltName',
altNames: [{
type: 2, // DNS
value: 'localhost'
}]
}, {
name: 'keyUsage',
keyCertSign: true,
digitalSignature: true,
keyEncipherment: true
}, {
name: 'extKeyUsage',
serverAuth: true,
clientAuth: true
},]);
cert.sign(keys.privateKey, forge.md.sha384.create());

const pemCert = forge.pki.certificateToPem(cert);
const pemKey = forge.pki.privateKeyToPem(keys.privateKey);

resolve({
certificate: pemCert,
privateKey: pemKey,
certificateChain: pemCert
});
});
}

async function importCertificate(certificate: string, privateKey: string, certificateChain: string): Promise<string> {
const client = new ACMClient({ region: process.env.AWS_REGION });

const command = new ImportCertificateCommand({
Certificate: Buffer.from(certificate),
PrivateKey: Buffer.from(privateKey),
CertificateChain: Buffer.from(certificateChain),
});

const response = await client.send(command);
return response.CertificateArn!;
}

async function deleteCertificate(certificateArn: string): Promise<void> {
const client = new ACMClient({ region: process.env.AWS_REGION });
const command = new DeleteCertificateCommand({
CertificateArn: certificateArn,
});

await client.send(command);
}

async function sendResponse(event: CloudFormationCustomResourceEvent, context: Context, responseStatus: string, responseData: { [key: string]: any }, physicalResourceId: string): Promise<CloudFormationCustomResourceResponse> {
const responseBody = JSON.stringify({
Status: responseStatus,
Reason: `See the details in CloudWatch Log Stream: ${context.logStreamName}`,
PhysicalResourceId: physicalResourceId,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
Data: responseData
});

console.log('Response body:', responseBody);

if (event.ResponseURL === "Local") {
console.log('Running locally, simulating response success.');
return {
Status: 'SUCCESS',
Reason: 'Running locally, response simulated.',
PhysicalResourceId: physicalResourceId,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
Data: responseData
};
}

const parsedUrl = new URL(event.ResponseURL);
const options = {
hostname: parsedUrl.hostname,
port: 443,
path: parsedUrl.pathname + parsedUrl.search,
method: 'PUT',
headers: {
'Content-Type': '',
'Content-Length': responseBody.length
}
};

return new Promise<CloudFormationCustomResourceResponse>((resolve, reject) => {
const request = https.request(options, (response) => {
console.log(`Status code: ${response.statusCode}`);
console.log(`Status message: ${response.statusMessage}`);
resolve({
Status: responseStatus === 'SUCCESS' || responseStatus === 'FAILED' ? responseStatus : 'FAILED',
Reason: `See the details in CloudWatch Log Stream: ${context.logStreamName}`,
PhysicalResourceId: physicalResourceId,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
Data: responseData
});
});

request.on('error', (error) => {
console.error('sendResponse Error:', error);
reject(error);
});

request.write(responseBody);
request.end();
});
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * as msk_ordered_endpoints_handler from './msk-ordered-endpoints-handler'
export * as msk_public_endpoint_handler from './msk-public-endpoint-handler'
export * as msk_public_endpoint_handler from './msk-public-endpoint-handler'
export * as acm_cert_importer_handler from './acm-cert-importer-handler'

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 24 additions & 1 deletion deployment/cdk/opensearch-service-migration/lib/network-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import {
import {Construct} from "constructs";
import {StackPropsExt} from "./stack-composer";
import {StringParameter} from "aws-cdk-lib/aws-ssm";
import { ApplicationLoadBalancer, ApplicationProtocol, ApplicationTargetGroup, IApplicationLoadBalancer, ListenerAction, SslPolicy, TargetType } from "aws-cdk-lib/aws-elasticloadbalancingv2";
import { ApplicationLoadBalancer, IApplicationLoadBalancer } from "aws-cdk-lib/aws-elasticloadbalancingv2";
import { Certificate, ICertificate } from "aws-cdk-lib/aws-certificatemanager";
import { ARecord, HostedZone, RecordTarget } from "aws-cdk-lib/aws-route53";
import { LoadBalancerTarget } from "aws-cdk-lib/aws-route53-targets";
import { AcmCertificateImporter } from "./service-stacks/acm-cert-importer";

export interface NetworkStackProps extends StackPropsExt {
readonly vpcId?: string;
Expand Down Expand Up @@ -122,7 +125,27 @@ export class NetworkStack extends Stack {
if (props.albAcmCertArn) {
const cert = Certificate.fromCertificateArn(this, 'ALBListenerCert', props.albAcmCertArn);
this.albListenerCert = cert;
} else {
const cert = new AcmCertificateImporter(this, 'ALBListenerCertImport').acmCert;
this.albListenerCert = cert;
}
// this.exportValue(this.albListenerCert);

const route53 = new HostedZone(this, 'ALBHostedZone', {
zoneName: `alb.migration.${props.stage}.local`,
vpcs: [this.vpc]
});

const albDnsRecord = new ARecord(this, 'albDnsRecord', {
zone: route53,
target: RecordTarget.fromAlias(new LoadBalancerTarget(this.alb)),
});

new StringParameter(this, 'SSMParameterAlbUrl', {
description: 'OpenSearch migration parameter for ALB to migration services',
parameterName: `/migration/${props.stage}/${props.defaultDeployId}/albMigrationUrl`,
stringValue: `https://${albDnsRecord.domainName}`
});
}

if (!props.addOnMigrationDeployId) {
Expand Down
Loading

0 comments on commit 6298d7b

Please sign in to comment.