├── cms ├── src │ ├── api │ │ └── .gitkeep │ ├── extensions │ │ └── .gitkeep │ ├── admin │ │ ├── tsconfig.json │ │ ├── webpack.config.example.js │ │ └── app.example.tsx │ └── index.ts ├── public │ ├── uploads │ │ └── .gitkeep │ └── robots.txt ├── database │ └── migrations │ │ └── .gitkeep ├── .dockerignore ├── favicon.png ├── config │ ├── api.ts │ ├── admin.ts │ ├── server.ts │ ├── middlewares.ts │ └── database.ts ├── .env.example ├── .editorconfig ├── tsconfig.json ├── package.json ├── Dockerfile ├── .gitignore └── README.md ├── infrastructure ├── .npmignore ├── .gitignore ├── jest.config.js ├── bin │ └── my-awesome-strapi.ts ├── README.md ├── tsconfig.json ├── lib │ ├── vpc.ts │ ├── certificate.ts │ ├── route53-record.ts │ ├── strapi.ts │ ├── database.ts │ └── ecs-service.ts ├── package.json └── cdk.json ├── README.md └── .github └── workflows └── main-pipeline.yml /cms/src/api/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cms/public/uploads/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cms/src/extensions/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cms/database/migrations/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cms/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /cms/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziedbentahar/strapi-on-aws-with-cdk/HEAD/cms/favicon.png -------------------------------------------------------------------------------- /infrastructure/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /cms/config/api.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | rest: { 3 | defaultLimit: 25, 4 | maxLimit: 100, 5 | withCount: true, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /cms/public/robots.txt: -------------------------------------------------------------------------------- 1 | # To prevent search engines from seeing the site altogether, uncomment the next two lines: 2 | # User-Agent: * 3 | # Disallow: / 4 | -------------------------------------------------------------------------------- /infrastructure/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | cdk.context.json 10 | -------------------------------------------------------------------------------- /cms/.env.example: -------------------------------------------------------------------------------- 1 | HOST=0.0.0.0 2 | PORT=1337 3 | APP_KEYS="toBeModified1,toBeModified2" 4 | API_TOKEN_SALT=tobemodified 5 | ADMIN_JWT_SECRET=tobemodified 6 | JWT_SECRET=tobemodified 7 | -------------------------------------------------------------------------------- /cms/config/admin.ts: -------------------------------------------------------------------------------- 1 | export default ({ env }) => ({ 2 | auth: { 3 | secret: env('ADMIN_JWT_SECRET'), 4 | }, 5 | apiToken: { 6 | salt: env('API_TOKEN_SALT'), 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /cms/config/server.ts: -------------------------------------------------------------------------------- 1 | export default ({ env }) => ({ 2 | host: env('HOST', '0.0.0.0'), 3 | port: env.int('PORT', 1337), 4 | app: { 5 | keys: env.array('APP_KEYS'), 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /infrastructure/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /cms/config/middlewares.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'strapi::errors', 3 | 'strapi::security', 4 | 'strapi::cors', 5 | 'strapi::poweredBy', 6 | 'strapi::logger', 7 | 'strapi::query', 8 | 'strapi::body', 9 | 'strapi::session', 10 | 'strapi::favicon', 11 | 'strapi::public', 12 | ]; 13 | -------------------------------------------------------------------------------- /cms/src/admin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@strapi/typescript-utils/tsconfigs/admin", 3 | "include": [ 4 | "../plugins/**/admin/src/**/*", 5 | "./" 6 | ], 7 | "exclude": [ 8 | "node_modules/", 9 | "build/", 10 | "dist/", 11 | "**/*.test.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /cms/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [{package.json,*.yml}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /cms/src/admin/webpack.config.example.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-disable no-unused-vars */ 4 | module.exports = (config, webpack) => { 5 | // Note: we provide webpack above so you should not `require` it 6 | // Perform customizations to webpack config 7 | // Important: return the modified config 8 | return config; 9 | }; 10 | -------------------------------------------------------------------------------- /infrastructure/bin/my-awesome-strapi.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as cdk from "aws-cdk-lib"; 3 | import "source-map-support/register"; 4 | import { StrapiStack } from "../lib/strapi"; 5 | 6 | const app = new cdk.App(); 7 | const env = { 8 | account: process.env.CDK_DEFAULT_ACCOUNT, 9 | region: process.env.CDK_DEFAULT_REGION, 10 | }; 11 | 12 | new StrapiStack(app, StrapiStack.name, { stackName: StrapiStack.name, env }); 13 | -------------------------------------------------------------------------------- /cms/config/database.ts: -------------------------------------------------------------------------------- 1 | export default ({ env }) => ({ 2 | connection: { 3 | client: "postgres", 4 | connection: { 5 | host: env("DATABASE_HOST", "127.0.0.1"), 6 | port: env.int("DATABASE_PORT", 5432), 7 | database: env("DATABASE_NAME", "strapi"), 8 | user: env("DATABASE_USERNAME", "admin"), 9 | password: env("DATABASE_PASSWORD", "admin"), 10 | ssl: env.bool("DATABASE_SSL", false), 11 | }, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /cms/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@strapi/typescript-utils/tsconfigs/server", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "." 6 | }, 7 | "include": [ 8 | "./", 9 | "./**/*.ts", 10 | "./**/*.js", 11 | "src/**/*.json" 12 | ], 13 | "exclude": [ 14 | "node_modules/", 15 | "build/", 16 | "dist/", 17 | ".cache/", 18 | ".tmp/", 19 | "src/admin/", 20 | "**/*.test.*", 21 | "src/plugins/**" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # strapi-on-aws-with-cdk 2 | 3 | [![strapi on aws pipeline](https://github.com/ziedbentahar/strapi-on-aws-with-cdk/actions/workflows/main-pipeline.yml/badge.svg)](https://github.com/ziedbentahar/strapi-on-aws-with-cdk/actions/workflows/main-pipeline.yml) 4 | 5 | Full code and Github actions workflow for this bog post: 6 | 7 | https://medium.com/@zied-ben-tahar/deploying-strapi-cms-on-aws-with-ecs-on-fargate-aurora-serverless-v2-and-cdk-3c9b9ec732a3 8 | 9 | ![image](https://user-images.githubusercontent.com/6813975/209032867-8009de8a-3284-4bb6-ad3a-1fc610da246c.png) 10 | -------------------------------------------------------------------------------- /cms/src/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | /** 3 | * An asynchronous register function that runs before 4 | * your application is initialized. 5 | * 6 | * This gives you an opportunity to extend code. 7 | */ 8 | register({ strapi }) { 9 | /*configureDbConnection(strapi);*/ 10 | }, 11 | 12 | /** 13 | * An asynchronous bootstrap function that runs before 14 | * your application gets started. 15 | * 16 | * This gives you an opportunity to set up your data model, 17 | * run jobs, or perform some special logic. 18 | */ 19 | bootstrap(/*{ strapi }*/) {}, 20 | }; 21 | -------------------------------------------------------------------------------- /infrastructure/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your CDK TypeScript project 2 | 3 | This is a blank project for CDK development with TypeScript. 4 | 5 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 6 | 7 | ## Useful commands 8 | 9 | * `npm run build` compile typescript to js 10 | * `npm run watch` watch for changes and compile 11 | * `npm run test` perform the jest unit tests 12 | * `cdk deploy` deploy this stack to your default AWS account/region 13 | * `cdk diff` compare deployed stack with current state 14 | * `cdk synth` emits the synthesized CloudFormation template 15 | -------------------------------------------------------------------------------- /cms/src/admin/app.example.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | locales: [ 4 | // 'ar', 5 | // 'fr', 6 | // 'cs', 7 | // 'de', 8 | // 'dk', 9 | // 'es', 10 | // 'he', 11 | // 'id', 12 | // 'it', 13 | // 'ja', 14 | // 'ko', 15 | // 'ms', 16 | // 'nl', 17 | // 'no', 18 | // 'pl', 19 | // 'pt-BR', 20 | // 'pt', 21 | // 'ru', 22 | // 'sk', 23 | // 'sv', 24 | // 'th', 25 | // 'tr', 26 | // 'uk', 27 | // 'vi', 28 | // 'zh-Hans', 29 | // 'zh', 30 | ], 31 | }, 32 | bootstrap(app) { 33 | console.log(app); 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /cms/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-awesome-strapi", 3 | "private": true, 4 | "version": "0.1.0", 5 | "description": "A Strapi application", 6 | "scripts": { 7 | "develop": "strapi develop", 8 | "start": "strapi start", 9 | "build": "strapi build", 10 | "strapi": "strapi" 11 | }, 12 | "dependencies": { 13 | "@strapi/plugin-i18n": "4.5.2", 14 | "@strapi/plugin-users-permissions": "4.5.2", 15 | "@strapi/strapi": "4.5.2", 16 | "aws-sdk": "^2.1264.0", 17 | "pg": "8.6.0" 18 | }, 19 | "author": { 20 | "name": "A Strapi developer" 21 | }, 22 | "strapi": { 23 | "uuid": "a8e391e4-88b8-465f-9c13-6109ff390fd3" 24 | }, 25 | "engines": { 26 | "node": ">=14.19.1 <=18.x.x", 27 | "npm": ">=6.0.0" 28 | }, 29 | "license": "MIT" 30 | } 31 | -------------------------------------------------------------------------------- /infrastructure/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020" 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "typeRoots": [ 23 | "./node_modules/@types" 24 | ] 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "cdk.out" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /cms/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine as build 2 | # Installing libvips-dev for sharp Compatibility 3 | RUN apk update && apk add build-base gcc autoconf automake zlib-dev libpng-dev vips-dev && rm -rf /var/cache/apk/* > /dev/null 2>&1 4 | ARG NODE_ENV=production 5 | ENV NODE_ENV=${NODE_ENV} 6 | WORKDIR /opt/ 7 | COPY ./package.json ./package-lock.json ./ 8 | ENV PATH /opt/node_modules/.bin:$PATH 9 | RUN npm install --production 10 | WORKDIR /opt/app 11 | COPY ./ . 12 | RUN npm run build 13 | 14 | 15 | FROM node:16-alpine 16 | # Installing libvips-dev for sharp Compatibility 17 | RUN apk add vips-dev 18 | RUN rm -rf /var/cache/apk/* 19 | ARG NODE_ENV=production 20 | ENV NODE_ENV=${NODE_ENV} 21 | WORKDIR /opt/app 22 | COPY --from=build /opt/node_modules ./node_modules 23 | ENV PATH /opt/node_modules/.bin:$PATH 24 | COPY --from=build /opt/app ./ 25 | EXPOSE 1337 26 | CMD ["npm", "run","start"] 27 | -------------------------------------------------------------------------------- /infrastructure/lib/vpc.ts: -------------------------------------------------------------------------------- 1 | import { NestedStack, StackProps } from "aws-cdk-lib"; 2 | import { IpAddresses, IVpc, SubnetType, Vpc } from "aws-cdk-lib/aws-ec2"; 3 | import { Construct } from "constructs"; 4 | 5 | export class StrapiVpc extends NestedStack { 6 | public readonly vpc: IVpc; 7 | 8 | constructor(scope: Construct, id: string, props?: StackProps) { 9 | super(scope, id, props); 10 | 11 | this.vpc = new Vpc(this, "StrapiVPC", { 12 | ipAddresses: IpAddresses.cidr("10.0.0.0/24"), 13 | subnetConfiguration: [ 14 | { name: "public-subnet", subnetType: SubnetType.PUBLIC }, 15 | { 16 | name: "privat-subnet", 17 | subnetType: SubnetType.PRIVATE_WITH_EGRESS, 18 | }, 19 | { 20 | name: "isolated-subnet", 21 | subnetType: SubnetType.PRIVATE_ISOLATED, 22 | }, 23 | ], 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/main-pipeline.yml: -------------------------------------------------------------------------------- 1 | name: strapi on aws pipeline 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | 8 | pull_request: 9 | branches: [ main ] 10 | 11 | env: 12 | APPLICATION_NAME: awesome-strapi 13 | 14 | jobs: 15 | 16 | build-and-deploy-strapi: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Configure AWS Credentials 23 | uses: aws-actions/configure-aws-credentials@v1 24 | with: 25 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 26 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 27 | aws-region: eu-west-1 28 | 29 | - name: Build & Deploy Strapi 30 | run: | 31 | cd infrastructure \ 32 | && npm install \ 33 | && npx cdk deploy --require-approval never \ 34 | --context applicationName=strapi \ 35 | --context hostedZoneDomainName=inflow-it-labs.tk \ 36 | --context authorizedIPsForAdminAccess=${{ secrets.AUTHORIZED_IP_ADDRESSES_FOR_ADMIN_ACCESS }} 37 | -------------------------------------------------------------------------------- /infrastructure/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-awesome-strapi", 3 | "version": "0.1.0", 4 | "bin": { 5 | "my-awesome-strapi": "bin/my-awesome-strapi.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@aws-cdk/aws-ecs": "^1.181.0", 15 | "@aws-cdk/aws-ecs-patterns": "^1.181.0", 16 | "@aws-cdk/aws-rds": "^1.181.0", 17 | "@aws-cdk/aws-ssm": "^1.181.0", 18 | "@types/jest": "^27.5.2", 19 | "@types/jsonwebtoken": "^8.5.9", 20 | "@types/node": "10.17.27", 21 | "@types/prettier": "2.6.0", 22 | "@types/uuid": "^9.0.0", 23 | "aws-cdk": "2.91.0", 24 | "jest": "^27.5.1", 25 | "ts-jest": "^27.1.4", 26 | "ts-node": "^10.9.1", 27 | "typescript": "~3.9.7" 28 | }, 29 | "dependencies": { 30 | "aws-cdk-lib": "2.90.0", 31 | "constructs": "^10.0.0", 32 | "jsonwebtoken": "^8.5.1", 33 | "nodejs-base64-converter": "^1.0.5", 34 | "source-map-support": "^0.5.21", 35 | "uuid": "^9.0.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /infrastructure/lib/certificate.ts: -------------------------------------------------------------------------------- 1 | import { 2 | aws_certificatemanager, 3 | aws_route53, 4 | NestedStack, 5 | NestedStackProps, 6 | } from "aws-cdk-lib"; 7 | import { ICertificate } from "aws-cdk-lib/aws-certificatemanager"; 8 | import { Construct } from "constructs"; 9 | 10 | export interface CertificateProps extends NestedStackProps { 11 | hostedZoneDomainName: string; 12 | domainName: string; 13 | } 14 | 15 | export class Certificate extends NestedStack { 16 | public readonly certificate: ICertificate; 17 | 18 | constructor(scope: Construct, id: string, props: CertificateProps) { 19 | super(scope, id, props); 20 | 21 | const { hostedZoneDomainName, domainName } = props!; 22 | 23 | const hostedZone = aws_route53.HostedZone.fromLookup(this, "hosted-zone", { 24 | domainName: hostedZoneDomainName, 25 | }); 26 | 27 | this.certificate = new aws_certificatemanager.Certificate( 28 | this, 29 | "some-api-certificate", 30 | { 31 | domainName, 32 | validation: 33 | aws_certificatemanager.CertificateValidation.fromDns(hostedZone), 34 | } 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /infrastructure/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/my-awesome-strapi.ts", 3 | "watch": { 4 | "include": ["**"], 5 | "exclude": [ 6 | "README.md", 7 | "cdk*.json", 8 | "**/*.d.ts", 9 | "**/*.js", 10 | "tsconfig.json", 11 | "package*.json", 12 | "yarn.lock", 13 | "node_modules", 14 | "test" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 19 | "@aws-cdk/core:checkSecretUsage": true, 20 | "@aws-cdk/core:target-partitions": ["aws", "aws-cn"], 21 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 22 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 23 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 24 | "@aws-cdk/aws-iam:minimizePolicies": true, 25 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 26 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 27 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 28 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 29 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 30 | "@aws-cdk/core:enablePartitionLiterals": true, 31 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 32 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 33 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /infrastructure/lib/route53-record.ts: -------------------------------------------------------------------------------- 1 | import { 2 | aws_route53, 3 | aws_route53_targets, 4 | Duration, 5 | NestedStack, 6 | NestedStackProps, 7 | } from "aws-cdk-lib"; 8 | import { IApplicationLoadBalancer } from "aws-cdk-lib/aws-elasticloadbalancingv2"; 9 | import { Construct } from "constructs"; 10 | 11 | export interface Route53RecordProps extends NestedStackProps { 12 | loadBalancer: IApplicationLoadBalancer; 13 | hostedZoneDomainName: string; 14 | applicationName: string; 15 | } 16 | 17 | export class Route53Record extends NestedStack { 18 | constructor(scope: Construct, id: string, props: Route53RecordProps) { 19 | super(scope, id, props); 20 | 21 | const { loadBalancer, hostedZoneDomainName, applicationName } = props!; 22 | 23 | const hostedZone = aws_route53.HostedZone.fromLookup(this, "hosted-zone", { 24 | domainName: hostedZoneDomainName, 25 | }); 26 | 27 | new aws_route53.ARecord(this, "a-dns-record", { 28 | recordName: applicationName, 29 | zone: hostedZone, 30 | target: aws_route53.RecordTarget.fromAlias( 31 | new aws_route53_targets.LoadBalancerTarget(loadBalancer) 32 | ), 33 | ttl: Duration.minutes(1), 34 | }); 35 | 36 | new aws_route53.AaaaRecord(this, "aaaa-dns-record", { 37 | recordName: applicationName, 38 | zone: hostedZone, 39 | target: aws_route53.RecordTarget.fromAlias( 40 | new aws_route53_targets.LoadBalancerTarget(loadBalancer) 41 | ), 42 | ttl: Duration.minutes(1), 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /cms/.gitignore: -------------------------------------------------------------------------------- 1 | ############################ 2 | # OS X 3 | ############################ 4 | 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | Icon 9 | .Spotlight-V100 10 | .Trashes 11 | ._* 12 | 13 | 14 | ############################ 15 | # Linux 16 | ############################ 17 | 18 | *~ 19 | 20 | 21 | ############################ 22 | # Windows 23 | ############################ 24 | 25 | Thumbs.db 26 | ehthumbs.db 27 | Desktop.ini 28 | $RECYCLE.BIN/ 29 | *.cab 30 | *.msi 31 | *.msm 32 | *.msp 33 | 34 | 35 | ############################ 36 | # Packages 37 | ############################ 38 | 39 | *.7z 40 | *.csv 41 | *.dat 42 | *.dmg 43 | *.gz 44 | *.iso 45 | *.jar 46 | *.rar 47 | *.tar 48 | *.zip 49 | *.com 50 | *.class 51 | *.dll 52 | *.exe 53 | *.o 54 | *.seed 55 | *.so 56 | *.swo 57 | *.swp 58 | *.swn 59 | *.swm 60 | *.out 61 | *.pid 62 | 63 | 64 | ############################ 65 | # Logs and databases 66 | ############################ 67 | 68 | .tmp 69 | *.log 70 | *.sql 71 | *.sqlite 72 | *.sqlite3 73 | 74 | 75 | ############################ 76 | # Misc. 77 | ############################ 78 | 79 | *# 80 | ssl 81 | .idea 82 | nbproject 83 | public/uploads/* 84 | !public/uploads/.gitkeep 85 | 86 | ############################ 87 | # Node.js 88 | ############################ 89 | 90 | lib-cov 91 | lcov.info 92 | pids 93 | logs 94 | results 95 | node_modules 96 | .node_history 97 | 98 | ############################ 99 | # Tests 100 | ############################ 101 | 102 | testApp 103 | coverage 104 | 105 | ############################ 106 | # Strapi 107 | ############################ 108 | 109 | .env 110 | license.txt 111 | exports 112 | *.cache 113 | dist 114 | build 115 | .strapi-updater.json 116 | -------------------------------------------------------------------------------- /infrastructure/lib/strapi.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps } from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import { Certificate } from "./certificate"; 4 | import Database from "./database"; 5 | import { ECSService } from "./ecs-service"; 6 | import { Route53Record } from "./route53-record"; 7 | import { StrapiVpc } from "./vpc"; 8 | 9 | class StrapiStack extends Stack { 10 | constructor(scope: Construct, id: string, props?: StackProps) { 11 | super(scope, id, props); 12 | 13 | const vpc = new StrapiVpc(this, StrapiVpc.name, {}); 14 | 15 | const applicationName = this.node.tryGetContext("applicationName"); 16 | const hostedZoneDomainName = this.node.tryGetContext( 17 | "hostedZoneDomainName" 18 | ); 19 | const authorizedIPsForAdminAccess: string[] = this.node 20 | .tryGetContext("authorizedIPsForAdminAccess") 21 | .split(","); 22 | 23 | const domainName = `${applicationName}.${hostedZoneDomainName}`; 24 | 25 | const database = new Database(this, Database.name, { 26 | applicationName, 27 | vpc: vpc.vpc, 28 | }); 29 | 30 | const certificate = new Certificate(this, Certificate.name, { 31 | hostedZoneDomainName, 32 | domainName, 33 | }); 34 | 35 | const ecsServiceStack = new ECSService(this, ECSService.name, { 36 | certificate: certificate.certificate, 37 | dbHostname: database.dbCluster.clusterEndpoint.hostname.toString(), 38 | dbPort: database.dbCluster.clusterEndpoint.port.toString(), 39 | dbName: applicationName, 40 | dbSecret: database.dbSecret, 41 | vpc: vpc.vpc, 42 | applicationName, 43 | authorizedIPsForAdminAccess, 44 | }); 45 | 46 | const records = new Route53Record(this, Route53Record.name, { 47 | hostedZoneDomainName, 48 | applicationName, 49 | loadBalancer: ecsServiceStack.loadBalancer, 50 | }); 51 | } 52 | } 53 | 54 | export { StrapiStack }; 55 | -------------------------------------------------------------------------------- /cms/README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Getting started with Strapi 2 | 3 | Strapi comes with a full featured [Command Line Interface](https://docs.strapi.io/developer-docs/latest/developer-resources/cli/CLI.html) (CLI) which lets you scaffold and manage your project in seconds. 4 | 5 | ### `develop` 6 | 7 | Start your Strapi application with autoReload enabled. [Learn more](https://docs.strapi.io/developer-docs/latest/developer-resources/cli/CLI.html#strapi-develop) 8 | 9 | ``` 10 | npm run develop 11 | # or 12 | yarn develop 13 | ``` 14 | 15 | ### `start` 16 | 17 | Start your Strapi application with autoReload disabled. [Learn more](https://docs.strapi.io/developer-docs/latest/developer-resources/cli/CLI.html#strapi-start) 18 | 19 | ``` 20 | npm run start 21 | # or 22 | yarn start 23 | ``` 24 | 25 | ### `build` 26 | 27 | Build your admin panel. [Learn more](https://docs.strapi.io/developer-docs/latest/developer-resources/cli/CLI.html#strapi-build) 28 | 29 | ``` 30 | npm run build 31 | # or 32 | yarn build 33 | ``` 34 | 35 | ## ⚙️ Deployment 36 | 37 | Strapi gives you many possible deployment options for your project. Find the one that suits you on the [deployment section of the documentation](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/deployment.html). 38 | 39 | ## 📚 Learn more 40 | 41 | - [Resource center](https://strapi.io/resource-center) - Strapi resource center. 42 | - [Strapi documentation](https://docs.strapi.io) - Official Strapi documentation. 43 | - [Strapi tutorials](https://strapi.io/tutorials) - List of tutorials made by the core team and the community. 44 | - [Strapi blog](https://docs.strapi.io) - Official Strapi blog containing articles made by the Strapi team and the community. 45 | - [Changelog](https://strapi.io/changelog) - Find out about the Strapi product updates, new features and general improvements. 46 | 47 | Feel free to check out the [Strapi GitHub repository](https://github.com/strapi/strapi). Your feedback and contributions are welcome! 48 | 49 | ## ✨ Community 50 | 51 | - [Discord](https://discord.strapi.io) - Come chat with the Strapi community including the core team. 52 | - [Forum](https://forum.strapi.io/) - Place to discuss, ask questions and find answers, show your Strapi project and get feedback or just talk with other Community members. 53 | - [Awesome Strapi](https://github.com/strapi/awesome-strapi) - A curated list of awesome things related to Strapi. 54 | 55 | --- 56 | 57 | 🤫 Psst! [Strapi is hiring](https://strapi.io/careers). 58 | -------------------------------------------------------------------------------- /infrastructure/lib/database.ts: -------------------------------------------------------------------------------- 1 | import { NestedStack, NestedStackProps } from "aws-cdk-lib"; 2 | import { 3 | IVpc, 4 | Peer, 5 | Port, 6 | SecurityGroup, 7 | SubnetType, 8 | } from "aws-cdk-lib/aws-ec2"; 9 | import { 10 | AuroraPostgresEngineVersion, 11 | ClusterInstance, 12 | Credentials, 13 | DatabaseCluster, 14 | DatabaseClusterEngine, 15 | IDatabaseCluster, 16 | } from "aws-cdk-lib/aws-rds"; 17 | import { ISecret, Secret } from "aws-cdk-lib/aws-secretsmanager"; 18 | import { Construct } from "constructs"; 19 | 20 | interface DatabaseProps extends NestedStackProps { 21 | vpc: IVpc; 22 | applicationName: string; 23 | } 24 | 25 | class Database extends NestedStack { 26 | public readonly dbCluster: IDatabaseCluster; 27 | public readonly dbSecret: ISecret; 28 | public readonly dbName: string; 29 | 30 | constructor(scope: Construct, id: string, props?: DatabaseProps) { 31 | super(scope, id, props); 32 | const { vpc, applicationName } = props!; 33 | const dbSecurityGroup = new SecurityGroup(this, "DBClusterSecurityGroup", { 34 | vpc, 35 | }); 36 | 37 | dbSecurityGroup.addIngressRule( 38 | Peer.ipv4(vpc.privateSubnets[0].ipv4CidrBlock), 39 | Port.tcp(5432) 40 | ); 41 | 42 | this.dbSecret = new Secret(this, "DBCredentialsSecret", { 43 | secretName: `${applicationName}-credentials`, 44 | generateSecretString: { 45 | secretStringTemplate: JSON.stringify({ 46 | username: applicationName, 47 | }), 48 | excludePunctuation: true, 49 | includeSpace: false, 50 | generateStringKey: "password", 51 | }, 52 | }); 53 | 54 | this.dbCluster = new DatabaseCluster(this, "Database", { 55 | engine: DatabaseClusterEngine.auroraPostgres({ 56 | version: AuroraPostgresEngineVersion.VER_14_7, 57 | }), 58 | defaultDatabaseName: applicationName, 59 | writer: ClusterInstance.serverlessV2("writer"), 60 | serverlessV2MinCapacity: 0.5, 61 | serverlessV2MaxCapacity: 1, 62 | readers: [ 63 | ClusterInstance.serverlessV2("reader", { scaleWithWriter: true }), 64 | ], 65 | vpc, 66 | vpcSubnets: vpc.selectSubnets({ 67 | subnetType: SubnetType.PRIVATE_ISOLATED, 68 | }), 69 | credentials: Credentials.fromPassword( 70 | this.dbSecret.secretValueFromJson("username").unsafeUnwrap(), 71 | this.dbSecret.secretValueFromJson("password") 72 | ), 73 | port: 5432, 74 | securityGroups: [dbSecurityGroup], 75 | }); 76 | } 77 | } 78 | 79 | export default Database; 80 | -------------------------------------------------------------------------------- /infrastructure/lib/ecs-service.ts: -------------------------------------------------------------------------------- 1 | import { NestedStack, NestedStackProps } from "aws-cdk-lib"; 2 | import { ICertificate } from "aws-cdk-lib/aws-certificatemanager"; 3 | import { IVpc } from "aws-cdk-lib/aws-ec2"; 4 | import { 5 | Cluster, 6 | ContainerImage, 7 | Secret as ecs_Secret, 8 | } from "aws-cdk-lib/aws-ecs"; 9 | import { ApplicationLoadBalancedFargateService } from "aws-cdk-lib/aws-ecs-patterns"; 10 | import { 11 | IApplicationLoadBalancer, 12 | ListenerAction, 13 | ListenerCondition, 14 | } from "aws-cdk-lib/aws-elasticloadbalancingv2"; 15 | import { PolicyStatement } from "aws-cdk-lib/aws-iam"; 16 | import { ISecret, Secret } from "aws-cdk-lib/aws-secretsmanager"; 17 | import { Construct } from "constructs"; 18 | 19 | export interface ECSServiceProps extends NestedStackProps { 20 | vpc: IVpc; 21 | dbSecret: ISecret; 22 | certificate: ICertificate; 23 | dbName: string; 24 | dbHostname: string; 25 | dbPort: string; 26 | applicationName: string; 27 | authorizedIPsForAdminAccess: string[]; 28 | } 29 | 30 | export class ECSService extends NestedStack { 31 | public readonly loadBalancer: IApplicationLoadBalancer; 32 | 33 | constructor(scope: Construct, id: string, props?: ECSServiceProps) { 34 | super(scope, id, props); 35 | 36 | const { 37 | vpc, 38 | dbSecret, 39 | dbHostname, 40 | dbName, 41 | dbPort, 42 | certificate, 43 | applicationName, 44 | authorizedIPsForAdminAccess, 45 | } = props!; 46 | 47 | const strapiSecret = new Secret(this, "StrapiSecret", { 48 | secretName: `${applicationName}-strapi-secret`, 49 | 50 | generateSecretString: { 51 | secretStringTemplate: JSON.stringify({}), 52 | generateStringKey: "StrapiKey", 53 | excludePunctuation: true, 54 | }, 55 | }); 56 | 57 | const cluster = new Cluster(this, "Cluster", { vpc }); 58 | const loadBalancedService = new ApplicationLoadBalancedFargateService( 59 | this, 60 | "FargateService", 61 | { 62 | cluster, 63 | taskImageOptions: { 64 | secrets: { 65 | ...this.getSecretsDefinition(dbSecret, strapiSecret), 66 | }, 67 | image: ContainerImage.fromAsset("../cms"), 68 | containerPort: 1337, 69 | environment: { 70 | DATABASE_CLIENT: "postgres", 71 | DATABASE_HOST: dbHostname, 72 | DATABASE_PORT: dbPort, 73 | DATABASE_NAME: dbName, 74 | HOST: "0.0.0.0", 75 | PORT: "1337", 76 | }, 77 | }, 78 | certificate, 79 | } 80 | ); 81 | 82 | const policyStatement = new PolicyStatement({ 83 | resources: [dbSecret.secretFullArn!, strapiSecret.secretFullArn!], 84 | actions: ["secretsmanager:GetSecretValue"], 85 | }); 86 | 87 | loadBalancedService.taskDefinition.addToExecutionRolePolicy( 88 | policyStatement 89 | ); 90 | 91 | this.restricAccessToAdmin(loadBalancedService, authorizedIPsForAdminAccess); 92 | 93 | this.loadBalancer = loadBalancedService.loadBalancer; 94 | } 95 | 96 | private getSecretsDefinition(dbSecret: ISecret, strapiSecret: ISecret) { 97 | return { 98 | DATABASE_USERNAME: ecs_Secret.fromSecretsManager(dbSecret, "username"), 99 | DATABASE_PASSWORD: ecs_Secret.fromSecretsManager(dbSecret, "password"), 100 | JWT_SECRET: ecs_Secret.fromSecretsManager(strapiSecret, "StrapiKey"), 101 | APP_KEYS: ecs_Secret.fromSecretsManager(strapiSecret, "StrapiKey"), 102 | API_TOKEN_SALT: ecs_Secret.fromSecretsManager(strapiSecret, "StrapiKey"), 103 | ADMIN_JWT_SECRET: ecs_Secret.fromSecretsManager( 104 | strapiSecret, 105 | "StrapiKey" 106 | ), 107 | }; 108 | } 109 | 110 | private restricAccessToAdmin( 111 | loadBalancedService: ApplicationLoadBalancedFargateService, 112 | authorizedIPsForAdminAccess: string[] 113 | ) { 114 | loadBalancedService.listener.addAction("accept", { 115 | priority: 1, 116 | conditions: [ 117 | ListenerCondition.pathPatterns(["/admin/*"]), 118 | ListenerCondition.sourceIps(authorizedIPsForAdminAccess), 119 | ], 120 | action: ListenerAction.forward([loadBalancedService.targetGroup]), 121 | }); 122 | 123 | loadBalancedService.listener.addAction("forbidden", { 124 | priority: 2, 125 | conditions: [ListenerCondition.pathPatterns(["/admin/*"])], 126 | action: ListenerAction.fixedResponse(403, { 127 | contentType: "text/html", 128 | messageBody: "Your IP address is not authorized", 129 | }), 130 | }); 131 | } 132 | } 133 | --------------------------------------------------------------------------------