@paulbarmstrong/cdk-managed-objects-bucket
Version:
A CDK construct representing a bucket and the objects within it, which can be defined by an Asset or directly in the CDK. It extends the Bucket construct.
211 lines • 9.06 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ManagedObjectsBucket = exports.ObjectChangeAction = void 0;
const child_process_1 = require("child_process");
const path_1 = __importDefault(require("path"));
const fs_1 = require("fs");
const cdk = __importStar(require("aws-cdk-lib"));
const s3 = __importStar(require("aws-cdk-lib/aws-s3"));
const lambda = __importStar(require("aws-cdk-lib/aws-lambda"));
const iam = __importStar(require("aws-cdk-lib/aws-iam"));
/**
* An action to be performed when changes are made to the objects in the bucket.
*/
class ObjectChangeAction {
/** @hidden */
#classname = "ObjectChangeAction";
/** ObjectChangeAction for performing a CloudFront invalidation after objects
* in the bucket have changed. */
static cloudFrontInvalidation(props) {
return new CloudFrontInvalidationObjectChangeAction(props);
}
}
exports.ObjectChangeAction = ObjectChangeAction;
/** ObjectChangeAction for performing an invalidation on a CloudFront distribution after objects
* in the bucket have changed. */
class CloudFrontInvalidationObjectChangeAction extends ObjectChangeAction {
distribution;
waitForCompletion;
constructor(props) {
super();
this.distribution = props.distribution;
this.waitForCompletion = props.waitForCompletion;
}
}
/**
* An S3 Bucket where the objects in the bucket are completely managed by CDK. A
* `Custom::ManagedBucketObjects` CFN resource internal to the ManagedObjectsBucket construct
* mutates objects in the bucket to align the bucket with the objects defined in the CDK
* definition. The objects in the bucket are otherwise read-only. Objects are added by calling
* the `addObject` and `addObjectsFromAsset` methods.
*
* ManagedObjectsBucket extends [Bucket](
* https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3.Bucket.html). All props from
* Bucket are allowed except:
*
* 1. `removalPolicy` and `autoDeleteObjects` are not configurable. ManagedObjectsBuckets are
* always emptied and destroyed on removal.
*
* In the event of ManagedObjectsBucket's `Custom::ManagedBucketObjects` custom resource having
* a persistent problem, you can unblock your stack by adding the following environment variable
* to the ObjectManagerFunction lambda function also inside the stack:
*
* Key: `SKIP`, Value: `true`
*/
class ManagedObjectsBucket extends s3.Bucket {
/** @hidden */
#inlineBucketObjects;
/** @hidden */
#assets;
/** @hidden */
#ObjectChangeActions;
/** @hidden */
#handlerRole;
constructor(scope, id, props) {
super(scope, id, {
removalPolicy: cdk.RemovalPolicy.DESTROY,
...props
});
this.#inlineBucketObjects = [];
this.#assets = [];
this.#ObjectChangeActions = [];
const codePackagePath = path_1.default.join(__dirname, "..", "..", "handler");
if (!(0, fs_1.readdirSync)(codePackagePath).includes("node_modules")) {
(0, child_process_1.execSync)("npm install", { cwd: codePackagePath });
}
this.#handlerRole = new iam.Role(this, "ObjectManagerRole", {
managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")],
assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com")
});
this.#handlerRole.addToPolicy(new iam.PolicyStatement({
actions: [
"s3:ListBucket",
"s3:PutObject",
"s3:PutObjectAcl",
"s3:DeleteObject"
],
resources: [this.bucketArn, `${this.bucketArn}/*`]
}));
this.addToResourcePolicy(new iam.PolicyStatement({
principals: [new iam.StarPrincipal()],
effect: iam.Effect.DENY,
actions: ["s3:PutObject", "s3:DeleteObject"],
resources: [`${this.bucketArn}/*`],
conditions: {
StringNotLike: {
"aws:userId": `${this.#handlerRole.roleId}:*`
}
}
}));
const handler = new lambda.Function(this, "ObjectManagerFunction", {
runtime: lambda.Runtime.NODEJS_24_X,
role: this.#handlerRole,
code: lambda.Code.fromAsset(codePackagePath),
handler: "index.handler",
timeout: cdk.Duration.seconds(900),
ephemeralStorageSize: cdk.Size.mebibytes(10240),
logGroup: props.objectManagerLogGroup
});
new cdk.CustomResource(this, "Objects", {
resourceType: "Custom::ManagedBucketObjects",
serviceToken: handler.functionArn,
properties: {
props: cdk.Lazy.any({
produce: () => ({
bucketUrl: `s3://${this.bucketName}`,
assets: this.#assets.map(asset => ({
hash: asset.assetHash,
s3BucketName: asset.s3BucketName,
s3ObjectKey: asset.s3ObjectKey
})),
objects: this.#inlineBucketObjects,
invalidationActions: this.#ObjectChangeActions
.filter(action => action.distribution !== undefined)
.map(action => ({
distributionId: action.distribution.distributionId,
waitForCompletion: action.waitForCompletion
}))
})
})
}
});
}
/**
* Add an object to the bucket based on a given key and body. Deploy-time values from the CDK
* like resource ARNs can be used here.
*/
addObject(props) {
if (this.#inlineBucketObjects.find(x => x.key === props.key)) {
throw new Error(`Cannot add object with duplicate key ${props.key} to ${this.node.id}.`);
}
this.#inlineBucketObjects.push(props);
}
/** Add objects to the bucket based on an [Asset](
* https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3_assets-readme.html).
* For example:
*
* `bucket.addObjectsFromAsset({ asset: new Asset(this, "MyAsset", { path: "./my-local-files" }) })`
*/
addObjectsFromAsset(props) {
if (this.#assets.find(x => x.assetHash === props.asset.assetHash)) {
throw new Error(`Cannot add objects from asset ${props.asset.assetHash} to ${this.node.id} twice.`);
}
this.#assets.push(props.asset);
this.#handlerRole.addToPolicy(new iam.PolicyStatement({
actions: ["s3:GetObject"],
resources: [`${props.asset.bucket.bucketArn}/*`]
}));
}
/** Add an action to be performed when objects in the bucket are changed. */
addObjectChangeAction(action) {
this.#ObjectChangeActions.push(action);
if (action.distribution !== undefined) {
this.#handlerRole.addToPolicy(new iam.PolicyStatement({
actions: ["cloudfront:CreateInvalidation", "cloudfront:GetInvalidation"],
resources: [getDistributionArn(action.distribution)]
}));
}
}
}
exports.ManagedObjectsBucket = ManagedObjectsBucket;
/** @hidden */
function getDistributionArn(distribution) {
return `arn:aws:cloudfront::${distribution.stack.account}:distribution/${distribution.distributionId}`;
}
//# sourceMappingURL=managed-objects-bucket.js.map