diff --git a/nx-monorepo/.gitignore b/nx-monorepo/.gitignore new file mode 100644 index 000000000..9b6edcb5a --- /dev/null +++ b/nx-monorepo/.gitignore @@ -0,0 +1,3 @@ +.nx/cache +generated-website +dist \ No newline at end of file diff --git a/nx-monorepo/README.md b/nx-monorepo/README.md new file mode 100644 index 000000000..6f7c9b2db --- /dev/null +++ b/nx-monorepo/README.md @@ -0,0 +1,195 @@ +# Nx Monorepo + +This example shows how to use Nx to organize a mono repo and track dependencies. + +The example consists of the following components: + +``` + - packages: + - s3folder: ComponentResource that manages a S3 bucket + - website-deploy: ComponentResource resource that manages files in a S3 bucket + - website-builder: Mock website generator that creates HTML output + - infra: Pulumi program that uses the s3folder and website ComponentResources to deploy a website +``` + +To deploy the latest version of the website, we need to respect the following dependencies: + +- website-builder needs to be compiled before we can use it to generate the HTML output. +- s3folder and website-deploy need to be compiled before we can build infra. +- We need to generate HTML output before we can deploy the infra. +- infra needs to be compiled before we can deploy. + +These dependecies can be defined using Nx, for example in [infra/package.json](./infra//package.json) we declare that the `deploy` needs its dependencies to be built, and the HTML to generated: + +``` + ... + "nx": { + "targets": { + "deploy": { + "dependsOn": [ + "build", + "website-builder:generate" + ] + } + } + } +``` + +Nx can visualize the dependencies for us using `npx nx deploy infra --graph` + +![Dependency Graph](./dependency-graph.png) + +## Deploying + +### Prerequisites + +1. [Install Pulumi](https://www.pulumi.com/docs/get-started/install/) +2. [Configure AWS Credentials](https://www.pulumi.com/docs/intro/cloud-providers/aws/setup/) + +### Steps + +Since Nx manages the interdependencies, all we have to do is to install our node dependencies + +```bash +npm install +``` + +and then run nx: + +```bash +npx nx deploy infra +``` + +``` + ✔ 4/4 dependent project tasks succeeded [0 read from cache] + + Hint: you can run the command with --verbose to see the full dependent project outputs + +——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— + + +> nx run infra:build + + +> infra@1.0.0 build +> tsc + + +> nx run infra:deploy + + +> infra@1.0.0 deploy +> pulumi up --stack dev + +The stack 'dev' does not exist. + +If you would like to create this stack now, please press , otherwise press ^C: +Created stack 'dev' +Previewing update (dev) + +View in Browser (Ctrl+O): https://app.pulumi.com/julienp/nx-monorepo/dev/previews/fc7630fd-7dc4-4c7e-baa0-3d6e014fc90a + + Type Name Plan + + pulumi:pulumi:Stack nx-monorepo-dev create + + ├─ pulumi:examples:WebsiteDeploy my-website create + + │ └─ aws:s3:BucketObject index.html create + + └─ pulumi:examples:S3Folder my-folder create + + ├─ aws:s3:Bucket my-folder create + + ├─ aws:s3:BucketPublicAccessBlock public-access-block create + + └─ aws:s3:BucketPolicy bucketPolicy create + +Outputs: + websiteUrl: output + +Resources: + + 7 to create + +Do you want to perform this update? yes +Updating (dev) + +View in Browser (Ctrl+O): https://app.pulumi.com/julienp/nx-monorepo/dev/updates/1 + + Type Name Status + + pulumi:pulumi:Stack nx-monorepo-dev created (6s) + + ├─ pulumi:examples:S3Folder my-folder created (5s) + + │ ├─ aws:s3:Bucket my-folder created (1s) + + │ ├─ aws:s3:BucketPublicAccessBlock public-access-block created (0.76s) + + │ └─ aws:s3:BucketPolicy bucketPolicy created (0.85s) + + └─ pulumi:examples:WebsiteDeploy my-website created (2s) + + └─ aws:s3:BucketObject index.html created (0.80s) + +Outputs: + websiteUrl: "my-folder-a64ab3c.s3-website.eu-central-1.amazonaws.com" + +Resources: + + 7 created + +Duration: 9s + + +——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— + + NX Successfully ran target deploy for project infra and 5 tasks it depends on (42s) +``` + +To destroy the stack, we run: + +``` +npx nx destroy infra +``` + +``` +> nx run infra:destroy + + +> infra@1.0.0 destroy +> pulumi destroy --stack dev + +Previewing destroy (dev) + +View in Browser (Ctrl+O): https://app.pulumi.com/julienp/nx-monorepo/dev/previews/b640cce7-a9df-49a5-b004-3fbdbe65c4eb + + Type Name Plan + - pulumi:pulumi:Stack nx-monorepo-dev delete + - ├─ pulumi:examples:S3Folder my-folder delete + - │ ├─ aws:s3:BucketPolicy bucketPolicy delete + - │ ├─ aws:s3:BucketPublicAccessBlock public-access-block delete + - │ └─ aws:s3:Bucket my-folder delete + - └─ pulumi:examples:WebsiteDeploy my-website delete + - └─ aws:s3:BucketObject index.html delete + +Outputs: + - websiteUrl: "my-folder-a64ab3c.s3-website.eu-central-1.amazonaws.com" + +Resources: + - 7 to delete + +Do you want to perform this destroy? yes +Destroying (dev) + +View in Browser (Ctrl+O): https://app.pulumi.com/julienp/nx-monorepo/dev/updates/2 + + Type Name Status + - pulumi:pulumi:Stack nx-monorepo-dev deleted (0.31s) + - ├─ pulumi:examples:WebsiteDeploy my-website deleted (0.60s) + - │ └─ aws:s3:BucketObject index.html deleted (0.88s) + - └─ pulumi:examples:S3Folder my-folder deleted (0.82s) + - ├─ aws:s3:BucketPolicy bucketPolicy deleted (0.96s) + - ├─ aws:s3:BucketPublicAccessBlock public-access-block deleted (0.91s) + - └─ aws:s3:Bucket my-folder deleted (0.74s) + +Outputs: + - websiteUrl: "my-folder-a64ab3c.s3-website.eu-central-1.amazonaws.com" + +Resources: + - 7 deleted + +Duration: 7s + +The resources in the stack have been deleted, but the history and configuration associated with the stack are still maintained. +If you want to remove the stack completely, run `pulumi stack rm dev`. + +——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— + + NX Successfully ran target destroy for project infra (18s) +``` diff --git a/nx-monorepo/dependency-graph.png b/nx-monorepo/dependency-graph.png new file mode 100644 index 000000000..3ee9dbfeb Binary files /dev/null and b/nx-monorepo/dependency-graph.png differ diff --git a/nx-monorepo/infra/Pulumi.yaml b/nx-monorepo/infra/Pulumi.yaml new file mode 100644 index 000000000..fc8393afe --- /dev/null +++ b/nx-monorepo/infra/Pulumi.yaml @@ -0,0 +1,7 @@ +name: nx-monorepo +description: A project using an Nx monorepo. +main: dist/index.js +runtime: + name: nodejs + options: + typescript: false \ No newline at end of file diff --git a/nx-monorepo/infra/index.ts b/nx-monorepo/infra/index.ts new file mode 100644 index 000000000..a8191a63a --- /dev/null +++ b/nx-monorepo/infra/index.ts @@ -0,0 +1,11 @@ +import * as path from "path" +import * as s3folder from "s3folder" +import * as websiteDeploy from "website-deploy" + +// Create the folder to hold our website files +const folder = new s3folder.S3Folder("my-folder", {}) +export const websiteUrl = folder.websiteUrl + +// Deploy the website to the folder +const generatedWebsite = path.join("..", "..", "generated-website") +const website = new websiteDeploy.WebsiteDeploy("my-website", folder.bucket, generatedWebsite, {}) diff --git a/nx-monorepo/infra/package.json b/nx-monorepo/infra/package.json new file mode 100644 index 000000000..ca125c1fb --- /dev/null +++ b/nx-monorepo/infra/package.json @@ -0,0 +1,26 @@ +{ + "name": "infra", + "main": "dist/index.js", + "version": "1.0.0", + "dependencies": { + "@pulumi/pulumi": "latest", + "s3folder": "*", + "website-deploy": "*", + "website-builder": "*" + }, + "scripts": { + "build": "tsc", + "deploy": "pulumi up --stack dev", + "destroy": "pulumi destroy --stack dev" + }, + "nx": { + "targets": { + "deploy": { + "dependsOn": [ + "build", + "website-builder:generate" + ] + } + } + } +} \ No newline at end of file diff --git a/nx-monorepo/infra/tsconfig.json b/nx-monorepo/infra/tsconfig.json new file mode 100644 index 000000000..49c207c93 --- /dev/null +++ b/nx-monorepo/infra/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + } +} diff --git a/nx-monorepo/nx.json b/nx-monorepo/nx.json new file mode 100644 index 000000000..80a8b51ec --- /dev/null +++ b/nx-monorepo/nx.json @@ -0,0 +1,12 @@ +{ + "extends": "nx/presets/npm.json", + "$schema": "./node_modules/nx/schemas/nx-schema.json", + "targetDefaults": { + "build": { + "cache": true, + "dependsOn": [ + "^build" + ] + } + } +} \ No newline at end of file diff --git a/nx-monorepo/package.json b/nx-monorepo/package.json new file mode 100644 index 000000000..9ceabfa2b --- /dev/null +++ b/nx-monorepo/package.json @@ -0,0 +1,15 @@ +{ + "name": "nx-repo", + "version": "1.0.0", + "scripts": {}, + "private": true, + "devDependencies": { + "@nx/js": "18.0.5", + "nx": "18.0.5", + "typescript": "^5.3.3" + }, + "workspaces": [ + "packages/*", + "infra" + ] +} \ No newline at end of file diff --git a/nx-monorepo/packages/s3folder/index.ts b/nx-monorepo/packages/s3folder/index.ts new file mode 100644 index 000000000..6a736fd8b --- /dev/null +++ b/nx-monorepo/packages/s3folder/index.ts @@ -0,0 +1,54 @@ +import * as aws from "@pulumi/aws"; +import * as pulumi from "@pulumi/pulumi"; + +export class S3Folder extends pulumi.ComponentResource { + readonly bucket: pulumi.Output; + readonly websiteUrl: pulumi.Output; + + constructor(bucketName: string, opts: pulumi.ComponentResourceOptions) { + super("pulumi:examples:S3Folder", bucketName, {}, opts); + + // Create a bucket and expose a website index document + let siteBucket = new aws.s3.Bucket(bucketName, { + website: { + indexDocument: "index.html", + }, + }, { parent: this }); // specify resource parent + + const publicAccessBlock = new aws.s3.BucketPublicAccessBlock("public-access-block", { + bucket: siteBucket.id, + blockPublicAcls: false, + }, { parent: this }); + + // Set the access policy for the bucket so all objects are readable + let bucketPolicy = new aws.s3.BucketPolicy("bucketPolicy", { + bucket: siteBucket.bucket, + policy: siteBucket.bucket.apply(this.publicReadPolicyForBucket), + }, { parent: this, dependsOn: publicAccessBlock }); // specify resource parent + + this.bucket = pulumi.output(siteBucket); + this.websiteUrl = siteBucket.websiteEndpoint; + + // Register output properties for this component + this.registerOutputs({ + bucket: this.bucket, + websiteUrl: this.websiteUrl, + }); + } + + publicReadPolicyForBucket(bucketName: string) { + return JSON.stringify({ + Version: "2012-10-17", + Statement: [{ + Effect: "Allow", + Principal: "*", + Action: [ + "s3:GetObject" + ], + Resource: [ + `arn:aws:s3:::${bucketName}/*` + ] + }] + }); + } +} diff --git a/nx-monorepo/packages/s3folder/package.json b/nx-monorepo/packages/s3folder/package.json new file mode 100644 index 000000000..890e9109e --- /dev/null +++ b/nx-monorepo/packages/s3folder/package.json @@ -0,0 +1,12 @@ +{ + "name": "s3folder", + "main": "dist/index.js", + "version": "1.0.0", + "dependencies": { + "@pulumi/pulumi": "latest", + "@pulumi/aws": "^6.23.0" + }, + "scripts": { + "build": "tsc" + } +} \ No newline at end of file diff --git a/nx-monorepo/packages/s3folder/tsconfig.json b/nx-monorepo/packages/s3folder/tsconfig.json new file mode 100644 index 000000000..b4e69ae1f --- /dev/null +++ b/nx-monorepo/packages/s3folder/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + } +} diff --git a/nx-monorepo/packages/website-builder/index.ts b/nx-monorepo/packages/website-builder/index.ts new file mode 100644 index 000000000..edf06f76e --- /dev/null +++ b/nx-monorepo/packages/website-builder/index.ts @@ -0,0 +1,9 @@ + +import * as fs from 'fs'; +import * as path from "path"; + +const outDir = path.join("..", "..", "generated-website"); +const indexPath = path.join(outDir, "index.html") + +fs.mkdirSync(outDir, { recursive: true }) +fs.writeFileSync(indexPath, `Hello, world! ${new Date().toISOString()}`) \ No newline at end of file diff --git a/nx-monorepo/packages/website-builder/package.json b/nx-monorepo/packages/website-builder/package.json new file mode 100644 index 000000000..0b7d168d0 --- /dev/null +++ b/nx-monorepo/packages/website-builder/package.json @@ -0,0 +1,22 @@ +{ + "name": "website-builder", + "main": "dist/index.js", + "version": "1.0.0", + "dependencies": {}, + "scripts": { + "build": "tsc", + "generate": "node ./dist/index.js" + }, + "nx": { + "targets": { + "generate": { + "outputs": [ + "{workspaceRoot}/generated-website" + ], + "dependsOn": [ + "website-builder:build" + ] + } + } + } +} \ No newline at end of file diff --git a/nx-monorepo/packages/website-builder/tsconfig.json b/nx-monorepo/packages/website-builder/tsconfig.json new file mode 100644 index 000000000..b4e69ae1f --- /dev/null +++ b/nx-monorepo/packages/website-builder/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + } +} diff --git a/nx-monorepo/packages/website-deploy/index.ts b/nx-monorepo/packages/website-deploy/index.ts new file mode 100644 index 000000000..0dcd0d6a5 --- /dev/null +++ b/nx-monorepo/packages/website-deploy/index.ts @@ -0,0 +1,19 @@ +import * as aws from "@pulumi/aws"; +import * as pulumi from "@pulumi/pulumi"; +import * as mime from "mime"; + +export class WebsiteDeploy extends pulumi.ComponentResource { + constructor(name: string, bucket: pulumi.Input, path: string, opts: pulumi.ComponentResourceOptions) { + super("pulumi:examples:WebsiteDeploy", name, {}, opts); + + // For each file in the directory, create an S3 object stored in `bucket` + for (let item of require("fs").readdirSync(path)) { + let filePath = require("path").join(path, item); + let object = new aws.s3.BucketObject(item, { + bucket: bucket, + source: new pulumi.asset.FileAsset(filePath), // use FileAsset to point to a file + contentType: mime.getType(filePath) || undefined, // set the MIME type of the file + }, { parent: this }); // specify resource parent + } + } +} diff --git a/nx-monorepo/packages/website-deploy/package.json b/nx-monorepo/packages/website-deploy/package.json new file mode 100644 index 000000000..74a85d2aa --- /dev/null +++ b/nx-monorepo/packages/website-deploy/package.json @@ -0,0 +1,16 @@ +{ + "name": "website-deploy", + "main": "dist/index.js", + "version": "1.0.0", + "dependencies": { + "@pulumi/aws": "^6.23.0", + "@pulumi/pulumi": "latest", + "mime": "^2.6.0" + }, + "scripts": { + "build": "tsc" + }, + "devDependencies": { + "@types/mime": "^3.0.4" + } +} \ No newline at end of file diff --git a/nx-monorepo/packages/website-deploy/tsconfig.json b/nx-monorepo/packages/website-deploy/tsconfig.json new file mode 100644 index 000000000..b4e69ae1f --- /dev/null +++ b/nx-monorepo/packages/website-deploy/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + } +} diff --git a/nx-monorepo/tsconfig.json b/nx-monorepo/tsconfig.json new file mode 100644 index 000000000..b95b11bbf --- /dev/null +++ b/nx-monorepo/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "strict": true, + "target": "es6", + "module": "commonjs", + "moduleResolution": "node", + "sourceMap": true, + "experimentalDecorators": true, + "pretty": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "forceConsistentCasingInFileNames": true + } +}