Open curran opened 4 days ago
After some time working with ChatGPT o1-preview, here's something that looks viable potentially. This is for Strapi, not VizHub, but gives a sense of what's possible:
ChatGPT output below:
Deploying Strapi to AWS ECS with CDK (TypeScript) and Continuous Deployment via CodePipeline
Introduction
This guide provides a comprehensive walkthrough for deploying a customized Strapi application to Amazon Web Services (AWS) using the AWS Cloud Development Kit (CDK) with TypeScript. We will leverage Amazon Elastic Container Service (ECS) instead of EC2 instances and set up a continuous deployment pipeline using AWS CodePipeline that automatically deploys changes from the production
branch of your GitHub repository.
npm install -g aws-cdk
.production
branch.We will set up the following AWS resources:
Create a new directory and initialize a CDK TypeScript project.
mkdir strapi-aws-cdk
cd strapi-aws-cdk
cdk init app --language typescript
This command sets up a basic CDK project structure with a bin/
and lib/
directory.
We'll define all necessary AWS resources in the lib/strapi-aws-cdk-stack.ts
file.
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as path from 'path';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ecs_patterns from 'aws-cdk-lib/aws-ecs-patterns';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as ecr_assets from 'aws-cdk-lib/aws-ecr-assets';
import * as codebuild from 'aws-cdk-lib/aws-codebuild';
import * as codepipeline from 'aws-cdk-lib/aws-codepipeline';
import * as codepipeline_actions from 'aws-cdk-lib/aws-codepipeline-actions';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
const vpc = new ec2.Vpc(this, 'StrapiVPC', {
maxAzs: 2,
natGateways: 1,
});
const dbCredentialsSecret = new secretsmanager.Secret(this, 'DBCredentialsSecret', {
secretName: 'strapi-db-credentials',
generateSecretString: {
secretStringTemplate: JSON.stringify({ username: 'strapiuser' }),
generateStringKey: 'password',
excludePunctuation: true,
},
});
const database = new rds.DatabaseInstance(this, 'StrapiDB', {
engine: rds.DatabaseInstanceEngine.postgres({
version: rds.PostgresEngineVersion.VER_13_8,
}),
vpc,
credentials: rds.Credentials.fromSecret(dbCredentialsSecret),
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
securityGroups: [], // We'll set up security groups later
multiAz: false,
allocatedStorage: 20,
maxAllocatedStorage: 100,
publiclyAccessible: false,
removalPolicy: cdk.RemovalPolicy.DESTROY, // Change to RETAIN for production
deletionProtection: false,
});
const s3Bucket = new s3.Bucket(this, 'StrapiAssetsBucket', {
bucketName: 'your-unique-bucket-name', // Replace with a unique name
removalPolicy: cdk.RemovalPolicy.RETAIN,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
});
const cluster = new ecs.Cluster(this, 'StrapiCluster', {
vpc,
});
const ecrRepository = new ecr.Repository(this, 'StrapiECRRepo', {
repositoryName: 'strapi-repo',
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
const fargateService = new ecs_patterns.ApplicationLoadBalancedFargateService(this, 'StrapiService', {
cluster,
cpu: 512,
memoryLimitMiB: 1024,
desiredCount: 1,
taskImageOptions: {
image: ecs.ContainerImage.fromEcrRepository(ecrRepository),
containerPort: 1337,
environment: {
DATABASE_CLIENT: 'postgres',
DATABASE_HOST: database.dbInstanceEndpointAddress,
DATABASE_PORT: database.dbInstanceEndpointPort,
DATABASE_NAME: 'strapi',
DATABASE_USERNAME: dbCredentialsSecret.secretValueFromJson('username').unsafeUnwrap(),
DATABASE_PASSWORD: dbCredentialsSecret.secretValueFromJson('password').unsafeUnwrap(),
AWS_ACCESS_KEY_ID: '<<ACCESS_KEY_ID>>',
AWS_ACCESS_SECRET: '<<SECRET_ACCESS_KEY>>',
AWS_REGION: cdk.Stack.of(this).region,
AWS_BUCKET_NAME: s3Bucket.bucketName,
},
secrets: {
DATABASE_PASSWORD: ecs.Secret.fromSecretsManager(dbCredentialsSecret, 'password'),
},
},
publicLoadBalancer: true,
});
<<ACCESS_KEY_ID>>
and <<SECRET_ACCESS_KEY>>
with appropriate values or use AWS Secrets Manager.Allow the Fargate tasks to connect to the RDS instance.
database.connections.allowDefaultPortFrom(fargateService.service, 'Allow ECS to access RDS');
const sourceOutput = new codepipeline.Artifact();
const project = new codebuild.PipelineProject(this, 'StrapiBuildProject', {
environment: {
buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_3,
privileged: true, // Required to run Docker commands
},
environmentVariables: {
REPOSITORY_URI: { value: ecrRepository.repositoryUri },
AWS_ACCOUNT_ID: { value: cdk.Aws.ACCOUNT_ID },
AWS_REGION: { value: cdk.Aws.REGION },
},
buildSpec: codebuild.BuildSpec.fromObject({
version: '0.2',
phases: {
pre_build: {
commands: [
'echo Logging in to Amazon ECR...',
'aws --version',
'$(aws ecr get-login --region $AWS_REGION --no-include-email)',
],
},
build: {
commands: [
'echo Build started on `date`',
'echo Building the Docker image...',
'docker build -t $REPOSITORY_URI:latest .',
'docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$CODEBUILD_RESOLVED_SOURCE_VERSION',
],
},
post_build: {
commands: [
'echo Build completed on `date`',
'echo Pushing the Docker image...',
'docker push $REPOSITORY_URI:latest',
'docker push $REPOSITORY_URI:$CODEBUILD_RESOLVED_SOURCE_VERSION',
],
},
},
artifacts: {
files: ['imagedefinitions.json'],
},
}),
});
ecrRepository.grantPullPush(project.role!);
const pipeline = new codepipeline.Pipeline(this, 'StrapiPipeline', {
pipelineName: 'StrapiPipeline',
restartExecutionOnUpdate: true,
});
pipeline.addStage({
stageName: 'Source',
actions: [
new codepipeline_actions.GitHubSourceAction({
actionName: 'GitHub_Source',
owner: 'your-github-username',
repo: 'your-strapi-repo',
branch: 'production',
oauthToken: cdk.SecretValue.secretsManager('github-token'),
output: sourceOutput,
}),
],
});
'github-token'
.pipeline.addStage({
stageName: 'Build',
actions: [
new codepipeline_actions.CodeBuildAction({
actionName: 'Docker_Build',
project,
input: sourceOutput,
outputs: [new codepipeline.Artifact()], // Optional
}),
],
});
pipeline.addStage({
stageName: 'Deploy',
actions: [
new codepipeline_actions.EcsDeployAction({
actionName: 'Deploy_to_ECS',
service: fargateService.service,
input: sourceOutput,
}),
],
});
Ensure that all necessary permissions are in place.
dbCredentialsSecret.grantRead(fargateService.taskDefinition.taskRole);
ecrRepository.grantPullPush(project.role!);
If your build needs to access Secrets Manager:
dbCredentialsSecret.grantRead(project.role!);
Create a Dockerfile
in your project's root directory:
# Use an official Node.js runtime as a parent image
FROM node:16-alpine
# Set the working directory
WORKDIR /usr/src/app
# Copy the package.json and package-lock.json
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the rest of the application code
COPY . .
# Build the Strapi application
RUN npm run build
# Expose port
EXPOSE 1337
# Start the application
CMD ["npm", "start"]
Ensure your Strapi application reads configuration from environment variables.
module.exports = ({ env }) => ({
connection: {
client: 'postgres',
connection: {
host: env('DATABASE_HOST', '127.0.0.1'),
port: env.int('DATABASE_PORT', 5432),
database: env('DATABASE_NAME', 'strapi'),
user: env('DATABASE_USERNAME', 'strapiuser'),
password: env('DATABASE_PASSWORD', ''),
ssl: env.bool('DATABASE_SSL', false),
},
debug: false,
},
});
module.exports = ({ env }) => ({
upload: {
config: {
provider: 'aws-s3',
providerOptions: {
accessKeyId: env('AWS_ACCESS_KEY_ID'),
secretAccessKey: env('AWS_ACCESS_SECRET'),
region: env('AWS_REGION'),
params: {
Bucket: env('AWS_BUCKET_NAME'),
},
},
},
},
});
Ensure all your changes, including the Dockerfile
and configuration updates, are committed to the production
branch of your GitHub repository.
git add .
git commit -m "Prepare Strapi for AWS ECS deployment"
git push origin production
If you haven't bootstrapped your AWS environment for CDK:
cdk bootstrap aws://ACCOUNT-NUMBER/REGION
Deploy the stack:
cdk deploy
Confirm the deployment when prompted.
Any changes pushed to the production
branch of your GitHub repository will trigger the pipeline.
Add an output to display the Load Balancer DNS name.
new cdk.CfnOutput(this, 'LoadBalancerDNS', {
value: fargateService.loadBalancer.loadBalancerDnsName,
});
lib/strapi-aws-cdk-stack.ts
)Below is the unified and cleaned-up CDK stack file:
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as path from 'path';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ecs_patterns from 'aws-cdk-lib/aws-ecs-patterns';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as codebuild from 'aws-cdk-lib/aws-codebuild';
import * as codepipeline from 'aws-cdk-lib/aws-codepipeline';
import * as codepipeline_actions from 'aws-cdk-lib/aws-codepipeline-actions';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
export class StrapiAwsCdkStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// VPC
const vpc = new ec2.Vpc(this, 'StrapiVPC', {
maxAzs: 2,
natGateways: 1,
});
// RDS Credentials
const dbCredentialsSecret = new secretsmanager.Secret(this, 'DBCredentialsSecret', {
secretName: 'strapi-db-credentials',
generateSecretString: {
secretStringTemplate: JSON.stringify({ username: 'strapiuser' }),
generateStringKey: 'password',
excludePunctuation: true,
},
});
// RDS Database
const database = new rds.DatabaseInstance(this, 'StrapiDB', {
engine: rds.DatabaseInstanceEngine.postgres({
version: rds.PostgresEngineVersion.VER_13_8,
}),
vpc,
credentials: rds.Credentials.fromSecret(dbCredentialsSecret),
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
multiAz: false,
allocatedStorage: 20,
maxAllocatedStorage: 100,
publiclyAccessible: false,
removalPolicy: cdk.RemovalPolicy.DESTROY,
deletionProtection: false,
});
// S3 Bucket
const s3Bucket = new s3.Bucket(this, 'StrapiAssetsBucket', {
bucketName: 'your-unique-bucket-name',
removalPolicy: cdk.RemovalPolicy.RETAIN,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
});
// ECS Cluster
const cluster = new ecs.Cluster(this, 'StrapiCluster', {
vpc,
});
// ECR Repository
const ecrRepository = new ecr.Repository(this, 'StrapiECRRepo', {
repositoryName: 'strapi-repo',
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
// Fargate Service
const fargateService = new ecs_patterns.ApplicationLoadBalancedFargateService(this, 'StrapiService', {
cluster,
cpu: 512,
memoryLimitMiB: 1024,
desiredCount: 1,
taskImageOptions: {
image: ecs.ContainerImage.fromEcrRepository(ecrRepository),
containerPort: 1337,
environment: {
DATABASE_CLIENT: 'postgres',
DATABASE_HOST: database.dbInstanceEndpointAddress,
DATABASE_PORT: database.dbInstanceEndpointPort,
DATABASE_NAME: 'strapi',
DATABASE_USERNAME: dbCredentialsSecret.secretValueFromJson('username').unsafeUnwrap(),
AWS_ACCESS_KEY_ID: '<<ACCESS_KEY_ID>>',
AWS_ACCESS_SECRET: '<<SECRET_ACCESS_KEY>>',
AWS_REGION: cdk.Stack.of(this).region,
AWS_BUCKET_NAME: s3Bucket.bucketName,
},
secrets: {
DATABASE_PASSWORD: ecs.Secret.fromSecretsManager(dbCredentialsSecret, 'password'),
},
},
publicLoadBalancer: true,
});
// Allow ECS to access RDS
database.connections.allowDefaultPortFrom(fargateService.service, 'Allow ECS to access RDS');
// Grant permissions to access Secrets
dbCredentialsSecret.grantRead(fargateService.taskDefinition.taskRole);
// CodePipeline Artifacts
const sourceOutput = new codepipeline.Artifact();
// CodeBuild Project
const project = new codebuild.PipelineProject(this, 'StrapiBuildProject', {
environment: {
buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_3,
privileged: true,
},
environmentVariables: {
REPOSITORY_URI: { value: ecrRepository.repositoryUri },
AWS_ACCOUNT_ID: { value: cdk.Aws.ACCOUNT_ID },
AWS_REGION: { value: cdk.Aws.REGION },
},
buildSpec: codebuild.BuildSpec.fromObject({
version: '0.2',
phases: {
pre_build: {
commands: [
'echo Logging in to Amazon ECR...',
'aws --version',
'$(aws ecr get-login --region $AWS_REGION --no-include-email)',
],
},
build: {
commands: [
'echo Build started on `date`',
'echo Building the Docker image...',
'docker build -t $REPOSITORY_URI:latest .',
'docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$CODEBUILD_RESOLVED_SOURCE_VERSION',
],
},
post_build: {
commands: [
'echo Build completed on `date`',
'echo Pushing the Docker image...',
'docker push $REPOSITORY_URI:latest',
'docker push $REPOSITORY_URI:$CODEBUILD_RESOLVED_SOURCE_VERSION',
`printf '[{"name":"strapi","imageUri":"%s"}]' $REPOSITORY_URI:latest > imagedefinitions.json`,
],
},
},
artifacts: {
files: ['imagedefinitions.json'],
},
}),
});
// Permissions for CodeBuild
ecrRepository.grantPullPush(project.role!);
project.addToRolePolicy(
new iam.PolicyStatement({
actions: ['ecr:GetAuthorizationToken'],
resources: ['*'],
}),
);
// CodePipeline
const pipeline = new codepipeline.Pipeline(this, 'StrapiPipeline', {
pipelineName: 'StrapiPipeline',
restartExecutionOnUpdate: true,
});
// Source Stage
pipeline.addStage({
stageName: 'Source',
actions: [
new codepipeline_actions.GitHubSourceAction({
actionName: 'GitHub_Source',
owner: 'your-github-username',
repo: 'your-strapi-repo',
branch: 'production',
oauthToken: cdk.SecretValue.secretsManager('github-token'),
output: sourceOutput,
}),
],
});
// Build Stage
pipeline.addStage({
stageName: 'Build',
actions: [
new codepipeline_actions.CodeBuildAction({
actionName: 'Docker_Build',
project,
input: sourceOutput,
outputs: [new codepipeline.Artifact()],
}),
],
});
// Deploy Stage
pipeline.addStage({
stageName: 'Deploy',
actions: [
new codepipeline_actions.EcsDeployAction({
actionName: 'Deploy_to_ECS',
service: fargateService.service,
input: new codepipeline.Artifact('imagedefinitions.json'),
}),
],
});
// Outputs
new cdk.CfnOutput(this, 'LoadBalancerDNS', {
value: fargateService.loadBalancer.loadBalancerDnsName,
});
}
}
'github-token'
.If using GitHub webhooks is preferred over the default polling:
push
events.By following this guide, you've successfully deployed a Strapi application to AWS ECS using CDK with TypeScript and set up continuous deployment from a GitHub repository's production
branch using AWS CodePipeline. This infrastructure as code approach enhances consistency, scalability, and maintainability for your application deployment.
Feel free to ask if you need further assistance or clarifications!
User story:
Acceptance criteria: