scripts/aws/cloudformation/tasking-manager.template.js (740 lines of code) (raw):

const cf = require('/usr/local/lib/node_modules/@mapbox/cloudfriend'); const Parameters = { GitSha: { Type: 'String', }, NetworkEnvironment: { Type: 'String', AllowedValues: ['staging', 'production'] }, AutoscalingPolicy: { Type: 'String', AllowedValues: ['development', 'demo', 'production'], Description: "development: min 1, max 1 instance; demo: min 1 max 3 instances; production: min 2 max 9 instances" }, DBSnapshot: { Type: 'String', Description: 'Specify an RDS snapshot ID, if you want to create the DB from a snapshot.', Default: '' }, DatabaseDump: { Type: 'String', Description: 'Path to database dump on S3; Ex: s3://my-bkt/tm.sql' }, NewRelicLicense: { Type: 'String', Description: 'NEW_RELIC_LICENSE', }, PostgresDB: { Type: 'String', Description: 'POSTGRES_DB', }, PostgresPassword: { Type: 'String', Description: 'POSTGRES_PASSWORD', }, PostgresUser: { Type: 'String', Description: 'POSTGRES_USER', }, DatabaseSize: { Description: 'Database size in GB', Type: 'String', Default: '100' }, ELBSubnets: { Description: 'ELB subnets', Type: 'String', }, CloudfrontSSLCertificateIdentifier: { Type: 'String', Description: 'SSL certificate for HTTPS protocol used for Cloudfront (must be in us-east-1)', }, LoadBalancerSSLCertificateIdentifier: { Type: 'String', Description: 'SSL certificate for HTTPS protocol', }, TaskingManagerLogDirectory: { Description: 'TM_LOG_DIR environment variable', Type: 'String', }, TaskingManagerConsumerKey: { Description: 'TM_CONSUMER_KEY', Type: 'String', }, TaskingManagerConsumerSecret: { Description: 'TM_CONSUMER_SECRET', Type: 'String', }, TaskingManagerSecret: { Description: 'TM_SECRET', Type: 'String', }, TaskingManagerAppBaseUrl: { Type: 'String', Description: 'TM_APP_BASE_URL; Ex: https://example.hotosm.org', }, TaskingManagerEmailFromAddress: { Description: 'TM_EMAIL_FROM_ADDRESS', Type: 'String', }, TaskingManagerEmailContactAddress: { Description: 'TM_EMAIL_CONTACT_ADDRESS', Type: 'String', }, TaskingManagerLogLevel: { Description: 'TM_LOG_LEVEL', Type: 'String', Default: 'INFO' }, TaskingManagerImageUploadAPIURL: { Description: 'URL for image upload service', Type: 'String' }, TaskingManagerImageUploadAPIKey: { Description: 'API Key for image upload service', Type: 'String' }, TaskingManagerSMTPHost: { Description: 'TM_SMTP_HOST environment variable', Type: 'String', }, TaskingManagerSMTPPassword: { Description: 'TM_SMTP_PASSWORD environment variable', Type: 'String' }, TaskingManagerSMTPUser: { Description: 'TM_SMTP_USER environment variable', Type: 'String' }, TaskingManagerSMTPPort: { Description: 'TM_SMTP_PORT environment variable', Type: 'String' }, TaskingManagerDefaultChangesetComment: { Description: 'TM_DEFAULT_CHANGESET_COMMENT environment variable', Type: 'String', }, TaskingManagerURL: { Description: 'URL for setting CNAME in Distribution; Ex: example.hotosm.org', Type: 'String', AllowedPattern: '^([a-zA-Z0-9-]*\\.){2}(\\w){2,20}$', ConstraintDescription: 'Parameter must be in the form of a url with subdomain.', }, TaskingManagerOrgName: { Description: 'Org Name', Type: 'String', }, TaskingManagerOrgCode: { Description: 'Org Code', Type: 'String', }, SentryBackendDSN: { Description: "DSN for sentry", Type: 'String' }, TaskingManagerLogo: { Description: "URL for logo", Type: "String" } }; const Conditions = { UseASnapshot: cf.notEquals(cf.ref('DBSnapshot'), ''), DatabaseDumpFileGiven: cf.notEquals(cf.ref('DatabaseDump'), ''), IsTaskingManagerProduction: cf.equals(cf.ref('AutoscalingPolicy'), 'production'), IsTaskingManagerDemo: cf.equals(cf.ref('AutoscalingPolicy'), 'Demo (max 3)'), IsTaskingManagerDevelopment: cf.equals(cf.ref('AutoscalingPolicy'), 'development'), IsHOTOSMUrl: cf.equals( cf.select('1', cf.split('.', cf.ref('TaskingManagerURL'))) , 'hotosm') }; const Resources = { TaskingManagerASG: { DependsOn: 'TaskingManagerLaunchConfiguration', Type: 'AWS::AutoScaling::AutoScalingGroup', Properties: { AutoScalingGroupName: cf.stackName, Cooldown: 300, MinSize: cf.if('IsTaskingManagerProduction', 3, 1), DesiredCapacity: cf.if('IsTaskingManagerProduction', 3, 1), MaxSize: cf.if('IsTaskingManagerProduction', 9, cf.if('IsTaskingManagerDemo', 3, 1)), HealthCheckGracePeriod: 600, LaunchConfigurationName: cf.ref('TaskingManagerLaunchConfiguration'), TargetGroupARNs: [cf.ref('TaskingManagerTargetGroup')], HealthCheckType: 'EC2', AvailabilityZones: ['us-west-1a', 'us-west-1b'], Tags: [{ Key: 'Name', PropagateAtLaunch: true, Value: cf.stackName }] }, UpdatePolicy: { AutoScalingRollingUpdate: { PauseTime: cf.if('IsTaskingManagerDevelopment', 'PT0S', 'PT60M'), MaxBatchSize: 2, WaitOnResourceSignals: cf.if('IsTaskingManagerDevelopment', false, true) } } }, TaskingManagerScaleUp: { Type: "AWS::AutoScaling::ScalingPolicy", Properties: { AutoScalingGroupName: cf.ref('TaskingManagerASG'), PolicyType: 'TargetTrackingScaling', TargetTrackingConfiguration: { TargetValue: 500, PredefinedMetricSpecification: { PredefinedMetricType: 'ALBRequestCountPerTarget', ResourceLabel: cf.join('/', [ cf.select(1, cf.split('loadbalancer/', cf.select(5, cf.split(':', cf.ref("TaskingManagerLoadBalancer")) ) ) ), cf.select(5, cf.split(':', cf.ref("TaskingManagerTargetGroup")) ) ]) } }, Cooldown: 300 } }, TaskingManagerLaunchConfiguration: { Type: "AWS::AutoScaling::LaunchConfiguration", Metadata: { "AWS::CloudFormation::Init": { "configSets": { "default": [ "01_setupCfnHup", "02_config-amazon-cloudwatch-agent", "03_restart_amazon-cloudwatch-agent" ], "UpdateEnvironment": [ "02_config-amazon-cloudwatch-agent", "03_restart_amazon-cloudwatch-agent" ] }, // Definition of json configuration of AmazonCloudWatchAgent, you can change the configuration below. "02_config-amazon-cloudwatch-agent": { "files": { '/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json': { "content": cf.join("\n", [ "{\"logs\": {", "\"logs_collected\": {", "\"files\": {", "\"collect_list\": [", "{", "\"file_path\": \"/opt/aws/amazon-cloudwatch-agent/logs/amazon-cloudwatch-agent.log\",", cf.sub("\"log_group_name\": \"${AWS::StackName}.log\","), cf.sub("\"log_stream_name\": \"${AWS::StackName}-cloudwatch-agent.log\","), "\"timezone\": \"UTC\"", "},", "{", cf.sub("\"file_path\": \"${TaskingManagerLogDirectory}/tasking-manager.log\","), cf.sub("\"log_group_name\": \"${AWS::StackName}.log\","), cf.sub("\"log_stream_name\": \"${AWS::StackName}.log\","), "\"timezone\": \"UTC\"", "},", "{", cf.sub("\"file_path\": \"${TaskingManagerLogDirectory}/gunicorn-access.log\","), cf.sub("\"log_group_name\": \"${AWS::StackName}.log\","), cf.sub("\"log_stream_name\": \"${AWS::StackName}-gunicorn.log\","), "\"timezone\": \"UTC\"", "}]}},", cf.sub("\"log_stream_name\": \"${AWS::StackName}-logs\","), "\"force_flush_interval\" : 15", "}}" ]) } } }, // Invoke amazon-cloudwatch-agent-ctl to restart the AmazonCloudWatchAgent. "03_restart_amazon-cloudwatch-agent": { "commands": { "01_stop_service": { "command": "/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a stop" }, "02_start_service": { "command": "/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -c file:/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json -s" } } }, // Cfn-hup setting, it is to monitor the change of metadata. // When there is change in the contents of json file in the metadata section, cfn-hup will call cfn-init to restart the AmazonCloudWatchAgent. "01_setupCfnHup": { "files": { "/etc/cfn/cfn-hup.conf": { "content": cf.join('\n', [ "[main]", cf.sub("stack=${!AWS::StackName}"), cf.sub("region=${!AWS::Region}"), "interval=1" ]), "mode": "000400", "owner": "root", "group": "root" }, "/etc/cfn/hooks.d/amazon-cloudwatch-agent-auto-reloader.conf": { "content": cf.join('\n', [ "[cfn-auto-reloader-hook]", "triggers=post.update", "path=Resources.EC2Instance.Metadata.AWS::CloudFormation::Init.02_config-amazon-cloudwatch-agent", cf.sub("action=cfn-init -v --stack ${AWS::StackName} --resource EC2Instance --region ${AWS::Region} --configsets UpdateEnvironment"), "runas=root" ]), "mode": "000400", "owner": "root", "group": "root" }, "/lib/systemd/system/cfn-hup.service": { "content": cf.join('\n', [ "[Unit]", "Description=cfn-hup daemon", "[Service]", "Type=simple", "ExecStart=/opt/aws/bin/cfn-hup", "Restart=always", "[Install]", "WantedBy=multi-user.target" ]) } }, "commands": { "01enable_cfn_hup": { "command": "systemctl enable cfn-hup.service" }, "02start_cfn_hup": { "command": "systemctl start cfn-hup.service" } } } } }, Properties: { IamInstanceProfile: cf.ref('TaskingManagerEC2InstanceProfile'), ImageId: 'ami-066c6938fb715719f', InstanceType: 'c5d.large', SecurityGroups: [cf.importValue(cf.join('-', ['mapwithai-network-production', cf.ref('NetworkEnvironment'), 'ec2s-security-group', cf.region]))], UserData: cf.userData([ '#!/bin/bash', 'set -x', 'export DEBIAN_FRONTEND=noninteractive', 'export LC_ALL="en_US.UTF-8"', 'export LC_CTYPE="en_US.UTF-8"', 'dpkg-reconfigure --frontend=noninteractive locales', 'sudo apt-get -y update', 'sudo DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" dist-upgrade', 'sudo apt-get -y install curl', 'wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -', 'sudo sh -c \'echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -sc)-pgdg main" > /etc/apt/sources.list.d/PostgreSQL.list\'', 'sudo apt-get update -y', 'sudo apt-get install -y postgresql-12', 'sudo apt-get -y install postgresql-12-postgis-3', 'sudo apt-get -y install postgresql-12-postgis-3-scripts', 'sudo apt-get -y install postgis', 'sudo apt-get -y install libpq-dev', 'sudo apt-get -y install libxml2', 'sudo apt-get -y install wget libxml2-dev', 'sudo apt-get -y install libgeos-3.8.0', 'sudo apt-get -y install libgeos-dev', 'sudo apt-get -y install libproj15', 'sudo apt-get -y install libproj-dev', 'sudo apt-get -y install python3-pip libgdal-dev libpq-dev python3-psycopg2 python3.8-venv', 'sudo apt-get -y install libjson-c-dev', 'sudo apt-get -y install git', 'sudo apt-get -y install awscli', 'sudo apt-get -y install ruby', 'pushd /home/ubuntu', 'wget https://aws-codedeploy-us-west-1.s3.us-west-1.amazonaws.com/latest/install', 'chmod +x ./install && sudo ./install auto', 'sudo systemctl start codedeploy-agent', 'popd', 'git clone --recursive https://github.com/facebookincubator/OSM-HOT-Tasking-Manager.git', 'cd OSM-HOT-Tasking-Manager/', cf.sub('git reset --hard ${GitSha}'), 'python3 -m venv ./venv', '. ./venv/bin/activate', 'pip install --upgrade pip', 'pip install -r requirements.txt', 'echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf', 'export LC_ALL=C', 'wget https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/amd64/latest/amazon-cloudwatch-agent.deb -O /tmp/amazon-cloudwatch-agent.deb', 'dpkg -i /tmp/amazon-cloudwatch-agent.deb', 'wget https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz', 'python3 -m easy_install --script-dir /opt/aws/bin aws-cfn-bootstrap-py3-latest.tar.gz', 'echo "Exporting environment variables:"', cf.sub('export NEW_RELIC_LICENSE=${NewRelicLicense}'), cf.join('', ['export POSTGRES_ENDPOINT=', cf.getAtt('TaskingManagerRDS', 'Endpoint.Address')]), cf.sub('export POSTGRES_DB=${PostgresDB}'), cf.sub('export POSTGRES_PASSWORD="${PostgresPassword}"'), cf.sub('export POSTGRES_USER="${PostgresUser}"'), cf.sub('export TM_APP_BASE_URL="${TaskingManagerAppBaseUrl}"'), cf.sub('export TM_ENVIRONMENT="${AWS::StackName}"'), cf.sub('export TM_CONSUMER_KEY="${TaskingManagerConsumerKey}"'), cf.sub('export TM_CONSUMER_SECRET="${TaskingManagerConsumerSecret}"'), cf.sub('export TM_SECRET="${TaskingManagerSecret}"'), cf.sub('export TM_SMTP_HOST="${TaskingManagerSMTPHost}"'), cf.sub('export TM_SMTP_PASSWORD="${TaskingManagerSMTPPassword}"'), cf.sub('export TM_SMTP_PORT="${TaskingManagerSMTPPort}"'), cf.sub('export TM_SMTP_USER="${TaskingManagerSMTPUser}"'), cf.sub('export TM_DEFAULT_CHANGESET_COMMENT="${TaskingManagerDefaultChangesetComment}"'), cf.sub('export TM_EMAIL_FROM_ADDRESS="${TaskingManagerEmailFromAddress}"'), cf.sub('export TM_EMAIL_CONTACT_ADDRESS="${TaskingManagerEmailContactAddress}"'), cf.sub('export TM_LOG_LEVEL="${TaskingManagerLogLevel}"'), cf.sub('export TM_LOG_DIR="${TaskingManagerLogDirectory}"'), cf.sub('export TM_ORG_NAME="${TaskingManagerOrgName}"'), cf.sub('export TM_ORG_CODE="${TaskingManagerOrgCode}"'), cf.sub('export TM_ORG_LOGO="${TaskingManagerLogo}"'), cf.sub('export TM_IMAGE_UPLOAD_API_URL="${TaskingManagerImageUploadAPIURL}"'), cf.sub('export TM_IMAGE_UPLOAD_API_KEY="${TaskingManagerImageUploadAPIKey}"'), 'psql "host=$POSTGRES_ENDPOINT dbname=$POSTGRES_DB user=$POSTGRES_USER password=$POSTGRES_PASSWORD" -c "CREATE EXTENSION IF NOT EXISTS postgis"', cf.if('DatabaseDumpFileGiven', cf.sub('aws s3 cp ${DatabaseDump} dump.sql; sudo -u postgres psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_ENDPOINT/$POSTGRES_DB" < dump.sql'), ''), './venv/bin/python3 manage.py db upgrade', 'echo "------------------------------------------------------------"', cf.sub('export NEW_RELIC_LICENSE_KEY="${NewRelicLicense}"'), cf.sub('export TM_SENTRY_BACKEND_DSN="${SentryBackendDSN}"'), 'export NEW_RELIC_ENVIRONMENT=$TM_ENVIRONMENT', cf.sub('NEW_RELIC_CONFIG_FILE=./scripts/aws/cloudformation/newrelic.ini newrelic-admin run-program gunicorn -b 0.0.0.0:8000 --worker-class gevent --workers 5 --timeout 179 --access-logfile ${TaskingManagerLogDirectory}/gunicorn-access.log --access-logformat \'%(h)s %(l)s %(u)s %(t)s \"%(r)s\" %(s)s %(b)s %(T)s \"%(f)s\" \"%(a)s\"\' manage:application &'), cf.sub('sudo /opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource TaskingManagerLaunchConfiguration --region ${AWS::Region} --configsets default'), cf.sub('/opt/aws/bin/cfn-signal --exit-code $? --region ${AWS::Region} --resource TaskingManagerASG --stack ${AWS::StackName}') ]), KeyName: 'tm4' } }, TaskingManagerEC2Role: { Type: 'AWS::IAM::Role', Properties: { AssumeRolePolicyDocument: { Version: "2012-10-17", Statement: [{ Effect: "Allow", Principal: { Service: ["ec2.amazonaws.com"] }, Action: ["sts:AssumeRole"] }] }, ManagedPolicyArns: [ 'arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforAWSCodeDeploy', 'arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy', 'arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore' ], Policies: [{ PolicyName: "RDSPolicy", PolicyDocument: { Version: "2012-10-17", Statement: [{ Action: ['rds:DescribeDBInstances'], Effect: 'Allow', Resource: ['arn:aws:rds:*'] }] } }, { PolicyName: "CloudFormationPermissions", PolicyDocument: { Version: "2012-10-17", Statement: [{ Action: [ 'cloudformation:SignalResource', 'cloudformation:DescribeStackResource' ], Effect: 'Allow', Resource: ['arn:aws:cloudformation:*'] }] } } ], RoleName: cf.join('-', [cf.stackName, 'ec2', 'role']) } }, TaskingManagerDatabaseDumpAccessRole: { Condition: 'DatabaseDumpFileGiven', Type: 'AWS::IAM::Role', Properties: { AssumeRolePolicyDocument: { Version: "2012-10-17", Statement: [{ Effect: "Allow", Principal: { Service: ["ec2.amazonaws.com"] }, Action: ["sts:AssumeRole"] }] }, Policies: [{ PolicyName: "RDSPolicy", PolicyDocument: { Version: "2012-10-17", Statement: [{ Action: ['rds:DescribeDBInstances'], Effect: 'Allow', Resource: ['arn:aws:rds:*'] }] } }, { PolicyName: "CloudFormationPermissions", PolicyDocument: { Version: "2012-10-17", Statement: [{ Action: [ 'cloudformation:SignalResource', 'cloudformation:DescribeStackResource' ], Effect: 'Allow', Resource: ['arn:aws:cloudformation:*'] }] } }, { PolicyName: "AccessToDatabaseDump", PolicyDocument: { Version: "2012-10-17", Statement: [{ Action: ['s3:ListBucket'], Effect: 'Allow', Resource: [cf.join('', ['arn:aws:s3:::', cf.select(0, cf.split('/', cf.select(1, cf.split('s3://', cf.ref('DatabaseDump')) ) ) ) ] )] }, { Action: [ 's3:GetObject', 's3:GetObjectAcl', 's3:ListObjects', 's3:ListBucket' ], Effect: 'Allow', Resource: [cf.join('', ['arn:aws:s3:::', cf.select(1, cf.split('s3://', cf.ref('DatabaseDump')) )] )] }] } }], RoleName: cf.join('-', [cf.stackName, 'ec2', 'database-dump-access', 'role']) } }, TaskingManagerEC2InstanceProfile: { Type: "AWS::IAM::InstanceProfile", Properties: { Roles: cf.if('DatabaseDumpFileGiven', [cf.ref('TaskingManagerDatabaseDumpAccessRole')], [cf.ref('TaskingManagerEC2Role')]), InstanceProfileName: cf.join('-', [cf.stackName, 'ec2', 'instance', 'profile']) } }, TaskingManagerLoadBalancer: { Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer', Properties: { Name: cf.stackName, SecurityGroups: [cf.importValue(cf.join('-', ['mapwithai-network-production', cf.ref('NetworkEnvironment'), 'elbs-security-group', cf.region]))], Subnets: cf.split(',', cf.ref('ELBSubnets')), Type: 'application' } }, TaskingManagerLoadBalancerRoute53: { Type: 'AWS::Route53::RecordSet', Properties: { Name: cf.join('-', [cf.stackName, 'api.mapwith.ai']), Type: 'A', AliasTarget: { DNSName: cf.getAtt('TaskingManagerLoadBalancer', 'DNSName'), HostedZoneId: cf.getAtt('TaskingManagerLoadBalancer', 'CanonicalHostedZoneID') }, HostedZoneId: 'Z101197737ML3WN063NTD', } }, TaskingManagerTargetGroup: { Type: 'AWS::ElasticLoadBalancingV2::TargetGroup', Properties: { HealthCheckIntervalSeconds: 60, HealthCheckPort: 8000, HealthCheckProtocol: 'HTTP', HealthCheckTimeoutSeconds: 10, HealthyThresholdCount: 3, UnhealthyThresholdCount: 3, HealthCheckPath: '/api/v2/system/heartbeat/', Port: 8000, Protocol: 'HTTP', VpcId: cf.importValue(cf.join('-', ['mapwithai-network-production', 'default-vpc', cf.region])), Tags: [{ "Key": "stack_name", "Value": cf.stackName }], Matcher: { HttpCode: '200,202,302,304' } } }, TaskingManagerLoadBalancerHTTPSListener: { Type: 'AWS::ElasticLoadBalancingV2::Listener', Properties: { Certificates: [{ CertificateArn: cf.arn('acm', cf.join('/', ['certificate', cf.ref('LoadBalancerSSLCertificateIdentifier')])) }], DefaultActions: [{ Type: 'forward', TargetGroupArn: cf.ref('TaskingManagerTargetGroup') }], LoadBalancerArn: cf.ref('TaskingManagerLoadBalancer'), Port: 443, Protocol: 'HTTPS', SslPolicy: 'ELBSecurityPolicy-FS-1-2-2019-08' } }, TaskingManagerLoadBalancerHTTPListener: { Type: 'AWS::ElasticLoadBalancingV2::Listener', Properties: { DefaultActions: [{ Type: 'redirect', RedirectConfig: { Protocol: 'HTTPS', Port: '443', Host: '#{host}', Path: '/#{path}', Query: '#{query}', StatusCode: 'HTTP_301' } }], LoadBalancerArn: cf.ref('TaskingManagerLoadBalancer'), Port: 80, Protocol: 'HTTP' } }, TaskingManagerRDS: { Type: 'AWS::RDS::DBInstance', Properties: { Engine: 'postgres', DBName: cf.if('UseASnapshot', cf.noValue, cf.ref('PostgresDB')), EngineVersion: '11.12', MasterUsername: cf.if('UseASnapshot', cf.noValue, cf.ref('PostgresUser')), MasterUserPassword: cf.if('UseASnapshot', cf.noValue, cf.ref('PostgresPassword')), AllocatedStorage: cf.ref('DatabaseSize'), BackupRetentionPeriod: 10, StorageType: 'gp2', DBParameterGroupName: 'tm4-logging-postgres11', EnableCloudwatchLogsExports: ['postgresql'], DBInstanceClass: cf.if('IsTaskingManagerProduction', 'db.t3.xlarge', 'db.t2.small'), DBSnapshotIdentifier: cf.if('UseASnapshot', cf.ref('DBSnapshot'), cf.noValue), VPCSecurityGroups: [cf.importValue(cf.join('-', ['mapwithai-network-production', cf.ref('NetworkEnvironment'), 'ec2s-security-group', cf.region]))], } }, TaskingManagerReactBucket: { Type: 'AWS::S3::Bucket', Properties: { BucketName: cf.join('-', [cf.stackName, 'react-app']), AccessControl: "PublicRead", PublicAccessBlockConfiguration: { BlockPublicAcls: false, BlockPublicPolicy: false, IgnorePublicAcls: false, RestrictPublicBuckets: false }, WebsiteConfiguration: { ErrorDocument: 'index.html', IndexDocument: 'index.html' } } }, TaskingManagerReactBucketPolicy: { Type: 'AWS::S3::BucketPolicy', Properties: { Bucket: cf.ref('TaskingManagerReactBucket'), PolicyDocument: { Version: "2012-10-17", Statement: [{ Action: ['s3:GetObject'], Effect: 'Allow', Principal: '*', Resource: [cf.join('', [ cf.getAtt('TaskingManagerReactBucket', 'Arn'), '/*' ] )], Sid: 'AddPerm' }] } } }, TaskingManagerReactCloudfront: { Type: "AWS::CloudFront::Distribution", Properties: { DistributionConfig: { DefaultRootObject: 'index.html', Aliases: [ cf.ref('TaskingManagerURL') ], Enabled: true, Origins: [{ Id: cf.join('-', [cf.stackName, 'react-app']), DomainName: cf.getAtt('TaskingManagerReactBucket', 'DomainName'), CustomOriginConfig: { OriginProtocolPolicy: 'https-only' } }], CustomErrorResponses: [{ ErrorCachingMinTTL: 0, ErrorCode: 403, ResponseCode: 200, ResponsePagePath: '/index.html' }, { ErrorCachingMinTTL: 0, ErrorCode: 404, ResponseCode: 200, ResponsePagePath: '/index.html' }], DefaultCacheBehavior: { AllowedMethods: ['GET', 'HEAD', 'OPTIONS'], CachedMethods: ['GET', 'HEAD', 'OPTIONS'], ForwardedValues: { QueryString: true, Cookies: { Forward: 'all' }, Headers: ['Accept', 'Referer'] }, Compress: true, TargetOriginId: cf.join('-', [cf.stackName, 'react-app']), ViewerProtocolPolicy: "redirect-to-https" }, ViewerCertificate: { // This one has to be handled specially because a cert for a Cloudfront certificate // MUST be imported to the us-east-1 region regardless of where the rest of the stack lives AcmCertificateArn: cf.sub( 'arn:${AWS::Partition}:${service}:${region}:${AWS::AccountId}:certificate/${suffix}', { 'service': 'acm', 'suffix': cf.ref('CloudfrontSSLCertificateIdentifier'), 'region': 'us-east-1', } ), MinimumProtocolVersion: 'TLSv1.2_2018', SslSupportMethod: 'sni-only' } } } }, TaskingManagerRoute53: { Type: 'AWS::Route53::RecordSet', // Condition: 'IsHOTOSMUrl', Properties: { Name: cf.ref('TaskingManagerURL'), Type: 'A', AliasTarget: { DNSName: cf.getAtt('TaskingManagerReactCloudfront', 'DomainName'), HostedZoneId: 'Z2FDTNDATAQYW2' }, HostedZoneId: 'Z101197737ML3WN063NTD', } } }; const Outputs = { CloudfrontDistributionID: { Value: cf.ref('TaskingManagerReactCloudfront'), Export: { Name: cf.join('-', [cf.stackName, 'cloudfront-id', cf.region]) } } } module.exports = { Parameters, Resources, Conditions, Outputs }