Last active
February 3, 2022 21:17
-
-
Save maxcan/c10e48ebfe6d9b19e22dc67ab9a41860 to your computer and use it in GitHub Desktop.
pulumi - fargate / s3 / lambda
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// https://github.com/pulumi/examples/blob/master/aws-ts-pulumi-miniflux/index.ts | |
import * as pulumi from "@pulumi/pulumi"; | |
import * as mime from "mime"; | |
import * as aws from "@pulumi/aws"; | |
import * as awsx from "@pulumi/awsx"; | |
import { PrivateVirtualInterface } from "@pulumi/aws/directconnect"; | |
import { Output } from "@pulumi/pulumi"; | |
import { AnyTxtRecord } from "dns"; | |
import { GraphQLApi } from "@pulumi/aws/appsync"; | |
import { input } from "@pulumi/aws/types"; | |
import { lambda } from "@pulumi/aws/types/enums"; | |
import { | |
getDomainAndSubdomain, | |
createWWWAliasRecord, | |
createAliasRecord, | |
tenMinutes, | |
} from "./util"; | |
const config = new pulumi.Config(); | |
const awsConfig = new pulumi.Config("aws"); | |
const awsRegion = awsConfig.get("region"); | |
const env = config.require("ENV"); | |
const isProduction = env.trim().toLowerCase() === "production"; | |
const rootDomain = config.require("ROOT_DOMAIN"); | |
const subdomainAppAdmin = config.require("SUBDOMAIN_APP_ADMIN"); | |
const mkFqdn = (s: string) => `${s}${envFqdnComp}.${rootDomain}`; | |
const fqdnAppClient = isProduction | |
? rootDomain | |
: mkFqdn(config.require("SUBDOMAIN_APP_CLIENT")); | |
const name = config.require("NAME"); | |
const dbUser = config.require("HASURA_DB_USERNAME"); | |
const dbPass = config.requireSecret("HASURA_DB_PASSWORD"); | |
// const dbPass = config.require("HASURA_DB_PASSWORD"); | |
const dbName = `hasura${env}`; | |
const mediaBucketName = config.require("AWS_MEDIA_BUCKET"); | |
// Create an AWS resource (S3 Bucket) | |
const mediaBucket = new aws.s3.Bucket(mediaBucketName, { | |
bucket: mediaBucketName, | |
// website: { | |
// indexDocument: "index.html", | |
// }, | |
}); | |
const vpc = new awsx.ec2.Vpc(`hasura-${env}`, {}); | |
const cluster = new awsx.ecs.Cluster(`hasura-${env}`, { | |
name: `hasura-${env}-${name}`, | |
vpc, | |
}); | |
const awsEastProvider = new aws.Provider(`aws-east-provider`, { | |
profile: aws.config.profile, | |
region: "us-east-1", // Per AWS, ACM certificate must be in the us-east-1 region. | |
}); | |
console.log(rootDomain); | |
const envFqdnComp = isProduction ? "" : pulumi.interpolate`-${env}`; | |
// if config.includeWWW include required subjectAlternativeNames to support the www subdomain | |
const certificateConfig: aws.acm.CertificateArgs = { | |
domainName: rootDomain, | |
validationMethod: "DNS", | |
subjectAlternativeNames: [pulumi.interpolate`*.${rootDomain}`], | |
}; | |
const certificate = new aws.acm.Certificate( | |
`certificate-${env}`, | |
certificateConfig, | |
{ | |
provider: awsEastProvider, | |
} | |
); | |
const domainParts = getDomainAndSubdomain(rootDomain); | |
console.log("🚀 ~ file: index.ts ~ line 73 ~ domainParts", domainParts); | |
const hostedZoneId = aws.route53 | |
.getZone({ name: domainParts.parentDomain }, { async: true }) | |
.then((zone) => zone.zoneId); | |
/** | |
* Create a DNS record to prove that we _own_ the domain we're requesting a certificate for. | |
* See https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-dns.html for more info. | |
*/ | |
// const certificateValidationDomain = new aws.route53.Record( | |
// `${config.require("ROOT_DOMAIN")}-validation`, | |
// { | |
// name: certificate.domainValidationOptions[0].resourceRecordName, | |
// zoneId: hostedZoneId, | |
// type: certificate.domainValidationOptions[0].resourceRecordType, | |
// records: [certificate.domainValidationOptions[0].resourceRecordValue], | |
// ttl: 60 * 3, | |
// } | |
// ); | |
// certificateArn = certificateValidation.certificateArn; | |
// https://www.pulumi.com/docs/guides/crosswalk/aws/api-gateway/ | |
const certValidationRecords: Output<aws.route53.Record[]> = | |
certificate.domainValidationOptions.apply((domainValidationOptions) => { | |
const opts = domainValidationOptions | |
.filter( | |
(dvo, idx) => | |
domainValidationOptions | |
.map((d) => d.resourceRecordName) | |
.indexOf(dvo.resourceRecordName) == idx | |
) | |
.map((dvo) => { | |
return new aws.route53.Record( | |
`validation-${env}-${dvo.resourceRecordValue}`, | |
{ | |
allowOverwrite: true, | |
name: dvo.resourceRecordName, | |
records: [dvo.resourceRecordValue], | |
ttl: 60, | |
type: dvo.resourceRecordType, | |
zoneId: hostedZoneId, | |
} | |
); | |
}); | |
return opts; | |
}); | |
const sslCertificateValidation = certValidationRecords.apply( | |
(validations) => | |
new aws.acm.CertificateValidation( | |
`sslCertificateValidation-${env}`, | |
{ | |
certificateArn: certificate.arn, | |
validationRecordFqdns: validations.map( | |
(exampleRecord) => exampleRecord.fqdn | |
), | |
}, | |
{ provider: awsEastProvider } | |
) | |
); | |
// Configure an edge-optimized domain for our API Gateway. This will configure a Cloudfront CDN | |
// distribution behind the scenes and serve our API Gateway at a custom domain name over SSL. | |
// const webDomain = new aws.apigateway.DomainName("webCdn", { | |
// certificateArn: sslCertificateValidation.certificateArn, | |
// domainName: domain, | |
// }); | |
// const webDomainMapping = new aws.apigateway.BasePathMapping( | |
// "webDomainMapping", | |
// { | |
// restApi: web.restAPI, | |
// stageName: web.stage.stageName, | |
// domainName: webDomain.id, | |
// } | |
// ); | |
// Create a new subnet group for the database. | |
const subnetGroup = new aws.rds.SubnetGroup(`dbsubnets-${env}`, { | |
subnetIds: vpc.publicSubnetIds, | |
}); | |
// Create a new database, using the subnet and cluster groups. | |
const db = new aws.rds.Instance(`hasura-${env}`, { | |
engine: "postgres", | |
instanceClass: aws.rds.InstanceTypes.T3_Micro, | |
allocatedStorage: 5, | |
dbSubnetGroupName: subnetGroup.id, | |
vpcSecurityGroupIds: cluster.securityGroups.map((g) => g.id), | |
name: dbName, | |
username: dbUser, | |
password: pulumi.interpolate`${dbPass}`, | |
skipFinalSnapshot: true, | |
}); | |
// Assemble a connection string for the Miniflux service. | |
const connectionString = pulumi.interpolate`postgres://${dbUser}:${dbPass}@${db.endpoint}/${dbName}?sslmode=disable`; | |
const allowHttpsAndEightyEighty = new aws.ec2.SecurityGroup(`lb-sg-01-${env}`, { | |
name: `lb-sg-${env}-${(Math.random() + "9").substring(4, 10)}`, | |
vpcId: vpc.id, | |
ingress: [ | |
{ | |
description: `https-${name}-${env}`, | |
fromPort: 443, | |
toPort: 443, | |
protocol: "tcp", | |
cidrBlocks: ["0.0.0.0/0"], | |
}, | |
], | |
egress: [ | |
{ | |
description: `https-internal-${name}-${env}`, | |
fromPort: 8080, | |
toPort: 8080, | |
protocol: "tcp", | |
cidrBlocks: ["0.0.0.0/0"], | |
}, | |
{ | |
description: `https-${name}-${env}`, | |
fromPort: 443, | |
toPort: 443, | |
protocol: "tcp", | |
cidrBlocks: ["0.0.0.0/0"], | |
}, | |
], | |
}); | |
// Create an NetworkListener to forward HTTP traffic on port 8080. | |
const lb = new awsx.lb.ApplicationLoadBalancer(`hsra-2-${env}`, { | |
vpc, | |
securityGroups: [allowHttpsAndEightyEighty.id], | |
}); | |
const atg = lb.createTargetGroup(`hsra-tg-${env}`, { | |
port: 8080, | |
protocol: "HTTP", | |
deregistrationDelay: 90, | |
healthCheck: { | |
unhealthyThreshold: 5, | |
timeout: 20, | |
path: "/healthz", | |
}, | |
}); | |
const listener = atg.createListener(`hsra-2-listener-${env}`, { | |
protocol: "HTTPS", | |
vpc, | |
certificateArn: sslCertificateValidation.certificateArn, | |
}); | |
// const atg = lb.createTargetGroup(`hasura-${env}`, { | |
// port: 8000, | |
// deregistrationDelay: 0, | |
// }); | |
const hasuraDnsRecord = new aws.route53.Record( | |
`hasura-${env}`, | |
{ | |
name: pulumi.interpolate`${config.require( | |
"SUBDOMAIN_HASURA" | |
)}.${rootDomain}`, | |
type: "CNAME", | |
zoneId: hostedZoneId, | |
ttl: 60, | |
records: [listener.endpoint.hostname], | |
}, | |
{ dependsOn: sslCertificateValidation } | |
); | |
// const hasuraImage = awsx.ecs.Image.fromDockerBuild( | |
// "hasura-migrations-img-x86", | |
// { | |
// context: "./hasura-docker", | |
// args: { | |
// "--platform": "linux/amd64,linux/arm64", | |
// }, | |
// } | |
// ); | |
const hasuraImage = awsx.ecs.Image.fromPath( | |
"hasura-migrations-img-x86-2", | |
"./hasura-docker" | |
); | |
const hasuraKeyType = config.require("HASURA_JWT_KEY_TYPE"); | |
const hasuraJwtKey = config.requireSecret("HASURA_JWT_KEY"); | |
const hasuraAdminSecret = config.requireSecret("HASURA_GRAPHQL_ADMIN_SECRET"); | |
// Give our Lambda access to the Dynamo DB table, CloudWatch Logs and Metrics. | |
const role = new aws.iam.Role("mylambda-role", { | |
assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({ | |
Service: "lambda.amazonaws.com", | |
}), | |
}); | |
const policy = new aws.iam.RolePolicy(`mylambda-policy-${env}`, { | |
role, | |
policy: pulumi.output({ | |
Version: "2012-10-17", | |
Statement: [ | |
{ | |
Action: ["s3:*", "cloudwatch:*", "logs:*"], | |
Resource: "*", | |
Effect: "Allow", | |
}, | |
], | |
}), | |
}); | |
const lambdaEnv: pulumi.Input<input.lambda.FunctionEnvironment> = { | |
variables: { | |
APPLE_CONNECT_SHARED_SECRET: config.requireSecret( | |
"APPLE_CONNECT_SHARED_SECRET" | |
), | |
ADMIN_EMAIL_ALLOWED: config.require("ADMIN_EMAIL_ALLOWED"), | |
ADMIN_EMAIL_SOURCE: config.require("ADMIN_EMAIL_SOURCE"), | |
AWS_MEDIA_BUCKET: config.require("AWS_MEDIA_BUCKET"), | |
ENV: config.require("ENV"), | |
HASURA_GRAPHQL_ADMIN_SECRET: config.requireSecret( | |
"HASURA_GRAPHQL_ADMIN_SECRET" | |
), | |
HASURA_JWT_KEY: config.requireSecret("HASURA_JWT_KEY"), | |
HASURA_JWT_KEY_TYPE: config.require("HASURA_JWT_KEY_TYPE"), | |
LAMBDA_LOCAL_PORT: config.require("LAMBDA_LOCAL_PORT"), | |
NODE_ENV: config.require("ENV"), | |
REACT_APP_BASE_URI: config.require("REACT_APP_BASE_URI"), | |
REACT_APP_HASURA_ENDPOINT: config.require("REACT_APP_HASURA_ENDPOINT"), | |
TWILIO_AUTH_TOKEN: config.requireSecret("TWILIO_AUTH_TOKEN"), | |
TWILIO_FROM: config.require("TWILIO_FROM"), | |
TWILIO_SID: config.requireSecret("TWILIO_SID"), | |
}, | |
}; | |
const lambdaRoutes = { | |
events: "hasura_event_handler", | |
gql: "graphql", | |
ping: "ping", | |
}; | |
const lambdaApi = new awsx.apigateway.API(`lambda-${env}-${name}`, { | |
stageName: env, | |
routes: [ | |
{ | |
path: `/${lambdaRoutes.events}`, | |
method: "POST", | |
eventHandler: new aws.lambda.Function(`hasura-events-${env}`, { | |
runtime: aws.lambda.Runtime.NodeJS14dX, | |
code: new pulumi.asset.FileArchive( | |
`../backend-lambdas/.webpack/${config.require( | |
"NAME" | |
)}-${env}-lambdas.zip` | |
), | |
handler: "src/functions/hasura_event_handler/handler.main", | |
role: role.arn, | |
environment: lambdaEnv, | |
}), | |
}, | |
{ | |
path: `/${lambdaRoutes.gql}`, | |
method: "POST", | |
eventHandler: new aws.lambda.Function(`graphql-${env}`, { | |
runtime: aws.lambda.Runtime.NodeJS14dX, | |
code: new pulumi.asset.FileArchive( | |
`../backend-lambdas/.webpack/${config.require( | |
"NAME" | |
)}-${env}-lambdas.zip` | |
), | |
handler: "src/functions/graphql/handler.main", | |
role: role.arn, | |
environment: lambdaEnv, | |
}), | |
}, | |
{ | |
path: `/${lambdaRoutes.ping}`, | |
method: "POST", | |
eventHandler: new aws.lambda.Function(`ping-${env}`, { | |
runtime: aws.lambda.Runtime.NodeJS14dX, | |
code: new pulumi.asset.FileArchive( | |
`../backend-lambdas/.webpack/${config.require( | |
"NAME" | |
)}-${env}-lambdas.zip` | |
), | |
handler: "src/functions/ping/handler.main", | |
role: role.arn, | |
environment: lambdaEnv, | |
}), | |
}, | |
], | |
}); | |
const logGroup = new aws.cloudwatch.LogGroup(`hasura-${env}`); | |
// Create a Fargate service consisting of just one container instance (since that's all we | |
// really need), passing it the cluster, DB connection and Pulumi config settings. | |
const hasuraService = new awsx.ecs.FargateService( | |
`hasura-${env}`, | |
{ | |
cluster, | |
desiredCount: 1, | |
taskDefinitionArgs: { | |
logGroup: logGroup, | |
cpu: "1024", | |
memory: "2048", | |
containers: { | |
[`hasura-${env}`]: { | |
// logConfiguration: { | |
// logDriver: "awslogs", | |
// options: { | |
// "awslogs-group": logGroup.id, | |
// "awslogs-region": awsRegion!, | |
// "awslogs-stream-prefix": `hasura-${env}`, | |
// }, | |
// }, | |
image: hasuraImage, | |
portMappings: [listener], | |
environment: [ | |
{ | |
name: "LAMBDA_HASURA_EVENT_HANDLER", | |
value: pulumi.interpolate`${lambdaApi.url}${lambdaRoutes.events}`, | |
}, | |
{ | |
name: "LAMBDA_ENDPOINT", | |
value: pulumi.interpolate`${lambdaApi.url}${lambdaRoutes.gql}`, | |
}, | |
// { name: "HASURA_GRAPHQL_LOG_LEVEL", value: "debug" }, | |
{ name: "HASURA_GRAPHQL_DATABASE_URL", value: connectionString }, | |
{ | |
name: "HASURA_GRAPHQL_ENABLE_ALLOWLIST", | |
value: "true", | |
}, | |
{ | |
name: "HASURA_GRAPHQL_ENABLE_CONSOLE", | |
value: isProduction ? "false" : "true", | |
}, | |
{ | |
name: "HASURA_GRAPHQL_ENABLED_APIS", | |
value: "graphql,metadata", | |
}, | |
{ | |
name: "HASURA_GRAPHQL_ENABLE_DEV_MODE", | |
value: isProduction ? "false" : "true", | |
}, | |
{ | |
name: "HASURA_GRAPHQL_ENABLED_LOG_TYPES", | |
value: "startup, http-log, webhook-log, websocket-log, query-log", | |
}, | |
{ | |
name: "HASURA_GRAPHQL_ADMIN_SECRET", | |
value: hasuraAdminSecret, | |
}, | |
{ | |
name: "HASURA_GRAPHQL_JWT_SECRET", | |
value: pulumi.interpolate`{"type": "${hasuraKeyType}", "key": "${hasuraJwtKey}"}`, | |
}, | |
{ name: "HASURA_GRAPHQL_UNAUTHORIZED_ROLE", value: "anonymous" }, | |
{ name: "HASURA_GRAPHQL_MIGRATIONS_DIR", value: "/migrations" }, | |
{ name: "HASURA_GRAPHQL_METADATA_DIR", value: "/metadata" }, | |
], | |
}, | |
}, | |
}, | |
}, | |
{ | |
customTimeouts: { | |
create: "20m", | |
update: "20m", | |
delete: "20m", | |
}, | |
} | |
); | |
// const bucketObject = new aws.s3.BucketObject("index.html", { | |
// acl: "public-read", | |
// contentType: "text/html", | |
// bucket: mediaBucket, | |
// source: new pulumi.asset.FileAsset("index.html"), | |
// }); | |
// const hasuraLoadBalancer = new awsx.elasticloadbalancingv2.NetworkListener( | |
// "hasura", | |
// { port: 80 } | |
// ); | |
// Export the name of the bucket | |
export const bucketName = mediaBucket.id; | |
// export const bucketEndpoint = pulumi.interpolate`http://${mediaBucket.websiteEndpoint}`; | |
export const dbStr = connectionString; | |
export const lambdaGql = lambdaApi.url.apply( | |
(u) => pulumi.interpolate`${u}${lambdaRoutes.gql}` | |
); | |
export const hasura = hasuraDnsRecord.name; | |
// START REACT_ADMIN | |
// Generate Origin Access Identity to access the private s3 bucket. | |
const originAccessIdentityAdmin = new aws.cloudfront.OriginAccessIdentity( | |
"originAccessIdentityAdmin", | |
{ | |
comment: "this is needed to setup s3 polices and make s3 not public.", | |
} | |
); | |
const fqdnAppAdmin = mkFqdn(subdomainAppAdmin); | |
// Need to fix the bucket policy so that the principle is "*" | |
const contentBucketAdmin = new aws.s3.Bucket( | |
`${env}-admin-${rootDomain}`, | |
// `${env}-admin-${rootDomain}-static-web`, | |
{ | |
// bucket: `${env}-admin-${rootDomain}-static-web`, // fqdnAppAdmin, | |
// Configure S3 to serve bucket contents as a website. This way S3 will automatically convert | |
// requests for "foo/" to "foo/index.html". | |
website: { | |
indexDocument: "index.html", | |
errorDocument: "404.html", | |
}, | |
acl: "public-read", | |
corsRules: [ | |
{ | |
allowedHeaders: ["*"], | |
allowedMethods: ["PUT", "POST"], | |
allowedOrigins: ["*"], | |
exposeHeaders: ["ETag"], | |
maxAgeSeconds: 3000, | |
}, | |
], | |
} | |
); | |
// logsBucket is an S3 bucket that will contain the CDN's request logs. | |
const wwwAccessLogsAdmin = new aws.s3.Bucket( | |
`${env}-admin-${rootDomain}-logs`, | |
{ | |
// bucket: pulumi.interpolate`${fqdnAppAdmin}-logs`, | |
acl: "private", | |
} | |
); | |
// distributionArgs configures the CloudFront distribution. Relevant documentation: | |
// https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html | |
// https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html | |
const distributionArgsAdmin: aws.cloudfront.DistributionArgs = { | |
enabled: true, | |
// Alternate aliases the CloudFront distribution can be reached at, in addition to https://xxxx.cloudfront.net. | |
// Required if you want to access the distribution via config.targetDomain as well. | |
aliases: [fqdnAppAdmin], | |
// We only specify one origin for this distribution, the S3 content bucket. | |
origins: [ | |
{ | |
originId: contentBucketAdmin.arn, | |
domainName: contentBucketAdmin.websiteEndpoint, | |
customOriginConfig: { | |
httpPort: 80, | |
httpsPort: 443, | |
originProtocolPolicy: "http-only", | |
originSslProtocols: ["SSLv3", "TLSv1.2"], | |
}, | |
// s3OriginConfig: { | |
// originAccessIdentity: | |
// originAccessIdentityAdmin.cloudfrontAccessIdentityPath, | |
// }, | |
}, | |
], | |
defaultRootObject: "index.html", | |
// A CloudFront distribution can configure different cache behaviors based on the request path. | |
// Here we just specify a single, default cache behavior which is just read-only requests to S3. | |
defaultCacheBehavior: { | |
targetOriginId: contentBucketAdmin.arn, | |
viewerProtocolPolicy: "redirect-to-https", | |
allowedMethods: ["GET", "HEAD", "OPTIONS"], | |
cachedMethods: ["GET", "HEAD", "OPTIONS"], | |
forwardedValues: { | |
cookies: { forward: "none" }, | |
queryString: false, | |
}, | |
minTtl: 0, | |
defaultTtl: tenMinutes, | |
maxTtl: tenMinutes, | |
}, | |
// "All" is the most broad distribution, and also the most expensive. | |
// "100" is the least broad, and also the least expensive. | |
priceClass: "PriceClass_100", | |
// You can customize error responses. When CloudFront receives an error from the origin (e.g. S3 or some other | |
// web service) it can return a different error code, and return the response for a different resource. | |
customErrorResponses: [ | |
{ errorCode: 404, responseCode: 404, responsePagePath: "/404.html" }, | |
], | |
restrictions: { | |
geoRestriction: { | |
restrictionType: "none", | |
}, | |
}, | |
viewerCertificate: { | |
acmCertificateArn: certificate.arn, // Per AWS, ACM certificate must be in the us-east-1 region. | |
sslSupportMethod: "sni-only", | |
}, | |
loggingConfig: { | |
bucket: wwwAccessLogsAdmin.bucketDomainName, | |
includeCookies: false, | |
prefix: fqdnAppAdmin, | |
}, | |
}; | |
const cdnAdmin = new aws.cloudfront.Distribution( | |
"cdn-admin", | |
distributionArgsAdmin | |
); | |
// Creates a new Route53 DNS record pointing the domain to the CloudFront distribution. | |
const bucketPolicyAdmin = new aws.s3.BucketPolicy(`www-access-admin-${env}`, { | |
bucket: contentBucketAdmin.id, // refer to the bucket created earlier | |
policy: pulumi | |
.all([originAccessIdentityAdmin.iamArn, contentBucketAdmin.arn]) | |
.apply(([oaiArn, bucketArn]) => | |
JSON.stringify({ | |
Version: "2012-10-17", | |
Statement: [ | |
{ | |
Effect: "Allow", | |
Principal: { | |
AWS: oaiArn, | |
}, // Only allow Cloudfront read access. | |
Action: ["s3:GetObject"], | |
Resource: [`${bucketArn}/*`], // Give Cloudfront access to the entire bucket. | |
}, | |
], | |
}) | |
), | |
}); | |
const aRecordAdmin = createAliasRecord(fqdnAppAdmin, cdnAdmin); | |
// if (config.includeWWW) { | |
// const cnameRecord = createWWWAliasRecord(config.targetDomain, cdn); | |
// } | |
// Export properties from this stack. This prints them at the end of `pulumi up` and | |
// makes them easier to access from the pulumi.com. | |
export const contentBucketAdminUri = pulumi.interpolate`s3://${contentBucketAdmin.bucket}`; | |
export const contentBucketAdminWebsiteEndpoint = | |
contentBucketAdmin.websiteEndpoint; | |
export const cloudFrontDomainAdmin = cdnAdmin.domainName; | |
export const targetDomainEndpointAdmin = `https://${fqdnAppAdmin}/`; | |
// END REACT_ADMIN STATIC SITE | |
// START REACT_CLIENT STATIC SITE | |
const contentBucketClient = new aws.s3.Bucket( | |
`${env}-client-${rootDomain}-static-web`, | |
{ | |
// bucket: `${env}-client-${rootDomain}-static-web`, | |
// Configure S3 to serve bucket contents as a website. This way S3 will automatically convert | |
// requests for "foo/" to "foo/index.html". | |
website: { | |
indexDocument: "index.html", | |
errorDocument: "404.html", | |
}, | |
acl: "public-read", | |
corsRules: [ | |
{ | |
allowedHeaders: ["*"], | |
allowedMethods: ["PUT", "POST"], | |
allowedOrigins: ["*"], | |
exposeHeaders: ["ETag"], | |
maxAgeSeconds: 3000, | |
}, | |
], | |
} | |
); | |
// logsBucket is an S3 bucket that will contain the CDN's request logs. | |
const wwwAccessLogsClient = new aws.s3.Bucket( | |
`${env}-client-${rootDomain}-logs`, | |
{ | |
// bucket: pulumi.interpolate`${fqdnAppClient}-logs`, | |
acl: "private", | |
} | |
); | |
// distributionArgs configures the CloudFront distribution. Relevant documentation: | |
// https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html | |
// https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html | |
const distributionArgsClient: aws.cloudfront.DistributionArgs = { | |
enabled: true, | |
// Alternate aliases the CloudFront distribution can be reached at, in addition to https://xxxx.cloudfront.net. | |
// Required if you want to access the distribution via config.targetDomain as well. | |
aliases: [fqdnAppClient, `www.${fqdnAppClient}`], | |
// We only specify one origin for this distribution, the S3 content bucket. | |
origins: [ | |
{ | |
originId: contentBucketClient.arn, | |
domainName: contentBucketClient.websiteEndpoint, | |
customOriginConfig: { | |
httpPort: 80, | |
httpsPort: 443, | |
originProtocolPolicy: "http-only", | |
originSslProtocols: ["SSLv3", "TLSv1.2"], | |
}, | |
// s3OriginConfig: { | |
// originAccessIdentity: | |
// originAccessIdentityAdmin.cloudfrontAccessIdentityPath, | |
// }, | |
}, | |
], | |
defaultRootObject: "index.html", | |
// A CloudFront distribution can configure different cache behaviors based on the request path. | |
// Here we just specify a single, default cache behavior which is just read-only requests to S3. | |
defaultCacheBehavior: { | |
targetOriginId: contentBucketClient.arn, | |
viewerProtocolPolicy: "redirect-to-https", | |
allowedMethods: ["GET", "HEAD", "OPTIONS"], | |
cachedMethods: ["GET", "HEAD", "OPTIONS"], | |
forwardedValues: { | |
cookies: { forward: "none" }, | |
queryString: false, | |
}, | |
minTtl: 0, | |
defaultTtl: tenMinutes, | |
maxTtl: tenMinutes, | |
}, | |
// "All" is the most broad distribution, and also the most expensive. | |
// "100" is the least broad, and also the least expensive. | |
priceClass: "PriceClass_100", | |
// You can customize error responses. When CloudFront receives an error from the origin (e.g. S3 or some other | |
// web service) it can return a different error code, and return the response for a different resource. | |
customErrorResponses: [ | |
{ errorCode: 404, responseCode: 404, responsePagePath: "/404.html" }, | |
], | |
restrictions: { | |
geoRestriction: { | |
restrictionType: "none", | |
}, | |
}, | |
viewerCertificate: { | |
acmCertificateArn: certificate.arn, // Per AWS, ACM certificate must be in the us-east-1 region. | |
sslSupportMethod: "sni-only", | |
}, | |
loggingConfig: { | |
bucket: wwwAccessLogsClient.bucketDomainName, | |
includeCookies: false, | |
prefix: fqdnAppClient, | |
}, | |
}; | |
const cdnClient = new aws.cloudfront.Distribution( | |
"cdn-client", | |
distributionArgsClient | |
); | |
// Creates a new Route53 DNS record pointing the domain to the CloudFront distribution. | |
const bucketPolicyClient = new aws.s3.BucketPolicy(`www-access-client-${env}`, { | |
bucket: contentBucketClient.id, // refer to the bucket created earlier | |
policy: pulumi | |
.all([originAccessIdentityAdmin.iamArn, contentBucketClient.arn]) | |
.apply(([oaiArn, bucketArn]) => | |
JSON.stringify({ | |
Version: "2012-10-17", | |
Statement: [ | |
{ | |
Effect: "Allow", | |
Principal: { | |
AWS: oaiArn, | |
}, // Only allow Cloudfront read access. | |
Action: ["s3:GetObject"], | |
Resource: [`${bucketArn}/*`], // Give Cloudfront access to the entire bucket. | |
}, | |
], | |
}) | |
), | |
}); | |
const aRecordClient = createAliasRecord(fqdnAppClient, cdnClient); | |
const cnameRecord = createWWWAliasRecord(fqdnAppClient, cdnClient); | |
// Export properties from this stack. This prints them at the end of `pulumi up` and | |
// makes them easier to access from the pulumi.com. | |
export const contentBucketClientUri = pulumi.interpolate`s3://${contentBucketClient.bucket}`; | |
export const contentBucketClientWebsiteEndpoint = | |
contentBucketClient.websiteEndpoint; | |
export const cloudFrontDomainClient = cdnClient.domainName; | |
export const targetDomainEndpointClient = `https://${fqdnAppClient}/`; | |
// END REACT_CLIENT STATIC SITE |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment