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..3ea3de7 100644 --- a/assets/OpenEMR.json +++ b/assets/OpenEMR.json @@ -1,1411 +1,2243 @@ { - "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-007.zip", + "MySQLVersion": "5.6.27", + "RegionBucket": "openemr-apsoutheast2", + "UbuntuAMI": "ami-e94e5e8a" + }, + "eu-west-1": { + "AmazonAMI": "ami-d7b9a2b1", + "ApplicationSource": "beanstalk/openemr-5.0.0-007.zip", + "MySQLVersion": "5.6.27", + "RegionBucket": "openemr-euwest1", + "UbuntuAMI": "ami-6d48500b" + }, + "us-east-1": { + "AmazonAMI": "ami-a4c7edb2", + "ApplicationSource": "beanstalk/openemr-5.0.0-007.zip", + "MySQLVersion": "5.6.27", + "RegionBucket": "openemr-useast1", + "UbuntuAMI": "ami-d15a75c7" + }, + "us-west-2": { + "AmazonAMI": "ami-6df1e514", + "ApplicationSource": "beanstalk/openemr-5.0.0-007.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 /root/replicator.ini /etc/couchdb/local.d\n", + "chown couchdb:couchdb /etc/couchdb/local.d/ip.ini /etc/couchdb/local.d/replicator.ini /etc/couchdb/local.d/ssl.ini\n", + "service couchdb start\nsleep 5\ncurl -k -X PUT https://127.0.0.1:6984/couchdb\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/replicator.ini": { + "content": { + "Fn::Join": [ + "", + [ + "[replicator]\n", + "ssl_trusted_certificates_file = /etc/couchdb/ca.crt\n", + "verify_ssl_certificates = true\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 -x\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" + ] + ] + } + }, + "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", + "OptionName": "Stickiness Policy", + "Value": "true" + }, + { + "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": "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" + } + }, + { + "Namespace": "aws:elasticbeanstalk:application:environment", + "OptionName": "TIMEZONE", + "Value": { + "Ref": "TimeZone" + } + } + ], + "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 -x\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 $? ", + " --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/eb/06-php-configuration.config b/assets/eb/06-php-configuration.config index da54a53..fb733f1 100644 --- a/assets/eb/06-php-configuration.config +++ b/assets/eb/06-php-configuration.config @@ -1,3 +1,7 @@ +option_settings: + aws:elasticbeanstalk:application:environment: + MOUNT_DIRECTORY: '/var/app/current/openemr/sites/default' + commands: create_post_dir: command: "mkdir /opt/elasticbeanstalk/hooks/appdeploy/post" @@ -13,7 +17,16 @@ files: then exit 0 fi - TIMEZONE=$(/opt/elasticbeanstalk/bin/get-config environment | jq -r '.TIMEZONE') + MOUNT_DIRECTORY=$(/opt/elasticbeanstalk/bin/get-config environment | jq -r '.MOUNT_DIRECTORY') + if [ -f ${MOUNT_DIRECTORY}/timezone.txt ] + then + echo Found cached timezone! + TIMEZONE=$(< ${MOUNT_DIRECTORY}/timezone.txt) + else + TIMEZONE=$(/opt/elasticbeanstalk/bin/get-config environment | jq -r '.TIMEZONE') + echo $TIMEZONE > ${MOUNT_DIRECTORY}/timezone.txt + echo Cached timezone! + fi echo "TIMEZONE: ${TIMEZONE} ..." diff --git a/assets/eb/07-redis-configuration.config b/assets/eb/07-redis-configuration.config index a380344..23b1df1 100644 --- a/assets/eb/07-redis-configuration.config +++ b/assets/eb/07-redis-configuration.config @@ -4,7 +4,7 @@ commands: ignoreErrors: true files: - "/opt/elasticbeanstalk/hooks/appdeploy/post/06-redis-configuration.sh": + "/opt/elasticbeanstalk/hooks/appdeploy/post/07-redis-configuration.sh": mode: "000755" content : | #!/bin/bash diff --git a/assets/eb/09-couchdb-configuration.config b/assets/eb/09-couchdb-configuration.config new file mode 100644 index 0000000..7a3fd33 --- /dev/null +++ b/assets/eb/09-couchdb-configuration.config @@ -0,0 +1,27 @@ +commands: + create_post_dir: + command: "mkdir /opt/elasticbeanstalk/hooks/appdeploy/post" + ignoreErrors: true + +files: + "/opt/elasticbeanstalk/hooks/appdeploy/post/09-couchdb-configuration.sh": + mode: "000755" + content : | + #!/bin/bash + + if [ "$(/opt/elasticbeanstalk/bin/get-config environment | jq -r .COUCHDBZONE)" == "null" ] + then exit 0 + fi + + if [ -f "/tmp/couchdbzone.json" ] + then exit 0 + fi + + CURRENTIP=`ip addr | grep 'state UP' -A2 | tail -n1 | awk '{print $2}' | cut -f1 -d'/'` + CURRENT24=`echo $CURRENTIP | sed -E 's/^([[:digit:]]*\.[[:digit:]]*\.[[:digit:]]*).*$/\1/'` + COUCHDBHOSTNAME=`/opt/elasticbeanstalk/bin/get-config environment | jq -r .COUCHDBZONE | jq -r .segment24\[\"$CURRENT24\"\]` + if [ "$COUCHDBHOSTNAME" != "null" ] + then + COUCHDBIP=`host $COUCHDBHOSTNAME | awk '/has address/ { print $4 ; exit }'` + echo $COUCHDBIP couchdb.openemr.local >> /etc/hosts + fi diff --git a/assets/eb/09-post-install-setup-file-deletion.config b/assets/eb/10-post-install-setup-file-deletion.config similarity index 91% rename from assets/eb/09-post-install-setup-file-deletion.config rename to assets/eb/10-post-install-setup-file-deletion.config index 25b4739..ff3fcf7 100644 --- a/assets/eb/09-post-install-setup-file-deletion.config +++ b/assets/eb/10-post-install-setup-file-deletion.config @@ -4,7 +4,7 @@ commands: ignoreErrors: true files: - "/opt/elasticbeanstalk/hooks/appdeploy/post/09-post-install-setup-file-deletion.sh": + "/opt/elasticbeanstalk/hooks/appdeploy/post/10-post-install-setup-file-deletion.sh": mode: "000755" content : | #!/bin/bash diff --git a/assets/troposphere/requirements.txt b/assets/troposphere/requirements.txt new file mode 100644 index 0000000..3093657 --- /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..13fe529 100644 --- a/assets/troposphere/stack.py +++ b/assets/troposphere/stack.py @@ -1,7 +1,8 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# https://github.com/cloudtools/troposphere +# TODO: fix timezone -- where is it?! [ready, rezip and repush] +# TODO: force two-way replication of views (or do I /need/ them?) from troposphere import Base64, FindInMap, GetAtt, GetAZs, Join, Select, Split, Output from troposphere import Parameter, Ref, Tags, Template @@ -14,6 +15,8 @@ ref_stack_name = Ref('AWS::StackName') ref_account = Ref('AWS::AccountId') +currentBeanstalkKey = 'beanstalk/openemr-5.0.0-007.zip' + def setInputs(t, args): t.add_parameter(Parameter( 'EC2KeyPair', @@ -21,23 +24,15 @@ def setInputs(t, args): Type = 'AWS::EC2::KeyPair::KeyName' )) - t.add_parameter(Parameter( - 'TimeZone', - Description = 'The timezone OpenEMR will run in', - Default = 'America/Chicago', - Type = 'String', - MaxLength = '41' - )) - 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( @@ -51,6 +46,14 @@ def setInputs(t, args): Type = 'String' )) else: + t.add_parameter(Parameter( + 'TimeZone', + Description = 'The timezone OpenEMR will run in', + Default = 'America/Chicago', + Type = 'String', + MaxLength = '41' + )) + t.add_parameter(Parameter( 'RDSPassword', NoEcho = True, @@ -77,32 +80,32 @@ def setInputs(t, args): )) return t -def setMappings(t): +def setMappings(t, args): t.add_mapping('RegionData', { "us-east-1" : { "RegionBucket": "openemr-useast1", - "ApplicationSource": "beanstalk/openemr-5.0.0-006.zip", + "ApplicationSource": args.beanstalk_key, "MySQLVersion": "5.6.27", "AmazonAMI": "ami-a4c7edb2", "UbuntuAMI": "ami-d15a75c7" }, "us-west-2" : { "RegionBucket": "openemr-uswest2", - "ApplicationSource": "beanstalk/openemr-5.0.0-006.zip", + "ApplicationSource": args.beanstalk_key, "MySQLVersion": "5.6.27", "AmazonAMI": "ami-6df1e514", "UbuntuAMI": "ami-835b4efa" }, "eu-west-1" : { "RegionBucket": "openemr-euwest1", - "ApplicationSource": "beanstalk/openemr-5.0.0-006.zip", + "ApplicationSource": args.beanstalk_key, "MySQLVersion": "5.6.27", "AmazonAMI": "ami-d7b9a2b1", "UbuntuAMI": "ami-6d48500b" }, "ap-southeast-2" : { "RegionBucket": "openemr-apsoutheast2", - "ApplicationSource": "beanstalk/openemr-5.0.0-006.zip", + "ApplicationSource": args.beanstalk_key, "MySQLVersion": "5.6.27", "AmazonAMI": "ami-10918173", "UbuntuAMI": "ami-e94e5e8a" @@ -110,7 +113,7 @@ def setMappings(t): }) return t -def buildVPC(t, dualAZ): +def buildVPC(t, dual_az): t.add_resource( ec2.VPC( 'VPC', @@ -203,7 +206,7 @@ def buildVPC(t, dualAZ): ) ) - if (dualAZ): + if (dual_az): t.add_resource( ec2.RouteTable( 'rtTablePrivate1', @@ -544,7 +547,7 @@ def buildEFS(t, dev): return t -def buildRedis(t, dualAZ): +def buildRedis(t, dual_az): t.add_resource( ec2.SecurityGroup( 'RedisSecurityGroup', @@ -571,32 +574,54 @@ def buildRedis(t, dualAZ): ) ) - t.add_resource( - elasticache.CacheCluster( - 'RedisCluster', - CacheNodeType = 'cache.t2.small', - VpcSecurityGroupIds = [GetAtt('RedisSecurityGroup', 'GroupId')], - CacheSubnetGroupName = Ref('RedisSubnets'), - Engine = 'redis', - NumCacheNodes = '2' if dualAZ else '1' + if (dual_az): + t.add_resource( + elasticache.ReplicationGroup( + 'RedisCluster', + AutomaticFailoverEnabled = True, + ReplicationGroupDescription = 'Beanstalk Sessions', + NumCacheClusters = 2, + Engine = 'redis', + CacheNodeType = 'cache.m3.medium', + CacheSubnetGroupName = Ref('RedisSubnets'), + SecurityGroupIds = [GetAtt('RedisSecurityGroup', 'GroupId')], + ) ) - ) - - t.add_resource( - route53.RecordSetType( - 'DNSRedis', - HostedZoneId = Ref('DNS'), - Name = 'redis.openemr.local', - Type = 'CNAME', - TTL = '900', - ResourceRecords = [GetAtt('RedisCluster', 'RedisEndpoint.Address')] + t.add_resource( + route53.RecordSetType( + 'DNSRedis', + HostedZoneId = Ref('DNS'), + Name = 'redis.openemr.local', + Type = 'CNAME', + TTL = '900', + ResourceRecords = [GetAtt('RedisCluster', 'PrimaryEndPoint.Address')] + ) + ) + else: + t.add_resource( + elasticache.CacheCluster( + 'RedisCluster', + CacheNodeType = 'cache.t2.small', + VpcSecurityGroupIds = [GetAtt('RedisSecurityGroup', 'GroupId')], + CacheSubnetGroupName = Ref('RedisSubnets'), + Engine = 'redis', + NumCacheNodes = 1 + ) + ) + t.add_resource( + route53.RecordSetType( + 'DNSRedis', + HostedZoneId = Ref('DNS'), + Name = 'redis.openemr.local', + Type = 'CNAME', + TTL = '900', + ResourceRecords = [GetAtt('RedisCluster', 'RedisEndpoint.Address')] + ) ) - ) return t def buildMySQL(t, args): - # TODO: verify dual-AZ t.add_resource( ec2.SecurityGroup( 'DBSecurityGroup', @@ -624,7 +649,6 @@ def buildMySQL(t, args): ) if (args.recovery): - #TODO: this comes up with the old key, not the new one! wrong, needs migration t.add_resource( rds.DBInstance( 'RDSInstance', @@ -634,7 +658,7 @@ def buildMySQL(t, args): PubliclyAccessible = False, DBSubnetGroupName = Ref('RDSSubnetGroup'), VPCSecurityGroups = [Ref('DBSecurityGroup')], - MultiAZ = args.dualAZ, + MultiAZ = args.dual_az, Tags = Tags(Name='Patient Records') ) ) @@ -655,7 +679,7 @@ def buildMySQL(t, args): VPCSecurityGroups = [Ref('DBSecurityGroup')], KmsKeyId = OpenEMRKeyID, StorageEncrypted = True, - MultiAZ = args.dualAZ, + MultiAZ = args.dual_az, Tags = Tags(Name='Patient Records') ) ) @@ -878,6 +902,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", @@ -963,7 +997,7 @@ def buildNFSBackup(t, args): ) bootstrapScript = [ - "#!/bin/bash -xe\n", + "#!/bin/bash -x\n", "exec > /tmp/part-001.log 2>&1\n", "apt-get -y update\n", "apt-get -y install python-pip\n", @@ -973,7 +1007,7 @@ def buildNFSBackup(t, args): " --resource NFSBackupInstance ", " --configsets Setup ", " --region ", ref_region, "\n", - "cfn-signal -e 0 ", + "cfn-signal -e $? ", " --stack ", ref_stack_name, " --resource NFSBackupInstance ", " --region ", ref_region, "\n" @@ -1105,7 +1139,7 @@ def buildNFSBackup(t, args): UserData = Base64(Join('', bootstrapScript)), CreationPolicy = { "ResourceSignal" : { - "Timeout" : "PT5M" + "Timeout" : "PT15M" if args.recovery else "PT5M" } } ) @@ -1226,7 +1260,7 @@ def buildDocumentStore(t, args): ) bootstrapScript = [ - "#!/bin/bash -xe\n", + "#!/bin/bash -x\n", "exec > /tmp/part-001.log 2>&1\n", "apt-get -y update\n", "apt-get -y install python-pip\n", @@ -1236,7 +1270,7 @@ def buildDocumentStore(t, args): " --resource CouchDBInstance ", " --configsets Setup ", " --region ", ref_region, "\n", - "cfn-signal -e 0 ", + "cfn-signal -e $? ", " --stack ", ref_stack_name, " --resource CouchDBInstance ", " --region ", ref_region, "\n" @@ -1257,6 +1291,12 @@ def buildDocumentStore(t, args): "cacert_file = /etc/couchdb/ca.crt\n" ] + replicatorIniFile = [ + "[replicator]\n", + "ssl_trusted_certificates_file = /etc/couchdb/ca.crt\n", + "verify_ssl_certificates = true\n" + ] + fstabFile = [ "/dev/xvdd /mnt/db ext4 defaults,nofail 0 0\n" ] @@ -1280,8 +1320,8 @@ def buildDocumentStore(t, args): "chown couchdb:couchdb /etc/couchdb/*.crt /etc/couchdb/*.key\n", "rm -rf /var/lib/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", + "cp /root/ip.ini /root/ssl.ini /root/replicator.ini /etc/couchdb/local.d\n", + "chown couchdb:couchdb /etc/couchdb/local.d/ip.ini /etc/couchdb/local.d/replicator.ini /etc/couchdb/local.d/ssl.ini\n", "service couchdb start\n" ] else: @@ -1304,9 +1344,11 @@ def buildDocumentStore(t, args): "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", + "cp /root/ip.ini /root/ssl.ini /root/replicator.ini /etc/couchdb/local.d\n", + "chown couchdb:couchdb /etc/couchdb/local.d/ip.ini /etc/couchdb/local.d/replicator.ini /etc/couchdb/local.d/ssl.ini\n", "service couchdb start\n" + "sleep 5\n" + "curl -k -X PUT https://127.0.0.1:6984/couchdb\n" ] bootstrapInstall = cloudformation.InitConfig( @@ -1329,6 +1371,12 @@ def buildDocumentStore(t, args): "owner" : "root", "group" : "root" }, + "/root/replicator.ini" : { + "content" : Join("", replicatorIniFile), + "mode" : "000400", + "owner" : "root", + "group" : "root" + }, "/root/fstab.append" : { "content" : Join("", fstabFile), "mode" : "000400", @@ -1343,6 +1391,34 @@ def buildDocumentStore(t, args): } ) + # this is incomplete -- design documents will not replicate between systems, since ''"user_ctx" = {"roles": ["_admin"]}' is not specified on local targets. + # Is this acceptable or is this broken? I don't think there's a document /search/ feature... + # Fix will involve either: + # * A: moving the replicate script to both servers to properly connect user_ctx to local target + # * B: configuring and employing admin user for replication + replicateScript = [ + "#!/bin/bash -xe\n", + "exec > /tmp/part-003.log 2>&1\n", + 'curl -k -X POST https://127.0.0.1:6984/_replicator -d \'{"source":"https://couchdb-az1.openemr.local:6984/couchdb", "target":"couchdb", "continuous":true}\' -H "Content-Type: application/json"\n', + 'curl -k -X POST https://127.0.0.1:6984/_replicator -d \'{"source":"couchdb", "target":"https://couchdb-az1.openemr.local:6984/couchdb", "continuous":true}\' -H "Content-Type: application/json"\n' + ] + + bootstrapReplicate = cloudformation.InitConfig( + files = { + "/root/couchdb.replicate.sh" : { + "content" : Join("", replicateScript), + "mode" : "000500", + "owner" : "root", + "group" : "root" + } + }, + commands = { + "01_setup" : { + "command" : "/root/couchdb.replicate.sh" + } + } + ) + bootstrapMetadata = cloudformation.Metadata( cloudformation.Init( cloudformation.InitConfigSets( @@ -1352,6 +1428,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,12 +1449,123 @@ def buildDocumentStore(t, args): UserData = Base64(Join('', bootstrapScript)), CreationPolicy = { "ResourceSignal" : { - "Timeout" : "PT5M" + "Timeout" : "PT25M" } } ) ) + + if (args.dual_az): + t.add_resource( + ec2.SecurityGroupIngress( + 'CouchDBSGIngress2', + GroupId = Ref('CouchDBSecurityGroup'), + IpProtocol = '-1', + SourceSecurityGroupId = Ref('CouchDBSecurityGroup') + ) + ) + + if (args.recovery): + t.add_resource( + ec2.Volume( + 'RCouchDBVolume', + DeletionPolicy = 'Delete', + AvailabilityZone = Select("1", GetAZs("")), + VolumeType = 'sc1', + SnapshotId = Ref('RecoveryCouchDBSnapshot'), + Tags=Tags(Name="Patient Documents") + ) + ) + else: + t.add_resource( + ec2.Volume( + 'RCouchDBVolume', + DeletionPolicy = 'Delete', + Size=Ref('DocumentStorage'), + AvailabilityZone = Select("1", GetAZs("")), + VolumeType = 'sc1', + Encrypted = True, + KmsKeyId = OpenEMRKeyID, + Tags=Tags(Name="Patient Documents") + ) + ) + + bootstrapReplicatedMetadata = cloudformation.Metadata( + cloudformation.Init( + cloudformation.InitConfigSets( + Setup = ['Install', 'StartReplication'] + ), + Install=bootstrapInstall, + StartReplication=bootstrapReplicate + ) + ) + + bootstrapReplicatorScript = [ + "#!/bin/bash -x\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_stack_name, + " --resource CouchReplicatedDBInstance ", + " --configsets Setup ", + " --region ", ref_region, "\n", + "cfn-signal -e $? ", + " --stack ", ref_stack_name, + " --resource CouchReplicatedDBInstance ", + " --region ", ref_region, "\n" + ] + + t.add_resource( + ec2.Instance( + 'CouchReplicatedDBInstance', + DependsOn = ['CertWriterInstance', 'CouchDBInstance'], + Metadata = bootstrapReplicatedMetadata, + ImageId = FindInMap('RegionData', ref_region, 'UbuntuAMI'), + InstanceType = 't2.micro', + SubnetId = Ref('PrivateSubnet2'), + KeyName = Ref('EC2KeyPair'), + SecurityGroupIds = [Ref('CouchDBSecurityGroup')], + IamInstanceProfile = Ref('CouchDBInstanceProfile'), + Volumes = [{ + "Device" : "/dev/sdd", + "VolumeId" : Ref('RCouchDBVolume') + }], + Tags = Tags(Name='Patient Document Store'), + InstanceInitiatedShutdownBehavior = 'stop', + UserData = Base64(Join('', bootstrapReplicatorScript)), + CreationPolicy = { + "ResourceSignal" : { + "Timeout" : "PT25M" + } + } + ) + ) + + t.add_resource( + route53.RecordSetType( + 'DNSCouchDBAZ1', + HostedZoneId = Ref('DNS'), + Name = 'couchdb-az1.openemr.local', + Type = 'CNAME', + TTL = '900', + ResourceRecords = [GetAtt('CouchDBInstance', 'PrivateDnsName')] + ) + ) + + t.add_resource( + route53.RecordSetType( + 'DNSCouchDBAZ2', + HostedZoneId = Ref('DNS'), + Name = 'couchdb-az2.openemr.local', + Type = 'CNAME', + TTL = '900', + ResourceRecords = [GetAtt('CouchReplicatedDBInstance', 'PrivateDnsName')] + ) + ) + t.add_resource( route53.RecordSetType( 'DNSCouchDB', @@ -1488,7 +1676,7 @@ def buildDocumentBackups(t): ) return t -def buildApplication(t): +def buildApplication(t, args): t.add_resource( iam.Role( @@ -1629,6 +1817,11 @@ def buildApplication(t): OptionName='ConnectionSettingIdleTimeout', Value='3600' ), + elasticbeanstalk.OptionSettings( + Namespace='aws:elb:policies', + OptionName='Stickiness Policy', + Value='true' + ), elasticbeanstalk.OptionSettings( Namespace='aws:elb:policies:backendencryption', OptionName='PublicKeyPolicyNames', @@ -1664,11 +1857,6 @@ def buildApplication(t): OptionName='Application Healthcheck URL', Value='HTTPS:443/openemr/version.php' ), - elasticbeanstalk.OptionSettings( - Namespace='aws:elasticbeanstalk:application:environment', - OptionName='TIMEZONE', - Value=Ref('TimeZone') - ), elasticbeanstalk.OptionSettings( Namespace='aws:elasticbeanstalk:application:environment', OptionName='REDIS_IP', @@ -1696,6 +1884,34 @@ def buildApplication(t): ) ] + if (args.dual_az): + couchDBZoneFile = [ + '{ "segment24": {', + '"10.0.1": "couchdb-az1.openemr.local",', + '"10.0.2": "couchdb-az1.openemr.local",', + '"10.0.3": "couchdb-az2.openemr.local",', + '"10.0.4": "couchdb-az2.openemr.local",', + '} }' + ] + options.extend([ + elasticbeanstalk.OptionSettings( + Namespace='aws:elasticbeanstalk:application:environment', + OptionName='COUCHDBZONE', + Value=Join("", couchDBZoneFile) + ), elasticbeanstalk.OptionSettings( + Namespace='aws:autoscaling:asg', + OptionName='MinSize', + Value='2' + ) + ]) + + if (not args.recovery): + options.append(elasticbeanstalk.OptionSettings( + Namespace='aws:elasticbeanstalk:application:environment', + OptionName='TIMEZONE', + Value=Ref('TimeZone') + )) + t.add_resource( elasticbeanstalk.Environment( 'EBEnvironment', @@ -1721,10 +1937,11 @@ def setOutputs(t, args): return t parser = argparse.ArgumentParser(description="OpenEMR stack builder") +parser.add_argument("--beanstalk-key", help="select compressed OpenEMR beanstalk", default=currentBeanstalkKey) +parser.add_argument("--dual-az", help="build AZ-hardened stack [in progress!]", action="store_true") +parser.add_argument("--recovery", help="load OpenEMR stack from backups", action="store_true") 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("--recovery", help="load OpenEMR stack from backups", action="store_true") args = parser.parse_args() t = Template() @@ -1735,10 +1952,12 @@ def setOutputs(t, args): descString+=' [developer]' if (args.force_bastion): descString+=' [keyhole]' -if (args.dualAZ): +if (args.dual_az): descString+=' [dual-AZ]' if (args.recovery): descString+=' [recovery]' +if (not args.beanstalk_key == currentBeanstalkKey): + descString+=' [eb: ' + args.beanstalk_key + ']' t.add_description(descString) # reduce to consistent names @@ -1750,19 +1969,19 @@ def setOutputs(t, args): OpenEMRKeyARN = GetAtt('OpenEMRKey', 'Arn') setInputs(t,args) -setMappings(t) -buildVPC(t, args.dualAZ) +setMappings(t,args) +buildVPC(t, args.dual_az) buildFoundation(t, args) if (args.dev or args.force_bastion): buildDeveloperBastion(t) buildEFS(t, args.dev) -buildRedis(t, args.dualAZ) +buildRedis(t, args.dual_az) buildMySQL(t, args) buildCertWriter(t, args.dev) buildNFSBackup(t, args) buildDocumentStore(t, args) buildDocumentBackups(t) -buildApplication(t) +buildApplication(t, args) setOutputs(t, args) print(t.to_json()) diff --git a/chapters/01-Getting-Started.md b/chapters/01-Getting-Started.md index c28d017..a6ded4a 100644 --- a/chapters/01-Getting-Started.md +++ b/chapters/01-Getting-Started.md @@ -25,10 +25,10 @@ This guide uses services that are _only_ available in certain AWS regions. As of ### Launch your Cloud 1. Click the region link below that corresponds to the region you created your keypair in: - * [N. Virginia](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?stackName=OpenEMR&templateURL=https://s3.amazonaws.com/openemr-useast1/OpenEMR.015.json) - * [Oregon](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/new?stackName=OpenEMR&templateURL=https://s3.amazonaws.com/openemr-uswest2/OpenEMR.015.json) - * [Ireland](https://console.aws.amazon.com/cloudformation/home?region=eu-west-1#/stacks/new?stackName=OpenEMR&templateURL=https://s3.amazonaws.com/openemr-euwest1/OpenEMR.015.json) - * [Sydney](https://console.aws.amazon.com/cloudformation/home?region=ap-southeast-2#/stacks/new?stackName=OpenEMR&templateURL=https://s3.amazonaws.com/openemr-apsoutheast2/OpenEMR.015.json) + * [N. Virginia](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?stackName=OpenEMR&templateURL=https://s3.amazonaws.com/openemr-useast1/OpenEMR.016.json) + * [Oregon](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/new?stackName=OpenEMR&templateURL=https://s3.amazonaws.com/openemr-uswest2/OpenEMR.016.json) + * [Ireland](https://console.aws.amazon.com/cloudformation/home?region=eu-west-1#/stacks/new?stackName=OpenEMR&templateURL=https://s3.amazonaws.com/openemr-euwest1/OpenEMR.016.json) + * [Sydney](https://console.aws.amazon.com/cloudformation/home?region=ap-southeast-2#/stacks/new?stackName=OpenEMR&templateURL=https://s3.amazonaws.com/openemr-apsoutheast2/OpenEMR.016.json) 2. Click **Next**, and configure your stack on this page. * For **DocumentStorage**, enter the size of your patient documents database in gigabytes. * For **EC2KeyPair**, select the key pair you created in the last section. 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..e532d5e --- /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 ``/cloud/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: + + * **--dual-az**: Builds a stack capable of running in two AWS Availability Zones, and continuing to function even if one AZ is down. + * **--beanstalk-key BEANSTALK-KEY**: Use a non-standard Elastic Beanstalk application archive. (Expect to hardcode the mappings to use your own regional deployment buckets.) + * **--recovery**: Builds a recovery OpenEMR stack that can accept snapshots and backups and restore the entire, configured application. + * **--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. + + ``` + $ cd cloud/assets/troposphere + $ pip install -r requirements.txt + $ python stack.py --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. 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. Note that the AWS stack builder tool can (within limits) verify the correctness of a stack before you attempt to launch it, and don't forget that AWS constantly evolves. You may need to update your Troposphere library to make use of recently-released AWS features with new CloudFormation elements and references. + +## 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 validity 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 your new archive bucket. + * 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. + * Only one of the two CouchDB masters is backed up -- since they're in master-master replication, this should be fine, but you may consider creating and keeping the other master's snapshot around too. + * 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, volume size, and timezone are going to be deduced from the recovered resources.) + 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.