AWS CloudFormationでS3のクロスリージョン相互レプリケーションを構築しよう

# はじめに
AWS CloudFormationを利用してS3のクロスリージョン相互レプリケーション構築のテンプレートのサンプルです。
東京リージョンと大阪リージョンにS3バケットを作成し、相互レプリケーションの設定をします。
S3バケットのARNを相互参照するため、一度作成した後に、変更セットでレプリケーションルールを設定します。

テンプレートの概要が分からない場合は、[はじめてのAWS CloudFormationテンプレートを理解する](https://qiita.com/miyabiz/items/eb7b0fa1675b5199b9b7)を参考にしてください。

コードは[GitHub](https://github.com/miyabiz-room/CloudFormation-Template/tree/s3-02)にもあります。

今回は、akane というシステムの dev 環境を想定しています。
同じ構成で違う環境を作成する場合は、{環境名}-parameters.jsonを別途作成します。

```text:ディレクトリ構成
akane (システム)
  ├── s3 (スタック)
  │   ├── dev-parameters.json (dev 環境のパラメータ)
  │   └── s3.yml (CFnテンプレート)
  └── s3-replication (スタック)
      ├── dev-parameters.json (dev 環境のパラメータ)
      └── s3-replication.yml -> ../s3/s3.yml (CFnテンプレートのシンボリックリンク)
```

# AWS リソース構築内容
  1. s3スタック
      - IAMロール (s3)
      - s3バケット (akane-dev-s3)


# 実行環境の準備
[AWS CloudFormationを動かすためのAWS CLIの設定](https://qiita.com/miyabiz/items/fed11796f0ea2b7608f4)を参考にしてください。

# AWS リソース構築手順
1. 下記を実行してスタックを作成

    ```
    ./create_stacks.sh
    ```

2. 下記を実行してスタックを削除

    ```
    ./delete_stacks.sh
    ```

# 構築テンプレート
**1. s3スタック**
 
```yaml:s3.yml
AWSTemplateFormatVersion: 2010-09-09
Description: S3 For Akane

# Metadata:

Parameters:
  SystemName:
    Type: String
    AllowedPattern: '[a-zA-Z0-9-]*'
  EnvType:
    Description: Environment type.
    Type: String
    AllowedValues: [all, dev, stg, prod]
    ConstraintDescription: must specify all, dev, stg, or prod.
  IsFirstTime:
    Type: String
    AllowedValues: [true, false]

Mappings:
  AnotherAz:
    ap-northeast-1:
      Region: ap-northeast-3
    ap-northeast-3:
      Region: ap-northeast-1

Conditions:
  IsFirstTime: !Not [ !Equals [ !Ref IsFirstTime, true ] ]

# Transform

Resources:
  # ロール作成
  akaneS3ReplicationRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - s3.amazonaws.com
            Action:
              - sts:AssumeRole
      Description: !Sub
        - ${SystemName}-${EnvType}-role-s3-replication-${AWS::Region}
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Path: /
      Policies:
        - PolicyName: !Sub
            - ${SystemName}-${EnvType}-policy-s3-replication-${AWS::Region}
            - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - s3:Get*
                  - s3:ListBucket
                Resource:
                  - !Join
                    - ''
                    - - 'arn:aws:s3:::'
                      - !Sub
                        - ${SystemName}-${EnvType}-s3-${AWS::Region}
                        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
                  - !Join
                    - ''
                    - - 'arn:aws:s3:::'
                      - !Sub
                        - ${SystemName}-${EnvType}-s3-${AWS::Region}
                        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
                      - /*
              - Effect: Allow
                Action:
                  - s3:ReplicateObject
                  - s3:ReplicateDelete
                  - s3:ReplicateTags
                  - s3:GetObjectVersionTagging
                Resource:
                  - !Join
                    - ''
                    - - 'arn:aws:s3:::'
                      - !Sub
                        - ${SystemName}-${EnvType}-s3-
                        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
                      - !FindInMap [AnotherAz, !Ref AWS::Region, Region]
                      - /*
      RoleName: !Sub
        - ${SystemName}-${EnvType}-role-s3-replication-${AWS::Region}
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-role-s3-replication-${AWS::Region}
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType

  # S3 Bucket作成
  akaneS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      AccessControl: Private
      BucketName: !Sub
        - ${SystemName}-${EnvType}-s3-${AWS::Region}
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      VersioningConfiguration:
        Status: Enabled
      ReplicationConfiguration: !If
        - IsFirstTime
        - Role: !GetAtt akaneS3ReplicationRole.Arn
          Rules:
            - Destination:
                Bucket: !Join
                  - ''
                  - - !Sub arn:aws:s3:::${SystemName}-${EnvType}-s3-
                    - !FindInMap [AnotherAz, !Ref AWS::Region, Region]
              Id: ClossRegionSync
              Prefix: ''
              Status: Enabled
        - !Ref AWS::NoValue
      Tags:
        - Key: Name
          Value: !Sub
            - ${SystemName}-${EnvType}-s3-${AWS::Region}
            - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType

Outputs:
  akaneS3BucketArn:
    Value: !GetAtt akaneS3Bucket.Arn
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-s3-arn-${AWS::Region}
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
```

```json:dev-parameters.json
{
    "Parameters": [
        {
            "ParameterKey": "SystemName",
            "ParameterValue": "akane"
        },
        {
            "ParameterKey": "EnvType",
            "ParameterValue": "dev"
        },
        {
            "ParameterKey": "IsFirstTime",
            "ParameterValue": "true"
        }
    ],
    "Capabilities": [
        "CAPABILITY_NAMED_IAM"
    ]
}
```

**２. s3スタック**

`s3-replication.yml` は、`s3.yml` のシンボリックリンクのため記載を割愛する。

```json:dev-parameters.json
{
    "Parameters": [
        {
            "ParameterKey": "SystemName",
            "ParameterValue": "akane"
        },
        {
            "ParameterKey": "EnvType",
            "ParameterValue": "dev"
        },
        {
            "ParameterKey": "IsFirstTime",
            "ParameterValue": "false"
        }
    ],
    "Capabilities": [
        "CAPABILITY_NAMED_IAM"
    ]
}
```

**3. 実行ファイル**

```sh:create_stacks.sh
#!/bin/sh

cd `dirname $0`

SYSTEM_NAME=akane

create_stack () {
    ENV_TYPE=$1
    STACK_NAME=$2
    export AWS_DEFAULT_REGION=$3
    aws cloudformation create-stack \
    --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME} \
    --template-body file://./${SYSTEM_NAME}/${STACK_NAME}/${STACK_NAME}.yml \
    --cli-input-json file://./${SYSTEM_NAME}/${STACK_NAME}/${ENV_TYPE}-parameters.json

    aws cloudformation wait stack-create-complete \
    --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME}
}

create_change_set () {
    ENV_TYPE=$1
    STACK_NAME=$2
    export AWS_DEFAULT_REGION=$3

    if [ ${STACK_NAME} == "s3-replication" ]; then
        STACK_FULLNAME=${SYSTEM_NAME}-${ENV_TYPE}-s3
    else
        STACK_FULLNAME=${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME}
    fi

    aws cloudformation create-change-set \
    --stack-name ${STACK_FULLNAME} \
    --change-set-name ${STACK_FULLNAME}-change-set \
    --template-body file://./${SYSTEM_NAME}/${STACK_NAME}/${STACK_NAME}.yml \
    --cli-input-json file://./${SYSTEM_NAME}/${STACK_NAME}/${ENV_TYPE}-parameters.json

    aws cloudformation wait change-set-create-complete \
    --stack-name ${STACK_FULLNAME} \
    --change-set-name ${STACK_FULLNAME}-change-set

    aws cloudformation execute-change-set \
    --stack-name ${STACK_FULLNAME} \
    --change-set-name ${STACK_FULLNAME}-change-set
}

create_stack dev s3 ap-northeast-1
create_stack dev s3 ap-northeast-3
create_change_set dev s3-replication ap-northeast-1
create_change_set dev s3-replication ap-northeast-3

exit 0
```

```sh:delete_stacks.sh
#!/bin/sh

cd `dirname $0`

SYSTEM_NAME=akane

delete_stack () {
    ENV_TYPE=$1
    STACK_NAME=$2
    export AWS_DEFAULT_REGION=$3
    aws cloudformation delete-stack \
    --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME}

    aws cloudformation wait stack-delete-complete \
    --stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME}
}

delete_stack dev s3 ap-northeast-1
delete_stack dev s3 ap-northeast-3

exit 0
```