diff --git a/README.md b/README.md index b3a8ba1..5a85b25 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ While this setup is mostly automated, you are asked to fill in certain details t 3. ▶ [Secure Domain Setup](chapters/03-Secure-Domain-Setup.md) 4. 📝 [VPN Access](chapters/04-VPN-Access.md) 5. 🎛 [Administration](chapters/05-Administration.md) +6. ☁ [Stack Operations](chapters/06-Stack-Operations.md) ## License diff --git a/assets/OpenEMR.json b/assets/OpenEMR.json index 4b9b738..25e0faa 100644 --- a/assets/OpenEMR.json +++ b/assets/OpenEMR.json @@ -1,1411 +1,2223 @@ { - "AWSTemplateFormatVersion" : "2010-09-09", - - "Description" : "Automated OpenEMR 5.0.0.4 Configuration", - - "Parameters" : { - "EC2KeyPair" : { - "Description" : "Amazon EC2 Key Pair", - "Type" : "AWS::EC2::KeyPair::KeyName" - }, - "RDSPassword" : { - "NoEcho" : "true", - "Description" : "The database admin account password", - "Type" : "String", - "MinLength" : "8", - "MaxLength" : "41" - }, - "TimeZone": { - "Type": "String", - "Default": "America/Chicago", - "MaxLength": "41", - "Description" : "The timezone OpenEMR will run in" - }, - "PatientRecords": { - "Type": "Number", - "Default": "10", - "MinValue": "10", - "Description": "Database storage for patient records (minimum is 10 in GB)" - }, - "DocumentStorage": { - "Type": "Number", - "Default": "500", - "MinValue": "500", - "Description": "Document database for patient documents (minimum is 500 in GB)" - } - }, - - "Conditions" : { - "DevOnly" : {"Fn::Equals" : ["false", "yes"]} - }, - - "Mappings" : { - - "RegionData" : { - "us-east-1" : { - "RegionBucket": "openemr-useast1", - "ApplicationSource": "beanstalk/openemr-5.0.0-006.zip", - "MySQLVersion": "5.6.27", - "AmazonAMI": "ami-a4c7edb2", - "UbuntuAMI": "ami-d15a75c7" - }, - "us-west-2" : { - "RegionBucket": "openemr-uswest2", - "ApplicationSource": "beanstalk/openemr-5.0.0-006.zip", - "MySQLVersion": "5.6.27", - "AmazonAMI": "ami-6df1e514", - "UbuntuAMI": "ami-835b4efa" - }, - "eu-west-1" : { - "RegionBucket": "openemr-euwest1", - "ApplicationSource": "beanstalk/openemr-5.0.0-006.zip", - "MySQLVersion": "5.6.27", - "AmazonAMI": "ami-d7b9a2b1", - "UbuntuAMI": "ami-6d48500b" - }, - "ap-southeast-2" : { - "RegionBucket": "openemr-apsoutheast2", - "ApplicationSource": "beanstalk/openemr-5.0.0-006.zip", - "MySQLVersion": "5.6.27", - "AmazonAMI": "ami-10918173", - "UbuntuAMI": "ami-e94e5e8a" - } - } - }, - - "Resources" : { - - "VPC" : { - "Type" : "AWS::EC2::VPC", - "Properties" : { - "CidrBlock" : "10.0.0.0/16", - "EnableDnsSupport" : "true", - "EnableDnsHostnames" : "true", - "Tags" : [ - {"Key" : "Name", "Value" : "OpenEMR" }, - {"Key" : "Application", "Value" : { "Ref" : "AWS::StackId"} } - ] - } - }, - - "SubnetPublic1" : { - "Type" : "AWS::EC2::Subnet", - "Properties" : { - "VpcId" : { "Ref" : "VPC" }, - "CidrBlock" : "10.0.1.0/24", - "AvailabilityZone": { "Fn::Select": [ "0", { "Fn::GetAZs": "" } ] }, - "Tags" : [ - {"Key" : "Name", "Value" : "Public #1" }, - {"Key" : "Application", "Value" : { "Ref" : "AWS::StackId"} } - ] - } - }, - - "SubnetPrivate1" : { - "Type" : "AWS::EC2::Subnet", - "Properties" : { - "VpcId" : { "Ref" : "VPC" }, - "CidrBlock" : "10.0.2.0/24", - "AvailabilityZone": { "Fn::Select": [ "0", { "Fn::GetAZs": "" } ] }, - "Tags" : [ - {"Key" : "Name", "Value" : "Private #1" }, - {"Key" : "Application", "Value" : { "Ref" : "AWS::StackId"} } - ] - } - }, - - "SubnetPublic2" : { - "Type" : "AWS::EC2::Subnet", - "Properties" : { - "VpcId" : { "Ref" : "VPC" }, - "CidrBlock" : "10.0.3.0/24", - "AvailabilityZone": { "Fn::Select": [ "1", { "Fn::GetAZs": "" } ] }, - "Tags" : [ - {"Key" : "Name", "Value" : "Public #2" }, - {"Key" : "Application", "Value" : { "Ref" : "AWS::StackId"} } - ] - } - }, - - "SubnetPrivate2" : { - "Type" : "AWS::EC2::Subnet", - "Properties" : { - "VpcId" : { "Ref" : "VPC" }, - "CidrBlock" : "10.0.4.0/24", - "AvailabilityZone": { "Fn::Select": [ "1", { "Fn::GetAZs": "" } ] }, - "Tags" : [ - {"Key" : "Name", "Value" : "Private #2" }, - {"Key" : "Application", "Value" : { "Ref" : "AWS::StackId"} } - ] - } - }, - - "InternetGateway" : { - "Type" : "AWS::EC2::InternetGateway", - "Properties" : { - "Tags" : [ {"Key" : "Application", "Value" : { "Ref" : "AWS::StackId"} } ] - } - }, - - "AttachGateway" : { - "Type" : "AWS::EC2::VPCGatewayAttachment", - "Properties" : { - "VpcId" : { "Ref" : "VPC" }, - "InternetGatewayId" : { "Ref" : "InternetGateway" } - } - }, - - "RouteTablePublic" : { - "Type" : "AWS::EC2::RouteTable", - "Properties" : { - "VpcId" : {"Ref" : "VPC"}, - "Tags" : [ {"Key" : "Application", "Value" : { "Ref" : "AWS::StackId"} } ] - } - }, - - "RoutePublic" : { - "Type" : "AWS::EC2::Route", - "DependsOn" : "AttachGateway", - "Properties" : { - "RouteTableId" : { "Ref" : "RouteTablePublic" }, - "DestinationCidrBlock" : "0.0.0.0/0", - "GatewayId" : { "Ref" : "InternetGateway" } - } - }, - - "SubnetRouteTableAssociationPublic1" : { - "Type" : "AWS::EC2::SubnetRouteTableAssociation", - "Properties" : { - "SubnetId" : { "Ref" : "SubnetPublic1" }, - "RouteTableId" : { "Ref" : "RouteTablePublic" } - } - }, - - "SubnetRouteTableAssociationPublic2" : { - "Type" : "AWS::EC2::SubnetRouteTableAssociation", - "Properties" : { - "SubnetId" : { "Ref" : "SubnetPublic2" }, - "RouteTableId" : { "Ref" : "RouteTablePublic" } - } - }, - - "RouteTablePrivate" : { - "Type" : "AWS::EC2::RouteTable", - "Properties" : { - "VpcId" : {"Ref" : "VPC"}, - "Tags" : [ {"Key" : "Application", "Value" : { "Ref" : "AWS::StackId"} } ] - } - }, - - "EIPNATGatewayPublic1" : { - "Type" : "AWS::EC2::EIP", - "Properties" : { - "Domain" : "vpc" - } - }, - - "NATGatewayPublic1" : { - "Type" : "AWS::EC2::NatGateway", - "Properties" : { - "AllocationId" : { "Fn::GetAtt" : ["EIPNATGatewayPublic1", "AllocationId"]}, - "SubnetId" : { "Ref" : "SubnetPublic1"} - } - }, - - "RoutePrivate" : { - "Type" : "AWS::EC2::Route", - "Properties" : { - "RouteTableId" : { "Ref" : "RouteTablePrivate" }, - "DestinationCidrBlock" : "0.0.0.0/0", - "NatGatewayId" : { "Ref" : "NATGatewayPublic1" } - } - }, - - "SubnetRouteTableAssociationPrivate1" : { - "Type" : "AWS::EC2::SubnetRouteTableAssociation", - "Properties" : { - "SubnetId" : { "Ref" : "SubnetPrivate1" }, - "RouteTableId" : { "Ref" : "RouteTablePrivate" } - } - }, - - "SubnetRouteTableAssociationPrivate2" : { - "Type" : "AWS::EC2::SubnetRouteTableAssociation", - "Properties" : { - "SubnetId" : { "Ref" : "SubnetPrivate2" }, - "RouteTableId" : { "Ref" : "RouteTablePrivate" } - } - }, - - "DNS": { - "Type": "AWS::Route53::HostedZone", - "Properties": { - "HostedZoneConfig": { - "Comment": "private OpenEMR domain" - }, - "Name": "openemr.local", - "VPCs": [{ - "VPCId": { "Ref" : "VPC"}, - "VPCRegion": { "Ref": "AWS::Region"} - }] - } - }, - - "ApplicationSecurityGroup" : { - "Type" : "AWS::EC2::SecurityGroup", - "Properties" : { - "GroupDescription" : "Application Security Group", - "Tags" : [ { "Key" : "Name", "Value" : "Application" } ], - "VpcId" : {"Ref" : "VPC"} - } - }, - - "AppSGIngress": { - "Type": "AWS::EC2::SecurityGroupIngress", - "Properties": { - "GroupId": { "Ref": "ApplicationSecurityGroup" }, - "IpProtocol": "-1", - "SourceSecurityGroupId" : { "Ref": "ApplicationSecurityGroup"} - } - }, - - "SSHSecurityGroup" : { - "Type" : "AWS::EC2::SecurityGroup", - "Condition": "DevOnly", - "Properties" : { - "GroupDescription" : "Public SSH Access", - "Tags" : [ { "Key" : "Name", "Value" : "Global SSH" } ], - "VpcId" : {"Ref" : "VPC"} - } - }, - - "SSHSGIngress": { - "Type": "AWS::EC2::SecurityGroupIngress", - "Condition": "DevOnly", - "Properties": { - "GroupId": { "Ref": "SSHSecurityGroup" }, - "IpProtocol": "tcp", - "CidrIp": "0.0.0.0/0", - "FromPort": "22", - "ToPort": "22" - } - }, - - "DeveloperBastion": { - "Type": "AWS::EC2::Instance", - "Condition" : "DevOnly", - "Properties": { - "ImageId" : { "Fn::FindInMap" : [ "RegionData", { "Ref" : "AWS::Region" }, "AmazonAMI"] }, - "InstanceType" : "t2.nano", - "KeyName" : { "Ref" : "EC2KeyPair" }, - "NetworkInterfaces": [ { - "AssociatePublicIpAddress": "true", - "DeviceIndex": "0", - "GroupSet": [ {"Ref" : "SSHSecurityGroup"}, {"Ref" : "ApplicationSecurityGroup"} ], - "SubnetId": { "Ref" : "SubnetPublic2" } - } ], - "Tags" : [ { "Key" : "Name", "Value" : "Developer Bastion" } ] - } + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "OpenEMR v5.0.0.4 cloud deployment", + "Mappings": { + "RegionData": { + "ap-southeast-2": { + "AmazonAMI": "ami-10918173", + "ApplicationSource": "beanstalk/openemr-5.0.0-006.zip", + "MySQLVersion": "5.6.27", + "RegionBucket": "openemr-apsoutheast2", + "UbuntuAMI": "ami-e94e5e8a" + }, + "eu-west-1": { + "AmazonAMI": "ami-d7b9a2b1", + "ApplicationSource": "beanstalk/openemr-5.0.0-006.zip", + "MySQLVersion": "5.6.27", + "RegionBucket": "openemr-euwest1", + "UbuntuAMI": "ami-6d48500b" + }, + "us-east-1": { + "AmazonAMI": "ami-a4c7edb2", + "ApplicationSource": "beanstalk/openemr-5.0.0-006.zip", + "MySQLVersion": "5.6.27", + "RegionBucket": "openemr-useast1", + "UbuntuAMI": "ami-d15a75c7" + }, + "us-west-2": { + "AmazonAMI": "ami-6df1e514", + "ApplicationSource": "beanstalk/openemr-5.0.0-006.zip", + "MySQLVersion": "5.6.27", + "RegionBucket": "openemr-uswest2", + "UbuntuAMI": "ami-835b4efa" + } + } }, - - "OpenEMRKey" : { - "Type" : "AWS::KMS::Key", - "DeletionPolicy": "Retain", - "Properties" : { - "Description" : "Encrypts Patient Records", - "KeyPolicy" : { - "Version": "2012-10-17", - "Id": "key-default-1", - "Statement": [ - { - "Sid": "Enable IAM User Permissions", - "Effect": "Allow", - "Principal": { - "AWS": [ - { - "Fn::Join": [ - ":", - [ - "arn:aws:iam:", + "Outputs": { + "OpenEMR": { + "Description": "OpenEMR Setup", + "Value": { + "Fn::Join": [ + "", + [ + "http://", { - "Ref": "AWS::AccountId" + "Fn::GetAtt": [ + "EBEnvironment", + "EndpointURL" + ] }, - "root" - ] + "/openemr" ] - } ] - }, - "Action": "kms:*", - "Resource": "*" } - ] } - } - }, - - "S3Bucket": { - "Type": "AWS::S3::Bucket", - "DeletionPolicy": "Retain", - "Properties": { - "BucketName" : { "Fn::Join" : [ "-", [ - "openemr", - { "Fn::Select" : [ "2", { "Fn::Split": ["/", {"Ref": "AWS::StackId"}]}] } - ] ] } - } }, - - "BucketPolicy" : { - "Type" : "AWS::S3::BucketPolicy", - "Properties" : { - "Bucket" : {"Ref" : "S3Bucket"}, - "PolicyDocument" : { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "AWSCloudTrailAclCheck", - "Effect": "Allow", - "Principal": { "Service":"cloudtrail.amazonaws.com"}, - "Action": "s3:GetBucketAcl", - "Resource": { "Fn::Join" : ["", ["arn:aws:s3:::", {"Ref":"S3Bucket"}]]} - }, - { - "Sid": "AWSCloudTrailWrite", - "Effect": "Allow", - "Principal": { "Service":"cloudtrail.amazonaws.com"}, - "Action": "s3:PutObject", - "Resource": { "Fn::Join" : ["", ["arn:aws:s3:::", {"Ref":"S3Bucket"}, "/AWSLogs/", {"Ref":"AWS::AccountId"}, "/*"]]}, - "Condition": { - "StringEquals": { - "s3:x-amz-acl": "bucket-owner-full-control" - } - } - } - ] - } - } - }, - - "CertWriterPolicy" : { - "Type" : "AWS::IAM::ManagedPolicy", - "Properties" : { - "Description" : "Policy for initial CA writer", - "PolicyDocument" : { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "Stmt1500612724000", - "Effect": "Allow", - "Action": [ - "s3:*" - ], - "Resource": [ - { "Fn::Join" : ["", ["arn:aws:s3:::", {"Ref":"S3Bucket"}, "/CA/*"]]} - ] - }, - { - "Sid": "Stmt1500612724001", - "Effect": "Allow", - "Action": [ - "s3:ListBucket" - ], - "Resource": [ - { "Fn::Join" : ["", ["arn:aws:s3:::", {"Ref":"S3Bucket"}]]} - ] - }, - { - "Sid": "Stmt1500612724002", - "Effect": "Allow", - "Action": [ - "kms:GenerateDataKey*" - ], - "Resource": [ - { "Fn::GetAtt" : ["OpenEMRKey", "Arn" ] } - ] - } - ] - } - } - }, - - "CertWriterRole" : { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Version" : "2012-10-17", - "Statement": [ { - "Effect": "Allow", - "Principal": { - "Service": [ "ec2.amazonaws.com" ] - }, - "Action": [ "sts:AssumeRole" ] - } ] - }, - "Path": "/", - "ManagedPolicyArns" : [{ "Ref" : "CertWriterPolicy"}] + "Parameters": { + "DocumentStorage": { + "Default": "500", + "Description": "Document database for patient documents (minimum 500 GB)", + "MinValue": "10", + "Type": "Number" + }, + "EC2KeyPair": { + "Description": "Amazon EC2 Key Pair", + "Type": "AWS::EC2::KeyPair::KeyName" + }, + "PatientRecords": { + "Default": "10", + "Description": "Database storage for patient records (minimum 10 GB)", + "MinValue": "10", + "Type": "Number" + }, + "RDSPassword": { + "Description": "The database admin account password", + "MaxLength": "41", + "MinLength": "8", + "NoEcho": true, + "Type": "String" + }, + "TimeZone": { + "Default": "America/Chicago", + "Description": "The timezone OpenEMR will run in", + "MaxLength": "41", + "Type": "String" } - }, - - "CertWriterInstanceProfile": { - "Type": "AWS::IAM::InstanceProfile", - "Properties": { - "Path": "/", - "Roles": [ - { - "Ref": "CertWriterRole" - } - ] - } - }, - - "CertWriterInstance": { - "Type": "AWS::EC2::Instance", - "DependsOn" : "SubnetRouteTableAssociationPrivate1", - "Properties": { - "ImageId" : { "Fn::FindInMap" : [ "RegionData", { "Ref" : "AWS::Region" }, "AmazonAMI"] }, - "InstanceType" : "t2.nano", - "SubnetId" : { "Ref": "SubnetPrivate1"}, - "KeyName" : { "Ref" : "EC2KeyPair" }, - "IamInstanceProfile": { "Ref": "CertWriterInstanceProfile" }, - "Tags" : [ { "Key" : "Name", "Value" : "Backend CA Processor" } ], - "InstanceInitiatedShutdownBehavior" : "terminate", - "UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [ - "#!/bin/bash -xe\n", - "cd /root\n", - "mkdir -m 700 CA CA/certs CA/keys CA/work\n", - "cd CA\n", - "openssl genrsa -out keys/ca.key 8192\n", - "openssl req -new -x509 -extensions v3_ca -key keys/ca.key -out certs/ca.crt -days 3650 -subj '/CN=OpenEMR Backend CA'\n", - "openssl req -new -nodes -newkey rsa:2048 -keyout keys/beanstalk.key -out work/beanstalk.csr -days 3648 -subj /CN=beanstalk.openemr.local\n", - "openssl x509 -req -in work/beanstalk.csr -out certs/beanstalk.crt -CA certs/ca.crt -CAkey keys/ca.key -CAcreateserial\n", - "openssl req -new -nodes -newkey rsa:2048 -keyout keys/couch.key -out work/couch.csr -days 3648 -subj /CN=couchdb.openemr.local\n", - "openssl x509 -req -in work/couch.csr -out certs/couch.crt -CA certs/ca.crt -CAkey keys/ca.key\n", - "aws s3 sync keys s3://", { "Ref" : "S3Bucket" }, "/CA/keys --sse aws:kms --sse-kms-key-id ", { "Ref" : "OpenEMRKey" }, " --acl private\n", - "aws s3 sync certs s3://", { "Ref" : "S3Bucket" }, "/CA/certs --acl public-read\n", - "/opt/aws/bin/cfn-signal -e 0 ", - " --stack ", { "Ref" : "AWS::StackName" }, - " --resource CertWriterInstance ", - " --region ", { "Ref" : "AWS::Region" }, "\n", - "#done forever\n", - "shutdown -h now", "\n" - ]]}} }, - "CreationPolicy" : { - "ResourceSignal" : { - "Timeout" : "PT5M" - } - } - }, - - "EFSSecurityGroup" : { - "Type" : "AWS::EC2::SecurityGroup", - "Properties" : { - "GroupDescription" : "EFS Access", - "Tags" : [ { "Key" : "Name", "Value" : "BeanstalkAccess" } ], - "VpcId" : {"Ref" : "VPC"} - } - }, - - "EFSSGIngress": { - "Type": "AWS::EC2::SecurityGroupIngress", - "Properties": { - "GroupId": { "Ref": "EFSSecurityGroup" }, - "IpProtocol": "-1", - "SourceSecurityGroupId" : { "Ref": "ApplicationSecurityGroup"} - } - }, - - "ElasticFileSystem" : { - "Type" : "AWS::EFS::FileSystem", - "DeletionPolicy": "Retain", - "Properties" : { - "FileSystemTags" : [ { "Key" : "Name", "Value" : "OpenEMR Codebase" } ] - } - }, - - "DNSEFS" : { - "Type" : "AWS::Route53::RecordSet", - "DependsOn" : ["EFSMountPrivate1", "EFSMountPrivate2"], - "Properties" : { - "HostedZoneId" : { "Ref" : "DNS" }, - "Name" : "nfs.openemr.local", - "Type" : "CNAME", - "TTL" : "900", - "ResourceRecords" : [ { "Fn::Join" : [ "", [ - {"Ref" : "ElasticFileSystem"}, - ".efs.", - {"Ref" : "AWS::Region"}, - ".amazonaws.com" - ]]}] - } - }, - - "EFSMountPrivate1": { - "Type": "AWS::EFS::MountTarget", - "Properties": { - "FileSystemId": { "Ref": "ElasticFileSystem" }, - "SubnetId": { "Ref": "SubnetPrivate1" }, - "SecurityGroups": [ { "Ref": "EFSSecurityGroup" } ] - } - }, - - "EFSMountPrivate2": { - "Type": "AWS::EFS::MountTarget", - "Properties": { - "FileSystemId": { "Ref": "ElasticFileSystem" }, - "SubnetId": { "Ref": "SubnetPrivate2" }, - "SecurityGroups": [ { "Ref": "EFSSecurityGroup" } ] - } - }, - - "EFSBackupSecurityGroup" : { - "Type" : "AWS::EC2::SecurityGroup", - "Properties" : { - "GroupDescription" : "EFS Backup Access", - "Tags" : [ { "Key" : "Name", "Value" : "BeanstalkBackupAccess" } ], - "VpcId" : {"Ref" : "VPC"} - } - }, - - "EFSBackupSGIngress": { - "Type": "AWS::EC2::SecurityGroupIngress", - "Properties": { - "GroupId": { "Ref": "EFSSecurityGroup" }, - "IpProtocol": "-1", - "SourceSecurityGroupId" : { "Ref": "EFSBackupSecurityGroup"} - } - }, - - "NFSBackupPolicy" : { - "Type" : "AWS::IAM::ManagedPolicy", - "Properties" : { - "Description" : "Policy to operate backups", - "PolicyDocument" : { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "Stmt1500699052003", - "Effect": "Allow", - "Action": ["s3:ListBucket"], - "Resource": { "Fn::Join" : ["", ["arn:aws:s3:::", {"Ref":"S3Bucket"}]]} - }, - { - "Sid": "Stmt1500699052000", - "Effect": "Allow", - "Action": [ - "s3:PutObject", - "s3:GetObject", - "s3:DeleteObject" + "Resources": { + "AppSGIngress": { + "Properties": { + "GroupId": { + "Ref": "ApplicationSecurityGroup" + }, + "IpProtocol": "-1", + "SourceSecurityGroupId": { + "Ref": "ApplicationSecurityGroup" + } + }, + "Type": "AWS::EC2::SecurityGroupIngress" + }, + "ApplicationSecurityGroup": { + "Properties": { + "GroupDescription": "Application Security Group", + "Tags": [ + { + "Key": "Name", + "Value": "Application" + } ], - "Resource": [ - { "Fn::Join" : ["", ["arn:aws:s3:::", {"Ref":"S3Bucket"}, "/Backup/*"]]} + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::SecurityGroup" + }, + "BarebonesLambdaRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Path": "/", + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:*" + ], + "Effect": "Allow", + "Resource": "arn:aws:logs:*:*:*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "root" + } ] }, - { - "Sid": "Stmt1500612724002", - "Effect": "Allow", - "Action": [ - "kms:Encrypt", - "kms:Decrypt", - "kms:GenerateDataKey*" + "Type": "AWS::IAM::Role" + }, + "BeanstalkInstanceProfile": { + "Properties": { + "Path": "/", + "Roles": [ + { + "Ref": "BeanstalkInstanceRole" + } + ] + }, + "Type": "AWS::IAM::InstanceProfile" + }, + "BeanstalkInstanceRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "ec2.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Path": "/", + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:Get*", + "s3:List*", + "s3:PutObject" + ], + "Effect": "Allow", + "Resource": [ + "arn:aws:s3:::elasticbeanstalk-*", + "arn:aws:s3:::elasticbeanstalk-*/*" + ], + "Sid": "BucketAccess" + }, + { + "Action": [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords" + ], + "Effect": "Allow", + "Resource": "*", + "Sid": "XRayAccess" + }, + { + "Action": [ + "logs:PutLogEvents", + "logs:CreateLogStream" + ], + "Effect": "Allow", + "Resource": [ + "arn:aws:logs:*:*:log-group:/aws/elasticbeanstalk*" + ], + "Sid": "CloudWatchLogsAccess" + }, + { + "Action": [ + "s3:GetObject" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + "Ref": "S3Bucket" + }, + "/CA/certs/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + "Ref": "S3Bucket" + }, + "/CA/keys/beanstalk.key" + ] + ] + } + ], + "Sid": "Stmt1500699052000" + }, + { + "Action": [ + "kms:Decrypt" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "OpenEMRKey", + "Arn" + ] + } + ], + "Sid": "Stmt1500612724002" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "root" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "BucketPolicy": { + "Properties": { + "Bucket": { + "Ref": "S3Bucket" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:GetBucketAcl", + "Effect": "Allow", + "Principal": { + "Service": "cloudtrail.amazonaws.com" + }, + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + "Ref": "S3Bucket" + } + ] + ] + }, + "Sid": "AWSCloudTrailAclCheck" + }, + { + "Action": "s3:PutObject", + "Condition": { + "StringEquals": { + "s3:x-amz-acl": "bucket-owner-full-control" + } + }, + "Effect": "Allow", + "Principal": { + "Service": "cloudtrail.amazonaws.com" + }, + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + "Ref": "S3Bucket" + }, + "/AWSLogs/", + { + "Ref": "AWS::AccountId" + }, + "/*" + ] + ] + }, + "Sid": "AWSCloudTrailWrite" + } + ], + "Version": "2012-10-17" + } + }, + "Type": "AWS::S3::BucketPolicy" + }, + "CertGrabberFunction": { + "Properties": { + "Code": { + "ZipFile": { + "Fn::Join": [ + "\n", + [ + "import urllib2", + "import json", + "def lambda_handler(event, context):", + " if (event['RequestType'] == 'Delete'):", + " sendResponse(event, context, 'SUCCESS', None)", + " return", + " sendResponse(event, context, 'SUCCESS', urllib2.urlopen(event['ResourceProperties']['Url']).read()[28:-27])", + "def sendResponse(event, context, responseStatus, responseData):", + " opener = urllib2.build_opener(urllib2.HTTPHandler)", + " o = {}", + " o['Status'] = responseStatus", + " o['Reason'] = 'log ' + context.log_stream_name", + " o['PhysicalResourceId'] = context.log_stream_name", + " o['StackId'] = event['StackId']", + " o['RequestId'] = event['RequestId']", + " o['LogicalResourceId'] = event['LogicalResourceId']", + " o['Data'] = {'PublicKey': responseData}", + " r = json.dumps(o)", + " request = urllib2.Request(event['ResponseURL'], data=r)", + " request.add_header('Content-Type', '')", + " request.add_header('Content-Length', len(r))", + " request.get_method = lambda: 'PUT'", + " url = opener.open(request)" + ] + ] + } + }, + "Description": "gets a certificates embedded key", + "Handler": "index.lambda_handler", + "Role": { + "Fn::GetAtt": [ + "BarebonesLambdaRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Timeout": "5" + }, + "Type": "AWS::Lambda::Function" + }, + "CertWriterInstance": { + "CreationPolicy": { + "ResourceSignal": { + "Timeout": "PT5M" + } + }, + "DependsOn": "rtPrivate1Attach", + "Properties": { + "IamInstanceProfile": { + "Ref": "CertWriterInstanceProfile" + }, + "ImageId": { + "Fn::FindInMap": [ + "RegionData", + { + "Ref": "AWS::Region" + }, + "AmazonAMI" + ] + }, + "InstanceInitiatedShutdownBehavior": "terminate", + "InstanceType": "t2.nano", + "KeyName": { + "Ref": "EC2KeyPair" + }, + "SubnetId": { + "Ref": "PrivateSubnet1" + }, + "Tags": [ + { + "Key": "Name", + "Value": "Backend CA Processor" + } ], - "Resource": [ { "Fn::GetAtt" : ["OpenEMRKey", "Arn" ] } ] - } - ] - } - } - }, - - "NFSBackupRole" : { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Version" : "2012-10-17", - "Statement": [ { - "Effect": "Allow", - "Principal": { - "Service": [ "ec2.amazonaws.com" ] - }, - "Action": [ "sts:AssumeRole" ] - } ] - }, - "Path": "/", - "ManagedPolicyArns" : [{ "Ref" : "NFSBackupPolicy"}] - } - }, - - "NFSBackupInstanceProfile": { - "Type": "AWS::IAM::InstanceProfile", - "Properties": { - "Path": "/", - "Roles": [ - { - "Ref": "NFSBackupRole" - } - ] - } - }, - - "NFSBackupInstance": { - "Type": "AWS::EC2::Instance", - "Metadata" : { - "AWS::CloudFormation::Init" : { - "configSets" : { - "Setup" : [ "Install" ] - }, - - "Install" : { - "files" : { - "/root/setup.sh" : { - "content" : { "Fn::Join" : ["", [ - "#!/bin/bash\n", - "S3=", { "Ref" : "S3Bucket" }, "\n", - "KMS=", { "Ref" : "OpenEMRKey" }, "\n", - "apt-get -y update\n", - "DEBIAN_FRONTEND=noninteractive apt-get dist-upgrade -y -o Dpkg::Options::=\"--force-confdef\" -o Dpkg::Options::=\"--force-confold\" --force-yes\n", - "apt-get -y install duplicity python-boto nfs-common awscli\n", - "mkdir /mnt/efs\n", - "echo \"nfs.openemr.local:/ /mnt/efs nfs4 nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2 0 0\" >> /etc/fstab\n", - "mount /mnt/efs\n", - "touch /tmp/mypass\n", - "chmod 500 /tmp/mypass\n", - "openssl rand -base64 32 >> /tmp/mypass\n", - "aws s3 cp /tmp/mypass s3://$S3/Backup/passphrase.txt --sse aws:kms --sse-kms-key-id $KMS\n", - "rm /tmp/mypass\n" - ]]}, - "mode" : "000500", - "owner" : "root", - "group" : "root" - }, - "/etc/cron.daily/backup.sh" : { - "content" : { "Fn::Join" : ["", [ - "#!/bin/bash\n", - "S3=", { "Ref" : "S3Bucket" }, "\n", - "KMS=", { "Ref" : "OpenEMRKey" }, "\n", - "PASSPHRASE=`aws s3 cp s3://$S3/Backup/passphrase.txt - --sse aws:kms --sse-kms-key-id $KMS`\n", - "export PASSPHRASE\n", - "duplicity --full-if-older-than 1M /mnt/efs s3://s3.amazonaws.com/$S3/Backup\n", - "duplicity remove-all-but-n-full 2 --force s3://s3.amazonaws.com/$S3/Backup\n" - ]]}, - "mode" : "000500", - "owner" : "root", - "group" : "root" - }, - "/root/recovery.sh" : { - "content" : { "Fn::Join" : ["", [ - "#!/bin/bash\n", - "S3=", { "Ref" : "S3Bucket" }, "\n", - "KMS=", { "Ref" : "OpenEMRKey" }, "\n", - "PASSPHRASE=`aws s3 cp s3://$S3/Backup/passphrase.txt - --sse aws:kms --sse-kms-key-id $KMS`\n", - "export PASSPHRASE\n", - "duplicity --force s3://s3.amazonaws.com/$S3/Backup /mnt/efs\n" - ]]}, - "mode" : "000500", - "owner" : "root", - "group" : "root" - } - }, - "commands" : { - "01_setup" : { - "command" : "/root/setup.sh" - } - - } - } - } - }, - "DependsOn" : "DNSEFS", - "Properties": { - "ImageId" : { "Fn::FindInMap" : [ "RegionData", { "Ref" : "AWS::Region" }, "UbuntuAMI"] }, - "InstanceType" : "t2.micro", - "SubnetId" : { "Ref": "SubnetPrivate2"}, - "SecurityGroupIds" : [ {"Ref" : "EFSBackupSecurityGroup"} ], - "KeyName" : { "Ref" : "EC2KeyPair" }, - "IamInstanceProfile": { "Ref": "NFSBackupInstanceProfile" }, - "Tags" : [ { "Key" : "Name", "Value" : "EFS Backup Server" } ], - "UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [ - "#!/bin/bash -xe\n", - "exec > /tmp/part-001.log 2>&1\n", - "apt-get -y update\n", - "apt-get -y install python-pip\n", - "pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz\n", - "cfn-init -v ", - " --stack ", { "Ref" : "AWS::StackName" }, - " --resource NFSBackupInstance ", - " --configsets Setup ", - " --region ", { "Ref" : "AWS::Region" }, "\n", - "cfn-signal -e 0 ", - " --stack ", { "Ref" : "AWS::StackName" }, - " --resource NFSBackupInstance ", - " --region ", { "Ref" : "AWS::Region" }, "\n" - ]]}} - }, - "CreationPolicy" : { - "ResourceSignal" : { - "Timeout" : "PT5M" - } - } - }, - - "RDSSubnetGroup": { - "Type" : "AWS::RDS::DBSubnetGroup", - "Properties" : { - "DBSubnetGroupDescription" : "OpenEMR DB Subnet", - "SubnetIds" : [ {"Ref":"SubnetPrivate1"}, {"Ref":"SubnetPrivate2"} ] - } - }, - - "DBSecurityGroup" : { - "Type" : "AWS::EC2::SecurityGroup", - "Properties" : { - "GroupDescription" : "RDS Access", - "Tags" : [ { "Key" : "Name", "Value" : "PatientRecordsAccess" } ], - "VpcId" : {"Ref" : "VPC"} - } - }, - - "DBSGIngress": { - "Type": "AWS::EC2::SecurityGroupIngress", - "Properties": { - "GroupId": { "Ref": "DBSecurityGroup" }, - "IpProtocol": "-1", - "SourceSecurityGroupId" : { "Ref": "ApplicationSecurityGroup"} - } - }, - - "RDSInstance" : { - "Type" : "AWS::RDS::DBInstance", - "DeletionPolicy": "Snapshot", - "Properties" : { - "DBName" : "openemr", - "AllocatedStorage" : {"Ref": "PatientRecords"}, - "DBInstanceClass" : "db.t2.small", - "Engine" : "MySQL", - "EngineVersion" : { "Fn::FindInMap" : [ "RegionData", { "Ref" : "AWS::Region" }, "MySQLVersion"]}, - "MasterUsername" : "openemr", - "MasterUserPassword" : { "Ref" : "RDSPassword" }, - "PubliclyAccessible": "false", - "DBSubnetGroupName": {"Ref": "RDSSubnetGroup"}, - "VPCSecurityGroups": [{"Ref": "DBSecurityGroup"}], - "KmsKeyId": {"Ref" : "OpenEMRKey"}, - "StorageEncrypted": "true", - "MultiAZ": "false", - "Tags" : [ { "Key" : "Name", "Value" : "Patient Records" } ] - } - }, - - "DNSRDS" : { - "Type" : "AWS::Route53::RecordSet", - "Properties" : { - "HostedZoneId" : { "Ref" : "DNS" }, - "Name" : "mysql.openemr.local", - "Type" : "CNAME", - "TTL" : "900", - "ResourceRecords" : [ { "Fn::GetAtt" : ["RDSInstance", "Endpoint.Address" ] } ] - } - }, - - "CloudTrail": { - "DependsOn" : ["BucketPolicy"], - "Type": "AWS::CloudTrail::Trail", - "Properties": { - "IsLogging": "true", - "IncludeGlobalServiceEvents" : "true", - "IsMultiRegionTrail" : "true", - "S3BucketName": { "Ref" : "S3Bucket" } - } - }, - - "RedisSubnetGroup" : { - "Type": "AWS::ElastiCache::SubnetGroup", - "Properties": { - "Description": "Redis node locations", - "SubnetIds": [ - { - "Ref": "SubnetPrivate2" - }, - { - "Ref": "SubnetPrivate1" - } - ] - } - }, - - "RedisSecurityGroup" : { - "Type" : "AWS::EC2::SecurityGroup", - "Properties" : { - "GroupDescription" : "Redis Access", - "Tags" : [ { "Key" : "Name", "Value" : "OpenEMR Sessions" } ], - "VpcId" : {"Ref" : "VPC"} - } - }, - - "RedisSGIngress": { - "Type": "AWS::EC2::SecurityGroupIngress", - "Properties": { - "GroupId": { "Ref": "RedisSecurityGroup" }, - "IpProtocol": "-1", - "SourceSecurityGroupId" : { "Ref": "ApplicationSecurityGroup"} - } - }, - - "RedisCluster" : { - "Type": "AWS::ElastiCache::CacheCluster", - "Properties": { - "CacheNodeType" : "cache.t2.small", - "VpcSecurityGroupIds": [{"Fn::GetAtt": [ "RedisSecurityGroup", "GroupId"]}], - "CacheSubnetGroupName" : { "Ref": "RedisSubnetGroup"}, - "Engine" : "redis", - "NumCacheNodes" : "1" - } - }, - - "DNSRedis" : { - "Type" : "AWS::Route53::RecordSet", - "Properties" : { - "HostedZoneId" : { "Ref" : "DNS" }, - "Name" : "redis.openemr.local", - "Type" : "CNAME", - "TTL" : "900", - "ResourceRecords" : [{ "Fn::GetAtt" : ["RedisCluster", "RedisEndpoint.Address"]} - ] - } - }, - - "CouchDBSecurityGroup" : { - "Type" : "AWS::EC2::SecurityGroup", - "Properties" : { - "GroupDescription" : "CouchDB Access", - "Tags" : [ { "Key" : "Name", "Value" : "PatientDocumentsAccess" } ], - "VpcId" : {"Ref" : "VPC"} - } - }, - - "CouchDBSGIngress": { - "Type": "AWS::EC2::SecurityGroupIngress", - "Properties": { - "GroupId": { "Ref": "CouchDBSecurityGroup" }, - "IpProtocol": "-1", - "SourceSecurityGroupId" : { "Ref": "ApplicationSecurityGroup"} - } - }, - - "CouchDBVolume" : { - "Type" : "AWS::EC2::Volume", - "DeletionPolicy": "Snapshot", - "Properties" : { - "Size" : {"Ref": "DocumentStorage"}, - "AvailabilityZone" : { "Fn::Select": [ "0", { "Fn::GetAZs": "" } ] }, - "VolumeType" : "sc1", - "Encrypted": "true", - "KmsKeyId" : {"Ref" : "OpenEMRKey"}, - "Tags" : [ { "Key" : "Name", "Value" : "Patient Documents" } ] - } - }, - - "CouchDBPolicy" : { - "Type" : "AWS::IAM::ManagedPolicy", - "Properties" : { - "Description" : "Policy to retrieve CouchDB SSL credentials", - "PolicyDocument" : { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "Stmt1500699052000", - "Effect": "Allow", - "Action": [ - "s3:GetObject" - ], - "Resource": [ - { "Fn::Join" : ["", ["arn:aws:s3:::", {"Ref":"S3Bucket"}, "/CA/certs/*"]]}, - { "Fn::Join" : ["", ["arn:aws:s3:::", {"Ref":"S3Bucket"}, "/CA/keys/couch.key"]]} - ] - }, - { - "Sid": "Stmt1500612724002", - "Effect": "Allow", - "Action": [ - "kms:Decrypt" - ], - "Resource": [ { "Fn::GetAtt" : ["OpenEMRKey", "Arn" ] } ] - } - ] - } - } - }, - - "CouchDBRole" : { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Version" : "2012-10-17", - "Statement": [ { - "Effect": "Allow", - "Principal": { - "Service": [ "ec2.amazonaws.com" ] - }, - "Action": [ "sts:AssumeRole" ] - } ] - }, - "Path": "/", - "ManagedPolicyArns" : [{ "Ref" : "CouchDBPolicy"}] - } - }, - - "CouchDBInstanceProfile": { - "Type": "AWS::IAM::InstanceProfile", - "Properties": { - "Path": "/", - "Roles": [ - { - "Ref": "CouchDBRole" - } - ] - } - }, - - "CouchDBInstance": { - "Type": "AWS::EC2::Instance", - "Metadata" : { - "AWS::CloudFormation::Init" : { - "configSets" : { - "Setup" : [ "Install" ] - }, - - "Install" : { - "files" : { - "/tmp/ip.ini" : { - "content" : { "Fn::Join" : ["\n", [ - "[httpd]", - "bind_address = 0.0.0.0" - ]]}, - "mode" : "000400", - "owner" : "root", - "group" : "root" - }, - "/tmp/ssl.ini" : { - "content" : { "Fn::Join" : ["\n", [ - "[daemons]", - "httpsd = {couch_httpd, start_link, [https]}", - "[ssl]", - "port = 6984", - "key_file = /etc/couchdb/couch.key", - "cert_file = /etc/couchdb/couch.crt", - "cacert_file = /etc/couchdb/ca.crt" - ]]}, - "mode" : "000400", - "owner" : "root", - "group" : "root" - }, - "/tmp/fstab.append" : { - "content" : { "Fn::Join" : ["", [ - "/dev/xvdd /mnt/db ext4 defaults,nofail 0 0\n" - ]]}, - "mode" : "000400", - "owner" : "root", - "group" : "root" - }, - "/tmp/couchdb.setup.sh" : { - "content" : { "Fn::Join" : ["", [ - "#!/bin/bash -xe\n", - "exec > /tmp/part-002.log 2>&1\n", - "DEBIAN_FRONTEND=noninteractive apt-get dist-upgrade -y -o Dpkg::Options::=\"--force-confdef\" -o Dpkg::Options::=\"--force-confold\" --force-yes\n", - "mkfs -t ext4 /dev/xvdd\n", - "mkdir /mnt/db\n", - "cat /tmp/fstab.append >> /etc/fstab\n", - "mount /mnt/db\n", - "apt-get -y install couchdb awscli\n", - "service couchdb stop\n", - "aws configure set s3.signature_version s3v4\n", - "aws s3 cp s3://" , { "Ref" : "S3Bucket" }, "/CA/certs/ca.crt /etc/couchdb\n", - "aws s3 cp s3://" , { "Ref" : "S3Bucket" }, "/CA/certs/couch.crt /etc/couchdb\n", - "chmod 664 /etc/couchdb/*.crt\n", - "aws s3 cp s3://" , { "Ref" : "S3Bucket" }, "/CA/keys/couch.key /etc/couchdb --sse aws:kms --sse-kms-key-id ", { "Ref" : "OpenEMRKey" }, "\n", - "chmod 660 /etc/couchdb/couch.key\n", - "chown couchdb:couchdb /etc/couchdb/*.crt /etc/couchdb/*.key\n", - "mv /var/lib/couchdb /mnt/db/couchdb\n", - "ln -s /mnt/db/couchdb /var/lib/couchdb\n", - "cp /tmp/ip.ini /tmp/ssl.ini /etc/couchdb/local.d\n", - "chown couchdb:couchdb /etc/couchdb/local.d/ip.ini /etc/couchdb/local.d/ssl.ini\n", - "service couchdb restart\n" - ]]}, - "mode" : "000500", - "owner" : "root", - "group" : "root" - } - }, - "commands" : { - "01_security" : { - "command" : "/tmp/couchdb.setup.sh" - } - - } - } - } - }, - "DependsOn" : "CertWriterInstance", - "Properties": { - "ImageId" : { "Fn::FindInMap" : [ "RegionData", { "Ref" : "AWS::Region" }, "UbuntuAMI"] }, - "InstanceType" : "t2.micro", - "SubnetId" : { "Ref": "SubnetPrivate1"}, - "SecurityGroupIds" : [ {"Ref" : "CouchDBSecurityGroup"} ], - "KeyName" : { "Ref" : "EC2KeyPair" }, - "IamInstanceProfile": { "Ref": "CouchDBInstanceProfile" }, - "Volumes" : [{ - "Device" : "/dev/sdd", - "VolumeId" : { "Ref" : "CouchDBVolume" } - }], - "Tags" : [ { "Key" : "Name", "Value" : "CouchDB Server" } ], - "UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [ - "#!/bin/bash -xe\n", - "exec > /tmp/part-001.log 2>&1\n", - "apt-get -y update\n", - "apt-get -y install python-pip\n", - "pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz\n", - "cfn-init -v ", - " --stack ", { "Ref" : "AWS::StackName" }, - " --resource CouchDBInstance ", - " --configsets Setup ", - " --region ", { "Ref" : "AWS::Region" }, "\n", - "cfn-signal -e $? ", - " --stack ", { "Ref" : "AWS::StackName" }, - " --resource CouchDBInstance ", - " --region ", { "Ref" : "AWS::Region" }, "\n" - ]]}} - }, - "CreationPolicy" : { - "ResourceSignal" : { - "Timeout" : "PT5M" - } - } - }, - - "DNSCouchDB" : { - "Type" : "AWS::Route53::RecordSet", - "Properties" : { - "HostedZoneId" : { "Ref" : "DNS" }, - "Name" : "couchdb.openemr.local", - "Type" : "CNAME", - "TTL" : "900", - "ResourceRecords" : [ { "Fn::GetAtt" : [ "CouchDBInstance", "PrivateDnsName" ] } ] - } - }, - - "DocumentBackupExecutionRole": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Version": "2012-10-17", - "Statement": [{ "Effect": "Allow", "Principal": {"Service": ["lambda.amazonaws.com"]}, "Action": ["sts:AssumeRole"] }] - }, - "Path": "/", - "Policies": [{ - "PolicyName": "root", - "PolicyDocument": { - "Version": "2012-10-17", - "Statement": [ - { "Effect": "Allow", "Action": ["logs:*"], "Resource": "arn:aws:logs:*:*:*" }, - { - "Effect": "Allow", - "Action": [ - "ec2:DescribeVolumeStatus", - "ec2:DescribeSnapshots", - "ec2:CreateSnapshot", - "ec2:DeleteSnapshot" - ], - "Resource": [ - "*" - ] - } - ] - } - }] - } - }, - - "DocumentBackupManagerFunction": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Description": "handles patient document (CouchDB) backups", - "Handler": "index.lambda_handler", - "Role": { "Fn::GetAtt" : ["DocumentBackupExecutionRole", "Arn"] }, - "Code": { - "ZipFile" : { "Fn::Join" : ["\n", [ - "import boto3", - "import os", - "def lambda_handler(event, context):", - " volume = boto3.session.Session(region_name = os.environ['AWS_DEFAULT_REGION']).resource('ec2').Volume(os.environ['VOLUME_ID'])", - " volume.create_snapshot(os.environ['DESCRIPTION'])", - " snapshots = sorted(volume.snapshots.all(), key=lambda x: x.start_time)", - " if len(snapshots) > os.environ['COUNTRETAINED']:", - " for i in range(0,len(snapshots)-os.environ['COUNTRETAINED']):", - " snapshots[i].delete()", - " return 'all OK'" - ]]} - }, - "Environment": { - "Variables": { - "VOLUME_ID" : { "Ref" : "CouchDBVolume"}, - "DESCRIPTION" : "OpenEMR document backup", - "COUNTRETAINED" : 3 - } - }, - "Runtime": "python2.7", - "Timeout": "15" - } - }, - - "DocumentBackupScheduler": { - "Type": "AWS::Events::Rule", - "Properties": { - "Description": "ScheduledRule", - "ScheduleExpression": "rate(1 day)", - "State": "ENABLED", - "Targets": [{ - "Arn": { "Fn::GetAtt": ["DocumentBackupManagerFunction", "Arn"] }, - "Id": "BackupManagerV1" - }] - } - }, - - "DocumentBackupSchedulerPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "FunctionName": { "Ref": "DocumentBackupManagerFunction" }, - "Action": "lambda:InvokeFunction", - "Principal": "events.amazonaws.com", - "SourceArn": { "Fn::GetAtt": ["DocumentBackupScheduler", "Arn"] } - } - }, - - "BarebonesLambdaRole": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Version": "2012-10-17", - "Statement": [{ "Effect": "Allow", "Principal": {"Service": ["lambda.amazonaws.com"]}, "Action": ["sts:AssumeRole"] }] - }, - "Path": "/", - "Policies": [{ - "PolicyName": "root", - "PolicyDocument": { - "Version": "2012-10-17", - "Statement": [ - { "Effect": "Allow", "Action": ["logs:*"], "Resource": "arn:aws:logs:*:*:*" } - ] - } - }] - } - }, - - "CertGrabberFunction": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Description": "gets a certificate's embedded key", - "Handler": "index.lambda_handler", - "Role": { "Fn::GetAtt" : ["BarebonesLambdaRole", "Arn"] }, - "Code": { - "ZipFile" : { "Fn::Join" : ["\n", [ - "import urllib2", - "import json", - "def lambda_handler(event, context):", - " if (event['RequestType'] == 'Delete'):", - " sendResponse(event, context, 'SUCCESS', None)", - " return", - " print('key: ', event['ResourceProperties']['Url'])", - " publicKey=urllib2.urlopen(event['ResourceProperties']['Url']).read()[28:-27]", - " sendResponse(event, context, 'SUCCESS', publicKey)", - "def sendResponse(event, context, responseStatus, responseData):", - " opener = urllib2.build_opener(urllib2.HTTPHandler)", - " o = {}", - " o['Status'] = responseStatus", - " o['Reason'] = 'log ' + context.log_stream_name", - " o['PhysicalResourceId'] = context.log_stream_name", - " o['StackId'] = event['StackId']", - " o['RequestId'] = event['RequestId']", - " o['LogicalResourceId'] = event['LogicalResourceId']", - " o['Data'] = {'PublicKey': responseData}", - " r = json.dumps(o)", - " request = urllib2.Request(event['ResponseURL'], data=r)", - " request.add_header('Content-Type', '')", - " request.add_header('Content-Length', len(r))", - " request.get_method = lambda: 'PUT'", - " url = opener.open(request)" - ]]} - }, - "Runtime": "python2.7", - "Timeout": "5" - } - }, - - "EBCert" : { - "Type" : "Custom::FetchPublicKey", - "DependsOn": "CertWriterInstance", - "Properties": { - "ServiceToken": { "Fn::GetAtt" : ["CertGrabberFunction", "Arn"] }, -<<<<<<< HEAD - "Url" : { "Fn::Join" : ["", ["https://", {"Ref" : "S3Bucket"}, ".s3.amazonaws.com/CA/certs/beanstalk.crt" ] ] } -======= - "Url" : { "Fn::Join" : ["", ["https://", {"Ref" : "S3Bucket"}, ".s3.amazonaws.com/CA/certs/beanstalk.crt" ] ] } ->>>>>>> master - } - }, - - "BeanstalkInstanceRole": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Version": "2012-10-17", - "Statement": [{ "Effect": "Allow", "Principal": {"Service": ["ec2.amazonaws.com"]}, "Action": ["sts:AssumeRole"] }] - }, - "Path": "/", - "Policies": [{ - "PolicyName": "rootBeanstalk", - "PolicyDocument": { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "BucketAccess", - "Action": [ - "s3:Get*", - "s3:List*", - "s3:PutObject" + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash -xe\n", + "cd /root\n", + "mkdir -m 700 CA CA/certs CA/keys CA/work\n", + "cd CA\n", + "openssl genrsa -out keys/ca.key 8192\n", + "openssl req -new -x509 -extensions v3_ca -key keys/ca.key -out certs/ca.crt -days 3650 -subj '/CN=OpenEMR Backend CA'\n", + "openssl req -new -nodes -newkey rsa:2048 -keyout keys/beanstalk.key -out work/beanstalk.csr -days 3648 -subj /CN=beanstalk.openemr.local\n", + "openssl x509 -req -in work/beanstalk.csr -out certs/beanstalk.crt -CA certs/ca.crt -CAkey keys/ca.key -CAcreateserial\n", + "openssl req -new -nodes -newkey rsa:2048 -keyout keys/couch.key -out work/couch.csr -days 3648 -subj /CN=couchdb.openemr.local\n", + "openssl x509 -req -in work/couch.csr -out certs/couch.crt -CA certs/ca.crt -CAkey keys/ca.key\n", + "aws s3 sync keys s3://", + { + "Ref": "S3Bucket" + }, + "/CA/keys --sse aws:kms --sse-kms-key-id ", + { + "Ref": "OpenEMRKey" + }, + " --acl private\n", + "aws s3 sync certs s3://", + { + "Ref": "S3Bucket" + }, + "/CA/certs --acl public-read\n", + "/opt/aws/bin/cfn-signal -e 0 ", + " --stack ", + { + "Ref": "AWS::StackName" + }, + " --resource CertWriterInstance ", + " --region ", + { + "Ref": "AWS::Region" + }, + "\n", + "shutdown -h now", + "\n" + ] + ] + } + } + }, + "Type": "AWS::EC2::Instance" + }, + "CertWriterInstanceProfile": { + "Properties": { + "Path": "/", + "Roles": [ + { + "Ref": "CertWriterRole" + } + ] + }, + "Type": "AWS::IAM::InstanceProfile" + }, + "CertWriterPolicy": { + "Properties": { + "Description": "Policy for initial CA writer", + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + "Ref": "S3Bucket" + }, + "/CA/*" + ] + ] + } + ], + "Sid": "Stmt1500612724000" + }, + { + "Action": [ + "s3:ListBucket" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + "Ref": "S3Bucket" + } + ] + ] + } + ], + "Sid": "Stmt1500612724001" + }, + { + "Action": [ + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "OpenEMRKey", + "Arn" + ] + } + ], + "Sid": "Stmt1500612724002" + } + ], + "Version": "2012-10-17" + } + }, + "Type": "AWS::IAM::ManagedPolicy" + }, + "CertWriterRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "ec2.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Ref": "CertWriterPolicy" + } ], - "Effect": "Allow", - "Resource": [ - "arn:aws:s3:::elasticbeanstalk-*", - "arn:aws:s3:::elasticbeanstalk-*/*" + "Path": "/" + }, + "Type": "AWS::IAM::Role" + }, + "CloudTrail": { + "DependsOn": "BucketPolicy", + "Properties": { + "IncludeGlobalServiceEvents": "true", + "IsLogging": "true", + "IsMultiRegionTrail": "true", + "S3BucketName": { + "Ref": "S3Bucket" + } + }, + "Type": "AWS::CloudTrail::Trail" + }, + "CouchDBInstance": { + "CreationPolicy": { + "ResourceSignal": { + "Timeout": "PT25M" + } + }, + "DependsOn": [ + "CertWriterInstance" + ], + "Metadata": { + "AWS::CloudFormation::Init": { + "Install": { + "commands": { + "01_setup": { + "command": "/root/couchdb.setup.sh" + } + }, + "files": { + "/root/couchdb.setup.sh": { + "content": { + "Fn::Join": [ + "", + [ + "#!/bin/bash -xe\n", + "exec > /tmp/part-002.log 2>&1\n", + "DEBIAN_FRONTEND=noninteractive apt-get dist-upgrade -y -o Dpkg::Options::=\"--force-confdef\" -o Dpkg::Options::=\"--force-confold\" --force-yes\n", + "mkfs -t ext4 /dev/xvdd\n", + "mkdir /mnt/db\n", + "cat /root/fstab.append >> /etc/fstab\n", + "mount /mnt/db\n", + "apt-get -y install couchdb awscli\n", + "service couchdb stop\n", + "aws configure set s3.signature_version s3v4\n", + "aws s3 cp s3://", + { + "Ref": "S3Bucket" + }, + "/CA/certs/ca.crt /etc/couchdb\n", + "aws s3 cp s3://", + { + "Ref": "S3Bucket" + }, + "/CA/certs/couch.crt /etc/couchdb\n", + "chmod 664 /etc/couchdb/*.crt\n", + "aws s3 cp s3://", + { + "Ref": "S3Bucket" + }, + "/CA/keys/couch.key /etc/couchdb --sse aws:kms --sse-kms-key-id ", + { + "Ref": "OpenEMRKey" + }, + "\n", + "chmod 660 /etc/couchdb/couch.key\n", + "chown couchdb:couchdb /etc/couchdb/*.crt /etc/couchdb/*.key\n", + "mv /var/lib/couchdb /mnt/db/couchdb\n", + "ln -s /mnt/db/couchdb /var/lib/couchdb\n", + "cp /root/ip.ini /root/ssl.ini /etc/couchdb/local.d\n", + "chown couchdb:couchdb /etc/couchdb/local.d/ip.ini /etc/couchdb/local.d/ssl.ini\n", + "service couchdb start\n" + ] + ] + }, + "group": "root", + "mode": "000500", + "owner": "root" + }, + "/root/fstab.append": { + "content": { + "Fn::Join": [ + "", + [ + "/dev/xvdd /mnt/db ext4 defaults,nofail 0 0\n" + ] + ] + }, + "group": "root", + "mode": "000400", + "owner": "root" + }, + "/root/ip.ini": { + "content": { + "Fn::Join": [ + "", + [ + "[httpd]\n", + "bind_address = 0.0.0.0\n" + ] + ] + }, + "group": "root", + "mode": "000400", + "owner": "root" + }, + "/root/ssl.ini": { + "content": { + "Fn::Join": [ + "", + [ + "[daemons]\n", + "httpsd = {couch_httpd, start_link, [https]}\n", + "[ssl]\n", + "port = 6984\n", + "key_file = /etc/couchdb/couch.key\n", + "cert_file = /etc/couchdb/couch.crt\n", + "cacert_file = /etc/couchdb/ca.crt\n" + ] + ] + }, + "group": "root", + "mode": "000400", + "owner": "root" + } + } + }, + "configSets": { + "Setup": [ + "Install" + ] + } + } + }, + "Properties": { + "IamInstanceProfile": { + "Ref": "CouchDBInstanceProfile" + }, + "ImageId": { + "Fn::FindInMap": [ + "RegionData", + { + "Ref": "AWS::Region" + }, + "UbuntuAMI" + ] + }, + "InstanceInitiatedShutdownBehavior": "stop", + "InstanceType": "t2.micro", + "KeyName": { + "Ref": "EC2KeyPair" + }, + "SecurityGroupIds": [ + { + "Ref": "CouchDBSecurityGroup" + } + ], + "SubnetId": { + "Ref": "PrivateSubnet1" + }, + "Tags": [ + { + "Key": "Name", + "Value": "Patient Document Store" + } + ], + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash -xe\n", + "exec > /tmp/part-001.log 2>&1\n", + "apt-get -y update\n", + "apt-get -y install python-pip\n", + "pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz\n", + "cfn-init -v ", + " --stack ", + { + "Ref": "AWS::StackName" + }, + " --resource CouchDBInstance ", + " --configsets Setup ", + " --region ", + { + "Ref": "AWS::Region" + }, + "\n", + "cfn-signal -e 0 ", + " --stack ", + { + "Ref": "AWS::StackName" + }, + " --resource CouchDBInstance ", + " --region ", + { + "Ref": "AWS::Region" + }, + "\n" + ] + ] + } + }, + "Volumes": [ + { + "Device": "/dev/sdd", + "VolumeId": { + "Ref": "CouchDBVolume" + } + } ] - }, - { - "Sid": "XRayAccess", - "Action":[ - "xray:PutTraceSegments", - "xray:PutTelemetryRecords" + }, + "Type": "AWS::EC2::Instance" + }, + "CouchDBInstanceProfile": { + "Properties": { + "Path": "/", + "Roles": [ + { + "Ref": "CouchDBRole" + } + ] + }, + "Type": "AWS::IAM::InstanceProfile" + }, + "CouchDBPolicy": { + "Properties": { + "Description": "Policy to retrieve CouchDB SSL credentials", + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + "Ref": "S3Bucket" + }, + "/CA/certs/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + "Ref": "S3Bucket" + }, + "/CA/keys/couch.key" + ] + ] + } + ], + "Sid": "Stmt1500699052000" + }, + { + "Action": [ + "kms:Decrypt" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "OpenEMRKey", + "Arn" + ] + } + ], + "Sid": "Stmt1500612724002" + } + ], + "Version": "2012-10-17" + } + }, + "Type": "AWS::IAM::ManagedPolicy" + }, + "CouchDBRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "ec2.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Ref": "CouchDBPolicy" + } + ], + "Path": "/" + }, + "Type": "AWS::IAM::Role" + }, + "CouchDBSGIngress": { + "Properties": { + "GroupId": { + "Ref": "CouchDBSecurityGroup" + }, + "IpProtocol": "-1", + "SourceSecurityGroupId": { + "Ref": "ApplicationSecurityGroup" + } + }, + "Type": "AWS::EC2::SecurityGroupIngress" + }, + "CouchDBSecurityGroup": { + "Properties": { + "GroupDescription": "Patient Document Access", + "Tags": [ + { + "Key": "Name", + "Value": "Patient Documents" + } + ], + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::SecurityGroup" + }, + "CouchDBVolume": { + "DeletionPolicy": "Snapshot", + "Properties": { + "AvailabilityZone": { + "Fn::Select": [ + "0", + { + "Fn::GetAZs": "" + } + ] + }, + "Encrypted": "true", + "KmsKeyId": { + "Ref": "OpenEMRKey" + }, + "Size": { + "Ref": "DocumentStorage" + }, + "Tags": [ + { + "Key": "Name", + "Value": "Patient Documents" + } ], - "Effect": "Allow", - "Resource": "*" - }, - { - "Sid": "CloudWatchLogsAccess", - "Action": [ - "logs:PutLogEvents", - "logs:CreateLogStream" + "VolumeType": "sc1" + }, + "Type": "AWS::EC2::Volume" + }, + "DBSGIngress": { + "Properties": { + "GroupId": { + "Ref": "DBSecurityGroup" + }, + "IpProtocol": "-1", + "SourceSecurityGroupId": { + "Ref": "ApplicationSecurityGroup" + } + }, + "Type": "AWS::EC2::SecurityGroupIngress" + }, + "DBSecurityGroup": { + "Properties": { + "GroupDescription": "Patient Records", + "Tags": [ + { + "Key": "Name", + "Value": "MySQL Access" + } ], - "Effect": "Allow", - "Resource": [ - "arn:aws:logs:*:*:log-group:/aws/elasticbeanstalk*" + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::SecurityGroup" + }, + "DNS": { + "Properties": { + "Name": "openemr.local", + "VPCs": [ + { + "VPCId": { + "Ref": "VPC" + }, + "VPCRegion": { + "Ref": "AWS::Region" + } + } ] - } - ] - } - }, - { - "PolicyName": "certGrabber", - "PolicyDocument": { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "Stmt1500699052000", - "Effect": "Allow", - "Action": [ - "s3:GetObject" - ], - "Resource": [ - { "Fn::Join" : ["", ["arn:aws:s3:::", {"Ref":"S3Bucket"}, "/CA/certs/*"]]}, - { "Fn::Join" : ["", ["arn:aws:s3:::", {"Ref":"S3Bucket"}, "/CA/keys/beanstalk.key"]]} - ] - }, - { - "Sid": "Stmt1500612724002", - "Effect": "Allow", - "Action": [ - "kms:Decrypt" - ], - "Resource": [ { "Fn::GetAtt" : ["OpenEMRKey", "Arn" ] } ] - } - ] - } - }] - } - }, - - "BeanstalkInstanceProfile": { - "Type": "AWS::IAM::InstanceProfile", - "Properties": { - "Path": "/", - "Roles": [ - { - "Ref": "BeanstalkInstanceRole" + }, + "Type": "AWS::Route53::HostedZone" + }, + "DNSBackupAgent": { + "Properties": { + "HostedZoneId": { + "Ref": "DNS" + }, + "Name": "nfsbackups.openemr.local", + "ResourceRecords": [ + { + "Fn::GetAtt": [ + "NFSBackupInstance", + "PrivateDnsName" + ] + } + ], + "TTL": "900", + "Type": "CNAME" + }, + "Type": "AWS::Route53::RecordSet" + }, + "DNSCouchDB": { + "Properties": { + "HostedZoneId": { + "Ref": "DNS" + }, + "Name": "couchdb.openemr.local", + "ResourceRecords": [ + { + "Fn::GetAtt": [ + "CouchDBInstance", + "PrivateDnsName" + ] + } + ], + "TTL": "900", + "Type": "CNAME" + }, + "Type": "AWS::Route53::RecordSet" + }, + "DNSEFS": { + "DependsOn": [ + "EFSMountPrivate1", + "EFSMountPrivate2" + ], + "Properties": { + "HostedZoneId": { + "Ref": "DNS" + }, + "Name": "nfs.openemr.local", + "ResourceRecords": [ + { + "Fn::Join": [ + "", + [ + { + "Ref": "ElasticFileSystem" + }, + ".efs.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + ], + "TTL": "900", + "Type": "CNAME" + }, + "Type": "AWS::Route53::RecordSet" + }, + "DNSMySQL": { + "Properties": { + "HostedZoneId": { + "Ref": "DNS" + }, + "Name": "mysql.openemr.local", + "ResourceRecords": [ + { + "Fn::GetAtt": [ + "RDSInstance", + "Endpoint.Address" + ] + } + ], + "TTL": "900", + "Type": "CNAME" + }, + "Type": "AWS::Route53::RecordSet" + }, + "DNSRedis": { + "Properties": { + "HostedZoneId": { + "Ref": "DNS" + }, + "Name": "redis.openemr.local", + "ResourceRecords": [ + { + "Fn::GetAtt": [ + "RedisCluster", + "RedisEndpoint.Address" + ] + } + ], + "TTL": "900", + "Type": "CNAME" + }, + "Type": "AWS::Route53::RecordSet" + }, + "DocumentBackupExecutionRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Path": "/", + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:*" + ], + "Effect": "Allow", + "Resource": "arn:aws:logs:*:*:*" + }, + { + "Action": [ + "ec2:DescribeVolumeStatus", + "ec2:DescribeSnapshots", + "ec2:CreateSnapshot", + "ec2:DeleteSnapshot" + ], + "Effect": "Allow", + "Resource": [ + "*" + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "root" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "DocumentBackupManagerFunction": { + "Properties": { + "Code": { + "ZipFile": { + "Fn::Join": [ + "\n", + [ + "import boto3", + "import os", + "def lambda_handler(event, context):", + " volume = boto3.session.Session(region_name = os.environ['AWS_DEFAULT_REGION']).resource('ec2').Volume(os.environ['VOLUME_ID'])", + " volume.create_snapshot(os.environ['DESCRIPTION'])", + " snapshots = sorted(volume.snapshots.all(), key=lambda x: x.start_time)", + " if len(snapshots) > os.environ['COUNTRETAINED']:", + " for i in range(0,len(snapshots)-os.environ['COUNTRETAINED']):", + " snapshots[i].delete()", + " return 'all OK'" + ] + ] + } + }, + "Description": "handles patient document (CouchDB) backups", + "Environment": { + "Variables": { + "COUNTRETAINED": 3, + "DESCRIPTION": "OpenEMR document backup", + "VOLUME_ID": { + "Ref": "CouchDBVolume" + } + } + }, + "Handler": "index.lambda_handler", + "Role": { + "Fn::GetAtt": [ + "DocumentBackupExecutionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Timeout": "15" + }, + "Type": "AWS::Lambda::Function" + }, + "DocumentBackupScheduler": { + "Properties": { + "Description": "BackupRule", + "ScheduleExpression": "rate(1 day)", + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "DocumentBackupManagerFunction", + "Arn" + ] + }, + "Id": "BackupManagerV1" + } + ] + }, + "Type": "AWS::Events::Rule" + }, + "DocumentBackupSchedulerPermission": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "DocumentBackupManagerFunction" + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "DocumentBackupScheduler", + "Arn" + ] } - ] - } - }, - - "EBApplication" : { - "Type" : "AWS::ElasticBeanstalk::Application", - "Properties" : { - "Description" : "OpenEMR Application Stack" - } - }, - - "EBApplicationVersion" : { - "Type" : "AWS::ElasticBeanstalk::ApplicationVersion", - "Properties" : { - "Description" : "Version 5.0.0", - "ApplicationName" : { "Ref" : "EBApplication" }, - "SourceBundle" : { - "S3Bucket" : { "Fn::FindInMap" : [ "RegionData", { "Ref" : "AWS::Region" }, "RegionBucket"]}, - "S3Key" : { "Fn::FindInMap" : [ "RegionData", { "Ref" : "AWS::Region" }, "ApplicationSource"]} + }, + "Type": "AWS::Lambda::Permission" + }, + "EBApplication": { + "Properties": { + "Description": "OpenEMR Application Stack" + }, + "Type": "AWS::ElasticBeanstalk::Application" + }, + "EBApplicationVersion": { + "Properties": { + "ApplicationName": { + "Ref": "EBApplication" + }, + "Description": "Version 1.0", + "SourceBundle": { + "S3Bucket": { + "Fn::FindInMap": [ + "RegionData", + { + "Ref": "AWS::Region" + }, + "RegionBucket" + ] + }, + "S3Key": { + "Fn::FindInMap": [ + "RegionData", + { + "Ref": "AWS::Region" + }, + "ApplicationSource" + ] + } + } + }, + "Type": "AWS::ElasticBeanstalk::ApplicationVersion" + }, + "EBCert": { + "DependsOn": "CertWriterInstance", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CertGrabberFunction", + "Arn" + ] + }, + "Url": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "S3Bucket" + }, + ".s3.amazonaws.com/CA/certs/beanstalk.crt" + ] + ] + } + }, + "Type": "AWS::CloudFormation::CustomResource" + }, + "EBEnvironment": { + "DependsOn": [ + "DNSEFS", + "DNSRedis", + "DNSCouchDB", + "DNSMySQL" + ], + "Properties": { + "ApplicationName": { + "Ref": "EBApplication" + }, + "Description": "OpenEMR v5.0.0 cloud deployment", + "OptionSettings": [ + { + "Namespace": "aws:autoscaling:launchconfiguration", + "OptionName": "SecurityGroups", + "Value": { + "Ref": "ApplicationSecurityGroup" + } + }, + { + "Namespace": "aws:autoscaling:launchconfiguration", + "OptionName": "EC2KeyName", + "Value": { + "Ref": "EC2KeyPair" + } + }, + { + "Namespace": "aws:autoscaling:launchconfiguration", + "OptionName": "IamInstanceProfile", + "Value": { + "Fn::GetAtt": [ + "BeanstalkInstanceProfile", + "Arn" + ] + } + }, + { + "Namespace": "aws:autoscaling:launchconfiguration", + "OptionName": "InstanceType", + "Value": "t2.micro" + }, + { + "Namespace": "aws:elb:listener", + "OptionName": "InstanceProtocol", + "Value": "HTTPS" + }, + { + "Namespace": "aws:elb:listener", + "OptionName": "InstancePort", + "Value": "443" + }, + { + "Namespace": "aws:elb:policies", + "OptionName": "ConnectionDrainingEnabled", + "Value": "true" + }, + { + "Namespace": "aws:elb:policies", + "OptionName": "ConnectionSettingIdleTimeout", + "Value": "3600" + }, + { + "Namespace": "aws:elb:policies:backendencryption", + "OptionName": "PublicKeyPolicyNames", + "Value": "backendkey" + }, + { + "Namespace": "aws:elb:policies:backendencryption", + "OptionName": "InstancePorts", + "Value": "443" + }, + { + "Namespace": "aws:elb:policies:backendkey", + "OptionName": "PublicKey", + "Value": { + "Fn::GetAtt": [ + "EBCert", + "PublicKey" + ] + } + }, + { + "Namespace": "aws:ec2:vpc", + "OptionName": "VPCId", + "Value": { + "Ref": "VPC" + } + }, + { + "Namespace": "aws:ec2:vpc", + "OptionName": "Subnets", + "Value": { + "Fn::Join": [ + ",", + [ + { + "Ref": "PrivateSubnet1" + }, + { + "Ref": "PrivateSubnet2" + } + ] + ] + } + }, + { + "Namespace": "aws:ec2:vpc", + "OptionName": "ELBSubnets", + "Value": { + "Fn::Join": [ + ",", + [ + { + "Ref": "PublicSubnet1" + }, + { + "Ref": "PublicSubnet2" + } + ] + ] + } + }, + { + "Namespace": "aws:elasticbeanstalk:application", + "OptionName": "Application Healthcheck URL", + "Value": "HTTPS:443/openemr/version.php" + }, + { + "Namespace": "aws:elasticbeanstalk:application:environment", + "OptionName": "TIMEZONE", + "Value": { + "Ref": "TimeZone" + } + }, + { + "Namespace": "aws:elasticbeanstalk:application:environment", + "OptionName": "REDIS_IP", + "Value": "redis.openemr.local" + }, + { + "Namespace": "aws:elasticbeanstalk:application:environment", + "OptionName": "FILE_SYSTEM_ID", + "Value": { + "Ref": "ElasticFileSystem" + } + }, + { + "Namespace": "aws:elasticbeanstalk:application:environment", + "OptionName": "NFS_HOSTNAME", + "Value": "nfs.openemr.local" + }, + { + "Namespace": "aws:elasticbeanstalk:application:environment", + "OptionName": "S3BUCKET", + "Value": { + "Ref": "S3Bucket" + } + }, + { + "Namespace": "aws:elasticbeanstalk:application:environment", + "OptionName": "KMSKEY", + "Value": { + "Ref": "OpenEMRKey" + } + } + ], + "SolutionStackName": "64bit Amazon Linux 2017.03 v2.4.3 running PHP 7.0", + "VersionLabel": { + "Ref": "EBApplicationVersion" + } + }, + "Type": "AWS::ElasticBeanstalk::Environment" + }, + "EFSMountPrivate1": { + "Properties": { + "FileSystemId": { + "Ref": "ElasticFileSystem" + }, + "SecurityGroups": [ + { + "Ref": "EFSSecurityGroup" + } + ], + "SubnetId": { + "Ref": "PrivateSubnet1" + } + }, + "Type": "AWS::EFS::MountTarget" + }, + "EFSMountPrivate2": { + "Properties": { + "FileSystemId": { + "Ref": "ElasticFileSystem" + }, + "SecurityGroups": [ + { + "Ref": "EFSSecurityGroup" + } + ], + "SubnetId": { + "Ref": "PrivateSubnet2" + } + }, + "Type": "AWS::EFS::MountTarget" + }, + "EFSSGIngress": { + "Properties": { + "GroupId": { + "Ref": "EFSSecurityGroup" + }, + "IpProtocol": "-1", + "SourceSecurityGroupId": { + "Ref": "ApplicationSecurityGroup" + } + }, + "Type": "AWS::EC2::SecurityGroupIngress" + }, + "EFSSecurityGroup": { + "Properties": { + "GroupDescription": "Webworker NFS Access", + "Tags": [ + { + "Key": "Name", + "Value": "NFS Access" + } + ], + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::SecurityGroup" + }, + "ElasticFileSystem": { + "DeletionPolicy": "Retain", + "Properties": { + "FileSystemTags": [ + { + "Key": "Name", + "Value": "OpenEMR Codebase" + } + ] + }, + "Type": "AWS::EFS::FileSystem" + }, + "NFSBackupInstance": { + "CreationPolicy": { + "ResourceSignal": { + "Timeout": "PT5M" + } + }, + "DependsOn": [ + "rtPrivate2Attach", + "DNSEFS" + ], + "Metadata": { + "AWS::CloudFormation::Init": { + "Install": { + "commands": { + "01_setup": { + "command": "/root/setup.sh" + } + }, + "files": { + "/etc/cron.daily/backup.sh": { + "content": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\n", + "S3=", + { + "Ref": "S3Bucket" + }, + "\n", + "KMS=", + { + "Ref": "OpenEMRKey" + }, + "\n", + "PASSPHRASE=`aws s3 cp s3://$S3/Backup/passphrase.txt - --sse aws:kms --sse-kms-key-id $KMS`\n", + "export PASSPHRASE\n", + "duplicity --full-if-older-than 1M /mnt/efs s3://s3.amazonaws.com/$S3/Backup\n", + "duplicity remove-all-but-n-full 2 --force s3://s3.amazonaws.com/$S3/Backup\n" + ] + ] + }, + "group": "root", + "mode": "000500", + "owner": "root" + }, + "/root/recovery.sh": { + "content": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\n", + "S3=", + { + "Ref": "S3Bucket" + }, + "\n", + "KMS=", + { + "Ref": "OpenEMRKey" + }, + "\n", + "PASSPHRASE=`aws s3 cp s3://$S3/Backup/passphrase.txt - --sse aws:kms --sse-kms-key-id $KMS`\n", + "export PASSPHRASE\n", + "duplicity --force s3://s3.amazonaws.com/$S3/Backup /mnt/efs\n" + ] + ] + }, + "group": "root", + "mode": "000500", + "owner": "root" + }, + "/root/setup.sh": { + "content": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\n", + "S3=", + { + "Ref": "S3Bucket" + }, + "\n", + "KMS=", + { + "Ref": "OpenEMRKey" + }, + "\n", + "apt-get -y update\n", + "DEBIAN_FRONTEND=noninteractive apt-get dist-upgrade -y -o Dpkg::Options::=\"--force-confdef\" -o Dpkg::Options::=\"--force-confold\" --force-yes\n", + "apt-get -y install duplicity python-boto nfs-common awscli\n", + "mkdir /mnt/efs\n", + "echo \"nfs.openemr.local:/ /mnt/efs nfs4 nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2 0 0\" >> /etc/fstab\n", + "mount /mnt/efs\n", + "touch /tmp/mypass\n", + "chmod 500 /tmp/mypass\n", + "openssl rand -base64 32 >> /tmp/mypass\n", + "aws s3 cp /tmp/mypass s3://$S3/Backup/passphrase.txt --sse aws:kms --sse-kms-key-id $KMS\n", + "rm /tmp/mypass\n" + ] + ] + }, + "group": "root", + "mode": "000500", + "owner": "root" + } + } + }, + "configSets": { + "Setup": [ + "Install" + ] + } + } + }, + "Properties": { + "IamInstanceProfile": { + "Ref": "NFSInstanceProfile" + }, + "ImageId": { + "Fn::FindInMap": [ + "RegionData", + { + "Ref": "AWS::Region" + }, + "UbuntuAMI" + ] + }, + "InstanceInitiatedShutdownBehavior": "stop", + "InstanceType": "t2.nano", + "KeyName": { + "Ref": "EC2KeyPair" + }, + "SecurityGroupIds": [ + { + "Ref": "NFSBackupSecurityGroup" + } + ], + "SubnetId": { + "Ref": "PrivateSubnet2" + }, + "Tags": [ + { + "Key": "Name", + "Value": "NFS Backup Agent" + } + ], + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash -xe\n", + "exec > /tmp/part-001.log 2>&1\n", + "apt-get -y update\n", + "apt-get -y install python-pip\n", + "pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz\n", + "cfn-init -v ", + " --stack ", + { + "Ref": "AWS::StackName" + }, + " --resource NFSBackupInstance ", + " --configsets Setup ", + " --region ", + { + "Ref": "AWS::Region" + }, + "\n", + "cfn-signal -e 0 ", + " --stack ", + { + "Ref": "AWS::StackName" + }, + " --resource NFSBackupInstance ", + " --region ", + { + "Ref": "AWS::Region" + }, + "\n" + ] + ] + } + } + }, + "Type": "AWS::EC2::Instance" + }, + "NFSBackupPolicy": { + "Properties": { + "Description": "Policy for ongoing NFS backup instance", + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:ListBucket" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + "Ref": "S3Bucket" + } + ] + ] + } + ], + "Sid": "Stmt1500699052003" + }, + { + "Action": [ + "s3:PutObject", + "s3:GetObject", + "s3:DeleteObject" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + "Ref": "S3Bucket" + }, + "/Backup/*" + ] + ] + } + ], + "Sid": "Stmt1500699052000" + }, + { + "Action": [ + "kms:Encrypt", + "kms:Decrypt", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "OpenEMRKey", + "Arn" + ] + } + ], + "Sid": "Stmt1500612724002" + } + ], + "Version": "2012-10-17" + } + }, + "Type": "AWS::IAM::ManagedPolicy" + }, + "NFSBackupRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "ec2.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Ref": "NFSBackupPolicy" + } + ], + "Path": "/" + }, + "Type": "AWS::IAM::Role" + }, + "NFSBackupSecurityGroup": { + "Properties": { + "GroupDescription": "NFS Backup Access", + "Tags": [ + { + "Key": "Name", + "Value": "NFS Backup Access" + } + ], + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::SecurityGroup" + }, + "NFSInstanceProfile": { + "Properties": { + "Path": "/", + "Roles": [ + { + "Ref": "NFSBackupRole" + } + ] + }, + "Type": "AWS::IAM::InstanceProfile" + }, + "NFSSGIngress": { + "Properties": { + "GroupId": { + "Ref": "EFSSecurityGroup" + }, + "IpProtocol": "-1", + "SourceSecurityGroupId": { + "Ref": "NFSBackupSecurityGroup" + } + }, + "Type": "AWS::EC2::SecurityGroupIngress" + }, + "OpenEMRKey": { + "DeletionPolicy": "Retain", + "Properties": { + "KeyPolicy": { + "Id": "key-default-1", + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": [ + { + "Fn::Join": [ + ":", + [ + "arn:aws:iam:", + { + "Ref": "AWS::AccountId" + }, + "root" + ] + ] + } + ] + }, + "Resource": "*", + "Sid": "1" + } + ], + "Version": "2012-10-17" + } + }, + "Type": "AWS::KMS::Key" + }, + "PrivateSubnet1": { + "Properties": { + "AvailabilityZone": { + "Fn::Select": [ + "0", + { + "Fn::GetAZs": "" + } + ] + }, + "CidrBlock": "10.0.2.0/24", + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::Subnet" + }, + "PrivateSubnet2": { + "Properties": { + "AvailabilityZone": { + "Fn::Select": [ + "1", + { + "Fn::GetAZs": "" + } + ] + }, + "CidrBlock": "10.0.4.0/24", + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::Subnet" + }, + "PublicSubnet1": { + "Properties": { + "AvailabilityZone": { + "Fn::Select": [ + "0", + { + "Fn::GetAZs": "" + } + ] + }, + "CidrBlock": "10.0.1.0/24", + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::Subnet" + }, + "PublicSubnet2": { + "Properties": { + "AvailabilityZone": { + "Fn::Select": [ + "1", + { + "Fn::GetAZs": "" + } + ] + }, + "CidrBlock": "10.0.3.0/24", + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::Subnet" + }, + "RDSInstance": { + "DeletionPolicy": "Snapshot", + "Properties": { + "AllocatedStorage": { + "Ref": "PatientRecords" + }, + "DBInstanceClass": "db.t2.small", + "DBName": "openemr", + "DBSubnetGroupName": { + "Ref": "RDSSubnetGroup" + }, + "Engine": "MySQL", + "EngineVersion": { + "Fn::FindInMap": [ + "RegionData", + { + "Ref": "AWS::Region" + }, + "MySQLVersion" + ] + }, + "KmsKeyId": { + "Ref": "OpenEMRKey" + }, + "MasterUserPassword": { + "Ref": "RDSPassword" + }, + "MasterUsername": "openemr", + "MultiAZ": "false", + "PubliclyAccessible": "false", + "StorageEncrypted": "true", + "Tags": [ + { + "Key": "Name", + "Value": "Patient Records" + } + ], + "VPCSecurityGroups": [ + { + "Ref": "DBSecurityGroup" + } + ] + }, + "Type": "AWS::RDS::DBInstance" + }, + "RDSSubnetGroup": { + "Properties": { + "DBSubnetGroupDescription": "MySQL node locations", + "SubnetIds": [ + { + "Ref": "PrivateSubnet1" + }, + { + "Ref": "PrivateSubnet2" + } + ] + }, + "Type": "AWS::RDS::DBSubnetGroup" + }, + "RedisCluster": { + "Properties": { + "CacheNodeType": "cache.t2.small", + "CacheSubnetGroupName": { + "Ref": "RedisSubnets" + }, + "Engine": "redis", + "NumCacheNodes": "1", + "VpcSecurityGroupIds": [ + { + "Fn::GetAtt": [ + "RedisSecurityGroup", + "GroupId" + ] + } + ] + }, + "Type": "AWS::ElastiCache::CacheCluster" + }, + "RedisSGIngress": { + "Properties": { + "GroupId": { + "Ref": "RedisSecurityGroup" + }, + "IpProtocol": "-1", + "SourceSecurityGroupId": { + "Ref": "ApplicationSecurityGroup" + } + }, + "Type": "AWS::EC2::SecurityGroupIngress" + }, + "RedisSecurityGroup": { + "Properties": { + "GroupDescription": "Webworker Session Store", + "Tags": [ + { + "Key": "Name", + "Value": "Redis Access" + } + ], + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::SecurityGroup" + }, + "RedisSubnets": { + "Properties": { + "Description": "Redis node locations", + "SubnetIds": [ + { + "Ref": "PrivateSubnet1" + }, + { + "Ref": "PrivateSubnet2" + } + ] + }, + "Type": "AWS::ElastiCache::SubnetGroup" + }, + "S3Bucket": { + "DeletionPolicy": "Retain", + "Properties": { + "BucketName": { + "Fn::Join": [ + "-", + [ + "openemr", + { + "Fn::Select": [ + "2", + { + "Fn::Split": [ + "/", + { + "Ref": "AWS::StackId" + } + ] + } + ] + } + ] + ] + } + }, + "Type": "AWS::S3::Bucket" + }, + "VPC": { + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": "true", + "EnableDnsSupport": "true" + }, + "Type": "AWS::EC2::VPC" + }, + "ig": { + "Type": "AWS::EC2::InternetGateway" + }, + "igAttach": { + "Properties": { + "InternetGatewayId": { + "Ref": "ig" + }, + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::VPCGatewayAttachment" + }, + "nat": { + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "natIp", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "PublicSubnet1" + } + }, + "Type": "AWS::EC2::NatGateway" + }, + "natIp": { + "Properties": { + "Domain": "vpc" + }, + "Type": "AWS::EC2::EIP" + }, + "rtPrivate": { + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "nat" + }, + "RouteTableId": { + "Ref": "rtTablePrivate" + } + }, + "Type": "AWS::EC2::Route" + }, + "rtPrivate1Attach": { + "Properties": { + "RouteTableId": { + "Ref": "rtTablePrivate" + }, + "SubnetId": { + "Ref": "PrivateSubnet1" + } + }, + "Type": "AWS::EC2::SubnetRouteTableAssociation" + }, + "rtPrivate2Attach": { + "Properties": { + "RouteTableId": { + "Ref": "rtTablePrivate" + }, + "SubnetId": { + "Ref": "PrivateSubnet2" + } + }, + "Type": "AWS::EC2::SubnetRouteTableAssociation" + }, + "rtPublic": { + "DependsOn": "igAttach", + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "ig" + }, + "RouteTableId": { + "Ref": "rtTablePublic" + } + }, + "Type": "AWS::EC2::Route" + }, + "rtPublic1Attach": { + "Properties": { + "RouteTableId": { + "Ref": "rtTablePublic" + }, + "SubnetId": { + "Ref": "PublicSubnet1" + } + }, + "Type": "AWS::EC2::SubnetRouteTableAssociation" + }, + "rtPublic2Attach": { + "Properties": { + "RouteTableId": { + "Ref": "rtTablePublic" + }, + "SubnetId": { + "Ref": "PublicSubnet2" + } + }, + "Type": "AWS::EC2::SubnetRouteTableAssociation" + }, + "rtTablePrivate": { + "Properties": { + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::RouteTable" + }, + "rtTablePublic": { + "Properties": { + "VpcId": { + "Ref": "VPC" + } + }, + "Type": "AWS::EC2::RouteTable" } - } - }, - - "EBEnvironment" : { - "Type" : "AWS::ElasticBeanstalk::Environment", - "DependsOn" : ["CertWriterInstance", "DNSEFS", "DNSRedis"], - "Properties" : { - "ApplicationName" : { "Ref" : "EBApplication" }, - "Description" : "OpenEMR v5.0.0 cloud deployment", - "SolutionStackName" : "64bit Amazon Linux 2017.03 v2.4.3 running PHP 7.0", - "VersionLabel" : { "Ref" : "EBApplicationVersion" }, - "OptionSettings" : [ - {"Namespace" : "aws:autoscaling:launchconfiguration", "OptionName" : "SecurityGroups", "Value" : { "Ref" : "ApplicationSecurityGroup" }}, - {"Namespace" : "aws:autoscaling:launchconfiguration", "OptionName" : "EC2KeyName", "Value" : { "Ref" : "EC2KeyPair" }}, - {"Namespace" : "aws:autoscaling:launchconfiguration", "OptionName" : "IamInstanceProfile", "Value" : {"Fn::GetAtt" : ["BeanstalkInstanceProfile", "Arn"] } }, - {"Namespace" : "aws:autoscaling:launchconfiguration", "OptionName" : "InstanceType", "Value" : "t2.micro" }, - {"Namespace" : "aws:elb:listener", "OptionName" : "InstanceProtocol", "Value" : "HTTPS"}, - {"Namespace" : "aws:elb:listener", "OptionName" : "InstancePort", "Value" : "443"}, - {"Namespace" : "aws:elb:policies", "OptionName" : "ConnectionDrainingEnabled", "Value" : "true"}, - {"Namespace" : "aws:elb:policies", "OptionName" : "ConnectionSettingIdleTimeout", "Value" : "3600"}, - {"Namespace" : "aws:elb:policies:backendencryption", "OptionName" : "PublicKeyPolicyNames", "Value" : "backendkey"}, - {"Namespace" : "aws:elb:policies:backendencryption", "OptionName" : "InstancePorts", "Value" : "443"}, - {"Namespace" : "aws:elb:policies:backendkey", "OptionName" : "PublicKey", "Value" : { "Fn::GetAtt" : ["EBCert", "PublicKey"] }}, - {"Namespace" : "aws:ec2:vpc", "OptionName" : "VPCId", "Value" : { "Ref" : "VPC" }}, - {"Namespace" : "aws:ec2:vpc", "OptionName" : "Subnets", "Value" : - { "Fn::Join" : [ ",", [ {"Ref": "SubnetPrivate1"}, {"Ref": "SubnetPrivate2"} ] ] } - }, - {"Namespace" : "aws:ec2:vpc", "OptionName" : "ELBSubnets", "Value" : - { "Fn::Join" : [ ",", [ {"Ref": "SubnetPublic1"}, {"Ref": "SubnetPublic2"} ] ] } - }, - {"Namespace" : "aws:elasticbeanstalk:application", "OptionName": "Application Healthcheck URL", "Value": "HTTPS:443/openemr/version.php"}, - {"Namespace" : "aws:elasticbeanstalk:application:environment", "OptionName": "TIMEZONE", "Value": {"Ref" : "TimeZone"}}, - {"Namespace" : "aws:elasticbeanstalk:application:environment", "OptionName": "REDIS_IP", "Value": "redis.openemr.local"}, - {"Namespace" : "aws:elasticbeanstalk:application:environment", "OptionName": "FILE_SYSTEM_ID", "Value": {"Ref" : "ElasticFileSystem"}}, - {"Namespace" : "aws:elasticbeanstalk:application:environment", "OptionName": "NFS_HOSTNAME", "Value": "nfs.openemr.local"}, - {"Namespace" : "aws:elasticbeanstalk:application:environment", "OptionName": "S3BUCKET", "Value": {"Ref" : "S3Bucket"}}, - {"Namespace" : "aws:elasticbeanstalk:application:environment", "OptionName": "KMSKEY", "Value": {"Ref" : "OpenEMRKey"}} - ] - } - } - - -}, - - "Outputs" : { - "OpenEMR" : { - "Description" : "OpenEMR Setup", - "Value" : { "Fn::Join" : [ "", [ "http://", { "Fn::GetAtt" : ["EBEnvironment", "EndpointURL"] }, "/openemr"]]} } - } } diff --git a/assets/eb/05-efs-mount-install.config b/assets/eb/05-efs-mount-install.config index 6bdad8d..916dcf8 100644 --- a/assets/eb/05-efs-mount-install.config +++ b/assets/eb/05-efs-mount-install.config @@ -22,11 +22,9 @@ files: printf '\n\n07 EFS Mount Install\n\n' EFS_REGION=$(/opt/elasticbeanstalk/bin/get-config environment | jq -r '.REGION') EFS_MOUNT_DIR=$(/opt/elasticbeanstalk/bin/get-config environment | jq -r '.MOUNT_DIRECTORY') - EFS_FILE_SYSTEM_ID=$(/opt/elasticbeanstalk/bin/get-config environment | jq -r '.FILE_SYSTEM_ID') EFS_HOSTNAME=$(/opt/elasticbeanstalk/bin/get-config environment | jq -r '.NFS_HOSTNAME') echo "Mounting EFS filesystem ${EFS_DNS_NAME} to directory ${EFS_MOUNT_DIR} ..." - echo "EFS File System ID: ${EFS_FILE_SYSTEM_ID} ..." echo "Region: ${REGION} ..." echo 'Stopping NFS ID Mapper...' diff --git a/assets/troposphere/requirements.txt b/assets/troposphere/requirements.txt new file mode 100644 index 0000000..e3dcd7d --- /dev/null +++ b/assets/troposphere/requirements.txt @@ -0,0 +1 @@ +troposphere==1.9.5 diff --git a/assets/troposphere/stack.py b/assets/troposphere/stack.py index dd28d74..a2e468c 100644 --- a/assets/troposphere/stack.py +++ b/assets/troposphere/stack.py @@ -32,12 +32,12 @@ def setInputs(t, args): if (args.recovery): t.add_parameter(Parameter( 'RecoveryKMSKey', - Description = 'The KMS key ARN for the previous stack (expressed as ''arn:aws:kms...'')', + Description = 'The KMS key ARN for the previous stack (''arn:aws:kms...'')', Type = 'String' )) t.add_parameter(Parameter( 'RecoveryRDSSnapshotARN', - Description = 'The database snapshot ARN for the previous stack', + Description = 'The database snapshot ARN for the previous stack (''arn:aws:rds...'')', Type = 'String' )) t.add_parameter(Parameter( @@ -878,6 +878,16 @@ def buildNFSBackup(t, args): ) ) + if (args.dev or args.force_bastion): + t.add_resource( + ec2.SecurityGroupIngress( + 'NFSBackupSGIngress', + GroupId = Ref('NFSBackupSecurityGroup'), + IpProtocol = '-1', + SourceSecurityGroupId = Ref('SSHSecurityGroup') + ) + ) + rolePolicyStatements = [ { "Sid": "Stmt1500699052003", @@ -1105,7 +1115,7 @@ def buildNFSBackup(t, args): UserData = Base64(Join('', bootstrapScript)), CreationPolicy = { "ResourceSignal" : { - "Timeout" : "PT5M" + "Timeout" : "PT15M" if args.recovery else "PT5M" } } ) @@ -1352,6 +1362,7 @@ def buildDocumentStore(t, args): ) ) + # it honestly should take <5, but I had it take almost 20 once in testing t.add_resource( ec2.Instance( 'CouchDBInstance', @@ -1372,7 +1383,7 @@ def buildDocumentStore(t, args): UserData = Base64(Join('', bootstrapScript)), CreationPolicy = { "ResourceSignal" : { - "Timeout" : "PT5M" + "Timeout" : "PT25M" } } ) @@ -1723,7 +1734,7 @@ def setOutputs(t, args): parser = argparse.ArgumentParser(description="OpenEMR stack builder") parser.add_argument("--dev", help="build [security breaching!] development resources", action="store_true") parser.add_argument("--force-bastion", help="force developer bastion outside of development", action="store_true") -parser.add_argument("--dualAZ", help="build AZ-hardened stack", action="store_true") +parser.add_argument("--dualAZ", help="build AZ-hardened stack [in progress!]", action="store_true") parser.add_argument("--recovery", help="load OpenEMR stack from backups", action="store_true") args = parser.parse_args() @@ -1760,6 +1771,7 @@ def setOutputs(t, args): buildMySQL(t, args) buildCertWriter(t, args.dev) buildNFSBackup(t, args) +# TODO: document store does not yet support multi-node cross-AZ replication buildDocumentStore(t, args) buildDocumentBackups(t) buildApplication(t) diff --git a/chapters/04-VPN-Access.md b/chapters/04-VPN-Access.md index 38550ed..065ee89 100644 --- a/chapters/04-VPN-Access.md +++ b/chapters/04-VPN-Access.md @@ -1,6 +1,6 @@ _[< previous chapter](03-Secure-Domain-Setup.md) | [next chapter >](05-Administration.md)_ -# ☁ VPN Access +# 📝 VPN Access You can use a VPN, or virtual private network, to tunnel into the Amazon VPC and connect directly to protected resources like the database and the application servers. However, you won't need to do so when you first create your stack — feel free to skip this for now and come back to it only after you need to explore or troubleshoot your OpenEMR installation. diff --git a/chapters/05-Administration.md b/chapters/05-Administration.md index e594324..83f029b 100644 --- a/chapters/05-Administration.md +++ b/chapters/05-Administration.md @@ -1,4 +1,4 @@ -_[< previous chapter](04-VPN-Access.md)_ +_[< previous chapter](04-VPN-Access.md) | [next chapter >](06-Stack-Operations.md)_ # 🎛 Administration diff --git a/chapters/06-Stack-Operations.md b/chapters/06-Stack-Operations.md new file mode 100644 index 0000000..bc2d678 --- /dev/null +++ b/chapters/06-Stack-Operations.md @@ -0,0 +1,55 @@ +_[< previous chapter](05-Administration.md)_ + +# ☁ Stack Operations + +Up until now, we've only discussed the basic form of the OpenEMR stack, a single-region OpenEMR installation lacking easy developer hooks or full-stack recovery options. This chapter is targeted at experienced administrators and, unfortunately, can make few concessions to users unfamiliar with Python package management or general DevOps chores. Although the contents of this chapter are important for _someone_ in your organization (or your contractor) to at least be familiar with, it doesn't have to be you and it doesn't have to be right now. + +## stack.py + +The CloudFormation template is constructed from ``../assets/troposphere/stack.py`` via the Troposphere library, taking command-line options and emitting the constructed CFN stack to standard output. It can take the following options: + + * **--dev**: Constructs a stack in developer mode, which will make the following concessions. + * Delete, instead of snapshot, as many resources as possible when the stack is deleted. + * Create a world-visible bastion instance you can ssh (and key forward) into, to enable easy instant access to stack internals without requiring OpenVPN. Be warned: This will **breach HIPAA** if used with live patient data, and should be reserved for testing purposes only. Preventing unthinking misuse of this feature is why a developer version of the stack is not provided in the codebase by default. See "DeveloperKeyhole" in the stack outputs for the public IP of this instance. + * **--force_bastion**: Constructs the developer bastion (as above) without changing the rest of the stack's construction. + * **--dualAZ**: Builds a stack capable of running in two AWS Availability Zones, and continuing to function even if one AZ is down. [Still in progress.] + * **--recovery**: Builds a recovery OpenEMR stack that can accept snapshots and backups and restore the entire, configured application from backups. + + A common approach to using the script is: + + ``` + $ cd ../assets/troposphere + $ pip install -r requirements.txt + $ python stack.py --dualAZ --recovery > stack.json + $ $EDITOR stack.json + ``` + + The CloudFormation template is a difficult read, but ``stack.py`` is significantly better-organized, which is good because you may find it necessary to modify it for your own specific tastes or environment. One problem that immediately springs to mind is that the stack builder is hardcoded to use a stock OpenEMR 5.0.0 beanstalk archive — if your environment has custom code changes you wish to preserve, you will need to host your revised beanstalk code on an S3 bucket (in the same region!) and then modify the hardcoded mappings to refer to your software instead of the stock master. Make the change, re-run ``stack.py``, and you can now manually create a new stack in the CloudFormation manager, uploading your just-produced stack on request. + +## Using the recovery stack + +Making backups is important, but an untested backup is no backup at all. Scheduling and implementing regular tests of your backups to ensure they can successfully recover your data is the single most-neglected task in IT, and to this end we provide a recovery stack, capable of taking the automated cloud backups and building the entire OpenEMR application stack from first principles. Use this facility not just to restore your application in the event of catastrophe, but to regularly insure that you **can** successfully recover your application, both in terms of backup stability and the expertise required to use the tools. + +### Before you begin + + * If you're using custom OpenEMR code, be sure that your revised, deployed beanstalk file is in S3, and modify the stack file to hardcode in your new archive bucket and key. + * The backup will require a full copy of the EFS mount, which must be performed /after/ the initial configuration of the system. This process normally happens once a day, but if you're seeking to perform a backup test immediately after installation, you may find it necessary to connect to the NFS Backup instance and run /root/backup.sh **after** you've completed OpenEMR setup. You can confirm that the backup has been successfully run by seeing a selection of .gz files in your S3 bucket's backup location. + * You're welcome to manually run the backups yourself, to ensure all three of the snapshots are taken at the same time. In production, there's no guarantee the backups will happen at the same time of day, which might cause odd desynchronizations between the patient records and patient documents. You might consider modifying the stack (or just the elements after the fact) to ensure that the RDS snapshot, the Lambda-powered document snapshot, and the cron-powered daily EFS backup all happen around the same time each day. + * If you want to restore in a new region, copy the snapshots to the new region before you begin. + +### Recover your backups + + 1. Run ``stack.py --recovery > OpenEMR-Recovery.json``. + 2. Start this stack in CloudFormation. You'll have four questions you aren't familiar with. + * **RecoveryCouchDBSnapshot**: The EC2 volume snapshot of the EC2 volume from the Patient Documents. (example: ``snap-0ebb4f155ff27040c``) + * **RecoveryKMSKey**: The ARN of the KMS key created by the original stack, which protects all the resources you're restoring. (example: ``arn:aws:kms:us-east-1:7...3:key/6fc10c90-d550-4fd5-bb5c-c2416e31839a``) + * **RecoveryRDSSnapshotARN**: The ARN of the RDS database from the original stack. (example: ``arn:aws:rds:us-east-1:7...3:snapshot:openemr063-backup``) + * **RecoveryS3Bucket**: The name of the bucket created by the original stack. (example: ``openemr-c49525c0-82e5-11e7-bcf4-50faeaa96461``) + * (Questions like database password and volume size are going to be deduced from the recovered resources. **TimeZone** isn't sticky yet, though.) + 3. Reassign front-end SSL, per chapter 3. Reassigning DNS is optional if you're only testing your backup. + +### Reconfigure your stack + + 1. You can now log in with your administration credentials, and verify the data and records have been successfully reloaded and that prior configuration settings have been retained. + 2. Reconfigure your VPN per chapter 4, if this is a full, long-term recovery stack. + 3. Avoid doing anything that would send email or alerts to patients, if you're testing with live patient data. If any OpenEMR plugins can be switched into a test mode, consider doing so.