AWS CloudFormationでS3エンドポイントを使用したS3バケットを構築しよう

# はじめに
AWS CloudFormationを利用してVPC構築のテンプレートのサンプルです。

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

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

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

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

# AWS リソース構築内容
  1. networkスタック
      - VPC (10.0.0.0/16)
      - Publicサブネット1 (10.0.1.0/24)
      - Publicサブネット2 (10.0.2.0/24)
      - Privateサブネット1 (10.0.11.0/24)
      - Privateサブネット2 (10.0.12.0/24)
      - インターネットゲートウェイ
      - Publicルートテーブル 
      - Elastic IP (NATゲートウェイ用)
      - NATゲートウェイ
      - Privateルートテーブル
  2. s3スタック
      - s3バケット (akane-dev-album)
      - バケットポリシー (s3:ListBucket, s3:GetObject, s3:PutObject)
      - S3エンドポイント (インターフェイスエンドポイント: Publicルートテーブル, Privateルートテーブルに関連づける)


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

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

    ```
    ./create_stacks.sh
    ```

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

    ```
    ./delete_stacks.sh
    ```

# 構築テンプレート
**1. networkスタック**
 
```yaml:network.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Network 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.
  VPCCidrBlock:
    Type: String
    AllowedPattern: (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/16
  PublicSubnet1:
    Type: String
    AllowedPattern: (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/24
  PublicSubnet2:
    Type: String
    AllowedPattern: (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/24
  PrivateSubnet1:
    Type: String
    AllowedPattern: (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/24
  PrivateSubnet2:
    Type: String
    AllowedPattern: (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/24

Mappings: 
  AzMap: 
    ap-northeast-1:
      1st: ap-northeast-1a
      2nd: ap-northeast-1c
      3rd: ap-northeast-1d

# Conditions

# Transform

Resources:
  # VPC作成
  akaneVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCidrBlock
      EnableDnsSupport: true
      EnableDnsHostnames: true
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-vpc
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType

###########################################################################
# Publicサブネット関連
###########################################################################
  # Publicサブネット作成
  akanePublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref akaneVPC
      CidrBlock: !Ref PublicSubnet1
      AvailabilityZone: !FindInMap [AzMap, !Ref AWS::Region, 1st]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-public-subnet1
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
  akanePublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref akaneVPC
      CidrBlock: !Ref PublicSubnet2
      AvailabilityZone: !FindInMap [AzMap, !Ref AWS::Region, 3rd]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-public-subnet2
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
  # インターネットゲートウェイ作成
  akaneInternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-igw
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
  # インターネットゲートウェイをVPCにアタッチ
  akaneVPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref akaneVPC
      InternetGatewayId: !Ref akaneInternetGateway
  # Publicルートテーブル作成
  akanePublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref akaneVPC
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-public-route
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
  # Publicルートテーブル サブネット関連付け
  akanePublicSubnetRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref akanePublicSubnet1
      RouteTableId: !Ref akanePublicRouteTable
  akanePublicSubnetRouteTableAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref akanePublicSubnet2
      RouteTableId: !Ref akanePublicRouteTable
  # Publicルートテーブル ルートにインターネットゲートウェイを関連付ける
  akanePublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref akanePublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref akaneInternetGateway

###########################################################################
# Privateサブネット関連
###########################################################################
  # Privateサブネット作成
  akanePrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref akaneVPC
      CidrBlock: !Ref PrivateSubnet1
      AvailabilityZone: !FindInMap [AzMap, !Ref AWS::Region, 1st]
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-private-db-1
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
  akanePrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref akaneVPC
      CidrBlock: !Ref PrivateSubnet2
      AvailabilityZone: !FindInMap [AzMap, !Ref AWS::Region, 3rd]
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-private-db-2
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
  # Privateルートテーブル作成
  akanePrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref akaneVPC
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-private-route
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
  # Privateルートテーブル サブネット関連付け
  akanePrivateSubnetRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref akanePrivateSubnet1
      RouteTableId: !Ref akanePrivateRouteTable
  akanePrivateSubnetRouteTableAssociationWeb2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref akanePrivateSubnet2
      RouteTableId: !Ref akanePrivateRouteTable
  # NATゲートウェイ用ElasticIP作成
  akaneNATGatewayEIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
      - Key: Name
        Value: !Sub
        - ${SystemName}-${EnvType}-natgw-eip
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      - Key: SystemName
        Value: !Ref SystemName
      - Key: EnvType
        Value: !Ref EnvType
  # NATゲートウェイ作成
  akaneNATGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt akaneNATGatewayEIP.AllocationId
      SubnetId: !Ref akanePrivateSubnet1
      Tags:
      - Key: Name
        Value: !Sub
        - ${SystemName}-${EnvType}-natgw
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      - Key: SystemName
        Value: !Ref SystemName
      - Key: EnvType
        Value: !Ref EnvType        
  # Privateルートテーブル ルートにNATゲートウェイを関連付ける
  akanePrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref akanePrivateRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref akaneNATGateway

Outputs:
  akaneVPC:
    Value: !Ref akaneVPC
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-vpc
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
  akanePublicSubnet1:
    Value: !Ref akanePublicSubnet1
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-public-subnet1
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
  akanePublicSubnet2:
    Value: !Ref akanePublicSubnet2
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-public-subnet2
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
  akanePrivateSubnet1:
    Value: !Ref akanePrivateSubnet1
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-private-subnet1
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
  akanePrivateSubnet2:
    Value: !Ref akanePrivateSubnet2
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-private-subnet2
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
  akanePublicRouteTable:
    Value: !Ref akanePublicRouteTable
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-public-route-table
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
  akanePrivateRouteTable:
    Value: !Ref akanePrivateRouteTable
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-private-route-table
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
```

