Skip to content

Commit

Permalink
feat: Add ability to pass architecture to Lambda function
Browse files Browse the repository at this point in the history
  • Loading branch information
eternity1984 committed Oct 29, 2024
1 parent 1ee9d5d commit 32996ca
Showing 1 changed file with 137 additions and 71 deletions.
208 changes: 137 additions & 71 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as path from 'path';
import * as path from "path";

import * as oneTimeEvents from '@renovosolutions/cdk-library-one-time-event';
import * as oneTimeEvents from "@renovosolutions/cdk-library-one-time-event";
import {
aws_ec2 as ec2,
aws_efs as efs,
Expand All @@ -15,32 +15,32 @@ import {
Duration,
RemovalPolicy,
Stack,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { assignRequiredPoliciesToRole } from './required-policies';
} from "aws-cdk-lib";
import { Construct } from "constructs";
import { assignRequiredPoliciesToRole } from "./required-policies";
import {
// configureBucketStorage,
configureSecretsManagerStorage,
configureSSMStorage,
} from './storage-helpers';
} from "./storage-helpers";

export enum CertificateStorageType {
/**
* Store the certificate in AWS Secrets Manager
*/
SECRETS_MANAGER = 'secretsmanager',
SECRETS_MANAGER = "secretsmanager",
/**
* Store the certificates in S3
*/
S3 = 's3',
S3 = "s3",
/**
* Store the certificates as a parameter in AWS Systems Manager Parameter Store with encryption
*/
SSM_SECURE = 'ssm_secure',
SSM_SECURE = "ssm_secure",
/**
* Store the certificates in EFS, mounted to the Lambda function
*/
EFS = 'efs',
EFS = "efs",
}

