From 4ee90103582ea6d45766f8b240c1b727ec403fd8 Mon Sep 17 00:00:00 2001 From: Danny Steenman Date: Tue, 14 Jan 2025 15:08:07 +0100 Subject: [PATCH 1/3] refactor(stacks): rename and restructure project stacks and helpers --- src/bin/env-helper.ts | 10 ++-- src/constructs/base-construct.ts | 6 +-- src/constructs/network-construct.ts | 20 ++++---- src/main.ts | 39 +++++++-------- src/stacks/README.md | 74 +++++++++++++++++++++-------- src/stacks/base-stack.ts | 22 --------- src/stacks/foundation-stack.ts | 72 ++++++++++++++++++++++++++++ src/stacks/github-oidc-stack.ts | 50 ------------------- src/stacks/index.ts | 5 +- src/stacks/starter-stack.ts | 66 +++++++++++++++++++++++++ src/stacks/toolkit-cleaner-stack.ts | 11 ----- 11 files changed, 230 insertions(+), 145 deletions(-) delete mode 100644 src/stacks/base-stack.ts create mode 100644 src/stacks/foundation-stack.ts delete mode 100644 src/stacks/github-oidc-stack.ts create mode 100644 src/stacks/starter-stack.ts delete mode 100644 src/stacks/toolkit-cleaner-stack.ts diff --git a/src/bin/env-helper.ts b/src/bin/env-helper.ts index 28b21a0..ccee0c1 100644 --- a/src/bin/env-helper.ts +++ b/src/bin/env-helper.ts @@ -86,14 +86,14 @@ export function extractCleanedBranchName(gitBranchRef: string | undefined): stri } /** - * Generates a unique resource name that includes a branch suffix for branch-based deployments, - * or an environment suffix when no branch is specified. - * The generated name has a maximum length of 64 characters. + * Creates an environment-aware resource name with a branch or environment suffix. + * The name is truncated to a maximum of 64 characters to comply with AWS naming constraints. + * * @param baseName - The base name for the resource. - * @returns A unique resource name with a branch or environment suffix. + * @returns A resource name with an environment or branch suffix. * @throws Error if GIT_BRANCH_REF is "main". */ -export function generateUniqueResourceName(baseName: string): string { +export function createEnvResourceName(baseName: string): string { const branchName = process.env.GIT_BRANCH_REF; const environment = process.env.ENVIRONMENT || 'dev'; diff --git a/src/constructs/base-construct.ts b/src/constructs/base-construct.ts index f8faaea..5408868 100644 --- a/src/constructs/base-construct.ts +++ b/src/constructs/base-construct.ts @@ -1,6 +1,6 @@ import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; -import { extractCleanedBranchName, generateUniqueResourceName } from '../bin/env-helper'; +import { createEnvResourceName, extractCleanedBranchName } from '../bin/env-helper'; export class BaseConstruct extends Construct { /** @@ -32,8 +32,8 @@ export class BaseConstruct extends Construct { super(scope, id); this.branch = extractCleanedBranchName(process.env.GIT_BRANCH_REF); - this.environment = process.env.ENVIRONMENT || 'dev'; this.account = cdk.Stack.of(this).account; + this.environment = process.env.ENVIRONMENT || 'dev'; this.region = cdk.Stack.of(this).region; } @@ -44,6 +44,6 @@ export class BaseConstruct extends Construct { * @returns A unique resource name with an optional branch suffix. */ unique(name: string): string { - return generateUniqueResourceName(name); + return createEnvResourceName(name); } } diff --git a/src/constructs/network-construct.ts b/src/constructs/network-construct.ts index 0de863a..a76b437 100644 --- a/src/constructs/network-construct.ts +++ b/src/constructs/network-construct.ts @@ -1,4 +1,4 @@ -import { CfnOutput, RemovalPolicy } from 'aws-cdk-lib'; +import { RemovalPolicy } from 'aws-cdk-lib'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as s3 from 'aws-cdk-lib/aws-s3'; import type { Construct } from 'constructs'; @@ -19,9 +19,9 @@ export class NetworkConstruct extends BaseConstruct { this.environment === 'production' ? {} : { - autoDeleteObjects: true, - removalPolicy: RemovalPolicy.DESTROY, - }; + autoDeleteObjects: true, + removalPolicy: RemovalPolicy.DESTROY, + }; // Create a VPC with 9 subnets divided over 3 AZ's (3 public, 3 private, 3 isolated) this.vpc = new ec2.Vpc(this, 'Vpc', { @@ -31,6 +31,14 @@ export class NetworkConstruct extends BaseConstruct { ), natGateways: this.environment === 'production' ? 3 : 1, maxAzs: 3, + gatewayEndpoints: { + s3Endpoint: { + service: ec2.GatewayVpcEndpointAwsService.S3, + }, + dynamoDBEndpoint: { + service: ec2.GatewayVpcEndpointAwsService.DYNAMODB, + }, + }, flowLogs: { s3: { destination: ec2.FlowLogDestination.toS3( @@ -63,9 +71,5 @@ export class NetworkConstruct extends BaseConstruct { }, ], }); - - new CfnOutput(this, 'VpcId', { - value: this.vpc.vpcId, - }); } } diff --git a/src/main.ts b/src/main.ts index db3d1c2..03f4009 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,10 +1,10 @@ import * as cdk from 'aws-cdk-lib'; -import { generateUniqueResourceName } from './bin/env-helper'; -import { BaseStack, GitHubOIDCStack, ToolkitCleanerStack } from './stacks'; +import { createEnvResourceName } from './bin/env-helper'; +import { FoundationStack, StarterStack } from './stacks'; // Inherit environment variables from npm run commands (displayed in .projen/tasks.json) const environment = process.env.ENVIRONMENT || 'dev'; -const awsEnvironment = { +const awsAccountConfig = { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, }; @@ -13,32 +13,27 @@ const awsEnvironment = { const app = new cdk.App(); /** - * These stacks should only be deployed a single time for each AWS account, - * and never deployed via branch-based deployments. + * Conditional deployment of the FoundationStack. + * + * This section of code is responsible for deploying the FoundationStack, which sets up + * crucial infrastructure components like GitHub OpenID Connect (OIDC) support and IAM roles + * for GitHub Actions deployments. + * + * @remarks + * - This stack should be deployed only once per AWS account. + * - It should not be part of branch-based deployments. + * - The deployment is conditional, based on the absence of the GIT_BRANCH_REF environment variable. */ if (!process.env.GIT_BRANCH_REF) { - /** - * The ToolkitCleaner construct creates a state machine that runs every day - * and removes unused S3 and ECR assets from your CDK Toolkit. - * The state machine outputs the number of deleted assets and the total reclaimed size in bytes. - */ - new ToolkitCleanerStack(app, generateUniqueResourceName('ToolkitCleanerStack'), { - env: awsEnvironment, - }); - /** - * Add GitHub OpenID Connect support and create an IAM role for GitHub Actions deployments. - * This stack is only created once per AWS environment, as the GitHub OIDC provider and - * deployment role can be reused across all deployments for that environment. - */ - new GitHubOIDCStack(app, generateUniqueResourceName('GitHubOIDCStack'), { - env: awsEnvironment, + new FoundationStack(app, createEnvResourceName('FoundationStack'), { + env: awsAccountConfig, environment: environment, }); } // Create a new stack with your resources -new BaseStack(app, generateUniqueResourceName('BaseStack'), { - env: awsEnvironment, +new StarterStack(app, createEnvResourceName('StarterStack'), { + env: awsAccountConfig, environment: environment, }); diff --git a/src/stacks/README.md b/src/stacks/README.md index 78d11a6..e99307e 100644 --- a/src/stacks/README.md +++ b/src/stacks/README.md @@ -1,69 +1,101 @@ -# AWS CDK Stacks: BaseStack and GitHubOIDCStack +# AWS CDK Stacks: StarterStack and FoundationStack -This documentation details the structure and functionality of two pivotal stacks within our AWS CDK TypeScript project: `BaseStack` and `GitHubOIDCStack`. These stacks lay the groundwork for deploying AWS resources with specific configurations and capabilities, tailored to different deployment stages and integration with GitHub Actions for CI/CD processes. +This documentation details the structure and functionality of three pivotal stacks within our AWS CDK TypeScript project: `StarterStack` and `FoundationStack`. These stacks lay the groundwork for deploying AWS resources with specific configurations and capabilities, tailored to different deployment stages and integration with GitHub Actions for CI/CD processes. -## BaseStack +## StarterStack -The `BaseStack` serves as a foundational stack that you can use to start instantiation your custom and cdk-lib constructs. +The `StarterStack` serves as a foundation for building your AWS infrastructure using CDK. It provides a starting point for adding and organizing your AWS resources and constructs. ### Properties -- `environment`: Optional. Specifies the deployment stage (e.g., `dev`, `test`, `staging`, `production`). It's crucial for tailoring the stack configuration to the target environment. +- `environment`: Optional. Specifies the deployment stage (e.g., `dev`, `test`, `staging`, `production`). If not provided, an error will be thrown. + +### Features + +- Includes a `NetworkConstruct` that creates a secure VPC. +- Designed to be easily extendable with additional AWS resources and constructs. + +### Usage + +The `StarterStack` is intended to be customized and extended based on your specific infrastructure needs. You can add new constructs and resources within the constructor of this class. ### Example Usage ```typescript import * as cdk from 'aws-cdk-lib'; -import { BaseStack } from './stacks'; +import { StarterStack } from './stacks'; -// Inherit environment variables from npm run commands (displayed in .projen/tasks.json) const environment = process.env.ENVIRONMENT || 'dev'; const awsEnvironment = { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, }; -// Instantiate the CDK app const app = new cdk.App(); -// Create a new stack with your resources -new BaseStack(app, `BaseStack-${environment}`, { +new StarterStack(app, `StarterStack-${environment}`, { env: awsEnvironment, environment: environment, }); ``` -## GitHubOIDCStack +### Extending the Stack -The GitHubOIDCStack is designed to facilitate secure CI/CD workflows by integrating AWS resources with GitHub Actions via OpenID Connect (OIDC). This allows for a more secure and streamlined deployment process directly from GitHub Actions. +To add new resources to the `StarterStack`, you can modify the constructor. For example, to add a new S3 bucket: + +```typescript +import * as s3 from 'aws-cdk-lib/aws-s3'; + +// In the constructor: +new s3.Bucket(this, 'MyBucket', { + bucketName: 'my-unique-bucket-name', + versioned: true, + encryption: s3.BucketEncryption.S3_MANAGED, +}); +``` + +### Best Practices + +- Organize your constructs logically within this stack. +- For complex infrastructures, consider creating additional stacks for different components. +- Remember to import necessary modules at the top of the file. +- Customize and extend the stack based on your specific infrastructure requirements. + +The `StarterStack` provides a flexible foundation for your AWS CDK project, allowing you to incrementally build and organize your infrastructure as code. + + +## FoundationStack + +The `FoundationStack` sets up fundamental infrastructure components for AWS deployments via GitHub Actions. It combines the functionality of the GitHubOIDCStack with additional features. ### Features -- GitHub Actions OIDC Provider: Sets up the OIDC provider for GitHub Actions within the AWS account, enabling trust relationships between GitHub and AWS. -- GitHub Deploy Role: Creates an IAM role with AdministratorAccess managed policy. This role is assumable by GitHub Actions workflows, granting them the permissions needed to deploy resources. +- GitHub Actions OIDC Provider: Sets up the OIDC provider for GitHub Actions within the AWS account. +- GitHub Deploy Role: Creates an IAM role with AdministratorAccess managed policy, assumable by GitHub Actions workflows. +- CDK Toolkit Cleaner: Implements a ToolkitCleaner for managing CDK toolkit resources. -### Configuration +### Properties -The stack automatically retrieves GitHub repository details (owner and repository name) using a helper function, ensuring that the OIDC provider and IAM roles are correctly configured for the specific GitHub repository. +- `environment`: Required. Specifies the deployment stage (e.g., `dev`, `test`, `staging`, `production`). ### Example Usage ```typescript import * as cdk from 'aws-cdk-lib'; -import { GitHubOIDCStack } from './stacks'; +import { FoundationStack } from './stacks'; -// Inherit environment variables from npm run commands (displayed in .projen/tasks.json) const environment = process.env.ENVIRONMENT || 'dev'; const awsEnvironment = { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, }; -// Instantiate the CDK app const app = new cdk.App(); -// Add GitHub OpenID Connect support and create an IAM role for GitHub -new GitHubOIDCStack(app, `GitHubOIDCStack-${environment}`, { +new FoundationStack(app, `FoundationStack-${environment}`, { env: awsEnvironment, + environment: environment, }); ``` + +The `FoundationStack` provides a more comprehensive setup for GitHub Actions integration and CDK resource management, making it suitable for projects that require both OIDC-based deployments and CDK toolkit cleaning. diff --git a/src/stacks/base-stack.ts b/src/stacks/base-stack.ts deleted file mode 100644 index 28d0627..0000000 --- a/src/stacks/base-stack.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as cdk from 'aws-cdk-lib'; -import type { Construct } from 'constructs'; -// import { NetworkConstruct } from '../constructs'; - -export interface BaseStackProps extends cdk.StackProps { - /** - * Determine the stage to which you want to deploy the stack - * - * @default - If not given, it will throw out an error - */ - readonly environment?: string; -} - -export class BaseStack extends cdk.Stack { - // biome-ignore lint/complexity/noUselessConstructor: - constructor(scope: Construct, id: string, props: BaseStackProps) { - super(scope, id, props); - - // ↓↓ instantiate your constructs here ↓↓ - // new NetworkConstruct(this, 'NetworkConstruct'); // sample construct that creates a VPC - } -} diff --git a/src/stacks/foundation-stack.ts b/src/stacks/foundation-stack.ts new file mode 100644 index 0000000..5bf6180 --- /dev/null +++ b/src/stacks/foundation-stack.ts @@ -0,0 +1,72 @@ +import * as cdk from 'aws-cdk-lib'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import { ToolkitCleaner } from 'cloudstructs/lib/toolkit-cleaner'; +import type { Construct } from 'constructs'; +import { getGitRepositoryDetails } from '../bin/git-helper'; + +export interface FoundationStackProps extends cdk.StackProps { + /** + * Determine the stage to which you want to deploy the stack + * + * @default - If not given, it will throw out an error + */ + readonly environment: string; +} + +/** + * FoundationStack + * + * This stack sets up fundamental infrastructure components for AWS deployments via GitHub Actions. + * It includes the creation of an OpenID Connect (OIDC) provider for GitHub and an IAM role for + * GitHub Actions deployments. + * + * @extends cdk.Stack + * + * @remarks + * - Creates a GitHub OIDC provider + * - Sets up an IAM role for GitHub Actions with necessary permissions + * - Implements a ToolkitCleaner for managing CDK toolkit resources + * + * @param scope - The scope in which to define this construct + * @param id - The scoped construct ID + * @param props - Stack properties + */ +export class FoundationStack extends cdk.Stack { + constructor(scope: Construct, id: string, props: FoundationStackProps) { + super(scope, id, props); + + //////////////////////////////// + // Setup GitHub OIDC support // + ////////////////////////////// + const { gitOwner, gitRepoName } = getGitRepositoryDetails(); + + const githubDomain = 'token.actions.githubusercontent.com'; + + const openIdConnectProvider = new iam.OpenIdConnectProvider(this, 'GithubProvider', { + url: `https://${githubDomain}`, + clientIds: ['sts.amazonaws.com'], + }); + + const conditions: iam.Conditions = { + StringLike: { + [`${githubDomain}:sub`]: `repo:${gitOwner}/${gitRepoName}:environment:${props.environment}`, + }, + StringEquals: { + [`${githubDomain}:aud`]: 'sts.amazonaws.com', + }, + }; + + new iam.Role(this, 'GitHubActionsServiceRole', { + assumedBy: new iam.WebIdentityPrincipal(openIdConnectProvider.openIdConnectProviderArn, conditions), + description: 'This role is used via GitHub Actions to deploy with AWS CDK or Terraform on the target AWS account', + managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess')], + maxSessionDuration: cdk.Duration.hours(2), + roleName: process.env.GITHUB_DEPLOY_ROLE ?? 'GitHubActionsServiceRole', + }); + + //////////////////////////////// + // Setup CDK Toolkit Cleaner // + ////////////////////////////// + new ToolkitCleaner(this, 'ToolkitCleaner'); + } +} diff --git a/src/stacks/github-oidc-stack.ts b/src/stacks/github-oidc-stack.ts deleted file mode 100644 index e1e4866..0000000 --- a/src/stacks/github-oidc-stack.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { GithubActionsRole } from 'aws-cdk-github-oidc'; -import * as cdk from 'aws-cdk-lib'; -import * as iam from 'aws-cdk-lib/aws-iam'; -import type { Construct } from 'constructs'; -import { getGitRepositoryDetails } from '../bin/git-helper'; - -export interface GitHubOIDCStackProps extends cdk.StackProps { - /** - * Determine the stage to which you want to deploy the stack - * - * @default - If not given, it will throw out an error - */ - readonly environment: string; -} - -/** - * A CDK stack that sets up a GitHub Actions OIDC provider and a deployment role. - * The deployment role is granted the 'AdministratorAccess' managed policy. - */ -export class GitHubOIDCStack extends cdk.Stack { - constructor(scope: Construct, id: string, props: GitHubOIDCStackProps) { - super(scope, id, props); - - // Retrieve the Git repository owner and name - const { gitOwner, gitRepoName } = getGitRepositoryDetails(); - - // Create a GitHub Actions OIDC provider - const openIdConnectProvider = new iam.OpenIdConnectProvider(this, 'GithubProvider', { - url: 'https://token.actions.githubusercontent.com', - clientIds: ['sts.amazonaws.com'], - }); - - // Create a GitHub Actions deployment role - const deployRole = new GithubActionsRole(this, 'GitHubDeployRole', { - provider: openIdConnectProvider, - owner: gitOwner, - repo: gitRepoName, - filter: `environment:${props.environment}`, - roleName: `${process.env.GITHUB_DEPLOY_ROLE}`, - managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess')], - description: 'This role is used via GitHub Actions to deploy your AWS CDK stacks on your AWS account', - maxSessionDuration: cdk.Duration.hours(2), - }); - - // Output the deployment role ARN - new cdk.CfnOutput(this, 'DeployRole', { - value: deployRole.roleArn, - }); - } -} diff --git a/src/stacks/index.ts b/src/stacks/index.ts index a4b5027..00f763a 100644 --- a/src/stacks/index.ts +++ b/src/stacks/index.ts @@ -1,3 +1,2 @@ -export { BaseStack } from './base-stack'; -export { GitHubOIDCStack } from './github-oidc-stack'; -export { ToolkitCleanerStack } from './toolkit-cleaner-stack'; +export { StarterStack } from './starter-stack'; +export { FoundationStack } from './foundation-stack'; diff --git a/src/stacks/starter-stack.ts b/src/stacks/starter-stack.ts new file mode 100644 index 0000000..639d789 --- /dev/null +++ b/src/stacks/starter-stack.ts @@ -0,0 +1,66 @@ +import * as cdk from 'aws-cdk-lib'; +import type { Construct } from 'constructs'; +// import { NetworkConstruct } from '../constructs'; + +export interface StarterStackProps extends cdk.StackProps { + /** + * Determine the stage to which you want to deploy the stack + * + * @default - If not given, it will throw out an error + */ + readonly environment?: string; +} + +/** + * StarterStack + * + * This stack serves as the starting point for building your AWS infrastructure using CDK. + * It provides a foundation for adding and organizing your AWS resources and constructs. + * + * @extends cdk.Stack + * + * @remarks + * - Use this stack to begin defining your infrastructure components + * - Add new constructs and resources within the constructor of this class + * - Customize and extend this stack based on your specific infrastructure needs + * + * @example + * To add a new S3 bucket: + * ``` + * import * as s3 from 'aws-cdk-lib/aws-s3'; + * + * // In the constructor: + * new s3.Bucket(this, 'MyBucket', { + * bucketName: 'my-unique-bucket-name', + * versioned: true, + * encryption: s3.BucketEncryption.S3_MANAGED, + * }); + * ``` + * + * @param scope - The scope in which to define this construct + * @param id - The scoped construct ID + * @param props - Stack properties including the deployment environment + */ +export class StarterStack extends cdk.Stack { + // biome-ignore lint/complexity/noUselessConstructor: + constructor(scope: Construct, id: string, props: StarterStackProps) { + super(scope, id, props); + + // ↓↓ Add your constructs and resources below ↓↓ + + // Sample construct that creates a secure VPC + // new NetworkConstruct(this, 'NetworkConstruct'); + + // TODO: Add your own constructs and resources here + // For example: + // - S3 buckets + // - Lambda functions + // - DynamoDB tables + // - API Gateway + // - etc. + + // Remember to import necessary modules at the top of the file + // and to organize your constructs logically within this stack + // or create additional stacks for different components of your infrastructure. + } +} diff --git a/src/stacks/toolkit-cleaner-stack.ts b/src/stacks/toolkit-cleaner-stack.ts deleted file mode 100644 index de00c5d..0000000 --- a/src/stacks/toolkit-cleaner-stack.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as cdk from 'aws-cdk-lib'; -import { ToolkitCleaner } from 'cloudstructs/lib/toolkit-cleaner'; -import type { Construct } from 'constructs'; - -export class ToolkitCleanerStack extends cdk.Stack { - constructor(scope: Construct, id: string, props?: cdk.StackProps) { - super(scope, id, props); - - new ToolkitCleaner(this, 'ToolkitCleaner'); - } -} From 93753cb51b1d6885c2b7d43ed30ec12a59d3a77f Mon Sep 17 00:00:00 2001 From: Danny Steenman Date: Tue, 14 Jan 2025 15:11:43 +0100 Subject: [PATCH 2/3] refactor(stack): Rename BaseStack to StarterStack in test file --- test/main.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/main.test.ts b/test/main.test.ts index 3bc24ec..883ecaf 100644 --- a/test/main.test.ts +++ b/test/main.test.ts @@ -1,10 +1,10 @@ import { App } from 'aws-cdk-lib'; import { Template } from 'aws-cdk-lib/assertions'; -import { BaseStack } from '../src/stacks'; +import { StarterStack } from '../src/stacks'; test('Snapshot', () => { const app = new App(); - const stack = new BaseStack(app, 'test', {}); + const stack = new StarterStack(app, 'test', {}); const template = Template.fromStack(stack); expect(template.toJSON()).toMatchSnapshot(); From eb08efb7ddfbd41e583be0110065d1e6fbc67880 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 14 Jan 2025 14:13:15 +0000 Subject: [PATCH 3/3] chore: self mutation Signed-off-by: github-actions --- src/constructs/network-construct.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/constructs/network-construct.ts b/src/constructs/network-construct.ts index a76b437..6442c95 100644 --- a/src/constructs/network-construct.ts +++ b/src/constructs/network-construct.ts @@ -19,9 +19,9 @@ export class NetworkConstruct extends BaseConstruct { this.environment === 'production' ? {} : { - autoDeleteObjects: true, - removalPolicy: RemovalPolicy.DESTROY, - }; + autoDeleteObjects: true, + removalPolicy: RemovalPolicy.DESTROY, + }; // Create a VPC with 9 subnets divided over 3 AZ's (3 public, 3 private, 3 isolated) this.vpc = new ec2.Vpc(this, 'Vpc', {