```json:dev-parameters.json
{
    "Parameters": [
        {
            "ParameterKey": "SystemName",
            "ParameterValue": "akane"
        },
        {
            "ParameterKey": "EnvType",
            "ParameterValue": "dev"
        },
        {
            "ParameterKey": "VPCCidrBlock",
            "ParameterValue": "10.0.0.0/16"
        },
        {
            "ParameterKey": "PublicSubnet1",
            "ParameterValue": "10.0.1.0/24"
        },
        {
            "ParameterKey": "PublicSubnet2",
            "ParameterValue": "10.0.2.0/24"
        },
        {
            "ParameterKey": "PrivateSubnet1",
            "ParameterValue": "10.0.11.0/24"
        },
        {
            "ParameterKey": "PrivateSubnet2",
            "ParameterValue": "10.0.12.0/24"
        }
    ]
}
```

**２. 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.
  BucketName:
    Type: String
    AllowedPattern: '[a-zA-Z0-9-]*'

# Mappings

# Conditions

# Transform

Resources:
  # バケット作成
  akaneS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub
      - ${SystemName}-${EnvType}-${BucketName}
      - {SystemName: !Ref SystemName, EnvType: !Ref EnvType, BucketName: !Ref BucketName}
      AccessControl: Private
      # BucketEncryption:
      #   ServerSideEncryptionConfiguration:
      #     - ServerSideEncryptionByDefault:
      #         SSEAlgorithm: AES256
      Tags:
      - Key: Name
        Value: !Sub
        - ${SystemName}-${EnvType}-s3
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      - Key: SystemName
        Value: !Ref SystemName
      - Key: EnvType
        Value: !Ref EnvType
  # バケットポリシー作成
  akaneS3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref akaneS3Bucket
      PolicyDocument:
        Statement:
          - Sid: !Sub
              - ${SystemName}-${EnvType}-${BucketName}
              - {SystemName: !Ref SystemName, EnvType: !Ref EnvType, BucketName: !Ref BucketName}
            Action: 
              - s3:ListBucket
              - s3:GetObject
              - s3:PutObject
            Effect: Allow
            Resource: 
              - !Join
                - ''
                - - 'arn:aws:s3:::'
                  - !Ref akaneS3Bucket
              - !Join
                - ''
                - - 'arn:aws:s3:::'
                  - !Ref akaneS3Bucket
                  - /*
            Principal: '*'
  # VPCエンドポイント作成
  akaneS3Endpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal: '*'
            Action:
              - s3:*
            Resource:
              - !Join
                - ''
                - - 'arn:aws:s3:::'
                  - !Ref akaneS3Bucket
              - !Join
                - ''
                - - 'arn:aws:s3:::'
                  - !Ref akaneS3Bucket
                  - /*
              - !Join
                - ''
                - - 'arn:aws:s3:::*'
                  - !Ref AWS::Region
                  - '.amazon.com/*'
              - !Join
                - ''
                - - 'arn:aws:s3:::*'
                  - !Ref AWS::Region
                  - '.amazon.com'
      RouteTableIds:
        - Fn::ImportValue: !Sub
          - ${SystemName}-${EnvType}-public-route-table
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Fn::ImportValue: !Sub
          - ${SystemName}-${EnvType}-private-route-table
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
      VpcId:
        Fn::ImportValue: !Sub
          - ${SystemName}-${EnvType}-vpc
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}

Outputs:
  akaneS3Bucket:
    Value: !Ref akaneS3Bucket
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-bucket
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}

```

```json:dev-parameters.json
{
    "Parameters": [
        {
            "ParameterKey": "SystemName",
            "ParameterValue": "akane"
        },
        {
            "ParameterKey": "EnvType",
            "ParameterValue": "dev"
        },
        {
            "ParameterKey": "BucketName",
            "ParameterValue": "album"
        }
    ]
}
```

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

```sh:create_stacks.sh
#!/bin/sh
SYSTEM_NAME=akane
ENV_TYPE=dev

create_stack () {
    STACK_NAME=$1
    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_stack network
create_stack s3
```

```sh:delete_stacks.sh
#!/bin/sh
SYSTEM_NAME=akane
ENV_TYPE=dev
STACK_NAME=network

delete_stack () {
    STACK_NAME=$1
    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 s3
delete_stack network

```