export interface CertbotProps {
Expand Down Expand Up @@ -111,6 +111,17 @@ export interface CertbotProps {
* @default Duraction.seconds(180)
*/
readonly timeout?: Duration;
/**
* The architecture for the Lambda function.
*
* This property allows you to specify the architecture type for your Lambda function.
* Supported values are 'x86_64' for the standard architecture and 'arm64' for the
* ARM architecture.
*
* @default lambda.Architecture.X86_64
*/
readonly architecture?: lambda.Architecture;

/**
* The schedule for the certificate check trigger.
*
Expand Down Expand Up @@ -193,7 +204,6 @@ export interface CertbotProps {
}

export class Certbot extends Construct {

public readonly handler: lambda.Function;

constructor(scope: Construct, id: string, props: CertbotProps) {
Expand All @@ -202,38 +212,52 @@ export class Certbot extends Construct {
// Set property defaults
let layers: lambda.ILayerVersion[] = props.layers ?? [];
let runOnDeploy: boolean = props.runOnDeploy ?? true;
let functionDescription: string = props.functionDescription ?? 'Certbot Renewal Lambda for domain ' + props.letsencryptDomains.split(',')[0];
let functionDescription: string =
props.functionDescription ??
"Certbot Renewal Lambda for domain " +
props.letsencryptDomains.split(",")[0];
let enableInsights: boolean = props.enableInsights ?? false;
let insightsARN: string = props.insightsARN ?? 'arn:aws:lambda:' + Stack.of(this).region + ':580247275435:layer:LambdaInsightsExtension:14';
let insightsARN: string =
props.insightsARN ??
"arn:aws:lambda:" +
Stack.of(this).region +
":580247275435:layer:LambdaInsightsExtension:14";

if (props.hostedZoneNames === undefined && props.hostedZones === undefined) {
throw new Error('You must provide either hostedZoneNames or hostedZones');
if (
props.hostedZoneNames === undefined &&
props.hostedZones === undefined
) {
throw new Error("You must provide either hostedZoneNames or hostedZones");
}

// Create an SNS topic if one is not provided and add the defined email to it
let snsTopic: sns.Topic = props.snsTopic ?? new sns.Topic(this, 'topic');
let snsTopic: sns.Topic = props.snsTopic ?? new sns.Topic(this, "topic");
if (props.snsTopic === undefined) {
snsTopic.addSubscription(new subscriptions.EmailSubscription(props.letsencryptEmail));
snsTopic.addSubscription(
new subscriptions.EmailSubscription(props.letsencryptEmail)
);
}

let hostedZones:string[] = [];
let hostedZones: string[] = [];
if (props.hostedZoneNames != undefined) {
props.hostedZoneNames.forEach( (domainName) => {
hostedZones.push(r53.HostedZone.fromLookup(this, 'zone' + domainName, {
domainName,
privateZone: false,
}).hostedZoneArn);
props.hostedZoneNames.forEach((domainName) => {
hostedZones.push(
r53.HostedZone.fromLookup(this, "zone" + domainName, {
domainName,
privateZone: false,
}).hostedZoneArn
);
});
}

if (props.hostedZones != undefined) {
props.hostedZones.forEach( (hostedZone) => {
props.hostedZones.forEach((hostedZone) => {
hostedZones.push(hostedZone.hostedZoneArn);
});
}

const role = new iam.Role(this, 'role', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
const role = new iam.Role(this, "role", {
assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
});

assignRequiredPoliciesToRole(this, {
Expand All @@ -242,127 +266,169 @@ export class Certbot extends Construct {
hostedZones,
});

const functionDir = path.join(__dirname, '../function/src');
const functionDir = path.join(__dirname, "../function/src");

const bundlingCmds = [
'mkdir -p /asset-output',
'pip install -r /asset-input/requirements.txt -t /asset-output',
'cp index.py /asset-output/index.py',
"mkdir -p /asset-output",
"pip install -r /asset-input/requirements.txt -t /asset-output",
"cp index.py /asset-output/index.py",
];

// Create the Lambda function
this.handler = new lambda.Function(this, 'handler', {
this.handler = new lambda.Function(this, "handler", {
runtime: lambda.Runtime.PYTHON_3_10,
role,
architecture: props.architecture || lambda.Architecture.X86_64,
code: lambda.Code.fromAsset(functionDir, {
bundling: {
image: lambda.Runtime.PYTHON_3_10.bundlingImage,
command: [
'bash', '-c', bundlingCmds.join(' && '),
],
command: ["bash", "-c", bundlingCmds.join(" && ")],
},
}),
handler: 'index.handler',
handler: "index.handler",
functionName: props.functionName,
description: functionDescription,
environment: {
LETSENCRYPT_DOMAINS: props.letsencryptDomains,
LETSENCRYPT_EMAIL: props.letsencryptEmail,
OBJECT_PREFIX: props.objectPrefix || '',
REISSUE_DAYS: (props.reIssueDays === undefined) ? '30' : String(props.reIssueDays),
PREFERRED_CHAIN: props.preferredChain || 'None',
KEY_TYPE: props.keyType || 'ecdsa',
OBJECT_PREFIX: props.objectPrefix || "",
REISSUE_DAYS:
props.reIssueDays === undefined ? "30" : String(props.reIssueDays),
PREFERRED_CHAIN: props.preferredChain || "None",
KEY_TYPE: props.keyType || "ecdsa",
NOTIFICATION_SNS_ARN: snsTopic.topicArn,
DRY_RUN: 'False',
DRY_RUN: "False",
},
layers,
timeout: props.timeout || Duration.seconds(180),
filesystem: props.efsAccessPoint ? lambda.FileSystem.fromEfsAccessPoint(props.efsAccessPoint, '/mnt/efs') : undefined,
filesystem: props.efsAccessPoint
? lambda.FileSystem.fromEfsAccessPoint(props.efsAccessPoint, "/mnt/efs")
: undefined,
vpc: props.vpc,
});

let bucket: s3.Bucket;

if (props.bucket === undefined && (props.certificateStorage == CertificateStorageType.S3 || props.certificateStorage == undefined)) {
bucket = new s3.Bucket(this, 'bucket', {
if (
props.bucket === undefined &&
(props.certificateStorage == CertificateStorageType.S3 ||
props.certificateStorage == undefined)
) {
bucket = new s3.Bucket(this, "bucket", {
objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_PREFERRED,
removalPolicy: props.removalPolicy || RemovalPolicy.RETAIN,
autoDeleteObjects: props.enableObjectDeletion ?? false,
versioned: true,
lifecycleRules: [{
enabled: true,
abortIncompleteMultipartUploadAfter: Duration.days(1),
}],
lifecycleRules: [
{
enabled: true,
abortIncompleteMultipartUploadAfter: Duration.days(1),
},
],
encryption: s3.BucketEncryption.S3_MANAGED,
enforceSSL: true,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
});

bucket.grantReadWrite(this.handler);
this.handler.addEnvironment('CERTIFICATE_BUCKET', bucket.bucketName);
this.handler.addEnvironment('CERTIFICATE_STORAGE', 's3');
this.handler.addEnvironment("CERTIFICATE_BUCKET", bucket.bucketName);
this.handler.addEnvironment("CERTIFICATE_STORAGE", "s3");
}

if (props.bucket && (props.certificateStorage == CertificateStorageType.S3 || props.certificateStorage == undefined)) {
if (
props.bucket &&
(props.certificateStorage == CertificateStorageType.S3 ||
props.certificateStorage == undefined)
) {
bucket = props.bucket;
bucket.grantReadWrite(this.handler);
this.handler.addEnvironment('CERTIFICATE_BUCKET', bucket.bucketName);
this.handler.addEnvironment('CERTIFICATE_STORAGE', 's3');
this.handler.addEnvironment("CERTIFICATE_BUCKET", bucket.bucketName);
this.handler.addEnvironment("CERTIFICATE_STORAGE", "s3");
}

if (props.certificateStorage == CertificateStorageType.SECRETS_MANAGER) {
this.handler.addEnvironment('CERTIFICATE_STORAGE', 'secretsmanager');
this.handler.addEnvironment('CERTIFICATE_SECRET_PATH', props.secretsManagerPath || `/certbot/certificates/${props.letsencryptDomains.split(',')[0]}/`);
this.handler.addEnvironment("CERTIFICATE_STORAGE", "secretsmanager");
this.handler.addEnvironment(
"CERTIFICATE_SECRET_PATH",
props.secretsManagerPath ||
`/certbot/certificates/${props.letsencryptDomains.split(",")[0]}/`
);
if (props.kmsKeyAlias) {
this.handler.addEnvironment('CUSTOM_KMS_KEY_ID', props.kmsKeyAlias);
this.handler.addEnvironment("CUSTOM_KMS_KEY_ID", props.kmsKeyAlias);
}
configureSecretsManagerStorage(this, {
role,
secretsManagerPath: props.secretsManagerPath || `/certbot/certificates/${props.letsencryptDomains.split(',')[0]}/`,
secretsManagerPath:
props.secretsManagerPath ||
`/certbot/certificates/${props.letsencryptDomains.split(",")[0]}/`,
kmsKeyAlias: props.kmsKeyAlias,
});
};
}

if (props.certificateStorage == CertificateStorageType.SSM_SECURE) {
this.handler.addEnvironment('CERTIFICATE_STORAGE', 'ssm_secure');
this.handler.addEnvironment('CERTIFICATE_PARAMETER_PATH', props.ssmSecurePath || `/certbot/certificates/${props.letsencryptDomains.split(',')[0]}/`);
this.handler.addEnvironment("CERTIFICATE_STORAGE", "ssm_secure");
this.handler.addEnvironment(
"CERTIFICATE_PARAMETER_PATH",
props.ssmSecurePath ||
`/certbot/certificates/${props.letsencryptDomains.split(",")[0]}/`
);
if (props.kmsKeyAlias) {
this.handler.addEnvironment('CUSTOM_KMS_KEY_ID', props.kmsKeyAlias);
this.handler.addEnvironment("CUSTOM_KMS_KEY_ID", props.kmsKeyAlias);
}
configureSSMStorage(this, {
role,
parameterStorePath: props.ssmSecurePath || `/certbot/certificates/${props.letsencryptDomains.split(',')[0]}/`,
parameterStorePath:
props.ssmSecurePath ||
`/certbot/certificates/${props.letsencryptDomains.split(",")[0]}/`,
kmsKeyAlias: props.kmsKeyAlias,
});
}

if (props.certificateStorage == CertificateStorageType.EFS) {
if (!props.efsAccessPoint) {
throw new Error('You must provide an EFS Access Point to use EFS storage');
throw new Error(
"You must provide an EFS Access Point to use EFS storage"
);
} else {
this.handler.addEnvironment('CERTIFICATE_STORAGE', 'efs');
this.handler.addEnvironment('EFS_PATH', '/mnt/efs');
this.handler.addEnvironment("CERTIFICATE_STORAGE", "efs");
this.handler.addEnvironment("EFS_PATH", "/mnt/efs");
}
}

if (props.vpc) {
role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaVPCAccessExecutionRole'));
role.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName(
"service-role/AWSLambdaVPCAccessExecutionRole"
)
);
}

if (enableInsights) {
role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLambdaInsightsExecutionRolePolicy'));
this.handler.addLayers(lambda.LayerVersion.fromLayerVersionArn(this, 'insightsLayer', insightsARN));
role.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName(
"CloudWatchLambdaInsightsExecutionRolePolicy"
)
);
this.handler.addLayers(
lambda.LayerVersion.fromLayerVersionArn(
this,
"insightsLayer",
insightsARN
)
);
}

// Add function triggers
new events.Rule(this, 'trigger', {
schedule: props.schedule || events.Schedule.cron({ minute: '0', hour: '0', weekDay: '1' }),
new events.Rule(this, "trigger", {
schedule:
props.schedule ||
events.Schedule.cron({ minute: "0", hour: "0", weekDay: "1" }),
targets: [new targets.LambdaFunction(this.handler)],
});

if (runOnDeploy) {
new events.Rule(this, 'triggerImmediate', {
schedule: new oneTimeEvents.OnDeploy(this, 'schedule', {
new events.Rule(this, "triggerImmediate", {
schedule: new oneTimeEvents.OnDeploy(this, "schedule", {
offsetMinutes: props.runOnDeployWaitMinutes || 10,
}).schedule,
targets: [new targets.LambdaFunction(this.handler)],
Expand Down

0 comments on commit 32996ca

Please sign in to comment.