Skip to content

Commit

Permalink
CDK API stack with Fargate / ALB (#817)
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswilty committed Mar 19, 2024
1 parent 3add2e3 commit 5d72051
Show file tree
Hide file tree
Showing 14 changed files with 2,791 additions and 3 deletions.
5 changes: 2 additions & 3 deletions .github/workflows/cloud.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
branches:
- main
- dev
- 'feature/+'
- 'feature/**'
paths:
- 'cloud/**'

Expand Down Expand Up @@ -38,5 +38,4 @@ jobs:
npm ci
- name: Run job
run: |
npm run lint
npm run format
npm run codecheck
29 changes: 29 additions & 0 deletions cloud/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* eslint-env node */
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended-type-checked',
'plugin:@typescript-eslint/stylistic-type-checked',
'plugin:@typescript-eslint/strict-type-checked',
],
parser: '@typescript-eslint/parser',
parserOptions: {
project: true,
tsconfigRootDir: __dirname,
},
plugins: ['@typescript-eslint'],
root: true,
ignorePatterns: ['node_modules', 'cdk.out'],
rules: {
'@typescript-eslint/init-declarations': 'error',
'@typescript-eslint/no-misused-promises': [
'error',
{
checksVoidReturn: false,
},
],
'@typescript-eslint/unbound-method': ['error', { ignoreStatic: true }],
'prefer-template': 'error',
eqeqeq: 'error',
},
};
7 changes: 7 additions & 0 deletions cloud/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
*.js
*.d.ts
node_modules

# CDK asset staging directory
.cdk.staging
cdk.out
1 change: 1 addition & 0 deletions cloud/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cdk.out
17 changes: 17 additions & 0 deletions cloud/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# CDK project for Prompt Injection

This project uses AWS CDK (using TypeScript) to define CloudFormation templates for remote deployment of all resources
for the Prompt Injection application.

The architecture is a typical containerized node-express API managed by AWS Fargate and ECS, with load-balancing (ELB),
plus an S3-hosted UI served through CloudFront, all secured using Cognito.

The `cdk.json` file tells the CDK Toolkit how to execute your app.

## Commands

- `npm run cdk:synth` - generates the CloudFormation templates, outputting into `./cdk.out` dir.
- `npm run cdk:deploy` - deploys the application stacks into the remote DEV stage.
_Caution! This will overwrite existing resources, so ensure the team are notified before running this manually._

Note that once the CDK Pipeline is in place, DEV deployment will happen automatically on merging into dev branch.
28 changes: 28 additions & 0 deletions cloud/bin/cloud.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env node
import { App } from 'aws-cdk-lib';
import 'source-map-support/register';

import {
appName,
environmentName,
resourceDescription,
stackName,
ApiStack,
} from '../lib';

const app = new App();
const tags = {
owner: appName,
classification: 'unrestricted',
'environment-type': environmentName(app),
'keep-alive': '8-6-without-weekends',
IaC: 'CDK',
};

const generateStackName = stackName(app);
const generateDescription = resourceDescription(app);

new ApiStack(app, generateStackName('api'), {
tags,
description: generateDescription('API stack'),
});
52 changes: 52 additions & 0 deletions cloud/cdk-developer-policy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
Resources:
PermissionsBoundary:
Type: AWS::IAM::ManagedPolicy
Properties:
PolicyDocument:
Statement:
# ----- Begin base policy ---------------
# If permission boundaries do not have an explicit allow
# then the effect is deny
- Sid: ExplicitAllowAll
Action: '*'
Effect: Allow
Resource: '*'
# Default permissions to prevent privilege escalation
- Sid: DenyAccessIfRequiredPermBoundaryIsNotBeingApplied
Action:
- iam:CreateUser
- iam:CreateRole
- iam:PutRolePermissionsBoundary
- iam:PutUserPermissionsBoundary
Condition:
StringNotEquals:
iam:PermissionsBoundary:
Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-developer-policy
Effect: Deny
Resource: '*'
- Sid: DenyPermBoundaryIAMPolicyAlteration
Action:
- iam:CreatePolicyVersion
- iam:DeletePolicy
- iam:DeletePolicyVersion
- iam:SetDefaultPolicyVersion
Effect: Deny
Resource:
Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-developer-policy
- Sid: DenyRemovalOfPermBoundaryFromAnyUserOrRole
Action:
- iam:DeleteUserPermissionsBoundary
- iam:DeleteRolePermissionsBoundary
Effect: Deny
Resource: '*'
# ----- End base policy ---------------
# -- Begin Custom Organization Policy --
- Sid: DenyModifyingConfig
Effect: Deny
Action: config:*
Resource: '*'
# -- End Custom Organization Policy --
Version: '2012-10-17'
Description: 'CDK Bootstrap Permission Boundary'
ManagedPolicyName: cdk-developer-policy
Path: /
61 changes: 61 additions & 0 deletions cloud/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"app": "npx ts-node --prefer-ts-exts bin/cloud.ts",
"watch": {
"include": ["**"],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"yarn.lock",
"node_modules",
"test"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:permissionsBoundary": {
"name": "cdk-developer-policy"
},
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": ["aws", "aws-cn"],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
"@aws-cdk/core:enablePartitionLiterals": true,
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
"@aws-cdk/aws-iam:standardizedServicePrincipals": true,
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
"@aws-cdk/aws-route53-patters:useCertificate": true,
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
"@aws-cdk/aws-redshift:columnId": true,
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
"@aws-cdk/aws-kms:aliasNameRef": true,
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
"@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
"@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
"@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true
}
}
81 changes: 81 additions & 0 deletions cloud/lib/api-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Stack, StackProps } from 'aws-cdk-lib/core';
import { Vpc } from 'aws-cdk-lib/aws-ec2';
import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets';
import {
Cluster,
ContainerImage,
Secret as EnvSecret,
} from 'aws-cdk-lib/aws-ecs';
import { ApplicationLoadBalancedFargateService } from 'aws-cdk-lib/aws-ecs-patterns';
import { Secret } from 'aws-cdk-lib/aws-secretsmanager';
import { Construct } from 'constructs';
import { join } from 'node:path';

import { resourceName, stageName } from './resourceNamingUtils';

export class ApiStack extends Stack {
stage: string;

constructor(scope: Construct, id: string, props: StackProps) {
super(scope, id, props);
this.stage = stageName(scope);

const generateResourceName = resourceName(scope);

const dockerImageAsset = new DockerImageAsset(
this,
generateResourceName('container-image'),
{
directory: join(__dirname, '../../backend/'),
}
);

// Default AZs is all in region, but for environment-agnostic stack, max is 2!
const vpcName = generateResourceName('vpc');
const vpc = new Vpc(this, vpcName, { vpcName, maxAzs: 2 });
const clusterName = generateResourceName('cluster');
const cluster = new Cluster(this, clusterName, { clusterName, vpc });

const apiKeySecret = Secret.fromSecretNameV2(
this,
generateResourceName('apiKey'),
'dev/SpyLogic/ApiKey'
);

// Create a load-balanced Fargate service and make it public
const containerPort = 3001;
const serviceName = generateResourceName('fargate');
const fargateService = new ApplicationLoadBalancedFargateService(
this,
serviceName,
{
serviceName,
cluster,
cpu: 256, // Default is 256
desiredCount: 1, // Bump this up for prod!
taskImageOptions: {
image: ContainerImage.fromDockerImageAsset(dockerImageAsset),
containerPort,
environment: {
NODE_ENV: 'production',
PORT: `${containerPort}`,
},
secrets: {
OPENAI_API_KEY: EnvSecret.fromSecretsManager(
apiKeySecret,
'OPENAI_API_KEY'
),
SESSION_SECRET: EnvSecret.fromSecretsManager(
apiKeySecret,
'SESSION_SECRET'
),
},
},
memoryLimitMiB: 512, // Default is 512
loadBalancerName: generateResourceName('elb'),
publicLoadBalancer: true, // Default is true
}
);
fargateService.targetGroup.configureHealthCheck({ path: '/health' });
}
}
2 changes: 2 additions & 0 deletions cloud/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './resourceNamingUtils';
export { ApiStack } from './api-stack';
26 changes: 26 additions & 0 deletions cloud/lib/resourceNamingUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Construct } from 'constructs';

export const appName = 'SpyLogic';

export const stageName = (construct: Construct) =>
(construct.node.tryGetContext('STAGE') as string) || 'dev';
export const environmentName = (() => {
const environments = {
dev: 'development',
test: 'testing',
prod: 'production',
};
return (construct: Construct) => {
const stage = stageName(construct) as keyof typeof environments;
return environments[stage] || 'unknown';
};
})();

export const resourceName = (construct: Construct) => (suffix: string) =>
`${stageName(construct)}-${appName}-${suffix}`.toLowerCase();

export const resourceDescription = (construct: Construct) => (prefix: string) =>
`${prefix} for ${appName} (${stageName(construct)})`;

export const stackName = (construct: Construct) => (name: string) =>
resourceName(construct)(`${name}-stack`);
Loading

0 comments on commit 5d72051

Please sign in to comment.