packages/deploy-aws/src/templates/seagull_project.ts
import { FS } from '@seagull/commands-fs'
import { SDK } from 'aws-cdk'
import * as dotenv from 'dotenv'
import 'ts-node/register'
import * as aws from '../aws_sdk_handler'
import { emptyBucket, S3Handler } from '../aws_sdk_handler'
import * as lib from '../lib'
import { ProvideAssetFolder } from '../provide_asset_folder'
import { SeagullApp } from '../seagull_app'
import { setCredsByProfile } from '../set_aws_credentials'
import { Rule } from '../types'
interface SeagullProjectProps {
account: string
appPath: string
branch: string
stage: string
profile: string
region: string
vpcId: string
subnetIds: string
handlers?: {
acmHandler?: aws.ACMHandler
cloudfrontHandler?: aws.CloudfrontHandler
stsHandler?: aws.STSHandler
}
}
export class SeagullProject {
account: string
appPath: string
branch: string
stage: string
pkgJson: any
profile: string
region: string
vpcId: string
subnetIds: string[]
acm: aws.ACMHandler
cloudfront: aws.CloudfrontHandler
sts: aws.STSHandler
constructor(props: SeagullProjectProps) {
this.account = props.account
this.appPath = props.appPath
this.branch = props.branch
this.stage = props.stage
this.profile = props.profile
this.region = props.region
this.pkgJson = require(`${this.appPath}/package.json`)
this.vpcId = props.vpcId
this.subnetIds = props.subnetIds && props.subnetIds.split(',') || []
setCredsByProfile(this.profile)
const propsACMHandler = props.handlers && props.handlers.acmHandler
const propsCFHandler = props.handlers && props.handlers.cloudfrontHandler
const propsSTSHandler = props.handlers && props.handlers.stsHandler
this.acm = propsACMHandler || new aws.ACMHandler()
this.cloudfront = propsCFHandler || new aws.CloudfrontHandler()
this.sts = propsSTSHandler || new aws.STSHandler()
}
async createSeagullApp() {
// preparations for deployment
const name = this.getAppName()
const globalProps = {
accountId: (await aws.getAccountId(this.sts)) || '',
branch: this.branch,
projectName: this.pkgJson.name,
region: this.region,
stage: this.stage,
}
const itemProps = Object.assign({}, globalProps, { topic: 'items' })
const logsProps = Object.assign({}, globalProps, { topic: 'logs' })
const errorProps = Object.assign({}, globalProps, { topic: 'errors' })
const itemBucketName = lib.getBucketName(itemProps)
const logBucketName = lib.getBucketName(logsProps, true)
const errorBucketName = lib.getBucketName(errorProps, true)
const actions: string[] = [
'sts:AssumeRole',
'logs:*',
'lambda:InvokeFunction',
'lambda:InvokeAsync',
'ses:*',
's3:*',
'ec2:*',
'events:*',
'cloudwatch:*',
]
const aliasConfig = await aws.checkForAliasConfig(this.pkgJson, this.acm)
// create the asset folder
await addResources(this.appPath, itemBucketName)
// construct the stack and the app
const app = await this.createBareApp()
const role = app.stack.addIAMRole('role', 'lambda.amazonaws.com', actions)
app.role = role
const env = await getEnv(name, this.appPath, this.stage, logBucketName)
const logBucket = app.stack.addS3(logBucketName, role, false)
const errorBucket = app.stack.addS3(errorBucketName, role, false)
errorBucket.grantPublicAccess()
const vpc = app.stack.addVPC('vpc', this.vpcId, this.subnetIds, [ this.region ])
const lambda = app.stack.addLambda('lambda', this.appPath, role, vpc, env)
const apiGW = app.stack.addUniversalApiGateway('apiGW', lambda, this.stage)
const cloudfrontConfig = {
aliasConfig,
apiGateway: apiGW,
errorBucket,
logBucket,
}
app.stack.addCloudfront('cloudfront', cloudfrontConfig)
const s3DeploymentNeeded = this.stage === 'prod' || this.branch === 'master'
const importS3 = () => app.stack.importS3(itemBucketName, role)
const addS3 = () => app.stack.addS3(itemBucketName, role)
s3DeploymentNeeded ? addS3() : importS3()
app.stack.addLogGroup(`/aws/lambda/${name}-lambda-handler`)
app.stack.addLogGroup(`/${name}/data-log`)
const addCrons = this.stage === 'prod' || this.branch === 'master'
const cronJson = addCrons ? await buildCronJson(this.appPath) : []
const addRule = (rule: Rule) => app.stack.addEventRule(rule, lambda)
cronJson.forEach(addRule)
return app
}
async createBareApp() {
const account = this.account || (await new SDK({}).defaultAccount())
const itemsProps = {
accountId: (await aws.getAccountId(this.sts)) || '',
branch: this.branch,
projectName: this.pkgJson.name,
region: this.region,
stage: this.stage,
topic: 'items',
}
const appProps = {
addAssets: true,
appPath: this.appPath,
itemsBucket: lib.getBucketName(itemsProps),
projectName: this.getAppName(),
stackProps: { env: { account, region: this.region } },
}
return new SeagullApp(appProps)
}
async customizeStack(app: SeagullApp) {
const extensionPath = `${this.appPath}/infrastructure-aws.ts`
const hasExtensionFile = await new FS.Exists(extensionPath).execute()
return hasExtensionFile && (await this.loadAndExecute(extensionPath, app))
}
async loadAndExecute(path: string, app: SeagullApp) {
const extensionFkt = (await import(`${path}`)).default
await extensionFkt(app)
}
getAppName() {
const suffix = this.stage === 'test' ? `-${this.branch}-${this.stage}` : ''
return `${this.pkgJson.name}${suffix}`.replace(/[^0-9A-Za-z-]/g, '')
}
async deployProject() {
this.validate()
const app = await this.createSeagullApp()
await this.customizeStack(app)
await app.deployStack()
const url = (await aws.getCFURL(this.getAppName(), this.cloudfront)) || ''
await new FS.WriteFile('/tmp/cfurl.txt', url).execute()
console.info('cloudfront-url', url)
}
async destroyProject() {
this.validate()
const app = await this.createBareApp()
const logsProps = {
accountId: (await aws.getAccountId(this.sts)) || '',
branch: this.branch,
projectName: this.pkgJson.name,
region: this.region,
stage: this.stage,
topic: 'logs',
}
const logBucket = await lib.getBucketName(logsProps, true)
await emptyBucket(new S3Handler(), logBucket)
await app.destroyStack()
}
async diffProject() {
const app = await this.createSeagullApp()
await this.customizeStack(app)
await app.diffStack()
}
validate() {
const hasValidProfile = setCredsByProfile(this.profile)
// tslint:disable-next-line:no-unused-expression
!hasValidProfile && lib.noCredentialsSet()
}
}
async function addResources(appPath: string, itemsBucket?: string) {
const provideAssetFolder = new ProvideAssetFolder(appPath)
await provideAssetFolder.execute()
// tslint:disable-next-line:no-unused-expression
itemsBucket && (await replaceS3BucketName(appPath, itemsBucket))
}
async function replaceS3BucketName(appPath: string, itemsBucket: string) {
const lambdaPath = `.seagull/deploy/dist/assets/backend/lambda.js`
const lambdaFile = `${appPath}/${lambdaPath}`
const lambda = await new FS.ReadFile(lambdaFile).execute()
const lambdaWithBucketName = lambda.replace('demo-bucket', itemsBucket)
await new FS.WriteFile(lambdaFile, lambdaWithBucketName).execute()
}
async function buildCronJson(appPath: string) {
const cronPath = `${appPath}/dist/cron.json`
const cronFile = await new FS.ReadFile(cronPath).execute()
return cronFile && cronFile !== '' ? JSON.parse(cronFile) : []
}
async function getEnv(
name: string,
appPath: string,
stage: string,
logBucket: string
) {
const env: any = {
APP: name,
LOG_BUCKET: logBucket,
MODE: 'cloud',
NODE_ENV: 'production',
STAGE: stage,
}
const configPath = `${appPath}/.env.${stage}`
const config: string = await new FS.ReadFile(configPath).execute()
const configEnv = dotenv.parse(config)
return {
...env,
...configEnv,
}
}