vizhub-core / vizhub

VizHub Platform V3
11 stars 1 forks source link

PoC Deployment in Corporate IT Infrastructure #1039

Open curran opened 4 days ago

curran commented 4 days ago

User story:

As a corporate IT manager, I want to do a test deployment VizHub on my own AWS infrastructure, so that I can evaluate it and determine if it provides value to my organization as an internal tool.

Acceptance criteria:

curran commented 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.


Prerequisites


Architecture Overview

We will set up the following AWS resources:


Step 1: Initialize the CDK Project

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.


Step 2: Define AWS Resources in CDK

We'll define all necessary AWS resources in the lib/strapi-aws-cdk-stack.ts file.

Import Necessary Modules

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';

Create a VPC

const vpc = new ec2.Vpc(this, 'StrapiVPC', {
  maxAzs: 2,
  natGateways: 1,
});

Step 3: Set Up an RDS PostgreSQL Database

Create a Secrets Manager Secret for Database Credentials

const dbCredentialsSecret = new secretsmanager.Secret(this, 'DBCredentialsSecret', {
  secretName: 'strapi-db-credentials',
  generateSecretString: {
    secretStringTemplate: JSON.stringify({ username: 'strapiuser' }),
    generateStringKey: 'password',
    excludePunctuation: true,
  },
});

Create the RDS Database Instance

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,
});

Step 4: Create an S3 Bucket for Asset Storage

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,
});

Step 5: Set Up an ECS Cluster and Fargate Service

Create an ECS Cluster

const cluster = new ecs.Cluster(this, 'StrapiCluster', {
  vpc,
});

Create an ECR Repository

const ecrRepository = new ecr.Repository(this, 'StrapiECRRepo', {
  repositoryName: 'strapi-repo',
  removalPolicy: cdk.RemovalPolicy.RETAIN,
});

Define the Fargate Service with Application Load Balancer

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,
});

Configure Security Groups

Allow the Fargate tasks to connect to the RDS instance.

database.connections.allowDefaultPortFrom(fargateService.service, 'Allow ECS to access RDS');

Step 6: Set Up CodePipeline for Continuous Deployment

Create a Source Artifact

const sourceOutput = new codepipeline.Artifact();

Create a Build Project

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!);

Create the Pipeline

const pipeline = new codepipeline.Pipeline(this, 'StrapiPipeline', {
  pipelineName: 'StrapiPipeline',
  restartExecutionOnUpdate: true,
});

Add Stages to the Pipeline

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()], // Optional
    }),
  ],
});

Deploy Stage

pipeline.addStage({
  stageName: 'Deploy',
  actions: [
    new codepipeline_actions.EcsDeployAction({
      actionName: 'Deploy_to_ECS',
      service: fargateService.service,
      input: sourceOutput,
    }),
  ],
});

Step 7: Configure IAM Roles and Policies

Ensure that all necessary permissions are in place.

Grant Permissions to Access Secrets

dbCredentialsSecret.grantRead(fargateService.taskDefinition.taskRole);

Allow CodeBuild Project to Access ECR

ecrRepository.grantPullPush(project.role!);

Allow CodeBuild Project to Access Secrets Manager

If your build needs to access Secrets Manager:

dbCredentialsSecret.grantRead(project.role!);

Step 8: Prepare Strapi Application

Dockerize Your Strapi Application

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"]

Update Strapi Configuration

Ensure your Strapi application reads configuration from environment variables.

config/database.js

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,
  },
});

config/plugins.js

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'),
        },
      },
    },
  },
});

Step 9: Commit and Push Changes to GitHub

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

Step 10: Deploy the CDK Stack

Bootstrap the CDK Environment

If you haven't bootstrapped your AWS environment for CDK:

cdk bootstrap aws://ACCOUNT-NUMBER/REGION

Deploy

Deploy the stack:

cdk deploy

Confirm the deployment when prompted.


Step 11: Test the Continuous Deployment Pipeline

Any changes pushed to the production branch of your GitHub repository will trigger the pipeline.


Step 12: Output the Load Balancer DNS

Add an output to display the Load Balancer DNS name.

new cdk.CfnOutput(this, 'LoadBalancerDNS', {
  value: fargateService.loadBalancer.loadBalancerDnsName,
});

Important Notes


Complete CDK Stack File (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,
    });
  }
}

Step 13: Configure GitHub Webhooks and OAuth Token

Store GitHub OAuth Token in Secrets Manager

Set Up Webhooks (Optional)

If using GitHub webhooks is preferred over the default polling:


Security and Best Practices


Conclusion

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.


References


Feel free to ask if you need further assistance or clarifications!