CloudFormation Resource Importで既存のスタックをリファクタリング！

## はじめに
CloudFormation で全世界待望？のリソースインポート機能がリリースされました。

**AWS CloudFormation Launches Resource Import**
https://aws.amazon.com/jp/about-aws/whats-new/2019/11/aws-cloudformation-launches-resource-import/

CloudFormationで管理されていないリソースをスタックに取り込むことができるようになったわけですが、
このリソースインポート機能を活用することで既存のスタック間でリソースを移動したり
複雑化したスタックのリファクタリングを行ったりすることもできます。

## スタック分割を例に考える
既存のスタックを分割しようとする場合、概ね以下のような流れとなります。

1. ソーステンプレート内の分割対象のリソースに対し `DeletionPolicy: Retain` を指定し、適用する
1. 分割対象のリソースをソーステンプレートから削除し、ターゲットテンプレートに追加する
1. 既存スタックを更新して分割対象のリソースを削除（管理対象外）にする
1. ターゲットテンプレートを使用して、管理対象外となった分割対象リソースをインポートする

ソーステンプレート: 既存スタックのテンプレート
ターゲットテンプレート: 分割する新しいスタックのテンプレート 

## やってみる
1つのテンプレートでVPCとEC2を一緒に管理していて、それぞれを別スタックに分割する例を考えてみます。
検証目的であるため、ここでは以下のようなシンプルなテンプレートで試してみます。

```source.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description:
  split stack test

Parameters:
  pLatestAmiId:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2'
  pInstanceType:
    Type: String
    Default: 't3.micro'

Resources:
# Create VPC
  rMyVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      InstanceTenancy: default
      Tags:
      - Key: Name
        Value: test-vpc

# Create Public RouteTable
  rPublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref rMyVPC
      Tags:
      - Key: Name
        Value: test-pubric-route

# Create Public Subnet 
  rPublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref rMyVPC
      CidrBlock: 10.0.0.0/24
      AvailabilityZone: !Select [ 0, !GetAZs "" ]
      MapPublicIpOnLaunch: true
      Tags:
      - Key: Name
        Value: test-public-subnet
  rRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref rPublicSubnet
      RouteTableId: !Ref rPublicRouteTable

# Create InternetGateway
  rInternetGateway:
    Type: "AWS::EC2::InternetGateway"
    Properties:
      Tags:
      - Key: Name
        Value: test-igw
  rAttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref rMyVPC
      InternetGatewayId: !Ref rInternetGateway

# Route for InternetGateway
  rRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref rPublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref rInternetGateway

# Create EC2 Secuirty Group
  rSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !Ref rMyVPC
      GroupDescription: Security Group for EC2 Instance
      SecurityGroupIngress:
        - CidrIp: 0.0.0.0/0
          FromPort: 443
          IpProtocol: tcp
          ToPort: 443
      Tags:
        - Key: Name
          Value: test-sg

# Create EC2 Instance
  rMyEC2:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref pLatestAmiId
      InstanceType: !Ref pInstanceType
      SecurityGroupIds:
        - !Ref rSecurityGroup
      SubnetId: !Ref rPublicSubnet
      Tags:
        - Key: Name
          Value: test-ec2
```

以降、前述の手順1～4に沿って順番にやっていきます。

### 手順1
EC2およびセキュリティグループを別スタックに分割するため、ソーステンプレートで
`DeletionPolicy: Retain` を追加します。
既存のスタックで既に設定されている場合はスキップ可能な作業です。

```diff
  rSecurityGroup:
    Type: AWS::EC2::SecurityGroup
+   DeletionPolicy: Retain
    Properties:

  rMyEC2:
    Type: AWS::EC2::Instance
+   DeletionPolicy: Retain
    Properties:
```

<details><summary>**編集後のソーステンプレート**</summary><div>

```source.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description:
  split stack test add Deletion Policy

Parameters:
  pLatestAmiId:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2'
  pInstanceType:
    Type: String
    Default: 't3.micro'

Resources:
# Create VPC
  rMyVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      InstanceTenancy: default
      Tags:
      - Key: Name
        Value: test-vpc

# Create Public RouteTable
  rPublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref rMyVPC
      Tags:
      - Key: Name
        Value: test-pubric-route

# Create Public Subnet 
  rPublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref rMyVPC
      CidrBlock: 10.0.0.0/24
      AvailabilityZone: !Select [ 0, !GetAZs "" ]
      MapPublicIpOnLaunch: true
      Tags:
      - Key: Name
        Value: test-public-subnet
  rRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref rPublicSubnet
      RouteTableId: !Ref rPublicRouteTable

# Create InternetGateway
  rInternetGateway:
    Type: "AWS::EC2::InternetGateway"
    Properties:
      Tags:
      - Key: Name
        Value: test-igw
  rAttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref rMyVPC
      InternetGatewayId: !Ref rInternetGateway

# Route for InternetGateway
  rRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref rPublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref rInternetGateway

# Create EC2 Secuirty Group
  rSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    DeletionPolicy: Retain
    Properties:
      VpcId: !Ref rMyVPC
      GroupDescription: Security Group for EC2 Instance
      SecurityGroupIngress:
        - CidrIp: 0.0.0.0/0
          FromPort: 443
          IpProtocol: tcp
          ToPort: 443
      Tags:
        - Key: Name
          Value: test-sg

# Create EC2 Instance
  rMyEC2:
    Type: AWS::EC2::Instance
    DeletionPolicy: Retain
    Properties:
      ImageId: !Ref pLatestAmiId
      InstanceType: !Ref pInstanceType
      SecurityGroupIds:
        - !Ref rSecurityGroup
      SubnetId: !Ref rPublicSubnet
      Tags:
        - Key: Name
          Value: test-ec2
```

</summary></div>

※クリックで展開

編集後のソーステンプレートを既存スタックに反映させます。
Change Set上は、変更なしでアップデートが完了します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/cfc72f92-5e4e-5186-5d4c-33aa40190212.png)


### 手順2
セキュリティグループおよびEC2インスタンスに関連する記述をソーステンプレートから削除します。
また分割先のスタックからVPCやSubnetの情報を参照できるように、リソースIDをExportしておきます。

```diff
- Parameters:
-  pLatestAmiId:
-    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
-    Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2'
-  pInstanceType:
-    Type: String
-    Default: 't3.micro'
-
-  rSecurityGroup:
-    Type: AWS::EC2::SecurityGroup
-    DeletionPolicy: Retain
-    Properties:
-      VpcId: !Ref rMyVPC
-      GroupDescription: Security Group for EC2 Instance
-      SecurityGroupIngress:
-        - CidrIp: 0.0.0.0/0
-          FromPort: 443
-          IpProtocol: tcp
-          ToPort: 443
-      Tags:
-        - Key: Name
-          Value: test-sg
-
-  rMyEC2:
-    Type: AWS::EC2::Instance
-    DeletionPolicy: Retain
-    Properties:
-      ImageId: !Ref pLatestAmiId
-      InstanceType: !Ref pInstanceType
-      SecurityGroupIds:
-        - !Ref rSecurityGroup
-      SubnetId: !Ref rPublicSubnet
-      Tags:
-        - Key: Name
-          Value: test-ec2
+
+Outputs:
+  rMyVPC:
+    Value: !Ref rMyVPC
+    Export
+     Name: rMyVPC
+  rPublicSubnet:
+    Value: !Ref rPublicSubnet
+   Export:
+     Name: rPublicSubnet
   
```

<details><summary>**編集後のソーステンプレート**</summary><div>

