--- AWSTemplateFormatVersion: 2010-09-09 Metadata: License: Apache-2.0 Description: 'AWS DataSync In-cloud Transfer Quick Start Scheduler: A Quick Start template that creates a one-time or recurring schedule to transfer files between source and destination Amazon EFS file systems. These file systems could be in the same or different AWS regions. **WARNING** This template creates an Amazon EC2 Fleet (AWS DataSync agent on EC2), SSM Document, SSM Maintenance Windows, and AWS DataSync resources. To completely delete all resources created by this template, view the CloudFormation stack outputs and run the ouput "ViewDeleteDataSyncResourcesScript" SSM command in a terminal window. This wil generate the delete commands that you must copy, paste, and run in a terminal window. These commands, when executed, will delete all the AWS DataSync resources that were dynamically created by a script executed on the DataSync agent EC2 instance at boot, as well as the CloudFormation stack. You will be billed for the AWS resources used if you create a stack from this template.' AWS::CloudFormation::Interface: ParameterGroups: - Label: default: AWS DataSync EC2 agent Parameters: - Ec2KeyName - InstanceType - SourceSubnetId - SourceSecurityGroupId - Label: default: Source NFS location Parameters: - SourceDnsName - SourceSubdirectory - Label: default: Destination EFS location Parameters: - DestinationLocationRegion - DestinationEfsFilesystemId - DestinationSubnetId - DestinationSecurityGroupId - DestinationSubdirectory - Label: default: Task start frequency (schedule) Parameters: - TaskSchedule ParameterLabels: DestinationEfsFilesystemId: default: Destination EFS filesystem id DestinationLocationRegion: default: Destination location region DestinationSecurityGroupId: default: Destination security group id DestinationSubdirectory: default: Destination EFS subdirectory DestinationSubnetId: default: Destination subnet id Ec2KeyName: default: EC2 key pair InstanceType: default: Instance type SourceDnsName: default: Source NFS DNS name SourceSecurityGroupId: default: Source security group id SourceSubdirectory: default: Source NFS subdirectory SourceSubnetId: default: Source subnet id TaskSchedule: default: Schedule of task run Parameters: DestinationEfsFilesystemId: AllowedPattern: ^(fs-)([a-z0-9]{8})$ Description: Destination EFS filesystem id. Type: String DestinationLocationRegion: AllowedValues: - us-east-1 - us-east-2 - us-west-1 - us-west-2 - eu-west-1 - eu-central-1 - ap-northeast-1 - ap-northeast-2 - ap-southeast-1 - ap-southeast-2 Description: Destination AWS region. Type: String DestinationSecurityGroupId: AllowedPattern: ^(sg-)(?=[a-z0-9]*?)(?:.{8}|.{17})$ Description: Destination EFS security group id. Type: String DestinationSubdirectory: Description: Destination EFS subdirectory. Type: String Default: "/" DestinationSubnetId: AllowedPattern: ^(subnet-)(?=[a-z0-9]*?)(?:.{8}|.{17})$ Description: Destination EFS subnet id. Type: String Ec2KeyName: Description: Name of an EC2 key pair. Type: AWS::EC2::KeyPair::KeyName InstanceType: AllowedValues: - c5.2xlarge - c5.4xlarge - c5.9xlarge - m5.2xlarge - m5.4xlarge - m5a.2xlarge - m5a.4xlarge Default: c5.2xlarge Description: Instance type Type: String SourceDnsName: Description: The DNS name of the source NFS file system. Type: String SourceSecurityGroupId: Description: Select the security group that has NFS (TCP 2049) network access to the source file system. Type: AWS::EC2::SecurityGroup::Id SourceSubdirectory: Default: "/" Description: The directory of the source NFS file system. Type: String SourceSubnetId: Description: Select existing subnets. Type: AWS::EC2::Subnet::Id TaskSchedule: Default: "0 0/1 * * ? *" Description: Task cron schedule (cron format in UTC - [minute hour day/of/month month day/of/week year], e.g. every day @ 1:15pm UTC would be 15 13 ? * * *) Type: String Resources: AmiInfoFunction: Type: AWS::Lambda::Function Properties: Code: S3Bucket: !Sub solution-references-${AWS::Region} S3Key: datasync/amilookup-datasync-agent.zip Handler: amilookup-datasync-agent.handler Runtime: nodejs12.x Timeout: 30 Role: !GetAtt LambdaExecutionRole.Arn AmiInfo: Type: Custom::AmiInfo Properties: ServiceToken: !GetAtt AmiInfoFunction.Arn Region: !Ref 'AWS::Region' OSName: "AWS DataSync EC2 Agent" LambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - 'arn:aws:iam::aws:policy/AWSLambdaExecute' Path: "/" Policies: - PolicyName: root PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ec2:DescribeImages Resource: "*" AgentFleet: Type: AWS::EC2::EC2Fleet Properties: ExcessCapacityTerminationPolicy: termination LaunchTemplateConfigs: - LaunchTemplateSpecification: LaunchTemplateId: !Ref LaunchTemplate Version: 1 ReplaceUnhealthyInstances: true TargetCapacitySpecification: DefaultTargetCapacityType: on-demand TotalTargetCapacity: 1 Type: maintain InstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: / Roles: - !Ref InstanceRole InstanceRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM - arn:aws:iam::aws:policy/AWSDataSyncFullAccess Path: / LaunchTemplate: Type: AWS::EC2::LaunchTemplate Properties: LaunchTemplateName: !Join [ '', [ 'DataSync-Agent-', !Ref 'AWS::StackName' ] ] LaunchTemplateData: IamInstanceProfile: Name: !Ref InstanceProfile ImageId: !GetAtt AmiInfo.Id InstanceType: !Ref InstanceType KeyName: !Ref Ec2KeyName Monitoring: Enabled: true NetworkInterfaces: - DeviceIndex: 0 Groups: - !Ref SourceSecurityGroupId SubnetId: !Ref SourceSubnetId TagSpecifications: - ResourceType: instance Tags: - Key: Name Value: DataSync Agent - Key: StackName Value: !Ref 'AWS::StackName' UserData: Fn::Base64: !Sub | #cloud-config repo_update: true repo_upgrade: all write_files: - content: | #!/bin/bash function getActivationKey { local destinationLocationRegion=$1 curl --silent "localhost:8080/?gatewayType=SYNC&activationRegion=${!destinationLocationRegion}&no_redirect" } function createAgent { local agentIp=$1 local destinationLocationRegion=$2 local stackName=$3 local activationKey=$4 aws datasync create-agent --activation-key ${!activationKey} --agent-name ${!agentIp} --region ${!destinationLocationRegion} --tags Key=StackName,Value=${!stackName} --output text } function createDataSyncLocationNfs { local serverHostname=$1 local sourceSubdirectory=$2 local agentArns=$3 local destinationLocationRegion=$4 local stackName=$5 aws datasync create-location-nfs --server-hostname ${!serverHostname} --subdirectory ${!sourceSubdirectory} --on-prem-config "AgentArns=${!agentArns}" --region ${!destinationLocationRegion} --tags Key=StackName,Value=${!stackName} --output text } function createDataSyncLocationEfs { local destinationEfsFilesystemId=$1 local destinationSubdirectory=$2 local destinationSubnetId=$3 local destinationSecurityGroupId=$4 local destinationLocationRegion=$5 local accountId=$6 local stackName=$7 local destinationEfsFilesystemArn="arn:aws:elasticfilesystem:${!destinationLocationRegion}:${!accountId}:file-system/${!destinationEfsFilesystemId}" local destinationSubnetArn="arn:aws:ec2:${!destinationLocationRegion}:${!accountId}:subnet/${!destinationSubnetId}" local destinationSecurityGroupArns="arn:aws:ec2:${!destinationLocationRegion}:${!accountId}:security-group/${!destinationSecurityGroupId}" aws datasync create-location-efs --efs-filesystem-arn ${!destinationEfsFilesystemArn} --subdirectory ${!destinationSubdirectory} --ec2-config "SubnetArn=${!destinationSubnetArn},SecurityGroupArns=${!destinationSecurityGroupArns}" --region ${!destinationLocationRegion} --tags Key=StackName,Value=${!stackName} --output text } function createDataSyncTask { local sourceLocationArn=$1 local destinationLocationArn=$2 local destinationLocationRegion=$3 local agentIp=$4 local stackName=$5 aws datasync create-task --source-location-arn ${!sourceLocationArn} --destination-location-arn ${!destinationLocationArn} --schedule ScheduleExpression='"cron(${TaskSchedule})"' --name "Stack:${!stackName} Agent:${!agentIp}" --region ${!destinationLocationRegion} --tags Key=StackName,Value=${!stackName} --output text } path: /tmp/functions_datasync.sh permissions: 0777 - content: | #!/bin/bash agentIp=${!1} sourceDnsName=${!2} sourceSubdirectory=${!3} destinationEfsFilesystemId=${!4} destinationSubdirectory=${!5} destinationSubnetId=${!6} destinationSecurityGroupId=${!7} destinationLocationRegion=${!8} accountId=${!9} stackName=${!10} if [ $# -lt 10 ]; then echo "Invalid # of arguments. Require: Source DNS name; Source Subdirectory; Destination EFS Filesystem Id; Destination Subdirectory; Destination Subnet Id; Destination Security Group Id; Destination Location Region; Account Id; Stack Name; Schedule" exit 1 fi # source datasync functions source /tmp/functions_datasync.sh # start create delete datasync resources script echo -e "aws cloudformation delete-stack --stack-name ${!stackName} --region ${AWS::Region}" >> /tmp/delete_datasync_resources.sh # get activation key echo -e "$(date -u +%FT%T.%3N)\tDataSync: Execute\tSelf-activating and generating activation key on agent on ${!agentIp}" >> /tmp/datasync_setup.log 2>&1 activationKey=$(getActivationKey ${!destinationLocationRegion}) if [ "$?" != "0" ] then echo -e "$(date -u +%FT%T.%3N)\tDataSync: Error\t\tFailed to self-activate or generate activation key on ${!agentIp}" >> /tmp/datasync_setup.log 2>&1 exit 1 fi echo -e "$(date -u +%FT%T.%3N)\tDataSync: Success\tKey generated: ${!activationKey}" >> /tmp/datasync_setup.log 2>&1 # activate agent echo -e "$(date -u +%FT%T.%3N)\tDataSync: Execute\tCreating agent in ${!destinationLocationRegion} for ${!agentIp} using activation key ${!activationKey}" >> /tmp/datasync_setup.log 2>&1 agentArn=$(createAgent ${!agentIp} ${!destinationLocationRegion} ${!stackName} ${!activationKey}) if [ "$?" != "0" ] then echo -e "$(date -u +%FT%T.%3N)\tDataSync: Error\t\tFailed to create agent in ${!destinationLocationRegion} for ${!agentIp} using activation key ${!activationKey}" >> /tmp/datasync_setup.log 2>&1 exit 1 fi echo -e "$(date -u +%FT%T.%3N)\tDataSync: Success\tAgent created: ${!agentArn}" >> /tmp/datasync_setup.log 2>&1 sed -i "1iaws datasync delete-agent --agent-arn ${!agentArn} --region ${!destinationLocationRegion}" /tmp/delete_datasync_resources.sh # create source location (nfs) echo -e "$(date -u +%FT%T.%3N)\tDataSync: Execute\tCreating source location(nfs) for ${!sourceDnsName}" >> /tmp/datasync_setup.log 2>&1 sourceLocationArn=$(createDataSyncLocationNfs ${!sourceDnsName} ${!sourceSubdirectory} ${!agentArn} ${!destinationLocationRegion} ${!stackName}) if [ "$?" != "0" ] then echo -e "$(date -u +%FT%T.%3N)\tDataSync: Error\t\tFailed to create source location for ${!sourceDnsName}" >> /tmp/datasync_setup.log 2>&1 exit 1 fi echo -e "$(date -u +%FT%T.%3N)\tDataSync: Success\tSource location(nfs) created: ${!sourceLocationArn}" >> /tmp/datasync_setup.log 2>&1 sed -i "1iaws datasync delete-location --location-arn ${!sourceLocationArn} --region ${!destinationLocationRegion}" /tmp/delete_datasync_resources.sh # create destination location (efs) echo -e "$(date -u +%FT%T.%3N)\tDataSync: Execute\tCreating destination location(efs) for ${!destinationEfsFilesystemId}" >> /tmp/datasync_setup.log 2>&1 destinationLocationArn=$(createDataSyncLocationEfs ${!destinationEfsFilesystemId} ${!destinationSubdirectory} ${!destinationSubnetId} ${!destinationSecurityGroupId} ${!destinationLocationRegion} ${!accountId} ${!stackName}) if [ "$?" != "0" ] then echo -e "$(date -u +%FT%T.%3N)\tDataSync: Error\t\tFailed to create destination location for ${!destinationEfsFilesystemId}" >> /tmp/datasync_setup.log 2>&1 exit 1 fi echo -e "$(date -u +%FT%T.%3N)\tDataSync: Success\tDestination location(efs) created: ${!destinationLocationArn}" >> /tmp/datasync_setup.log 2>&1 sed -i "1iaws datasync delete-location --location-arn ${!destinationLocationArn} --region ${!destinationLocationRegion}" /tmp/delete_datasync_resources.sh # create task echo -e "$(date -u +%FT%T.%3N)\tDataSync: Execute\tCreating task between ${!sourceDnsName}${!sourceSubdirectory} and ${!destinationEfsFilesystemId}${!destinationSubdirectory}" >> /tmp/datasync_setup.log 2>&1 taskArn=$(createDataSyncTask ${!sourceLocationArn} ${!destinationLocationArn} ${!destinationLocationRegion} ${!agentIp} ${!stackName}) if [ "$?" != "0" ] then echo -e "$(date -u +%FT%T.%3N)\tDataSync: Error\t\tFailed to create task between ${!sourceDnsName}${!sourceSubdirectory} and ${!destinationEfsFilesystemId}${!destinationSubdirectory}" >> /tmp/datasync_setup.log 2>&1 exit 1 fi echo -e "$(date -u +%FT%T.%3N)\tDataSync: Success\tTask created: ${!taskArn}" >> /tmp/datasync_setup.log 2>&1 sed -i "1iaws datasync delete-task --task-arn ${!taskArn} --region ${!destinationLocationRegion}" /tmp/delete_datasync_resources.sh path: /tmp/create_datasync_resources.sh permissions: 0777 runcmd: - curl "https://s3.amazonaws.com/aws-cli/awscli-bundle.zip" -o "awscli-bundle.zip" - unzip awscli-bundle.zip - ./awscli-bundle/install -i /usr/local/aws -b /usr/local/bin/aws - export PATH=/usr/local/bin:$PATH - source /tmp/functions_datasync.sh - agentIp=$(curl -s http://169.254.169.254/latest/meta-data/local-ipv4) - sleep 60 - echo /tmp/create_datasync_resources.sh ${!agentIp} ${SourceDnsName} ${SourceSubdirectory} ${DestinationEfsFilesystemId} ${DestinationSubdirectory} ${DestinationSubnetId} ${DestinationSecurityGroupId} ${DestinationLocationRegion} ${AWS::AccountId} ${AWS::StackName} - echo -e "$(date -u +%FT%T.%3N)\tDataSync":" Execute\tCreating datasync resources" >> /tmp/datasync_setup.log 2>&1 - /tmp/create_datasync_resources.sh ${!agentIp} ${SourceDnsName} ${SourceSubdirectory} ${DestinationEfsFilesystemId} ${DestinationSubdirectory} ${DestinationSubnetId} ${DestinationSecurityGroupId} ${DestinationLocationRegion} ${AWS::AccountId} ${AWS::StackName} - if [ "$?" != "0" ]; then echo -e "$(date -u +%FT%T.%3N)\tDataSync":" Error\t\tFailed to create datasync resources" >> /tmp/datasync_setup.log 2>&1; else echo -e "$(date -u +%FT%T.%3N)\tDataSync":" Success\tCreated datasync resources" >> /tmp/datasync_setup.log 2>&1; fi Outputs: ViewDataSyncSetupLog: Description: SSM command to view DataSync setup log Value: !Sub | commandId=$(aws ssm send-command --max-concurrency 1 --max-errors 0 --targets Key=tag:aws:ec2:fleet-id,Values=${AgentFleet} --document-name "AWS-RunShellScript" --parameters commands="sudo cat /tmp/datasync_setup.log" --query 'Command.CommandId' --output text --region ${AWS::Region}); instanceId=$(aws ec2 describe-fleet-instances --fleet-id ${AgentFleet} --query 'ActiveInstances[0].InstanceId' --output text --region ${AWS::Region}); aws ssm get-command-invocation --command-id ${!commandId} --instance-id ${!instanceId} --query 'StandardOutputContent' --output text --region ${AWS::Region} ViewDeleteDataSyncResourcesScript: Description: SSM command to view delete DataSync resources script Value: !Sub | commandId=$(aws ssm send-command --max-concurrency 1 --max-errors 0 --targets Key=tag:aws:ec2:fleet-id,Values=${AgentFleet} --document-name "AWS-RunShellScript" --parameters commands="sudo cat /tmp/delete_datasync_resources.sh" --query 'Command.CommandId' --output text --region ${AWS::Region}); instanceId=$(aws ec2 describe-fleet-instances --fleet-id ${AgentFleet} --query 'ActiveInstances[0].InstanceId' --output text --region ${AWS::Region}); aws ssm get-command-invocation --command-id ${!commandId} --instance-id ${!instanceId} --query 'StandardOutputContent' --output text --region ${AWS::Region}