# CloudFormation - Fundamentals

> [YAML](https://velog.io/@717lumos/YAML-%25EB%25AC%25B8%25EB%25B2%2595-%25EA%25B0%2584%25EB%258B%25A8-%25EC%25A0%2595%25EB%25A6%25AC)은 다들 아시죠?

### Infrastructure as Code (IaC)

- 지금까지는 모든 작업을 콘솔에서 수동으로 진행하였다.

- 이제 인프라를  생성/수정/삭제/배포를 자동으로 진행하게 하고 싶다.

- 그래서 인프라를 코드로 표현하는 방식을 IaC라고 한다.

- 코드로 인프라를 관리하면 버전 관리, 정적 테스트 등 개발 과정의 이점을 가져갈 수 있다.

- Do not reinvent the wheel!

# CloudFormation (CFN)

AWS 리소스를 선언해서 생성하는 IaC 서비스이다. S3에 템플릿이 업로드 되어야 하며, 버전 업그레이드를 통해서만 수정이 가능하다.

### 장점

**비용**

- 리소스별로 비용을 추적할 수 있다.

- CFN template 별로 비용을 예측할 수 있다.

- 특정 시간대에만 자동으로 삭제하고 다시 생성해 비용을 아낄 수 있다.

**생산성**

- 그때그때 삭제하고 다시 생성할 수 있다.

- CFN template의 다이어그램을 자동으로 만들어준다.

- VPC 스택, Network 스택 등 따로따로 관리할 수 있다. 

- 선언형 프로그래밍 (리소스의 생성 순서를 관리할 필요 없다)

### 템플릿 배포

템플릿을 배포한 것을 스택이라고 한다.

- **수동** : CFN designer로 템플릿을 수정하고 콘솔로 input을 정하고 배포한다.

- **자동** : YAML 파일로 템플릿을 수정하고 CLI로 input을 정하고 배포한다.

### 템플릿 속성

- **Resources** : AWS 리소스. 필수다! `!Ref`

- **Parameters** : 동적 변수 `!Ref`

- **Mapping** : 정적 변수, `!FindInMap`

- **Outputs** : 다른 스택 참조용 변수 `!ImportValue`

- **Conditionals**

- **Metadata**

### Parameters

- 지금 당장 결정할 수 없거나 재사용하고 싶은 변수를 파라미터로 선언한다.

- 그러면 템플릿을 교체하지 않고 파라미터만 수정해서 새로운 스택을 만들 수 있다.

- 아래 예시 외의 property도 일단 숙지해야 한다. [링크](https://docs.aws.amazon.com/ko_kr/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html#parameters-section-structure-properties)

```
Parameters:
  InstanceTypeParameter:
    Type: String
    Description: Enter t2.micro, m1.small. Default is t2.micro
    Default: t2.micro
    AllowedValues:
      - t2.micro
      - m1.small 
```

**Pseudo parameters : **`AWS::<parameter>` 꼴로 되어있는, AWS에서 제공하는 파라미터이다. [링크](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/pseudo-parameter-reference.html)

### Reference

다른 리소스나 파라미터를 참조해야 할 때 Ref 함수를 사용한다.

- `!Ref <Parameter>` : 파라미터의 값을 반환한다.

- `!Ref <Resource>` : Physical ID 같이 리소스를 특정할 수 있는 값은 반환한다.

```
VpcId:
	Ref: MyVPC      # 방법 1
VpcId: !Ref MyVPC # 방법 2
```

### Resources

실제 AWS 컴포넌트를 생성하며, 템플릿을 만들 때 꼭 필요한 부분이다. `AWS::aws-product-name::data-type-name` 꼴로 쓴다. [링크](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html)

- 동적으로 리소스를 만들 수 없다. 무조건 Resource 당 하나가 만들어진다.

- 거의 모든 서비스를 만들 수 있다. AWS Lambda Custom Resoruces로 그 외의 서비스도 사용 가능하다.

### Mappings

CFN 템플릿에서 고정된 변수이다. dev/prod 환경이나 리전, AMI 타입 등을 정의할 때 용이하다.

- `Fn::FindInMap` 함수를 통해 Mapping을 참조한다.

- `!FindInMap [MapName, TopLevelKey, SecondLevelKey]` 꼴로 쓴다.

```
Mappings: 
  RegionMap: 
    us-east-1:
      HVM64: ami-0ff8a91507f77f867
      HVMG2: ami-0a584ac55a7631c0c
    us-west-1:
      HVM64: ami-0bdb828fd58c52235
      HVMG2: ami-066ee5fd4a9ef77f1
Resources: 
  myEC2Instance: 
    Type: "AWS::EC2::Instance"
    Properties: 
      ImageId: !FindInMap [RegionMap, !Ref "AWS::Region", HVM64]
      InstanceType: m1.small
```

### Outputs

 모든 스택은 다른 스택의 output을 참조해서 값을 가져올 수 있다.

- 예를 들어, 네트워크 스택은 VPC 스택에서 VPC ID와 Subnet ID를 가져온다. 

- 다른 스택이 의존하고 있는 스택은 삭제할 수가 없다.

`Fn::ImportValue` 함수를 통해 다른 스택의 output 값을 불러온다.

```
Outputs:
	StackSSHSecurityGroup:
		Value: !Ref MyCompanyWideSSHSecurityGroup
		Export:
			Name: SSHSecurityGroup # 다른 스택에서 참조할 이름
---
Resources:
	MySecureInstance:
		Type: AWS::EC2::Instance
		Properties:
			...
			SecurityGroups:
				- !ImportValue: SSHSecurityGroup
```

### Conditions

Condition을 만들고 이를 리소스에 붙여, 해당 리소스를 생성할지 안할지 결정한다.

```
Conditions:
	CreateProdResources: !Equals [ !Ref EnvType, prod ]
Resources:
	MonutPoint:
		Type: "AWS::EC2::VolumeAttachment"
		Condition: CreateProdResources
```

**Logical Functions** : `Fn::And`, `Fn::Equals`, `Fn::If`, `Fn::Not`, `Fn::Or`

### Intrisic Functions

- `Fn::Ref` :  

- `Fn::GetAtt` : 리소스의 속성을 가져온다. `!GetAtt EC2Instance.AvailabilityZone` 꼴로 쓴다.

- `Fn::FindInMap` : Mapping의 값을 가져온다.  

- `Fn::ImportValue` : Outputs의 값을 가져온다.  

- `Fn::Join` : 문자열을 join한다. `!Join [ ":", [ a, b, c ] ]` → `a:b:c`

- `Fn:Sub` : Bash처럼 변수를 포함해 문자열을 만든다. 

```
# Name: www.example.com/path
Name: !Sub 
  - 'www.${Domain}/${Path}'
  - Domain: example.com
		Path: home

Name: !Sub 'arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:vpc/${vpc}'
```

### User data

EC2 인스턴스의 유저 데이터는 Base64 저장이 되기 때문에 `Fn::Base64` 함수를 통한 변환이 필요하다. 추가로 `/var/log/cloud-init-output.log` 에 유저 데이터가 저장된다.

```
UserData:
  Fn::Base64:
    !Sub |
      #!/bin/bash -xe
      yum update -y aws-cfn-bootstrap
      /opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource LaunchConfig --configsets wordpress_install --region ${AWS::Region}
      /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource WebServerGroup --region ${AWS::Region}
```

### cfn-init

User data의 복잡한 스크립트를 대체하여 좀 더 읽기 좋게 바꾼 것을 말한다. cfn-init의 로그는 `/var/log/cfn-init.log` 파일에서 확인할 수 있다.

1. 인스턴스가 launch되면 User data의 명령을 실행한다.

2. User data의 cfn-init 명령은 CloudFormation의 MyInstance 리소스에서 [Metadata](https://docs.aws.amazon.com/ko_kr/AWSCloudFormation/latest/UserGuide/aws-resource-init.html)를 가져와 실행한다.

```
Resources: 
  MyInstance: 
    Type: AWS::EC2::Instance
    Properties: 
      UserData:
				Fn::Base64:
					!Sub |
						#!/bin/bash -xe
						yum update aws-cfn-bootstrap
						/opt/aws/bin/cfn-init -s ${AWS:StackId} -r MyInstance --region ${AWS::Region} || error_exit 'Failed to run cfn-init'
    Metadata: 
      AWS::CloudFormation::Init: 
        config: 
          packages: 
            yum:
							httpd: [] # yum install httpd
          files: 
            "/var/www/html/index.html":
							content: |
								<h1> Hello world from EC2 Instance! </h1>
								<p> This was created using cfn-init </p>
							mode: '000644'
          commands: 
            hello:
							commnad: "echo 'Hello World'"
							cwd: "~"
          services: 
            sysvinit:
							httpd:
								enabled: 'true'       # service enable httpd 
								ensureRunning: 'true' # service start httpd
```

### cfn-signal & wait condition

- `cfn-signal` : CFN에게 이 리소스가 잘 만들어졌다고 알리는 스크립트

- `WaitCondition` : CFN에게 cfn-signal을 줘야지 생성이 완료되는 더미 리소스

```
Resources: 
  MyInstance: ...
		# MyInstance Resource의 Base64에 아래 명령을 추가한다.
		/opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource MyInstance --region ${AWS::Region}

	SampleWaitCondition:
		DependsOn: MyInstance # MyInstance가 생성 완료되어야 이 리소스를 생성 시작한다.
		CreationPolicy:    # 아래 조건을 만족해야 이 리소스가 생성 완료된다.
			ResourceSignal:  # cfn-signal을 기다린다.
				Timeout: PT2M  # 2분 동안 기다린다.
		Type: AWS:CloudFormation::WaitCondition
```

### CFN 스택 생성/업데이트 실패시

**스택 생성 실패** (CreateStackAPI)

- **ROLLBACK** : 기본값으로, 모두 지우고 다시 설치한다. 로그를 볼 수 있다.

- **DO_NOTHING** : 롤백을 하지 않고 직접 트러블슈팅을 할 수 있도록 한다.

- **DELETE **: 모두 지운다.

**스택 업데이트 실패** (UpdateStack API)

- 이전 상태로 자동으로 롤백한다. 로그와 오류 메시지는 볼 수 있다.

### Nested Stacks

스택 안의 스택이다. 😎 늘 밑바닥부터 만들 수 없으니 다른 사람들이 만든 템플릿을 가져다가 써야 한다.

```
Resources:
	myStack:
		Type: AWS::CloudFormation::Stack
		Properties:
			TemplateURL: https://...
			Parameters: # 해당 템플릿에서 요구하는 파라미터들
				...
```

### ChangeSets

스택을 업데이트할 경우 어떤 리소스가 생성/수정/삭제될 것인지 보여준다. 실제 적용하기 전의 계획서라고 보면 된다. 단, 업데이트한 스택이 성공한다고 보장하지는 않는다.

![Image](https://upload.cafenono.com/image/slashpageHome/20240820/134217_k10BezBOHlyS9o3nXn?q=80&s=1280x180&t=outside&f=webp)

### DeletionPolicy

스택을 삭제하면 각 리소스별로 어떤 행동을 취할지 DeletionPolicy에 정의할 수 있다.

- **Retain** : 리소스를 제거하지 않고 놔둔다.

- **Snapshot** : 저장 공간의 스냅샷을 저장한다.

    - EBS Volume, ElastiCache Cluster, ElastiCache ReplicationGroup

    - RDS DBInstance, RDS DBCluster, Redshift Cluster

- **Delete **: 디폴트이며, 리소스를 제거한다.

    - AWS::RDS::DBCluster는 스냅샷으로 저장한다.

    - S3 bucket은 삭제하기 전, 저장하고 있는 것이 없어야 한다.

### TerminationProtection

어떤 스택에 Termination protection이 enable 되어 있으면 해당 스택은 삭제할 수 없다. 삭제하기 위해서는 termination protection 기능을 disable한 후 지워야 한다. 삭제를 좀 더 번거롭게 만들고 과정이다.

For the site tree, see the [root Markdown](https://slashpage.com/kaonmir.md).