```source.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description:
  split stack test add vpc only

Resources:
# Create VPC
  rMyVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      InstanceTenancy: default
      Tags:
      - Key: Name
        Value: test-vpc

# Create Public RouteTable
  rPublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref rMyVPC
      Tags:
      - Key: Name
        Value: test-pubric-route

# Create Public Subnet 
  rPublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref rMyVPC
      CidrBlock: 10.0.0.0/24
      AvailabilityZone: !Select [ 0, !GetAZs "" ]
      MapPublicIpOnLaunch: true
      Tags:
      - Key: Name
        Value: test-public-subnet
  rRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref rPublicSubnet
      RouteTableId: !Ref rPublicRouteTable

# Create InternetGateway
  rInternetGateway:
    Type: "AWS::EC2::InternetGateway"
    Properties:
      Tags:
      - Key: Name
        Value: test-igw
  rAttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref rMyVPC
      InternetGatewayId: !Ref rInternetGateway

# Route for InternetGateway
  rRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref rPublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref rInternetGateway

Outputs:
  rMyVPC:
    Value: !Ref rMyVPC
    Export:
      Name: rMyVPC
  rPublicSubnet:
    Value: !Ref rPublicSubnet
    Export:
      Name: rPublicSubnet
```

</summary></div>

※クリックで展開

分割するセキュリティグループとEC2に関する記載はターゲットテンプレートとして新規作成します。
VPCIDおよびSubnetIDはクロススタック参照させています。
インポートするリソースにもテンプレートで DeletionPolicy 属性を指定する必要があります。

```target.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description:
  split stack test ec2 only

Parameters:
  pLatestAmiId:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2'
  pInstanceType:
    Type: String
    Default: 't3.micro'

Resources:
# Create EC2 Secuirty Group
  rSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    DeletionPolicy: Retain
    Properties:
      VpcId: !ImportValue rMyVPC
      GroupDescription: Security Group for EC2 Instance
      SecurityGroupIngress:
        - CidrIp: 0.0.0.0/0
          FromPort: 443
          IpProtocol: tcp
          ToPort: 443
      Tags:
        - Key: Name
          Value: test-sg

# Create EC2 Instance
  rMyEC2:
    Type: AWS::EC2::Instance
    DeletionPolicy: Retain
    Properties:
      ImageId: !Ref pLatestAmiId
      InstanceType: !Ref pInstanceType
      SecurityGroupIds:
        - !Ref rSecurityGroup
      SubnetId: !ImportValue rPublicSubnet
      Tags:
        - Key: Name
          Value: test-ec2
```

### 手順3 
更新したソーステンプレートを使用して、既存のスタックを Update します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/a86d3bbe-b48c-fdc0-9c4c-93d9c01f2de0.png)

DeletionPolicyにより、リソース自体の削除はスキップされますが、
この時点でセキュリティグループおよび、EC2は既存スタックの管理対象外となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/f212884b-8576-3f74-a390-635474cd1b0f.png)

### 手順4 
ターゲットテンプレートを使用して、管理対象外となったEC2とセキュリティグループをインポートします。
スタックの作成で 既存のリソースを使用 を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/c5830ffc-202f-f5e3-b8a3-e952963c4cc3.png)

テンプレートの指定で、ターゲットテンプレートをアップロードして次に進みます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/f03ace56-e91c-dfa6-2011-053e562b7c25.png)

リソースを識別で、インポート対象のEC2とセキュリティグループのリソースIDを指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/51b891ff-94f3-d838-0ad2-b73c81382377.png)

スタックの詳細を指定で、新たに作成するスタックの名前を設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/f9ca0dae-87f2-5e15-fe29-6e00079bceb9.png)

対象のリソースを確認して、インポートを実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/3b28ac7c-cdb2-8338-e69d-50dfb8c6be8b.png)

インポートが完了し、既存のEC2とセキュリティグループが新しいスタックの
管理対象となっていることを確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/4db0f49e-c583-e66b-2a8f-981c4956c92b.png)

無事、VPCとEC2のスタックに分割することができました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/f6600114-6cfa-11c7-d08e-19fdcadf4157.png)

## まとめ
移動するリソースに削除ポリシーを追加し、ソーススタックから削除＆ターゲットスタックに
インポートすることでスタック間でのリソースの移動を簡単に行うことができます。
だだし、現地時点でインポートに対応しているリソースは限られているのでご注意ください。

**Resources that Support Import Operations**
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resource-import-supported-resources.html
**Moving Resources Between Stacks**
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/refactor-stacks.html

以上です。
参考になれば幸いです。