Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PoC Deployment in Corporate IT Infrastructure #1039

Open
curran opened this issue Oct 13, 2024 · 1 comment
Open

PoC Deployment in Corporate IT Infrastructure #1039

curran opened this issue Oct 13, 2024 · 1 comment

Comments

@curran
Copy link
Collaborator

curran commented Oct 13, 2024

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:

  • For this issue to be closed, an actual corporate IT manager needs to get VizHub up and running in their infra, based just on open source code.
@curran
Copy link
Collaborator Author

curran commented Oct 13, 2024

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

  • AWS Account: Access with permissions to create AWS resources.
  • AWS CLI: Installed and configured with your AWS credentials.
  • Node.js and npm: Installed on your local machine.
  • AWS CDK: Installed globally via npm install -g aws-cdk.
  • GitHub Repository: Contains your customized Strapi source code in the production branch.
  • Basic Knowledge: Familiarity with TypeScript, AWS services, Docker, and CDK.

Architecture Overview

We will set up the following AWS resources:

  • Amazon VPC: For networking.
  • Amazon ECS (Fargate): To run the containerized Strapi application.
  • Amazon RDS (PostgreSQL): Managed database service for Strapi.
  • Amazon S3 Bucket: For storing uploaded assets.
  • AWS CodePipeline: For continuous deployment from GitHub.
  • AWS CodeBuild: To build Docker images.
  • AWS ECR: To store Docker images.
  • IAM Roles and Policies: For secure access between services.
  • Security Groups: To control inbound and outbound traffic.

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,
});
  • Note: Replace <<ACCESS_KEY_ID>> and <<SECRET_ACCESS_KEY>> with appropriate values or use AWS Secrets Manager.

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,
    }),
  ],
});
  • Note: Store your GitHub OAuth token securely in AWS Secrets Manager under the name 'github-token'.

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.

  • Verify Pipeline Execution: Navigate to the AWS CodePipeline console and check that the pipeline executes successfully.
  • Access Strapi: Once the deployment is complete, access your Strapi application via the Load Balancer URL output by CDK.

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

  • Sensitive Data: Use AWS Secrets Manager or Parameter Store to store sensitive data like database passwords and API keys.
  • Security Groups: Ensure that the security groups allow necessary traffic and follow best practices.
  • IAM Permissions: Assign least-privilege permissions to IAM roles.
  • Auto Scaling: Configure auto-scaling policies for your ECS service if needed.

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

  • Create a Secret: Store your GitHub OAuth token in AWS Secrets Manager under the name 'github-token'.
  • Permissions: Ensure the CodePipeline has permissions to access this secret.

Set Up Webhooks (Optional)

If using GitHub webhooks is preferred over the default polling:

  • In GitHub Repository Settings: Navigate to Settings > Webhooks and add a new webhook pointing to the AWS CodePipeline webhook URL.
  • Event Triggers: Set the webhook to trigger on push events.

Security and Best Practices

  • Secrets Management: Use AWS Secrets Manager to securely store and retrieve sensitive information.
  • Network Security: Ensure that security groups and network ACLs are configured correctly.
  • IAM Roles: Follow the principle of least privilege when assigning permissions.
  • Logging and Monitoring: Implement logging using AWS CloudWatch and set up alerts for failures in CodePipeline or ECS tasks.

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant