

기계 번역으로 제공되는 번역입니다. 제공된 번역과 원본 영어의 내용이 상충하는 경우에는 영어 버전이 우선합니다.

# CloudFormation CLI를 사용하여 사용자 지정 후크 개발
<a name="hooks-develop"></a>

이 섹션은 사용자 지정 후크를 개발하고 CloudFormation 레지스트리에 등록하려는 고객을 위한 것입니다. 후크의 구조에 대한 개요 CloudFormation 와 Python 또는 Java를 사용하여 자체 후크를 개발, 등록, 테스트, 관리 및 게시하기 위한 가이드를 제공합니다.

사용자 지정 후크를 개발하는 데는 세 가지 주요 단계가 있습니다.

1. **시작**

   사용자 지정 후크를 개발하려면 CloudFormation CLI를 구성하고 사용해야 합니다. 후크의 프로젝트와 필요한 파일을 시작하려면 CloudFormation CLI [https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-cli-init.html](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-cli-init.html) 명령을 사용하고 후크를 생성하도록 지정합니다. 자세한 내용은 [사용자 지정 CloudFormation 후크 프로젝트 시작](hooks-init.md) 단원을 참조하십시오.

1. **모델**

   후크 스키마를 모델링, 작성 및 검증하려면 후크, 해당 속성 및 속성을 정의합니다.

   CloudFormation CLI는 특정 후크 호출 지점에 해당하는 빈 핸들러 함수를 생성합니다. 이러한 핸들러에 자체 로직을 추가하여 대상 수명 주기의 각 단계에서 후크 호출 중에 발생하는 상황을 제어합니다. 자세한 내용은 [사용자 지정 CloudFormation 후크 모델링](hooks-model.md) 단원을 참조하십시오.

1. **등록**

   후크를 등록하려면 프라이빗 또는 퍼블릭 타사 익스텐션으로 등록할 후크를 제출합니다. `[submit](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-cli-submit.html)` 작업에 후크를 등록합니다. 자세한 내용은 [에 사용자 지정 후크 등록 CloudFormation](registering-hooks.md) 단원을 참조하십시오.

   후크 등록과 관련된 작업은 다음과 같습니다.

   1. *게시* - 후크가 레지스트리에 게시됩니다.

   1. *구성* - 후크는 유형 구성이 스택에 대해 호출할 때 구성됩니다.
**참고**  
후크는 30초 후에 시간 초과되고 최대 3회 재시도됩니다. 자세한 내용은 [제한 시간 및 재시도 제한](hooks-concepts.md#hook-timeout-and-retry-limits) 단원을 참조하십시오.

**Topics**
+ [사전 조건](hooks-prerequisites.md)
+ [후크 프로젝트 시작](hooks-init.md)
+ [후크 모델링](hooks-model.md)
+ [후크 등록](registering-hooks.md)
+ [후크 테스트](testing-hooks.md)
+ [후크 업데이트](updating-registered-hook.md)
+ [후크 등록 취소](deregistering-hooks.md)
+ [후크 게시](hooks-publishing.md)
+ [스키마 구문](hooks-schema.md)

# 사용자 지정 CloudFormation 후크 개발을 위한 사전 조건
<a name="hooks-prerequisites"></a>

Java 또는 Python을 사용하여 사용자 지정 후크를 개발할 수 있습니다. 다음은 사용자 지정 후크를 개발하기 위한 사전 조건입니다.

**Java 사전 조건**
+ [Apache Maven](https://maven.apache.org/install.html)
+ [JDK 17](https://www.oracle.com/java/technologies/downloads/#java17)
**참고**  
[CloudFormation 명령줄 인터페이스(CLI)](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/what-is-cloudformation-cli.html)를 사용하여 Java용 후크 프로젝트를 시작하려면 Python 3.8 이상도 설치해야 합니다. CloudFormation CLI용 Java 플러그인은 Python으로 분산된 `pip` (Python의 패키지 관리자)를 통해 설치할 수 있습니다.

Java Hooks 프로젝트에 대한 후크 핸들러를 구현하려면 [Java Hook 핸들러 예제 파일을](samples/java-handlers.zip) 다운로드하면 됩니다.

**Python 사전 조건**
+ [Python 버전 3.8](https://www.python.org/downloads/) 이상.

Python Hooks 프로젝트에 대한 후크 핸들러를 구현하려면 [Python Hook 핸들러 예제 파일을](samples/python-handlers.zip) 다운로드하면 됩니다.

## 후크 개발 권한
<a name="hooks-development-permissions"></a>

CloudFormation `Create`, `Update`및 `Delete` 스택 권한 외에도 다음 AWS CloudFormation 작업에 대한 액세스 권한이 필요합니다. 이러한 작업에 대한 액세스는 IAM 역할의 CloudFormation 정책을 통해 관리됩니다.
+ [https://docs.aws.amazon.com/cli/latest/reference/cloudformation/register-type.html](https://docs.aws.amazon.com/cli/latest/reference/cloudformation/register-type.html)
+ [https://docs.aws.amazon.com/cli/latest/reference/cloudformation/list-types.html](https://docs.aws.amazon.com/cli/latest/reference/cloudformation/list-types.html)
+ [https://docs.aws.amazon.com/cli/latest/reference/cloudformation/deregister-type.html](https://docs.aws.amazon.com/cli/latest/reference/cloudformation/deregister-type.html)
+ [https://docs.aws.amazon.com/cli/latest/reference/cloudformation/set-type-configuration.html](https://docs.aws.amazon.com/cli/latest/reference/cloudformation/set-type-configuration.html)

자세한 내용은 [CloudFormation 후크에 대한 IAM 권한 부여](grant-iam-permissions-for-hooks.md) 단원을 참조하십시오.

## 후크에 대한 개발 환경 설정
<a name="hooks-environment"></a>

후크를 개발하려면 [CloudFormation 템플릿](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-guide.html)과 Python 또는 Java에 익숙해야 합니다.

 

**CloudFormation CLI 및 관련 플러그인을 설치하려면:**

1. Python 패키지 관리자`pip`인를 사용하여 CloudFormation CLI를 설치합니다.

   ```
   pip3 install cloudformation-cli
   ```

1. CloudFormation CLI용 Python 또는 Java 플러그인을 설치합니다.

------
#### [ Python ]

   ```
   pip3 install cloudformation-cli-python-plugin
   ```

------
#### [ Java ]

   ```
   pip3 install cloudformation-cli-java-plugin
   ```

------

CloudFormation CLI와 플러그인을 업그레이드하려면 업그레이드 옵션을 사용할 수 있습니다.

------
#### [ Python ]

```
pip3 install --upgrade cloudformation-cli cloudformation-cli-python-plugin
```

------
#### [ Java ]

```
pip3 install --upgrade cloudformation-cli cloudformation-cli-java-plugin
```

------

# 사용자 지정 CloudFormation 후크 프로젝트 시작
<a name="hooks-init"></a>

사용자 지정 후크 프로젝트를 생성하는 첫 번째 단계는 프로젝트를 시작하는 것입니다. CloudFormation CLI `init` 명령을 사용하여 사용자 지정 후크 프로젝트를 시작할 수 있습니다.

`init` 명령은 후크 스키마 파일을 포함하여 프로젝트 설정을 안내하는 마법사를 시작합니다. 이 스키마 파일을 후크의 셰이프와 의미 체계를 정의하기 위한 시작점으로 사용합니다. 자세한 내용은 [스키마 구문](hooks-schema.md) 단원을 참조하십시오.

**후크 프로젝트를 시작하려면:**

1. 프로젝트의 디렉터리를 생성합니다.

   ```
   mkdir ~/mycompany-testing-mytesthook
   ```

1. 새 디렉터리로 이동합니다.

   ```
   cd ~/mycompany-testing-mytesthook
   ```

1. CloudFormation CLI `init` 명령을 사용하여 프로젝트를 시작합니다.

   ```
   cfn init
   ```

   명령은 다음 출력을 반환합니다.

   ```
   Initializing new project
   ```

1. `init` 명령은 프로젝트 설정을 안내하는 마법사를 시작합니다. 메시지가 표시되면 `h`를 입력하여 후크 프로젝트를 지정합니다.

   ```
   Do you want to develop a new resource(r) a module(m) or a hook(h)?
   ```

   ```
   h
   ```

1. 후크 유형의 이름을 입력합니다.

   ```
   What's the name of your hook type?
   (Organization::Service::Hook)
   ```

   ```
   MyCompany::Testing::MyTestHook
   ```

1. 언어 플러그인이 하나만 설치된 경우 기본적으로 선택됩니다. 언어 플러그인이 두 개 이상 설치된 경우 원하는 언어를 선택할 수 있습니다. 원하는 언어의 숫자 선택을 입력합니다.

   ```
   Select a language for code generation:
   [1] java
   [2] python38
   [3] python39
   (enter an integer):
   ```

1. 선택한 개발 언어를 기반으로 패키징을 설정합니다.

------
#### [ Python ]

   (*선택 사항*) 플랫폼과 독립적인 패키징을 위해 Docker를 선택합니다. Docker는 필요하지 않지만 더 쉽게 패키징할 수 있도록 하는 것이 좋습니다.

   ```
   Use docker for platform-independent packaging (Y/n)?
   This is highly recommended unless you are experienced with cross-platform Python packaging.
   ```

------
#### [ Java ]

   Java 패키지 이름을 설정하고 코드 생성 모델을 선택합니다. 기본 패키지 이름을 사용하거나 새 패키지 이름을 생성할 수 있습니다.

   ```
   Enter a package name (empty for default 'com.mycompany.testing.mytesthook'):
   ```

   ```
   Choose codegen model - 1 (default) or 2 (guided-aws):
   ```

------

*결과*: 프로젝트를 성공적으로 시작하고 후크를 개발하는 데 필요한 파일을 생성했습니다. 다음은 Python 3.8용 후크 프로젝트를 구성하는 디렉터리 및 파일의 예입니다.

```
mycompany-testing-mytesthook.json
rpdk.log
README.md
requirements.txt
hook-role.yaml
template.yml
docs
    README.md
src
    __init__.py
    handlers.py
    models.py
    target_models
        aws_s3_bucket.py
```

**참고**  
`src` 디렉터리의 파일은 언어 선택에 따라 생성됩니다. 생성된 파일에 몇 가지 유용한 설명과 예제가 있습니다. 와 같은 일부 파일은 핸들러에 대한 런타임 코드를 추가하기 위해 `generate` 명령을 실행할 때 이후 단계에서 `models.py`자동으로 업데이트됩니다.

# 사용자 지정 CloudFormation 후크 모델링
<a name="hooks-model"></a>

사용자 지정 CloudFormation 후크 모델링에는 후크, 속성 및 속성을 정의하는 스키마를 생성하는 작업이 포함됩니다. `cfn init` 명령을 사용하여 사용자 지정 후크 프로젝트를 생성하면 예제 후크 스키마가 JSON 형식의 텍스트 파일 로 생성됩니다`hook-name.json`.

대상 호출 지점 및 대상 작업은 후크가 호출되는 정확한 지점을 지정합니다. *후크 핸들러*는 이러한 지점에 대한 실행 파일 사용자 지정 로직을 호스팅합니다. 예를 들어 `CREATE` 작업의 대상 작업은 `preCreate` 핸들러를 사용합니다. 핸들러에 작성된 코드는 후크 대상 및 서비스가 일치하는 작업을 수행할 때 호출됩니다. *후크 대상*은 후크가 호출되는 대상입니다. CloudFormation 퍼블릭 리소스, 프라이빗 리소스 또는 사용자 지정 리소스와 같은 대상을 지정할 수 있습니다. 후크는 무제한의 후크 대상을 지원합니다.

스키마에는 후크에 필요한 권한이 포함되어 있습니다. 후크를 작성하려면 각 후크 핸들러에 대한 권한을 지정해야 합니다. CloudFormation은 작성자에게 *최소 권한을* 부여하거나 작업을 수행하는 데 필요한 권한만 부여하는 표준 보안 조언을 따르는 정책을 작성하도록 권장합니다. 사용자(및 역할)가 수행해야 할 작업을 결정한 다음 후크 작업에 대해 해당 작업*만* 수행하도록 허용하는 정책을 생성합니다. CloudFormation은 이러한 권한을 사용하여 후크 사용자가 제공한 권한을 범위 축소합니다. 이러한 권한은 후크로 전달됩니다. 후크 핸들러는 이러한 권한을 사용하여 AWS 리소스에 액세스합니다.

다음 스키마 파일을 시작점으로 사용하여 후크를 정의할 수 있습니다. 후크 스키마를 사용하여 구현할 핸들러를 지정합니다. 특정 핸들러를 구현하지 않도록 선택한 경우 후크 스키마의 핸들러 섹션에서 제거합니다. 스키마에 대한 자세한 내용은 섹션을 참조하세요[스키마 구문](hooks-schema.md).

```
{
    "typeName":"MyCompany::Testing::MyTestHook",
    "description":"Verifies S3 bucket and SQS queues properties before create and update",
    "sourceUrl":"https://mycorp.com/my-repo.git",
    "documentationUrl":"https://mycorp.com/documentation",
    "typeConfiguration":{
        "properties":{
            "minBuckets":{
                "description":"Minimum number of compliant buckets",
                "type":"string"
            },
            "minQueues":{
                "description":"Minimum number of compliant queues",
                "type":"string"
            },
            "encryptionAlgorithm":{
                "description":"Encryption algorithm for SSE",
                "default":"AES256",
                "type":"string"
            }
        },
        "required":[
            
        ],
        "additionalProperties":false
    },
    "handlers":{
        "preCreate":{
            "targetNames":[
                "AWS::S3::Bucket",
                "AWS::SQS::Queue"
            ],
            "permissions":[
                
            ]
        },
        "preUpdate":{
            "targetNames":[
                "AWS::S3::Bucket",
                "AWS::SQS::Queue"
            ],
            "permissions":[
                
            ]
        },
        "preDelete":{
            "targetNames":[
                "AWS::S3::Bucket",
                "AWS::SQS::Queue"
            ],
            "permissions":[
                "s3:ListBucket",
                "s3:ListAllMyBuckets",
                "s3:GetEncryptionConfiguration",
                "sqs:ListQueues",
                "sqs:GetQueueAttributes",
                "sqs:GetQueueUrl"
            ]
        }
    },
    "additionalProperties":false
}
```

**Topics**
+ [Java를 사용하여 사용자 지정 CloudFormation 후크 모델링](hooks-model-java.md)
+ [Python을 사용하여 사용자 지정 CloudFormation 후크 모델링](hooks-model-python.md)

# Java를 사용하여 사용자 지정 CloudFormation 후크 모델링
<a name="hooks-model-java"></a>

사용자 지정 CloudFormation 후크 모델링에는 후크, 해당 속성 및 속성을 정의하는 스키마를 생성하는 작업이 포함됩니다. 이 자습서에서는 Java를 사용하여 사용자 지정 후크를 모델링하는 방법을 안내합니다.

## 1단계: 프로젝트 종속성 추가
<a name="model-hook-project-dependencies"></a>

Java 기반 후크 프로젝트는 Maven의 `pom.xml` 파일을 종속성으로 사용합니다. 다음 섹션을 확장하고 소스 코드를 프로젝트 루트의 `pom.xml` 파일에 복사합니다.

### 후크 프로젝트 종속성(pom.xml)
<a name="hook-java-dependencies"></a>

```
<?xml version="1.0" encoding="UTF-8"?>
<project
    xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.mycompany.testing.mytesthook</groupId>
    <artifactId>mycompany-testing-mytesthook-handler</artifactId>
    <name>mycompany-testing-mytesthook-handler</name>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <aws.java.sdk.version>2.16.1</aws.java.sdk.version>
        <checkstyle.version>8.36.2</checkstyle.version>
        <commons-io.version>2.8.0</commons-io.version>
        <jackson.version>2.11.3</jackson.version>
        <maven-checkstyle-plugin.version>3.1.1</maven-checkstyle-plugin.version>
        <mockito.version>3.6.0</mockito.version>
        <spotbugs.version>4.1.4</spotbugs.version>
        <spotless.version>2.5.0</spotless.version>
        <maven-javadoc-plugin.version>3.2.0</maven-javadoc-plugin.version>
        <maven-source-plugin.version>3.2.1</maven-source-plugin.version>
        <cfn.generate.args/>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>software.amazon.awssdk</groupId>
                <artifactId>bom</artifactId>
                <version>2.16.1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/software.amazon.cloudformation/aws-cloudformation-rpdk-java-plugin -->
        <dependency>
            <groupId>software.amazon.cloudformation</groupId>
            <artifactId>aws-cloudformation-rpdk-java-plugin</artifactId>
            <version>[2.0.0,3.0.0)</version>
        </dependency>

        <!-- AWS Java SDK v2 Dependencies -->
        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>sdk-core</artifactId>
        </dependency>
        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>cloudformation</artifactId>
        </dependency>
        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>s3</artifactId>
        </dependency>
        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>utils</artifactId>
        </dependency>
        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>apache-client</artifactId>
        </dependency>
        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>sqs</artifactId>
        </dependency>

        <!-- Test dependency for Java Providers -->
        <dependency>
            <groupId>software.amazon.cloudformation</groupId>
            <artifactId>cloudformation-cli-java-plugin-testing-support</artifactId>
            <version>1.0.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-s3 -->
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk-s3</artifactId>
            <version>1.12.85</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>${commons-io.version}</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.9</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
            <version>4.4</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>29.0-jre</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-cloudformation -->
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk-cloudformation</artifactId>
            <version>1.11.555</version>
            <scope>test</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.14</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/software.amazon.cloudformation/aws-cloudformation-resource-schema -->
        <dependency>
            <groupId>software.amazon.cloudformation</groupId>
            <artifactId>aws-cloudformation-resource-schema</artifactId>
            <version>[2.0.5, 3.0.0)</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-databind -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-cbor -->
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-cbor</artifactId>
            <version>${jackson.version}</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>${jackson.version}</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.module/jackson-modules-java8 -->
        <dependency>
            <groupId>com.fasterxml.jackson.module</groupId>
            <artifactId>jackson-modules-java8</artifactId>
            <version>${jackson.version}</version>
            <type>pom</type>
            <scope>runtime</scope>
        </dependency>

         <!-- https://mvnrepository.com/artifact/org.json/json -->
        <dependency>
            <groupId>org.json</groupId>
            <artifactId>json</artifactId>
            <version>20180813</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-core -->
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk-core</artifactId>
            <version>1.11.1034</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.amazonaws/aws-lambda-java-core -->
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-core</artifactId>
            <version>1.2.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.amazonaws/aws-lambda-java-log4j2 -->
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-log4j2</artifactId>
            <version>1.2.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.8</version>
        </dependency>


        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.4</version>
            <scope>provided</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.17.1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.17.1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.17.1</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.assertj/assertj-core -->
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.12.2</version>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.5.0-M1</version>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>3.6.0</version>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter -->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-junit-jupiter</artifactId>
            <version>3.6.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <compilerArgs>
                        <arg>-Xlint:all,-options,-processing</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>2.3</version>
                <configuration>
                    <createDependencyReducedPom>false</createDependencyReducedPom>
                    <filters>
                        <filter>
                            <artifact>*:*</artifact>
                            <excludes>
                                <exclude>**/Log4j2Plugins.dat</exclude>
                            </excludes>
                        </filter>
                    </filters>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.6.0</version>
                <executions>
                    <execution>
                        <id>generate</id>
                        <phase>generate-sources</phase>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                        <configuration>
                            <executable>cfn</executable>
                            <commandlineArgs>generate ${cfn.generate.args}</commandlineArgs>
                            <workingDirectory>${project.basedir}</workingDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>build-helper-maven-plugin</artifactId>
                <version>3.0.0</version>
                <executions>
                    <execution>
                        <id>add-source</id>
                        <phase>generate-sources</phase>
                        <goals>
                            <goal>add-source</goal>
                        </goals>
                        <configuration>
                            <sources>
                                <source>${project.basedir}/target/generated-sources/rpdk</source>
                            </sources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>2.4</version>
            </plugin>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M3</version>
            </plugin>
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.8.4</version>
                <configuration>
                    <excludes>
                        <exclude>**/BaseHookConfiguration*</exclude>
                        <exclude>**/BaseHookHandler*</exclude>
                        <exclude>**/HookHandlerWrapper*</exclude>
                        <exclude>**/ResourceModel*</exclude>
                        <exclude>**/TypeConfigurationModel*</exclude>
                        <exclude>**/model/**/*</exclude>
                    </excludes>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>report</id>
                        <phase>test</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>jacoco-check</id>
                        <goals>
                            <goal>check</goal>
                        </goals>
                        <configuration>
                            <rules>
                                <rule>
                                    <element>PACKAGE</element>
                                    <limits>
                                        <limit>
                                            <counter>BRANCH</counter>
                                            <value>COVEREDRATIO</value>
                                            <minimum>0.8</minimum>
                                        </limit>
                                        <limit>
                                            <counter>INSTRUCTION</counter>
                                            <value>COVEREDRATIO</value>
                                            <minimum>0.8</minimum>
                                        </limit>
                                    </limits>
                                </rule>
                            </rules>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>${project.basedir}</directory>
                <includes>
                    <include>mycompany-testing-mytesthook.json</include>
                </includes>
            </resource>
            <resource>
                <directory>${project.basedir}/target/loaded-target-schemas</directory>
                <includes>
                    <include>**/*.json</include>
                </includes>
            </resource>
        </resources>
    </build>
</project>
```

## 2단계: 후크 프로젝트 패키지 생성
<a name="model-hook-project-package-java"></a>

후크 프로젝트 패키지를 생성합니다. CloudFormation CLI는 후크 사양에 정의된 대상 수명 주기의 특정 후크 작업에 해당하는 빈 핸들러 함수를 생성합니다.

```
cfn generate
```

명령은 다음 출력을 반환합니다.

```
Generated files for MyCompany::Testing::MyTestHook
```

**참고**  
더 이상 사용되지 않는 버전을 사용하지 않도록 Lambda 런타임이 up-to-date 상태인지 확인합니다. 자세한 내용은 [리소스 유형 및 후크에 대한 Lambda 런타임 업데이트를 참조하세요](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/runtime-update.html).

## 3단계: 후크 핸들러 추가
<a name="model-hook-project-add-handler-java"></a>

구현하도록 선택한 핸들러에 자체 후크 핸들러 런타임 코드를 추가합니다. 예를 들어 로깅을 위해 다음 코드를 추가할 수 있습니다.

```
logger.log("Internal testing Hook triggered for target: " + request.getHookContext().getTargetName());
```

CloudFormation CLI는 일반 구형 Java 객체(Java POJO)를 생성합니다. 다음은에서 생성된 출력 예제입니다`AWS::S3::Bucket`.

**Example AwsS3BucketTargetModel.java**  

```
package com.mycompany.testing.mytesthook.model.aws.s3.bucket;

import...


@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE)
public class AwsS3BucketTargetModel extends ResourceHookTargetModel<AwsS3Bucket> {

    @JsonIgnore
    private static final TypeReference<AwsS3Bucket> TARGET_REFERENCE =
        new TypeReference<AwsS3Bucket>() {};

    @JsonIgnore
    private static final TypeReference<AwsS3BucketTargetModel> MODEL_REFERENCE =
        new TypeReference<AwsS3BucketTargetModel>() {};

    @JsonIgnore
    public static final String TARGET_TYPE_NAME = "AWS::S3::Bucket";


    @JsonIgnore
    public TypeReference<AwsS3Bucket> getHookTargetTypeReference() {
        return TARGET_REFERENCE;
    }

    @JsonIgnore
    public TypeReference<AwsS3BucketTargetModel> getTargetModelTypeReference() {
        return MODEL_REFERENCE;
    }
}
```

**Example AwsS3Bucket.java**  

```
package com.mycompany.testing.mytesthook.model.aws.s3.bucket;

import ...


@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE)
public class AwsS3Bucket extends ResourceHookTarget {
    @JsonIgnore
    public static final String TYPE_NAME = "AWS::S3::Bucket";

    @JsonIgnore
    public static final String IDENTIFIER_KEY_ID = "/properties/Id";

    @JsonProperty("InventoryConfigurations")
    private List<InventoryConfiguration> inventoryConfigurations;

    @JsonProperty("WebsiteConfiguration")
    private WebsiteConfiguration websiteConfiguration;

    @JsonProperty("DualStackDomainName")
    private String dualStackDomainName;

    @JsonProperty("AccessControl")
    private String accessControl;

    @JsonProperty("AnalyticsConfigurations")
    private List<AnalyticsConfiguration> analyticsConfigurations;

    @JsonProperty("AccelerateConfiguration")
    private AccelerateConfiguration accelerateConfiguration;

    @JsonProperty("PublicAccessBlockConfiguration")
    private PublicAccessBlockConfiguration publicAccessBlockConfiguration;

    @JsonProperty("BucketName")
    private String bucketName;

    @JsonProperty("RegionalDomainName")
    private String regionalDomainName;

    @JsonProperty("OwnershipControls")
    private OwnershipControls ownershipControls;

    @JsonProperty("ObjectLockConfiguration")
    private ObjectLockConfiguration objectLockConfiguration;

    @JsonProperty("ObjectLockEnabled")
    private Boolean objectLockEnabled;

    @JsonProperty("LoggingConfiguration")
    private LoggingConfiguration loggingConfiguration;

    @JsonProperty("ReplicationConfiguration")
    private ReplicationConfiguration replicationConfiguration;

    @JsonProperty("Tags")
    private List<Tag> tags;

    @JsonProperty("DomainName")
    private String domainName;

    @JsonProperty("BucketEncryption")
    private BucketEncryption bucketEncryption;

    @JsonProperty("WebsiteURL")
    private String websiteURL;

    @JsonProperty("NotificationConfiguration")
    private NotificationConfiguration notificationConfiguration;

    @JsonProperty("LifecycleConfiguration")
    private LifecycleConfiguration lifecycleConfiguration;

    @JsonProperty("VersioningConfiguration")
    private VersioningConfiguration versioningConfiguration;

    @JsonProperty("MetricsConfigurations")
    private List<MetricsConfiguration> metricsConfigurations;

    @JsonProperty("IntelligentTieringConfigurations")
    private List<IntelligentTieringConfiguration> intelligentTieringConfigurations;

    @JsonProperty("CorsConfiguration")
    private CorsConfiguration corsConfiguration;

    @JsonProperty("Id")
    private String id;

    @JsonProperty("Arn")
    private String arn;

    @JsonIgnore
    public JSONObject getPrimaryIdentifier() {
        final JSONObject identifier = new JSONObject();
        if (this.getId() != null) {
            identifier.put(IDENTIFIER_KEY_ID, this.getId());
        }

        // only return the identifier if it can be used, i.e. if all components are present
        return identifier.length() == 1 ? identifier : null;
    }

    @JsonIgnore
    public List<JSONObject> getAdditionalIdentifiers() {
        final List<JSONObject> identifiers = new ArrayList<JSONObject>();
        // only return the identifiers if any can be used
        return identifiers.isEmpty() ? null : identifiers;
    }
}
```

**Example BucketEncryption.java**  

```
package software.amazon.testing.mytesthook.model.aws.s3.bucket;

import ...


@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE)
public class BucketEncryption {
    @JsonProperty("ServerSideEncryptionConfiguration")
    private List<ServerSideEncryptionRule> serverSideEncryptionConfiguration;

}
```

**Example ServerSideEncryptionRule.java**  

```
package com.mycompany.testing.mytesthook.model.aws.s3.bucket;

import ...


@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE)
public class ServerSideEncryptionRule {
    @JsonProperty("BucketKeyEnabled")
    private Boolean bucketKeyEnabled;

    @JsonProperty("ServerSideEncryptionByDefault")
    private ServerSideEncryptionByDefault serverSideEncryptionByDefault;

}
```

**Example ServerSideEncryptionByDefault.java**  

```
package com.mycompany.testing.mytesthook.model.aws.s3.bucket;

import ...


@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE)
public class ServerSideEncryptionByDefault {
    @JsonProperty("SSEAlgorithm")
    private String sSEAlgorithm;

    @JsonProperty("KMSMasterKeyID")
    private String kMSMasterKeyID;

}
```

POJOs 생성되면 이제 후크의 기능을 실제로 구현하는 핸들러를 작성할 수 있습니다. 이 예제에서는 핸들러에 대한 `preCreate` 및 `preUpdate` 호출 지점을 구현합니다.

## 4단계: 후크 핸들러 구현
<a name="model-hook-project-code-handler-java"></a>

**Topics**
+ [API 클라이언트 빌더 코딩](#java-code-api-client-builder)
+ [API 요청 생성자 코딩](#code-the-api-request-maker)
+ [헬퍼 코드 구현](#implement-helper-code)
+ [기본 핸들러 구현](#implement-base-hook-handler)
+ [`preCreate` 핸들러 구현](#implementing-precreate-handler)
+ [`preCreate` 핸들러 코딩](#coding-the-precreate-handler)
+ [`preCreate` 테스트 업데이트](#updating-the-precreate-handler)
+ [`preUpdate` 핸들러 구현](#implementing-preupdate-handler)
+ [`preUpdate` 핸들러 코딩](#coding-preupdate-handler)
+ [`preUpdate` 테스트 업데이트](#update-the-preupdate-handler)
+ [`preDelete` 핸들러 구현](#implement-the-predelete-hander)
+ [`preDelete` 핸들러 코딩](#code-the-predelete-handler)
+ [`preDelete` 핸들러 업데이트](#update-the-predelete-handler)

### API 클라이언트 빌더 코딩
<a name="java-code-api-client-builder"></a>

1. IDE에서 `src/main/java/com/mycompany/testing/mytesthook` 폴더에 있는 `ClientBuilder.java` 파일을 엽니다.

1. `ClientBuilder.java` 파일의 전체 내용을 다음 코드로 바꿉니다.  
**Example ClientBuilder.java**  

   ```
   package com.awscommunity.kms.encryptionsettings;
   
   import software.amazon.awssdk.services.ec2.Ec2Client;
   import software.amazon.cloudformation.HookLambdaWrapper;
   
   /**
    * Describes static HTTP clients (to consume less memory) for API calls that
    * this hook makes to a number of AWS services.
    */
   public final class ClientBuilder {
   
       private ClientBuilder() {
       }
   
       /**
        * Create an HTTP client for Amazon EC2.
        *
        * @return Ec2Client An {@link Ec2Client} object.
        */
       public static Ec2Client getEc2Client() {
           return Ec2Client.builder().httpClient(HookLambdaWrapper.HTTP_CLIENT).build();
       }
   }
   ```

### API 요청 생성자 코딩
<a name="code-the-api-request-maker"></a>

1. IDE에서 `src/main/java/com/mycompany/testing/mytesthook` 폴더에 있는 `Translator.java` 파일을 엽니다.

1. `Translator.java` 파일의 전체 내용을 다음 코드로 바꿉니다.  
**Example Translator.java**  

   ```
   package com.mycompany.testing.mytesthook;
   
   
   import software.amazon.awssdk.services.s3.model.GetBucketEncryptionRequest;
   import software.amazon.awssdk.services.s3.model.ListBucketsRequest;
   import software.amazon.awssdk.services.sqs.model.ListQueuesRequest;
   import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel;
   
   /**
    * This class is a centralized placeholder for
    *  - api request construction
    *  - object translation to/from aws sdk
    */
   
   public class Translator {
   
       static ListBucketsRequest translateToListBucketsRequest(final HookTargetModel targetModel) {
           return ListBucketsRequest.builder().build();
       }
   
       static ListQueuesRequest translateToListQueuesRequest(final String nextToken) {
           return ListQueuesRequest.builder().nextToken(nextToken).build();
       }
   
       static ListBucketsRequest createListBucketsRequest() {
           return ListBucketsRequest.builder().build();
       }
   
       static ListQueuesRequest createListQueuesRequest() {
           return createListQueuesRequest(null);
       }
   
       static ListQueuesRequest createListQueuesRequest(final String nextToken) {
           return ListQueuesRequest.builder().nextToken(nextToken).build();
       }
   
       static GetBucketEncryptionRequest createGetBucketEncryptionRequest(final String bucket) {
           return GetBucketEncryptionRequest.builder().bucket(bucket).build();
       }
   }
   ```

### 헬퍼 코드 구현
<a name="implement-helper-code"></a>

1. IDE에서 `src/main/java/com/mycompany/testing/mytesthook` 폴더에 있는 `AbstractTestBase.java` 파일을 엽니다.

1. `AbstractTestBase.java` 파일의 전체 내용을 다음 코드로 바꿉니다.  
**Example Translator.java**  

   ```
   package com.mycompany.testing.mytesthook;
   
   import com.google.common.collect.ImmutableMap;
   import org.mockito.Mockito;
   import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
   import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
   import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
   import software.amazon.awssdk.awscore.AwsRequest;
   import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration;
   import software.amazon.awssdk.awscore.AwsResponse;
   import software.amazon.awssdk.core.SdkClient;
   import software.amazon.awssdk.core.pagination.sync.SdkIterable;
   import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
   import software.amazon.cloudformation.proxy.Credentials;
   import software.amazon.cloudformation.proxy.LoggerProxy;
   import software.amazon.cloudformation.proxy.OperationStatus;
   import software.amazon.cloudformation.proxy.ProgressEvent;
   import software.amazon.cloudformation.proxy.ProxyClient;
   import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel;
   
   import javax.annotation.Nonnull;
   import java.time.Duration;
   import java.util.concurrent.CompletableFuture;
   import java.util.function.Function;
   import java.util.function.Supplier;
   
   import static org.assertj.core.api.Assertions.assertThat;
   
   @lombok.Getter
   public class AbstractTestBase {
       protected final AwsSessionCredentials awsSessionCredential;
       protected final AwsCredentialsProvider v2CredentialsProvider;
       protected final AwsRequestOverrideConfiguration configuration;
       protected final LoggerProxy loggerProxy;
       protected final Supplier<Long> awsLambdaRuntime = () -> Duration.ofMinutes(15).toMillis();
       protected final AmazonWebServicesClientProxy proxy;
       protected final Credentials mockCredentials =
           new Credentials("mockAccessId", "mockSecretKey", "mockSessionToken");
   
       @lombok.Setter
       private SdkClient serviceClient;
   
       protected AbstractTestBase() {
           loggerProxy = Mockito.mock(LoggerProxy.class);
           awsSessionCredential = AwsSessionCredentials.create(mockCredentials.getAccessKeyId(),
               mockCredentials.getSecretAccessKey(), mockCredentials.getSessionToken());
           v2CredentialsProvider = StaticCredentialsProvider.create(awsSessionCredential);
           configuration = AwsRequestOverrideConfiguration.builder()
               .credentialsProvider(v2CredentialsProvider)
               .build();
           proxy = new AmazonWebServicesClientProxy(
               loggerProxy,
               mockCredentials,
               awsLambdaRuntime
           ) {
               @Override
               public <ClientT> ProxyClient<ClientT> newProxy(@Nonnull Supplier<ClientT> client) {
                   return new ProxyClient<ClientT>() {
                       @Override
                       public <RequestT extends AwsRequest, ResponseT extends AwsResponse>
                           ResponseT injectCredentialsAndInvokeV2(RequestT request,
                                                                  Function<RequestT, ResponseT> requestFunction) {
                           return proxy.injectCredentialsAndInvokeV2(request, requestFunction);
                       }
   
                       @Override
                       public <RequestT extends AwsRequest, ResponseT extends AwsResponse> CompletableFuture<ResponseT>
                           injectCredentialsAndInvokeV2Async(RequestT request, Function<RequestT, CompletableFuture<ResponseT>> requestFunction) {
                           return proxy.injectCredentialsAndInvokeV2Async(request, requestFunction);
                       }
   
                       @Override
                       public <RequestT extends AwsRequest, ResponseT extends AwsResponse, IterableT extends SdkIterable<ResponseT>>
                           IterableT
                           injectCredentialsAndInvokeIterableV2(RequestT request, Function<RequestT, IterableT> requestFunction) {
                           return proxy.injectCredentialsAndInvokeIterableV2(request, requestFunction);
                       }
   
                       @SuppressWarnings("unchecked")
                       @Override
                       public ClientT client() {
                           return (ClientT) serviceClient;
                       }
                   };
               }
           };
       }
   
       protected void assertResponse(final ProgressEvent<HookTargetModel, CallbackContext> response, final OperationStatus expectedStatus, final String expectedMsg) {
           assertThat(response).isNotNull();
           assertThat(response.getStatus()).isEqualTo(expectedStatus);
           assertThat(response.getCallbackContext()).isNull();
           assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
           assertThat(response.getMessage()).isNotNull();
           assertThat(response.getMessage()).isEqualTo(expectedMsg);
       }
   
       protected HookTargetModel createHookTargetModel(final Object resourceProperties) {
           return HookTargetModel.of(ImmutableMap.of("ResourceProperties", resourceProperties));
       }
       
       protected HookTargetModel createHookTargetModel(final Object resourceProperties, final Object previousResourceProperties) {
           return HookTargetModel.of(
                   ImmutableMap.of(
                       "ResourceProperties", resourceProperties,
                       "PreviousResourceProperties", previousResourceProperties
                   )
           );
       }
   }
   ```

### 기본 핸들러 구현
<a name="implement-base-hook-handler"></a>

1. IDE에서 `src/main/java/com/mycompany/testing/mytesthook` 폴더에 있는 `BaseHookHandlerStd.java` 파일을 엽니다.

1. `BaseHookHandlerStd.java` 파일의 전체 내용을 다음 코드로 바꿉니다.  
**Example Translator.java**  

   ```
   package com.mycompany.testing.mytesthook;
   
   import com.mycompany.testing.mytesthook.model.aws.s3.bucket.AwsS3Bucket;
   import com.mycompany.testing.mytesthook.model.aws.sqs.queue.AwsSqsQueue;
   import software.amazon.awssdk.services.s3.S3Client;
   import software.amazon.awssdk.services.sqs.SqsClient;
   import software.amazon.cloudformation.exceptions.UnsupportedTargetException;
   import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
   import software.amazon.cloudformation.proxy.Logger;
   import software.amazon.cloudformation.proxy.ProgressEvent;
   import software.amazon.cloudformation.proxy.ProxyClient;
   import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
   import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel;
   
   
   public abstract class BaseHookHandlerStd extends BaseHookHandler<CallbackContext, TypeConfigurationModel> {
       public static final String HOOK_TYPE_NAME = "MyCompany::Testing::MyTestHook";
   
       protected Logger logger;
   
       @Override
       public ProgressEvent<HookTargetModel, CallbackContext> handleRequest(
               final AmazonWebServicesClientProxy proxy,
               final HookHandlerRequest request,
               final CallbackContext callbackContext,
               final Logger logger,
               final TypeConfigurationModel typeConfiguration
       ) {
           this.logger = logger;
   
           final String targetName = request.getHookContext().getTargetName();
   
           final ProgressEvent<HookTargetModel, CallbackContext> result;
           if (AwsS3Bucket.TYPE_NAME.equals(targetName)) {
               result = handleS3BucketRequest(
                       proxy,
                       request,
                       callbackContext != null ? callbackContext : new CallbackContext(),
                       proxy.newProxy(ClientBuilder::createS3Client),
                       typeConfiguration
               );
           } else if (AwsSqsQueue.TYPE_NAME.equals(targetName)) {
               result = handleSqsQueueRequest(
                       proxy,
                       request,
                       callbackContext != null ? callbackContext : new CallbackContext(),
                       proxy.newProxy(ClientBuilder::createSqsClient),
                       typeConfiguration
               );
           } else {
               throw new UnsupportedTargetException(targetName);
           }
   
           log(
               String.format(
                   "Result for [%s] invocation for target [%s] returned status [%s] with message [%s]",
                   request.getHookContext().getInvocationPoint(),
                   targetName,
                   result.getStatus(),
                   result.getMessage()
               )
           );
   
           return result;
       }
   
       protected abstract ProgressEvent<HookTargetModel, CallbackContext> handleS3BucketRequest(
               final AmazonWebServicesClientProxy proxy,
               final HookHandlerRequest request,
               final CallbackContext callbackContext,
               final ProxyClient<S3Client> proxyClient,
               final TypeConfigurationModel typeConfiguration
       );
   
       protected abstract ProgressEvent<HookTargetModel, CallbackContext> handleSqsQueueRequest(
               final AmazonWebServicesClientProxy proxy,
               final HookHandlerRequest request,
               final CallbackContext callbackContext,
               final ProxyClient<SqsClient> proxyClient,
               final TypeConfigurationModel typeConfiguration
       );
   
       protected void log(final String message) {
           if (logger != null) {
               logger.log(message);
           } else {
               System.out.println(message);
           }
       }
   }
   ```

### `preCreate` 핸들러 구현
<a name="implementing-precreate-handler"></a>

`preCreate` 핸들러는 `AWS::S3::Bucket` 또는 `AWS::SQS::Queue` 리소스에 대한 서버 측 암호화 설정을 확인합니다.
+ `AWS::S3::Bucket` 리소스의 경우 후크는 다음과 같은 경우에만 전달합니다.
  + Amazon S3 버킷 암호화가 설정되어 있습니다.
  + 버킷에 대해 Amazon S3 버킷 키가 활성화됩니다.
  + Amazon S3 버킷에 설정된 암호화 알고리즘이 필요한 올바른 알고리즘입니다.
  +  AWS Key Management Service 키 ID가 설정됩니다.
+ `AWS::SQS::Queue` 리소스의 경우 후크는 다음과 같은 경우에만 전달합니다.
  +  AWS Key Management Service 키 ID가 설정됩니다.

### `preCreate` 핸들러 코딩
<a name="coding-the-precreate-handler"></a>

1. IDE에서 `src/main/java/software/mycompany/testing/mytesthook` 폴더에 있는 `PreCreateHookHandler.java` 파일을 엽니다.

1. `PreCreateHookHandler.java` 파일의 전체 내용을 다음 코드로 바꿉니다.

   ```
   package com.mycompany.testing.mytesthook;
   
   import com.mycompany.testing.mytesthook.model.aws.s3.bucket.AwsS3Bucket;
   import com.mycompany.testing.mytesthook.model.aws.s3.bucket.AwsS3BucketTargetModel;
   import com.mycompany.testing.mytesthook.model.aws.s3.bucket.BucketEncryption;
   import com.mycompany.testing.mytesthook.model.aws.s3.bucket.ServerSideEncryptionByDefault;
   import com.mycompany.testing.mytesthook.model.aws.s3.bucket.ServerSideEncryptionRule;
   import com.mycompany.testing.mytesthook.model.aws.sqs.queue.AwsSqsQueue;
   import com.mycompany.testing.mytesthook.model.aws.sqs.queue.AwsSqsQueueTargetModel;
   import org.apache.commons.collections.CollectionUtils;
   import org.apache.commons.lang3.StringUtils;
   import software.amazon.cloudformation.exceptions.UnsupportedTargetException;
   import software.amazon.cloudformation.proxy.HandlerErrorCode;
   import software.amazon.cloudformation.proxy.Logger;
   import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
   import software.amazon.cloudformation.proxy.hook.HookStatus;
   import software.amazon.cloudformation.proxy.hook.HookProgressEvent;
   import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
   import software.amazon.cloudformation.proxy.hook.targetmodel.ResourceHookTargetModel;
   
   import java.util.List;
   
   public class PreCreateHookHandler extends BaseHookHandler<TypeConfigurationModel, CallbackContext> {
   
       @Override
       public HookProgressEvent<CallbackContext> handleRequest(
           final AmazonWebServicesClientProxy proxy,
           final HookHandlerRequest request,
           final CallbackContext callbackContext,
           final Logger logger,
           final TypeConfigurationModel typeConfiguration) {
   
           final String targetName = request.getHookContext().getTargetName();
           if ("AWS::S3::Bucket".equals(targetName)) {
               final ResourceHookTargetModel<AwsS3Bucket> targetModel = request.getHookContext().getTargetModel(AwsS3BucketTargetModel.class);
   
               final AwsS3Bucket bucket = targetModel.getResourceProperties();
               final String encryptionAlgorithm = typeConfiguration.getEncryptionAlgorithm();
   
               return validateS3BucketEncryption(bucket, encryptionAlgorithm);
   
           } else if ("AWS::SQS::Queue".equals(targetName)) {
               final ResourceHookTargetModel<AwsSqsQueue> targetModel = request.getHookContext().getTargetModel(AwsSqsQueueTargetModel.class);
   
               final AwsSqsQueue queue = targetModel.getResourceProperties();
               return validateSQSQueueEncryption(queue);
           } else {
               throw new UnsupportedTargetException(targetName);
           }
       }
   
       private HookProgressEvent<CallbackContext> validateS3BucketEncryption(final AwsS3Bucket bucket, final String requiredEncryptionAlgorithm) {
           HookStatus resultStatus = null;
           String resultMessage = null;
   
           if (bucket != null) {
               final BucketEncryption bucketEncryption = bucket.getBucketEncryption();
               if (bucketEncryption != null) {
                   final List<ServerSideEncryptionRule> serverSideEncryptionRules = bucketEncryption.getServerSideEncryptionConfiguration();
                   if (CollectionUtils.isNotEmpty(serverSideEncryptionRules)) {
                       for (final ServerSideEncryptionRule rule : serverSideEncryptionRules) {
                           final Boolean bucketKeyEnabled = rule.getBucketKeyEnabled();
                           if (bucketKeyEnabled) {
                               final ServerSideEncryptionByDefault serverSideEncryptionByDefault = rule.getServerSideEncryptionByDefault();
   
                               final String encryptionAlgorithm = serverSideEncryptionByDefault.getSSEAlgorithm();
                               final String kmsKeyId = serverSideEncryptionByDefault.getKMSMasterKeyID(); // "KMSMasterKeyID" is name of the property for an AWS::S3::Bucket;
   
                               if (!StringUtils.equals(encryptionAlgorithm, requiredEncryptionAlgorithm) && StringUtils.isBlank(kmsKeyId)) {
                                   resultStatus = HookStatus.FAILED;
                                   resultMessage = "KMS Key ID not set and SSE Encryption Algorithm is incorrect for bucket with name: " + bucket.getBucketName();
                               } else if (!StringUtils.equals(encryptionAlgorithm, requiredEncryptionAlgorithm)) {
                                   resultStatus = HookStatus.FAILED;
                                   resultMessage = "SSE Encryption Algorithm is incorrect for bucket with name: " + bucket.getBucketName();
                               } else if (StringUtils.isBlank(kmsKeyId)) {
                                   resultStatus = HookStatus.FAILED;
                                   resultMessage = "KMS Key ID not set for bucket with name: " + bucket.getBucketName();
                               } else {
                                   resultStatus = HookStatus.SUCCESS;
                                   resultMessage = "Successfully invoked PreCreateHookHandler for target: AWS::S3::Bucket";
                               }
                           } else {
                               resultStatus = HookStatus.FAILED;
                               resultMessage = "Bucket key not enabled for bucket with name: " + bucket.getBucketName();
                           }
   
                           if (resultStatus == HookStatus.FAILED) {
                               break;
                           }
                       }
                   } else {
                       resultStatus = HookStatus.FAILED;
                       resultMessage = "No SSE Encryption configurations for bucket with name: " + bucket.getBucketName();
                   }
               } else {
                   resultStatus = HookStatus.FAILED;
                   resultMessage = "Bucket Encryption not enabled for bucket with name: " + bucket.getBucketName();
               }
           } else {
               resultStatus = HookStatus.FAILED;
               resultMessage = "Resource properties for S3 Bucket target model are empty";
           }
   
           return HookProgressEvent.<CallbackContext>builder()
                   .status(resultStatus)
                   .message(resultMessage)
                   .errorCode(resultStatus == HookStatus.FAILED ? HandlerErrorCode.ResourceConflict : null)
                   .build();
       }
   
       private HookProgressEvent<CallbackContext> validateSQSQueueEncryption(final AwsSqsQueue queue) {
           if (queue == null) {
               return HookProgressEvent.<CallbackContext>builder()
                       .status(HookStatus.FAILED)
                       .message("Resource properties for SQS Queue target model are empty")
                       .errorCode(HandlerErrorCode.ResourceConflict)
                       .build();
           }
   
           final String kmsKeyId = queue.getKmsMasterKeyId(); // "KmsMasterKeyId" is name of the property for an AWS::SQS::Queue
           if (StringUtils.isBlank(kmsKeyId)) {
               return HookProgressEvent.<CallbackContext>builder()
                       .status(HookStatus.FAILED)
                       .message("Server side encryption turned off for queue with name: " + queue.getQueueName())
                       .errorCode(HandlerErrorCode.ResourceConflict)
                       .build();
           }
   
           return HookProgressEvent.<CallbackContext>builder()
                       .status(HookStatus.SUCCESS)
                       .message("Successfully invoked PreCreateHookHandler for target: AWS::SQS::Queue")
                       .build();
       }
   }
   ```

### `preCreate` 테스트 업데이트
<a name="updating-the-precreate-handler"></a>

1. IDE에서 `src/test/java/software/mycompany/testing/mytesthook` 폴더에 있는 `PreCreateHandlerTest.java` 파일을 엽니다.

1. `PreCreateHandlerTest.java` 파일의 전체 내용을 다음 코드로 바꿉니다.

   ```
   package com.mycompany.testing.mytesthook;
   
   import com.google.common.collect.ImmutableMap;
   import com.mycompany.testing.mytesthook.model.aws.s3.bucket.AwsS3Bucket;
   import com.mycompany.testing.mytesthook.model.aws.s3.bucket.BucketEncryption;
   import com.mycompany.testing.mytesthook.model.aws.s3.bucket.ServerSideEncryptionByDefault;
   import com.mycompany.testing.mytesthook.model.aws.s3.bucket.ServerSideEncryptionRule;
   import com.mycompany.testing.mytesthook.model.aws.sqs.queue.AwsSqsQueue;
   import org.junit.jupiter.api.BeforeEach;
   import org.junit.jupiter.api.Test;
   import org.junit.jupiter.api.extension.ExtendWith;
   import org.mockito.Mock;
   import org.mockito.junit.jupiter.MockitoExtension;
   import software.amazon.cloudformation.exceptions.UnsupportedTargetException;
   import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
   import software.amazon.cloudformation.proxy.HandlerErrorCode;
   import software.amazon.cloudformation.proxy.Logger;
   import software.amazon.cloudformation.proxy.hook.HookContext;
   import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
   import software.amazon.cloudformation.proxy.hook.HookProgressEvent;
   import software.amazon.cloudformation.proxy.hook.HookStatus;
   import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel;
   
   import java.util.Collections;
   import java.util.Map;
   
   import static org.assertj.core.api.Assertions.assertThat;
   import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
   import static org.mockito.Mockito.mock;
   
   @ExtendWith(MockitoExtension.class)
   public class PreCreateHookHandlerTest {
   
       @Mock
       private AmazonWebServicesClientProxy proxy;
   
       @Mock
       private Logger logger;
   
       @BeforeEach
       public void setup() {
           proxy = mock(AmazonWebServicesClientProxy.class);
           logger = mock(Logger.class);
       }
       
       @Test
       public void handleRequest_awsSqsQueueSuccess() {
           final PreCreateHookHandler handler = new PreCreateHookHandler();
   
           final AwsSqsQueue queue = buildSqsQueue("MyQueue", "KmsKey");
           final HookTargetModel targetModel = createHookTargetModel(queue);
           final TypeConfigurationModel typeConfiguration = TypeConfigurationModel.builder().encryptionAlgorithm("AES256").build();
   
           final HookHandlerRequest request = HookHandlerRequest.builder()
               .hookContext(HookContext.builder().targetName("AWS::SQS::Queue").targetModel(targetModel).build())
               .build();
   
           final HookProgressEvent<CallbackContext> response = handler.handleRequest(proxy, request, null, logger, typeConfiguration);
           assertResponse(response, HookStatus.SUCCESS, "Successfully invoked PreCreateHookHandler for target: AWS::SQS::Queue");
       }
   
       @Test
       public void handleRequest_awsS3BucketSuccess() {
           final PreCreateHookHandler handler = new PreCreateHookHandler();
   
           final AwsS3Bucket bucket = buildAwsS3Bucket("amzn-s3-demo-bucket", true, "AES256", "KmsKey");
           final HookTargetModel targetModel = createHookTargetModel(bucket);
           final TypeConfigurationModel typeConfiguration = TypeConfigurationModel.builder().encryptionAlgorithm("AES256").build();
   
           final HookHandlerRequest request = HookHandlerRequest.builder()
               .hookContext(HookContext.builder().targetName("AWS::S3::Bucket").targetModel(targetModel).build())
               .build();
   
           final HookProgressEvent<CallbackContext> response = handler.handleRequest(proxy, request, null, logger, typeConfiguration);
           assertResponse(response, HookStatus.SUCCESS, "Successfully invoked PreCreateHookHandler for target: AWS::S3::Bucket");
       }
   
       @Test
       public void handleRequest_awsS3BucketFail_bucketKeyNotEnabled() {
           final PreCreateHookHandler handler = new PreCreateHookHandler();
   
           final AwsS3Bucket bucket = buildAwsS3Bucket("amzn-s3-demo-bucket", false, "AES256", "KmsKey");
           final HookTargetModel targetModel = createHookTargetModel(bucket);
           final TypeConfigurationModel typeConfiguration = TypeConfigurationModel.builder().encryptionAlgorithm("AES256").build();
   
           final HookHandlerRequest request = HookHandlerRequest.builder()
               .hookContext(HookContext.builder().targetName("AWS::S3::Bucket").targetModel(targetModel).build())
               .build();
   
           final HookProgressEvent<CallbackContext> response = handler.handleRequest(proxy, request, null, logger, typeConfiguration);
           assertResponse(response, HookStatus.FAILED, "Bucket key not enabled for bucket with name: amzn-s3-demo-bucket");
       }
   
       @Test
       public void handleRequest_awsS3BucketFail_incorrectSSEEncryptionAlgorithm() {
           final PreCreateHookHandler handler = new PreCreateHookHandler();
   
           final AwsS3Bucket bucket = buildAwsS3Bucket("amzn-s3-demo-bucket", true, "SHA512", "KmsKey");
           final HookTargetModel targetModel = createHookTargetModel(bucket);
          final TypeConfigurationModel typeConfiguration = TypeConfigurationModel.builder().encryptionAlgorithm("AES256").build();
   
           final HookHandlerRequest request = HookHandlerRequest.builder()
               .hookContext(HookContext.builder().targetName("AWS::S3::Bucket").targetModel(targetModel).build())
               .build();
   
           final HookProgressEvent<CallbackContext> response = handler.handleRequest(proxy, request, null, logger, typeConfiguration);
           assertResponse(response, HookStatus.FAILED, "SSE Encryption Algorithm is incorrect for bucket with name: amzn-s3-demo-bucket");
       }
   
       @Test
       public void handleRequest_awsS3BucketFail_kmsKeyIdNotSet() {
           final PreCreateHookHandler handler = new PreCreateHookHandler();
   
           final AwsS3Bucket bucket = buildAwsS3Bucket("amzn-s3-demo-bucket", true, "AES256", null);
           final HookTargetModel targetModel = createHookTargetModel(bucket);
           final TypeConfigurationModel typeConfiguration = TypeConfigurationModel.builder().encryptionAlgorithm("AES256").build();
   
           final HookHandlerRequest request = HookHandlerRequest.builder()
               .hookContext(HookContext.builder().targetName("AWS::S3::Bucket").targetModel(targetModel).build())
               .build();
   
           final HookProgressEvent<CallbackContext> response = handler.handleRequest(proxy, request, null, logger, typeConfiguration);
           assertResponse(response, HookStatus.FAILED, "KMS Key ID not set for bucket with name: amzn-s3-demo-bucket");
       }
   
       @Test
       public void handleRequest_awsSqsQueueFail_serverSideEncryptionOff() {
           final PreCreateHookHandler handler = new PreCreateHookHandler();
   
           final AwsSqsQueue queue = buildSqsQueue("MyQueue", null);
           final HookTargetModel targetModel = createHookTargetModel(queue);
           final TypeConfigurationModel typeConfiguration = TypeConfigurationModel.builder().encryptionAlgorithm("AES256").build();
   
           final HookHandlerRequest request = HookHandlerRequest.builder()
               .hookContext(HookContext.builder().targetName("AWS::SQS::Queue").targetModel(targetModel).build())
               .build();
   
           final HookProgressEvent<CallbackContext> response = handler.handleRequest(proxy, request, null, logger, typeConfiguration);
           assertResponse(response, HookStatus.FAILED, "Server side encryption turned off for queue with name: MyQueue");
       }
   
       @Test
       public void handleRequest_unsupportedTarget() {
           final PreCreateHookHandler handler = new PreCreateHookHandler();
   
           final Map<String, Object> unsupportedTarget = ImmutableMap.of("ResourceName", "MyUnsupportedTarget");
           final HookTargetModel targetModel = createHookTargetModel(unsupportedTarget);
           final TypeConfigurationModel typeConfiguration = TypeConfigurationModel.builder().encryptionAlgorithm("AES256").build();
   
           final HookHandlerRequest request = HookHandlerRequest.builder()
               .hookContext(HookContext.builder().targetName("AWS::Unsupported::Target").targetModel(targetModel).build())
               .build();
   
           assertThatExceptionOfType(UnsupportedTargetException.class)
                   .isThrownBy(() -> handler.handleRequest(proxy, request, null, logger, typeConfiguration))
                   .withMessageContaining("Unsupported target")
                   .withMessageContaining("AWS::Unsupported::Target")
                   .satisfies(e -> assertThat(e.getErrorCode()).isEqualTo(HandlerErrorCode.InvalidRequest));
       }
   
       private void assertResponse(final HookProgressEvent<CallbackContext> response, final HookStatus expectedStatus, final String expectedErrorMsg) {
           assertThat(response).isNotNull();
           assertThat(response.getStatus()).isEqualTo(expectedStatus);
           assertThat(response.getCallbackContext()).isNull();
           assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
           assertThat(response.getMessage()).isNotNull();
           assertThat(response.getMessage()).isEqualTo(expectedErrorMsg);
       }
   
       private HookTargetModel createHookTargetModel(final Object resourceProperties) {
           return HookTargetModel.of(ImmutableMap.of("ResourceProperties", resourceProperties));
       }
   
       @SuppressWarnings("SameParameterValue")
       private AwsSqsQueue buildSqsQueue(final String queueName, final String kmsKeyId) {
           return AwsSqsQueue.builder()
                   .queueName(queueName)
                   .kmsMasterKeyId(kmsKeyId) // "KmsMasterKeyId" is name of the property for an AWS::SQS::Queue
                   .build();
       }
   
       @SuppressWarnings("SameParameterValue")
       private AwsS3Bucket buildAwsS3Bucket(
               final String bucketName,
               final Boolean bucketKeyEnabled,
               final String sseAlgorithm,
               final String kmsKeyId
       ) {
           return AwsS3Bucket.builder()
                   .bucketName(bucketName)
                   .bucketEncryption(
                       BucketEncryption.builder()
                           .serverSideEncryptionConfiguration(
                               Collections.singletonList(
                                   ServerSideEncryptionRule.builder()
                                       .bucketKeyEnabled(bucketKeyEnabled)
                                       .serverSideEncryptionByDefault(
                                           ServerSideEncryptionByDefault.builder()
                                               .sSEAlgorithm(sseAlgorithm)
                                               .kMSMasterKeyID(kmsKeyId) // "KMSMasterKeyID" is name of the property for an AWS::S3::Bucket
                                               .build()
                                       ).build()
                               )
                           ).build()
                   ).build();
       }
   }
   ```

### `preUpdate` 핸들러 구현
<a name="implementing-preupdate-handler"></a>

`preUpdate` 핸들러에서 지정된 모든 대상에 대한 업데이트 작업 전에 시작하는 핸들러를 구현합니다. `preUpdate` 핸들러는 다음을 수행합니다.
+ `AWS::S3::Bucket` 리소스의 경우 후크는 다음과 같은 경우에만 전달합니다.
  + Amazon S3 버킷의 버킷 암호화 알고리즘이 수정되지 않았습니다.

### `preUpdate` 핸들러 코딩
<a name="coding-preupdate-handler"></a>

1. IDE에서 `src/main/java/software/mycompany/testing/mytesthook` 폴더에 있는 `PreUpdateHookHandler.java` 파일을 엽니다.

1. `PreUpdateHookHandler.java` 파일의 전체 내용을 다음 코드로 바꿉니다.

   ```
   package com.mycompany.testing.mytesthook;
   
   import com.mycompany.testing.mytesthook.model.aws.s3.bucket.AwsS3Bucket;
   import com.mycompany.testing.mytesthook.model.aws.s3.bucket.AwsS3BucketTargetModel;
   import com.mycompany.testing.mytesthook.model.aws.s3.bucket.ServerSideEncryptionRule;
   import org.apache.commons.lang3.StringUtils;
   import software.amazon.cloudformation.exceptions.UnsupportedTargetException;
   import software.amazon.cloudformation.proxy.HandlerErrorCode;
   import software.amazon.cloudformation.proxy.Logger;
   import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
   import software.amazon.cloudformation.proxy.hook.HookStatus;
   import software.amazon.cloudformation.proxy.hook.HookProgressEvent;
   import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
   import software.amazon.cloudformation.proxy.hook.targetmodel.ResourceHookTargetModel;
   
   import java.util.List;
   
   public class PreUpdateHookHandler extends BaseHookHandler<TypeConfigurationModel, CallbackContext> {
   
       @Override
       public HookProgressEvent<CallbackContext> handleRequest(
           final AmazonWebServicesClientProxy proxy,
           final HookHandlerRequest request,
           final CallbackContext callbackContext,
           final Logger logger,
           final TypeConfigurationModel typeConfiguration) {
   
           final String targetName = request.getHookContext().getTargetName();
           if ("AWS::S3::Bucket".equals(targetName)) {
               final ResourceHookTargetModel<AwsS3Bucket> targetModel = request.getHookContext().getTargetModel(AwsS3BucketTargetModel.class);
   
               final AwsS3Bucket bucketProperties = targetModel.getResourceProperties();
               final AwsS3Bucket previousBucketProperties = targetModel.getPreviousResourceProperties();
   
               return validateBucketEncryptionRulesNotUpdated(bucketProperties, previousBucketProperties);
           } else {
               throw new UnsupportedTargetException(targetName);
           }
       }
   
       private HookProgressEvent<CallbackContext> validateBucketEncryptionRulesNotUpdated(final AwsS3Bucket resourceProperties, final AwsS3Bucket previousResourceProperties) {
           final List<ServerSideEncryptionRule> bucketEncryptionConfigs = resourceProperties.getBucketEncryption().getServerSideEncryptionConfiguration();
           final List<ServerSideEncryptionRule> previousBucketEncryptionConfigs = previousResourceProperties.getBucketEncryption().getServerSideEncryptionConfiguration();
   
           if (bucketEncryptionConfigs.size() != previousBucketEncryptionConfigs.size()) {
               return HookProgressEvent.<CallbackContext>builder()
                       .status(HookStatus.FAILED)
                       .errorCode(HandlerErrorCode.NotUpdatable)
                       .message(
                           String.format(
                               "Current number of bucket encryption configs does not match previous. Current has %d configs while previously there were %d configs",
                               bucketEncryptionConfigs.size(),
                               previousBucketEncryptionConfigs.size()
                           )
                       ).build();
           }
   
           for (int i = 0; i < bucketEncryptionConfigs.size(); ++i) {
               final String currentEncryptionAlgorithm = bucketEncryptionConfigs.get(i).getServerSideEncryptionByDefault().getSSEAlgorithm();
               final String previousEncryptionAlgorithm = previousBucketEncryptionConfigs.get(i).getServerSideEncryptionByDefault().getSSEAlgorithm();
   
               if (!StringUtils.equals(currentEncryptionAlgorithm, previousEncryptionAlgorithm)) {
                   return HookProgressEvent.<CallbackContext>builder()
                       .status(HookStatus.FAILED)
                       .errorCode(HandlerErrorCode.NotUpdatable)
                       .message(
                           String.format(
                               "Bucket Encryption algorithm can not be changed once set. The encryption algorithm was changed to '%s' from '%s'.",
                               currentEncryptionAlgorithm,
                               previousEncryptionAlgorithm
                           )
                       )
                       .build();
               }
           }
   
           return HookProgressEvent.<CallbackContext>builder()
                       .status(HookStatus.SUCCESS)
                       .message("Successfully invoked PreUpdateHookHandler for target: AWS::SQS::Queue")
                       .build();
       }
   }
   ```

### `preUpdate` 테스트 업데이트
<a name="update-the-preupdate-handler"></a>

1. IDE에서 `src/main/java/com/mycompany/testing/mytesthook` 폴더의 `PreUpdateHandlerTest.java` 파일을 엽니다.

1. `PreUpdateHandlerTest.java` 파일의 전체 내용을 다음 코드로 바꿉니다.

   ```
   package com.mycompany.testing.mytesthook;
   
   import com.google.common.collect.ImmutableMap;
   import com.mycompany.testing.mytesthook.model.aws.s3.bucket.AwsS3Bucket;
   import com.mycompany.testing.mytesthook.model.aws.s3.bucket.BucketEncryption;
   import com.mycompany.testing.mytesthook.model.aws.s3.bucket.ServerSideEncryptionByDefault;
   import com.mycompany.testing.mytesthook.model.aws.s3.bucket.ServerSideEncryptionRule;
   import org.junit.jupiter.api.BeforeEach;
   import org.junit.jupiter.api.Test;
   import org.junit.jupiter.api.extension.ExtendWith;
   import org.mockito.Mock;
   import org.mockito.junit.jupiter.MockitoExtension;
   import software.amazon.cloudformation.exceptions.UnsupportedTargetException;
   import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
   import software.amazon.cloudformation.proxy.HandlerErrorCode;
   import software.amazon.cloudformation.proxy.Logger;
   import software.amazon.cloudformation.proxy.hook.HookContext;
   import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
   import software.amazon.cloudformation.proxy.hook.HookProgressEvent;
   import software.amazon.cloudformation.proxy.hook.HookStatus;
   import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel;
   
   import java.util.Arrays;
   import java.util.stream.Stream;
   
   import static org.assertj.core.api.Assertions.assertThat;
   import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
   import static org.mockito.Mockito.mock;
   
   @ExtendWith(MockitoExtension.class)
   public class PreUpdateHookHandlerTest {
   
       @Mock
       private AmazonWebServicesClientProxy proxy;
   
       @Mock
       private Logger logger;
   
       @BeforeEach
       public void setup() {
           proxy = mock(AmazonWebServicesClientProxy.class);
           logger = mock(Logger.class);
       }
   
       @Test
       public void handleRequest_awsS3BucketSuccess() {
           final PreUpdateHookHandler handler = new PreUpdateHookHandler();
   
           final ServerSideEncryptionRule serverSideEncryptionRule = buildServerSideEncryptionRule("AES256");
           final AwsS3Bucket resourceProperties = buildAwsS3Bucket("amzn-s3-demo-bucket", serverSideEncryptionRule);
           final AwsS3Bucket previousResourceProperties = buildAwsS3Bucket("amzn-s3-demo-bucket", serverSideEncryptionRule);
           final HookTargetModel targetModel = createHookTargetModel(resourceProperties, previousResourceProperties);
           final TypeConfigurationModel typeConfiguration = TypeConfigurationModel.builder().encryptionAlgorithm("AES256").build();
   
           final HookHandlerRequest request = HookHandlerRequest.builder()
               .hookContext(HookContext.builder().targetName("AWS::S3::Bucket").targetModel(targetModel).build())
               .build();
   
           final HookProgressEvent<CallbackContext> response = handler.handleRequest(proxy, request, null, logger, typeConfiguration);
           assertResponse(response, HookStatus.SUCCESS, "Successfully invoked PreUpdateHookHandler for target: AWS::SQS::Queue");
       }
   
       @Test
       public void handleRequest_awsS3BucketFail_bucketEncryptionConfigsDontMatch() {
           final PreUpdateHookHandler handler = new PreUpdateHookHandler();
   
           final ServerSideEncryptionRule[] serverSideEncryptionRules = Stream.of("AES256", "SHA512", "AES32")
                   .map(this::buildServerSideEncryptionRule)
                   .toArray(ServerSideEncryptionRule[]::new);
   
           final AwsS3Bucket resourceProperties = buildAwsS3Bucket("amzn-s3-demo-bucket", serverSideEncryptionRules[0]);
           final AwsS3Bucket previousResourceProperties = buildAwsS3Bucket("amzn-s3-demo-bucket", serverSideEncryptionRules);
           final HookTargetModel targetModel = createHookTargetModel(resourceProperties, previousResourceProperties);
           final TypeConfigurationModel typeConfiguration = TypeConfigurationModel.builder().encryptionAlgorithm("AES256").build();
   
           final HookHandlerRequest request = HookHandlerRequest.builder()
               .hookContext(HookContext.builder().targetName("AWS::S3::Bucket").targetModel(targetModel).build())
               .build();
   
           final HookProgressEvent<CallbackContext> response = handler.handleRequest(proxy, request, null, logger, typeConfiguration);
           assertResponse(response, HookStatus.FAILED, "Current number of bucket encryption configs does not match previous. Current has 1 configs while previously there were 3 configs");
       }
   
       @Test
       public void handleRequest_awsS3BucketFail_bucketEncryptionAlgorithmDoesNotMatch() {
           final PreUpdateHookHandler handler = new PreUpdateHookHandler();
   
           final AwsS3Bucket resourceProperties = buildAwsS3Bucket("amzn-s3-demo-bucket", buildServerSideEncryptionRule("SHA512"));
           final AwsS3Bucket previousResourceProperties = buildAwsS3Bucket("amzn-s3-demo-bucket", buildServerSideEncryptionRule("AES256"));
           final HookTargetModel targetModel = createHookTargetModel(resourceProperties, previousResourceProperties);
           final TypeConfigurationModel typeConfiguration = TypeConfigurationModel.builder().encryptionAlgorithm("AES256").build();
   
           final HookHandlerRequest request = HookHandlerRequest.builder()
               .hookContext(HookContext.builder().targetName("AWS::S3::Bucket").targetModel(targetModel).build())
               .build();
   
           final HookProgressEvent<CallbackContext> response = handler.handleRequest(proxy, request, null, logger, typeConfiguration);
           assertResponse(response, HookStatus.FAILED, String.format("Bucket Encryption algorithm can not be changed once set. The encryption algorithm was changed to '%s' from '%s'.", "SHA512", "AES256"));
       }
   
       @Test
       public void handleRequest_unsupportedTarget() {
           final PreUpdateHookHandler handler = new PreUpdateHookHandler();
   
           final Object resourceProperties = ImmutableMap.of("FileSizeLimit", 256);
           final Object previousResourceProperties = ImmutableMap.of("FileSizeLimit", 512);
           final HookTargetModel targetModel = createHookTargetModel(resourceProperties, previousResourceProperties);
           final TypeConfigurationModel typeConfiguration = TypeConfigurationModel.builder().encryptionAlgorithm("AES256").build();
   
           final HookHandlerRequest request = HookHandlerRequest.builder()
               .hookContext(HookContext.builder().targetName("AWS::Unsupported::Target").targetModel(targetModel).build())
               .build();
   
           assertThatExceptionOfType(UnsupportedTargetException.class)
                   .isThrownBy(() -> handler.handleRequest(proxy, request, null, logger, typeConfiguration))
                   .withMessageContaining("Unsupported target")
                   .withMessageContaining("AWS::Unsupported::Target")
                   .satisfies(e -> assertThat(e.getErrorCode()).isEqualTo(HandlerErrorCode.InvalidRequest));
       }
   
       private void assertResponse(final HookProgressEvent<CallbackContext> response, final HookStatus expectedStatus, final String expectedErrorMsg) {
           assertThat(response).isNotNull();
           assertThat(response.getStatus()).isEqualTo(expectedStatus);
           assertThat(response.getCallbackContext()).isNull();
           assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
           assertThat(response.getMessage()).isNotNull();
           assertThat(response.getMessage()).isEqualTo(expectedErrorMsg);
       }
   
       private HookTargetModel createHookTargetModel(final Object resourceProperties, final Object previousResourceProperties) {
           return HookTargetModel.of(
                   ImmutableMap.of(
                       "ResourceProperties", resourceProperties,
                       "PreviousResourceProperties", previousResourceProperties
                   )
           );
       }
   
       @SuppressWarnings("SameParameterValue")
       private AwsS3Bucket buildAwsS3Bucket(
               final String bucketName,
               final ServerSideEncryptionRule ...serverSideEncryptionRules
               ) {
           return AwsS3Bucket.builder()
                   .bucketName(bucketName)
                   .bucketEncryption(
                       BucketEncryption.builder()
                           .serverSideEncryptionConfiguration(
                                   Arrays.asList(serverSideEncryptionRules)
                           ).build()
                   ).build();
       }
   
       private ServerSideEncryptionRule buildServerSideEncryptionRule(final String encryptionAlgorithm) {
           return ServerSideEncryptionRule.builder()
                   .bucketKeyEnabled(true)
                   .serverSideEncryptionByDefault(
                           ServerSideEncryptionByDefault.builder()
                                   .sSEAlgorithm(encryptionAlgorithm)
                                   .build()
                   ).build();
       }
   }
   ```

### `preDelete` 핸들러 구현
<a name="implement-the-predelete-hander"></a>

`preDelete` 핸들러에서 지정된 모든 대상에 대한 삭제 작업 전에 시작하는 핸들러를 구현합니다. `preDelete` 핸들러는 다음을 수행합니다.
+ `AWS::S3::Bucket` 리소스의 경우 후크는 다음과 같은 경우에만 전달합니다.
  + 리소스를 삭제한 후 계정에 필요한 최소 수신 거부 리소스가 있는지 확인합니다.
  + 최소 필수 수신 거부 리소스 양은 후크의 유형 구성에서 설정됩니다.

### `preDelete` 핸들러 코딩
<a name="code-the-predelete-handler"></a>

1. IDE에서 `src/main/java/com/mycompany/testing/mytesthook` 폴더의 `PreDeleteHookHandler.java` 파일을 엽니다.

1. `PreDeleteHookHandler.java` 파일의 전체 내용을 다음 코드로 바꿉니다.

   ```
   package com.mycompany.testing.mytesthook;
   
   import com.google.common.annotations.VisibleForTesting;
   import com.mycompany.testing.mytesthook.model.aws.s3.bucket.AwsS3Bucket;
   import com.mycompany.testing.mytesthook.model.aws.s3.bucket.AwsS3BucketTargetModel;
   import com.mycompany.testing.mytesthook.model.aws.sqs.queue.AwsSqsQueue;
   import com.mycompany.testing.mytesthook.model.aws.sqs.queue.AwsSqsQueueTargetModel;
   import org.apache.commons.lang3.StringUtils;
   import org.apache.commons.lang3.math.NumberUtils;
   import software.amazon.awssdk.services.cloudformation.CloudFormationClient;
   import software.amazon.awssdk.services.cloudformation.model.CloudFormationException;
   import software.amazon.awssdk.services.cloudformation.model.DescribeStackResourceRequest;
   import software.amazon.awssdk.services.s3.S3Client;
   import software.amazon.awssdk.services.s3.model.Bucket;
   import software.amazon.awssdk.services.s3.model.S3Exception;
   import software.amazon.awssdk.services.sqs.SqsClient;
   import software.amazon.awssdk.services.sqs.model.GetQueueAttributesRequest;
   import software.amazon.awssdk.services.sqs.model.GetQueueUrlRequest;
   import software.amazon.awssdk.services.sqs.model.ListQueuesRequest;
   import software.amazon.awssdk.services.sqs.model.ListQueuesResponse;
   import software.amazon.awssdk.services.sqs.model.QueueAttributeName;
   import software.amazon.awssdk.services.sqs.model.SqsException;
   import software.amazon.cloudformation.exceptions.CfnGeneralServiceException;
   import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
   import software.amazon.cloudformation.proxy.HandlerErrorCode;
   import software.amazon.cloudformation.proxy.OperationStatus;
   import software.amazon.cloudformation.proxy.ProgressEvent;
   import software.amazon.cloudformation.proxy.ProxyClient;
   import software.amazon.cloudformation.proxy.hook.HookContext;
   import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
   import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel;
   import software.amazon.cloudformation.proxy.hook.targetmodel.ResourceHookTargetModel;
   
   import java.util.ArrayList;
   import java.util.Collection;
   import java.util.HashSet;
   import java.util.List;
   import java.util.Objects;
   import java.util.stream.Collectors;
   
   public class PreDeleteHookHandler extends BaseHookHandlerStd {
   
       private ProxyClient<S3Client> s3Client;
       private ProxyClient<SqsClient> sqsClient;
   
       @Override
       protected ProgressEvent<HookTargetModel, CallbackContext> handleS3BucketRequest(
               final AmazonWebServicesClientProxy proxy,
               final HookHandlerRequest request,
               final CallbackContext callbackContext,
               final ProxyClient<S3Client> proxyClient,
               final TypeConfigurationModel typeConfiguration
       ) {
           final HookContext hookContext = request.getHookContext();
           final String targetName = hookContext.getTargetName();
           if (!AwsS3Bucket.TYPE_NAME.equals(targetName)) {
               throw new RuntimeException(String.format("Request target type [%s] is not 'AWS::S3::Bucket'", targetName));
           }
           this.s3Client = proxyClient;
   
           final String encryptionAlgorithm = typeConfiguration.getEncryptionAlgorithm();
           final int minBuckets = NumberUtils.toInt(typeConfiguration.getMinBuckets());
   
           final ResourceHookTargetModel<AwsS3Bucket> targetModel = hookContext.getTargetModel(AwsS3BucketTargetModel.class);
           final List<String> buckets = listBuckets().stream()
                   .filter(b -> !StringUtils.equals(b, targetModel.getResourceProperties().getBucketName()))
                   .collect(Collectors.toList());
   
           final List<String> compliantBuckets = new ArrayList<>();
           for (final String bucket : buckets) {
               if (getBucketSSEAlgorithm(bucket).contains(encryptionAlgorithm)) {
                   compliantBuckets.add(bucket);
               }
   
               if (compliantBuckets.size() >= minBuckets) {
                   return ProgressEvent.<HookTargetModel, CallbackContext>builder()
                           .status(OperationStatus.SUCCESS)
                           .message("Successfully invoked PreDeleteHookHandler for target: AWS::S3::Bucket")
                           .build();
               }
           }
   
           return ProgressEvent.<HookTargetModel, CallbackContext>builder()
                   .status(OperationStatus.FAILED)
                   .errorCode(HandlerErrorCode.NonCompliant)
                   .message(String.format("Failed to meet minimum of [%d] encrypted buckets.", minBuckets))
                   .build();
       }
   
       @Override
       protected ProgressEvent<HookTargetModel, CallbackContext> handleSqsQueueRequest(
               final AmazonWebServicesClientProxy proxy,
               final HookHandlerRequest request,
               final CallbackContext callbackContext,
               final ProxyClient<SqsClient> proxyClient,
               final TypeConfigurationModel typeConfiguration
       ) {
           final HookContext hookContext = request.getHookContext();
           final String targetName = hookContext.getTargetName();
           if (!AwsSqsQueue.TYPE_NAME.equals(targetName)) {
               throw new RuntimeException(String.format("Request target type [%s] is not 'AWS::SQS::Queue'", targetName));
           }
           this.sqsClient = proxyClient;
           final int minQueues = NumberUtils.toInt(typeConfiguration.getMinQueues());
   
           final ResourceHookTargetModel<AwsSqsQueue> targetModel = hookContext.getTargetModel(AwsSqsQueueTargetModel.class);
   
           final String queueName = Objects.toString(targetModel.getResourceProperties().get("QueueName"), null);
   
           String targetQueueUrl = null;
           if (queueName != null) {
               try {
                   targetQueueUrl = sqsClient.injectCredentialsAndInvokeV2(
                           GetQueueUrlRequest.builder().queueName(
                                   queueName
                           ).build(),
                           sqsClient.client()::getQueueUrl
                   ).queueUrl();
               } catch (SqsException e) {
                   log(String.format("Error while calling GetQueueUrl API for queue name [%s]: %s", queueName, e.getMessage()));
               }
           } else {
               log("Queue name is empty, attempting to get queue's physical ID");
               try {
                   final ProxyClient<CloudFormationClient> cfnClient = proxy.newProxy(ClientBuilder::createCloudFormationClient);
                   targetQueueUrl = cfnClient.injectCredentialsAndInvokeV2(
                           DescribeStackResourceRequest.builder()
                                   .stackName(hookContext.getTargetLogicalId())
                                   .logicalResourceId(hookContext.getTargetLogicalId())
                                   .build(),
                           cfnClient.client()::describeStackResource
                   ).stackResourceDetail().physicalResourceId();
               } catch (CloudFormationException e) {
                   log(String.format("Error while calling DescribeStackResource API for queue name: %s", e.getMessage()));
               }
           }
   
           // Creating final variable for the filter lambda
           final String finalTargetQueueUrl = targetQueueUrl;
   
           final List<String> compliantQueues = new ArrayList<>();
   
           String nextToken = null;
           do {
               final ListQueuesRequest req = Translator.createListQueuesRequest(nextToken);
               final ListQueuesResponse res = sqsClient.injectCredentialsAndInvokeV2(req, sqsClient.client()::listQueues);
               final List<String> queueUrls = res.queueUrls().stream()
                       .filter(q -> !StringUtils.equals(q, finalTargetQueueUrl))
                       .collect(Collectors.toList());
   
               for (final String queueUrl : queueUrls) {
                   if (isQueueEncrypted(queueUrl)) {
                       compliantQueues.add(queueUrl);
                   }
   
                   if (compliantQueues.size() >= minQueues) {
                       return ProgressEvent.<HookTargetModel, CallbackContext>builder()
                           .status(OperationStatus.SUCCESS)
                           .message("Successfully invoked PreDeleteHookHandler for target: AWS::SQS::Queue")
                           .build();
                   }
                   nextToken = res.nextToken();
               }
           } while (nextToken != null);
   
           return ProgressEvent.<HookTargetModel, CallbackContext>builder()
                   .status(OperationStatus.FAILED)
                   .errorCode(HandlerErrorCode.NonCompliant)
                   .message(String.format("Failed to meet minimum of [%d] encrypted queues.", minQueues))
                   .build();
       }
   
       private List<String> listBuckets() {
           try {
               return s3Client.injectCredentialsAndInvokeV2(Translator.createListBucketsRequest(), s3Client.client()::listBuckets)
                       .buckets()
                       .stream()
                       .map(Bucket::name)
                       .collect(Collectors.toList());
           } catch (S3Exception e) {
               throw new CfnGeneralServiceException("Error while calling S3 ListBuckets API", e);
           }
       }
   
       @VisibleForTesting
       Collection<String> getBucketSSEAlgorithm(final String bucket) {
           try {
               return s3Client.injectCredentialsAndInvokeV2(Translator.createGetBucketEncryptionRequest(bucket), s3Client.client()::getBucketEncryption)
                       .serverSideEncryptionConfiguration()
                       .rules()
                       .stream()
                       .filter(r -> Objects.nonNull(r.applyServerSideEncryptionByDefault()))
                       .map(r -> r.applyServerSideEncryptionByDefault().sseAlgorithmAsString())
                       .collect(Collectors.toSet());
           } catch (S3Exception e) {
               return new HashSet<>();
           }
       }
   
       @VisibleForTesting
       boolean isQueueEncrypted(final String queueUrl) {
           try {
               final GetQueueAttributesRequest request = GetQueueAttributesRequest.builder()
                       .queueUrl(queueUrl)
                       .attributeNames(QueueAttributeName.KMS_MASTER_KEY_ID)
                       .build();
               final String kmsKeyId = sqsClient.injectCredentialsAndInvokeV2(request, sqsClient.client()::getQueueAttributes)
                       .attributes()
                       .get(QueueAttributeName.KMS_MASTER_KEY_ID);
   
               return StringUtils.isNotBlank(kmsKeyId);
           } catch (SqsException e) {
               throw new CfnGeneralServiceException("Error while calling SQS GetQueueAttributes API", e);
           }
       }
   }
   ```

### `preDelete` 핸들러 업데이트
<a name="update-the-predelete-handler"></a>

1. IDE에서 `src/main/java/com/mycompany/testing/mytesthook` 폴더의 `PreDeleteHookHandler.java` 파일을 엽니다.

1. `PreDeleteHookHandler.java` 파일의 전체 내용을 다음 코드로 바꿉니다.

   ```
   package com.mycompany.testing.mytesthook;
   
   import com.google.common.collect.ImmutableList;
   import com.google.common.collect.ImmutableMap;
   import com.mycompany.testing.mytesthook.model.aws.s3.bucket.AwsS3Bucket;
   import org.junit.jupiter.api.BeforeEach;
   import org.junit.jupiter.api.Test;
   import org.junit.jupiter.api.extension.ExtendWith;
   import org.mockito.Mock;
   import org.mockito.Mockito;
   import org.mockito.junit.jupiter.MockitoExtension;
   import software.amazon.awssdk.services.s3.S3Client;
   import software.amazon.awssdk.services.s3.model.Bucket;
   import software.amazon.awssdk.services.s3.model.GetBucketEncryptionRequest;
   import software.amazon.awssdk.services.s3.model.GetBucketEncryptionResponse;
   import software.amazon.awssdk.services.s3.model.ListBucketsRequest;
   import software.amazon.awssdk.services.s3.model.ListBucketsResponse;
   import software.amazon.awssdk.services.s3.model.S3Exception;
   import software.amazon.awssdk.services.s3.model.ServerSideEncryptionByDefault;
   import software.amazon.awssdk.services.s3.model.ServerSideEncryptionConfiguration;
   import software.amazon.awssdk.services.s3.model.ServerSideEncryptionRule;
   import software.amazon.awssdk.services.sqs.SqsClient;
   import software.amazon.awssdk.services.sqs.model.GetQueueAttributesRequest;
   import software.amazon.awssdk.services.sqs.model.GetQueueAttributesResponse;
   import software.amazon.awssdk.services.sqs.model.GetQueueUrlRequest;
   import software.amazon.awssdk.services.sqs.model.GetQueueUrlResponse;
   import software.amazon.awssdk.services.sqs.model.ListQueuesRequest;
   import software.amazon.awssdk.services.sqs.model.ListQueuesResponse;
   import software.amazon.awssdk.services.sqs.model.QueueAttributeName;
   import software.amazon.cloudformation.proxy.Logger;
   import software.amazon.cloudformation.proxy.OperationStatus;
   import software.amazon.cloudformation.proxy.ProgressEvent;
   import software.amazon.cloudformation.proxy.hook.HookContext;
   import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
   import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel;
   
   import java.util.Arrays;
   import java.util.Collection;
   import java.util.HashMap;
   import java.util.List;
   import java.util.stream.Collectors;
   
   import static org.mockito.ArgumentMatchers.any;
   import static org.mockito.Mockito.mock;
   import static org.mockito.Mockito.never;
   import static org.mockito.Mockito.times;
   import static org.mockito.Mockito.verify;
   import static org.mockito.Mockito.when;
   
   @ExtendWith(MockitoExtension.class)
   public class PreDeleteHookHandlerTest extends AbstractTestBase {
   
       @Mock private S3Client s3Client;
       @Mock private SqsClient sqsClient;
       @Mock private Logger logger;
   
       @BeforeEach
       public void setup() {
           s3Client = mock(S3Client.class);
           sqsClient = mock(SqsClient.class);
           logger = mock(Logger.class);
       }
   
       @Test
       public void handleRequest_awsS3BucketSuccess() {
           final PreDeleteHookHandler handler = Mockito.spy(new PreDeleteHookHandler());
   
           final List<Bucket> bucketList = ImmutableList.of(
                   Bucket.builder().name("bucket1").build(),
                   Bucket.builder().name("bucket2").build(),
                   Bucket.builder().name("toBeDeletedBucket").build(),
                   Bucket.builder().name("bucket3").build(),
                   Bucket.builder().name("bucket4").build(),
                   Bucket.builder().name("bucket5").build()
           );
           final ListBucketsResponse mockResponse = ListBucketsResponse.builder().buckets(bucketList).build();
           when(s3Client.listBuckets(any(ListBucketsRequest.class))).thenReturn(mockResponse);
           when(s3Client.getBucketEncryption(any(GetBucketEncryptionRequest.class)))
                   .thenReturn(buildGetBucketEncryptionResponse("AES256"))
                   .thenReturn(buildGetBucketEncryptionResponse("AES256", "aws:kms"))
                   .thenThrow(S3Exception.builder().message("No Encrypt").build())
                   .thenReturn(buildGetBucketEncryptionResponse("aws:kms"))
                   .thenReturn(buildGetBucketEncryptionResponse("AES256"));
           setServiceClient(s3Client);
   
           final TypeConfigurationModel typeConfiguration = TypeConfigurationModel.builder()
                   .encryptionAlgorithm("AES256")
                   .minBuckets("3")
                   .build();
   
           final HookHandlerRequest request = HookHandlerRequest.builder()
                   .hookContext(
                       HookContext.builder()
                           .targetName("AWS::S3::Bucket")
                           .targetModel(
                               createHookTargetModel(
                                   AwsS3Bucket.builder()
                                       .bucketName("toBeDeletedBucket")
                                       .build()
                               )
                           )
                           .build())
                   .build();
   
           final ProgressEvent<HookTargetModel, CallbackContext> response = handler.handleRequest(proxy, request, null, logger, typeConfiguration);
   
           verify(s3Client, times(5)).getBucketEncryption(any(GetBucketEncryptionRequest.class));
           verify(handler, never()).getBucketSSEAlgorithm("toBeDeletedBucket");
   
           assertResponse(response, OperationStatus.SUCCESS, "Successfully invoked PreDeleteHookHandler for target: AWS::S3::Bucket");
       }
   
   
       @Test
       public void handleRequest_awsSqsQueueSuccess() {
           final PreDeleteHookHandler handler = Mockito.spy(new PreDeleteHookHandler());
   
           final List<String> queueUrls = ImmutableList.of(
                   "https://queue1.queue",
                   "https://queue2.queue",
                   "https://toBeDeletedQueue.queue",
                   "https://queue3.queue",
                   "https://queue4.queue",
                   "https://queue5.queue"
           );
   
           when(sqsClient.getQueueUrl(any(GetQueueUrlRequest.class)))
                   .thenReturn(GetQueueUrlResponse.builder().queueUrl("https://toBeDeletedQueue.queue").build());
           when(sqsClient.listQueues(any(ListQueuesRequest.class)))
                   .thenReturn(ListQueuesResponse.builder().queueUrls(queueUrls).build());
           when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class)))
                   .thenReturn(GetQueueAttributesResponse.builder().attributes(ImmutableMap.of(QueueAttributeName.KMS_MASTER_KEY_ID, "kmsKeyId")).build())
                   .thenReturn(GetQueueAttributesResponse.builder().attributes(new HashMap<>()).build())
                   .thenReturn(GetQueueAttributesResponse.builder().attributes(ImmutableMap.of(QueueAttributeName.KMS_MASTER_KEY_ID, "kmsKeyId")).build())
                   .thenReturn(GetQueueAttributesResponse.builder().attributes(new HashMap<>()).build())
                   .thenReturn(GetQueueAttributesResponse.builder().attributes(ImmutableMap.of(QueueAttributeName.KMS_MASTER_KEY_ID, "kmsKeyId")).build());
           setServiceClient(sqsClient);
   
           final TypeConfigurationModel typeConfiguration = TypeConfigurationModel.builder()
                   .minQueues("3")
                   .build();
   
           final HookHandlerRequest request = HookHandlerRequest.builder()
                   .hookContext(
                       HookContext.builder()
                           .targetName("AWS::SQS::Queue")
                           .targetModel(
                               createHookTargetModel(
                                   ImmutableMap.of("QueueName", "toBeDeletedQueue")
                               )
                           )
                           .build())
                   .build();
   
           final ProgressEvent<HookTargetModel, CallbackContext> response = handler.handleRequest(proxy, request, null, logger, typeConfiguration);
   
           verify(sqsClient, times(5)).getQueueAttributes(any(GetQueueAttributesRequest.class));
           verify(handler, never()).isQueueEncrypted("toBeDeletedQueue");
   
           assertResponse(response, OperationStatus.SUCCESS, "Successfully invoked PreDeleteHookHandler for target: AWS::SQS::Queue");
       }
   
       @Test
       public void handleRequest_awsS3BucketFailed() {
           final PreDeleteHookHandler handler = Mockito.spy(new PreDeleteHookHandler());
   
           final List<Bucket> bucketList = ImmutableList.of(
                   Bucket.builder().name("bucket1").build(),
                   Bucket.builder().name("bucket2").build(),
                   Bucket.builder().name("toBeDeletedBucket").build(),
                   Bucket.builder().name("bucket3").build(),
                   Bucket.builder().name("bucket4").build(),
                   Bucket.builder().name("bucket5").build()
           );
           final ListBucketsResponse mockResponse = ListBucketsResponse.builder().buckets(bucketList).build();
           when(s3Client.listBuckets(any(ListBucketsRequest.class))).thenReturn(mockResponse);
           when(s3Client.getBucketEncryption(any(GetBucketEncryptionRequest.class)))
                   .thenReturn(buildGetBucketEncryptionResponse("AES256"))
                   .thenReturn(buildGetBucketEncryptionResponse("AES256", "aws:kms"))
                   .thenThrow(S3Exception.builder().message("No Encrypt").build())
                   .thenReturn(buildGetBucketEncryptionResponse("aws:kms"))
                   .thenReturn(buildGetBucketEncryptionResponse("AES256"));
           setServiceClient(s3Client);
   
           final TypeConfigurationModel typeConfiguration = TypeConfigurationModel.builder()
                   .encryptionAlgorithm("AES256")
                   .minBuckets("10")
                   .build();
   
           final HookHandlerRequest request = HookHandlerRequest.builder()
                   .hookContext(
                       HookContext.builder()
                           .targetName("AWS::S3::Bucket")
                           .targetModel(
                               createHookTargetModel(
                                   AwsS3Bucket.builder()
                                       .bucketName("toBeDeletedBucket")
                                       .build()
                               )
                           )
                           .build())
                   .build();
   
           final ProgressEvent<HookTargetModel, CallbackContext> response = handler.handleRequest(proxy, request, null, logger, typeConfiguration);
   
           verify(s3Client, times(5)).getBucketEncryption(any(GetBucketEncryptionRequest.class));
           verify(handler, never()).getBucketSSEAlgorithm("toBeDeletedBucket");
   
           assertResponse(response, OperationStatus.FAILED, "Failed to meet minimum of [10] encrypted buckets.");
       }
   
   
       @Test
       public void handleRequest_awsSqsQueueFailed() {
           final PreDeleteHookHandler handler = Mockito.spy(new PreDeleteHookHandler());
   
           final List<String> queueUrls = ImmutableList.of(
                   "https://queue1.queue",
                   "https://queue2.queue",
                   "https://toBeDeletedQueue.queue",
                   "https://queue3.queue",
                   "https://queue4.queue",
                   "https://queue5.queue"
           );
   
           when(sqsClient.getQueueUrl(any(GetQueueUrlRequest.class)))
                   .thenReturn(GetQueueUrlResponse.builder().queueUrl("https://toBeDeletedQueue.queue").build());
           when(sqsClient.listQueues(any(ListQueuesRequest.class)))
                   .thenReturn(ListQueuesResponse.builder().queueUrls(queueUrls).build());
           when(sqsClient.getQueueAttributes(any(GetQueueAttributesRequest.class)))
                   .thenReturn(GetQueueAttributesResponse.builder().attributes(ImmutableMap.of(QueueAttributeName.KMS_MASTER_KEY_ID, "kmsKeyId")).build())
                   .thenReturn(GetQueueAttributesResponse.builder().attributes(new HashMap<>()).build())
                   .thenReturn(GetQueueAttributesResponse.builder().attributes(ImmutableMap.of(QueueAttributeName.KMS_MASTER_KEY_ID, "kmsKeyId")).build())
                   .thenReturn(GetQueueAttributesResponse.builder().attributes(new HashMap<>()).build())
                   .thenReturn(GetQueueAttributesResponse.builder().attributes(ImmutableMap.of(QueueAttributeName.KMS_MASTER_KEY_ID, "kmsKeyId")).build());
           setServiceClient(sqsClient);
   
           final TypeConfigurationModel typeConfiguration = TypeConfigurationModel.builder()
                   .minQueues("10")
                   .build();
   
           final HookHandlerRequest request = HookHandlerRequest.builder()
                   .hookContext(
                       HookContext.builder()
                           .targetName("AWS::SQS::Queue")
                           .targetModel(
                               createHookTargetModel(
                                   ImmutableMap.of("QueueName", "toBeDeletedQueue")
                               )
                           )
                           .build())
                   .build();
   
           final ProgressEvent<HookTargetModel, CallbackContext> response = handler.handleRequest(proxy, request, null, logger, typeConfiguration);
   
           verify(sqsClient, times(5)).getQueueAttributes(any(GetQueueAttributesRequest.class));
           verify(handler, never()).isQueueEncrypted("toBeDeletedQueue");
   
           assertResponse(response, OperationStatus.FAILED, "Failed to meet minimum of [10] encrypted queues.");
       }
   
       private GetBucketEncryptionResponse buildGetBucketEncryptionResponse(final String ...sseAlgorithm) {
           return buildGetBucketEncryptionResponse(
                   Arrays.stream(sseAlgorithm)
                       .map(a -> ServerSideEncryptionRule.builder().applyServerSideEncryptionByDefault(
                           ServerSideEncryptionByDefault.builder()
                               .sseAlgorithm(a)
                               .build()
                           ).build()
                       )
                       .collect(Collectors.toList())
           );
       }
   
       private GetBucketEncryptionResponse buildGetBucketEncryptionResponse(final Collection<ServerSideEncryptionRule> rules) {
           return GetBucketEncryptionResponse.builder()
                   .serverSideEncryptionConfiguration(
                       ServerSideEncryptionConfiguration.builder().rules(
                           rules
                       ).build()
                   ).build();
       }
   }
   ```

# Python을 사용하여 사용자 지정 CloudFormation 후크 모델링
<a name="hooks-model-python"></a>

사용자 지정 CloudFormation 후크 모델링에는 후크, 해당 속성 및 속성을 정의하는 스키마를 생성하는 작업이 포함됩니다. 이 자습서에서는 Python을 사용하여 사용자 지정 후크를 모델링하는 방법을 안내합니다.

## 1단계: 후크 프로젝트 패키지 생성
<a name="model-hook-project-package-python"></a>

후크 프로젝트 패키지를 생성합니다. CloudFormation CLI는 후크 사양에 정의된 대상 수명 주기의 특정 후크 작업에 해당하는 빈 핸들러 함수를 생성합니다.

```
cfn generate
```

명령은 다음 출력을 반환합니다.

```
Generated files for MyCompany::Testing::MyTestHook
```

**참고**  
더 이상 사용되지 않는 버전을 사용하지 않도록 Lambda 런타임이 up-to-date 상태인지 확인합니다. 자세한 내용은 [리소스 유형 및 후크에 대한 Lambda 런타임 업데이트를 참조하세요](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/runtime-update.html).

## 2단계: 후크 핸들러 추가
<a name="model-hook-project-add-handler"></a>

구현하도록 선택한 핸들러에 자체 후크 핸들러 런타임 코드를 추가합니다. 예를 들어 로깅을 위해 다음 코드를 추가할 수 있습니다.

```
LOG.setLevel(logging.INFO)
LOG.info("Internal testing Hook triggered for target: " + request.hookContext.targetName);
```

CloudFormation CLI는에서 `src/models.py` 파일을 생성합니다[후크 구성 스키마 구문 참조](hook-configuration-schema.md).

**Example models.py**  

```
import sys
from dataclasses import dataclass
from inspect import getmembers, isclass
from typing import (
    AbstractSet,
    Any,
    Generic,
    Mapping,
    MutableMapping,
    Optional,
    Sequence,
    Type,
    TypeVar,
)

from cloudformation_cli_python_lib.interface import (
    BaseModel,
    BaseHookHandlerRequest,
)
from cloudformation_cli_python_lib.recast import recast_object
from cloudformation_cli_python_lib.utils import deserialize_list

T = TypeVar("T")


def set_or_none(value: Optional[Sequence[T]]) -> Optional[AbstractSet[T]]:
    if value:
        return set(value)
    return None


@dataclass
class HookHandlerRequest(BaseHookHandlerRequest):
    pass


@dataclass
class TypeConfigurationModel(BaseModel):
    limitSize: Optional[str]
    cidr: Optional[str]
    encryptionAlgorithm: Optional[str]

    @classmethod
    def _deserialize(
        cls: Type["_TypeConfigurationModel"],
        json_data: Optional[Mapping[str, Any]],
    ) -> Optional["_TypeConfigurationModel"]:
        if not json_data:
            return None
        return cls(
            limitSize=json_data.get("limitSize"),
            cidr=json_data.get("cidr"),
            encryptionAlgorithm=json_data.get("encryptionAlgorithm"),
        )


_TypeConfigurationModel = TypeConfigurationModel
```

## 3단계: 후크 핸들러 구현
<a name="model-hook-project-code-handler-python"></a>

Python 데이터 클래스가 생성되면 후크의 기능을 실제로 구현하는 핸들러를 작성할 수 있습니다. 이 예제에서는 핸들러에 대한 `preCreate``preUpdate`, 및 `preDelete` 호출 지점을 구현합니다.

**Topics**
+ [preCreate 핸들러 구현](#model-hook-project-code-handler-python-precreate)
+ [preUpdate 핸들러 구현](#model-hook-project-code-handler-python-preupdate)
+ [preDelete 핸들러 구현](#model-hook-project-code-handler-python-predelete)
+ [후크 핸들러 구현](#model-hook-project-code-handler-python-hook-handler)

### preCreate 핸들러 구현
<a name="model-hook-project-code-handler-python-precreate"></a>

`preCreate` 핸들러는 `AWS::S3::Bucket ` 또는 `AWS::SQS::Queue` 리소스에 대한 서버 측 암호화 설정을 확인합니다.
+ `AWS::S3::Bucket` 리소스의 경우 후크는 다음이 true인 경우에만 전달합니다.
  + Amazon S3 버킷 암호화가 설정되어 있습니다.
  + 버킷에 대해 Amazon S3 버킷 키가 활성화됩니다.
  + Amazon S3 버킷에 설정된 암호화 알고리즘이 필요한 올바른 알고리즘입니다.
  +  AWS Key Management Service 키 ID가 설정됩니다.
+ `AWS::SQS::Queue` 리소스의 경우 후크는 다음과 같은 경우에만 전달합니다.
  +  AWS Key Management Service 키 ID가 설정됩니다.

### preUpdate 핸들러 구현
<a name="model-hook-project-code-handler-python-preupdate"></a>

`preUpdate` 핸들러에서 지정된 모든 대상에 대한 업데이트 작업 전에 시작하는 핸들러를 구현합니다. `preUpdate` 핸들러는 다음을 수행합니다.
+ `AWS::S3::Bucket` 리소스의 경우 후크는 다음과 같은 경우에만 전달합니다.
  + Amazon S3 버킷의 버킷 암호화 알고리즘이 수정되지 않았습니다.

### preDelete 핸들러 구현
<a name="model-hook-project-code-handler-python-predelete"></a>

`preDelete` 핸들러에서 지정된 모든 대상에 대한 삭제 작업 전에 시작하는 핸들러를 구현합니다. `preDelete` 핸들러는 다음을 수행합니다.
+ `AWS::S3::Bucket` 리소스의 경우 후크는 다음과 같은 경우에만 전달합니다.
  + 리소스를 삭제한 후 계정에 필요한 최소 규정 준수 리소스가 존재하는지 확인합니다.
  + 필요한 최소 규정 준수 리소스 양은 후크의 구성에서 설정됩니다.

### 후크 핸들러 구현
<a name="model-hook-project-code-handler-python-hook-handler"></a>

1. IDE에서 `src` 폴더에 있는 `handlers.py` 파일을 엽니다.

1. `handlers.py` 파일의 전체 내용을 다음 코드로 바꿉니다.  
**Example handlers.py**  

   ```
   import logging
   from typing import Any, MutableMapping, Optional
   import botocore
   
   from cloudformation_cli_python_lib import (
       BaseHookHandlerRequest,
       HandlerErrorCode,
       Hook,
       HookInvocationPoint,
       OperationStatus,
       ProgressEvent,
       SessionProxy,
       exceptions,
   )
   
   from .models import HookHandlerRequest, TypeConfigurationModel
   
   # Use this logger to forward log messages to CloudWatch Logs.
   LOG = logging.getLogger(__name__)
   TYPE_NAME = "MyCompany::Testing::MyTestHook"
   
   LOG.setLevel(logging.INFO)
   
   hook = Hook(TYPE_NAME, TypeConfigurationModel)
   test_entrypoint = hook.test_entrypoint
   
   
   def _validate_s3_bucket_encryption(
       bucket: MutableMapping[str, Any], required_encryption_algorithm: str
   ) -> ProgressEvent:
       status = None
       message = ""
       error_code = None
   
       if bucket:
           bucket_name = bucket.get("BucketName")
   
           bucket_encryption = bucket.get("BucketEncryption")
           if bucket_encryption:
               server_side_encryption_rules = bucket_encryption.get(
                   "ServerSideEncryptionConfiguration"
               )
               if server_side_encryption_rules:
                   for rule in server_side_encryption_rules:
                       bucket_key_enabled = rule.get("BucketKeyEnabled")
                       if bucket_key_enabled:
                           server_side_encryption_by_default = rule.get(
                               "ServerSideEncryptionByDefault"
                           )
   
                           encryption_algorithm = server_side_encryption_by_default.get(
                               "SSEAlgorithm"
                           )
                           kms_key_id = server_side_encryption_by_default.get(
                               "KMSMasterKeyID"
                           )  # "KMSMasterKeyID" is name of the property for an AWS::S3::Bucket
   
                           if encryption_algorithm == required_encryption_algorithm:
                               if encryption_algorithm == "aws:kms" and not kms_key_id:
                                   status = OperationStatus.FAILED
                                   message = f"KMS Key ID not set for bucket with name: f{bucket_name}"
                               else:
                                   status = OperationStatus.SUCCESS
                                   message = f"Successfully invoked PreCreateHookHandler for AWS::S3::Bucket with name: {bucket_name}"
                           else:
                               status = OperationStatus.FAILED
                               message = f"SSE Encryption Algorithm is incorrect for bucket with name: {bucket_name}"
                       else:
                           status = OperationStatus.FAILED
                           message = f"Bucket key not enabled for bucket with name: {bucket_name}"
   
                       if status == OperationStatus.FAILED:
                           break
               else:
                   status = OperationStatus.FAILED
                   message = f"No SSE Encryption configurations for bucket with name: {bucket_name}"
           else:
               status = OperationStatus.FAILED
               message = (
                   f"Bucket Encryption not enabled for bucket with name: {bucket_name}"
               )
       else:
           status = OperationStatus.FAILED
           message = "Resource properties for S3 Bucket target model are empty"
   
       if status == OperationStatus.FAILED:
           error_code = HandlerErrorCode.NonCompliant
   
       return ProgressEvent(status=status, message=message, errorCode=error_code)
   
   
   def _validate_sqs_queue_encryption(queue: MutableMapping[str, Any]) -> ProgressEvent:
       if not queue:
           return ProgressEvent(
               status=OperationStatus.FAILED,
               message="Resource properties for SQS Queue target model are empty",
               errorCode=HandlerErrorCode.NonCompliant,
           )
       queue_name = queue.get("QueueName")
   
       kms_key_id = queue.get(
           "KmsMasterKeyId"
       )  # "KmsMasterKeyId" is name of the property for an AWS::SQS::Queue
       if not kms_key_id:
           return ProgressEvent(
               status=OperationStatus.FAILED,
               message=f"Server side encryption turned off for queue with name: {queue_name}",
               errorCode=HandlerErrorCode.NonCompliant,
           )
   
       return ProgressEvent(
           status=OperationStatus.SUCCESS,
           message=f"Successfully invoked PreCreateHookHandler for targetAWS::SQS::Queue with name: {queue_name}",
       )
   
   
   @hook.handler(HookInvocationPoint.CREATE_PRE_PROVISION)
   def pre_create_handler(
       session: Optional[SessionProxy],
       request: HookHandlerRequest,
       callback_context: MutableMapping[str, Any],
       type_configuration: TypeConfigurationModel,
   ) -> ProgressEvent:
       target_name = request.hookContext.targetName
       if "AWS::S3::Bucket" == target_name:
           return _validate_s3_bucket_encryption(
               request.hookContext.targetModel.get("resourceProperties"),
               type_configuration.encryptionAlgorithm,
           )
       elif "AWS::SQS::Queue" == target_name:
           return _validate_sqs_queue_encryption(
               request.hookContext.targetModel.get("resourceProperties")
           )
       else:
           raise exceptions.InvalidRequest(f"Unknown target type: {target_name}")
   
   
   def _validate_bucket_encryption_rules_not_updated(
       resource_properties, previous_resource_properties
   ) -> ProgressEvent:
       bucket_encryption_configs = resource_properties.get("BucketEncryption", {}).get(
           "ServerSideEncryptionConfiguration", []
       )
       previous_bucket_encryption_configs = previous_resource_properties.get(
           "BucketEncryption", {}
       ).get("ServerSideEncryptionConfiguration", [])
   
       if len(bucket_encryption_configs) != len(previous_bucket_encryption_configs):
           return ProgressEvent(
               status=OperationStatus.FAILED,
               message=f"Current number of bucket encryption configs does not match previous. Current has {str(len(bucket_encryption_configs))} configs while previously there were {str(len(previous_bucket_encryption_configs))} configs",
               errorCode=HandlerErrorCode.NonCompliant,
           )
   
       for i in range(len(bucket_encryption_configs)):
           current_encryption_algorithm = (
               bucket_encryption_configs[i]
               .get("ServerSideEncryptionByDefault", {})
               .get("SSEAlgorithm")
           )
           previous_encryption_algorithm = (
               previous_bucket_encryption_configs[i]
               .get("ServerSideEncryptionByDefault", {})
               .get("SSEAlgorithm")
           )
   
           if current_encryption_algorithm != previous_encryption_algorithm:
               return ProgressEvent(
                   status=OperationStatus.FAILED,
                   message=f"Bucket Encryption algorithm can not be changed once set. The encryption algorithm was changed to {current_encryption_algorithm} from {previous_encryption_algorithm}.",
                   errorCode=HandlerErrorCode.NonCompliant,
               )
   
       return ProgressEvent(
           status=OperationStatus.SUCCESS,
           message="Successfully invoked PreUpdateHookHandler for target: AWS::SQS::Queue",
       )
   
   
   def _validate_queue_encryption_not_disabled(
       resource_properties, previous_resource_properties
   ) -> ProgressEvent:
       if previous_resource_properties.get(
           "KmsMasterKeyId"
       ) and not resource_properties.get("KmsMasterKeyId"):
           return ProgressEvent(
               status=OperationStatus.FAILED,
               errorCode=HandlerErrorCode.NonCompliant,
               message="Queue encryption can not be disable",
           )
       else:
           return ProgressEvent(status=OperationStatus.SUCCESS)
   
   
   @hook.handler(HookInvocationPoint.UPDATE_PRE_PROVISION)
   def pre_update_handler(
       session: Optional[SessionProxy],
       request: BaseHookHandlerRequest,
       callback_context: MutableMapping[str, Any],
       type_configuration: MutableMapping[str, Any],
   ) -> ProgressEvent:
       target_name = request.hookContext.targetName
       if "AWS::S3::Bucket" == target_name:
           resource_properties = request.hookContext.targetModel.get("resourceProperties")
           previous_resource_properties = request.hookContext.targetModel.get(
               "previousResourceProperties"
           )
   
           return _validate_bucket_encryption_rules_not_updated(
               resource_properties, previous_resource_properties
           )
       elif "AWS::SQS::Queue" == target_name:
           resource_properties = request.hookContext.targetModel.get("resourceProperties")
           previous_resource_properties = request.hookContext.targetModel.get(
               "previousResourceProperties"
           )
   
           return _validate_queue_encryption_not_disabled(
               resource_properties, previous_resource_properties
           )
       else:
           raise exceptions.InvalidRequest(f"Unknown target type: {target_name}")
   ```

다음 항목인 [에 사용자 지정 후크 등록 CloudFormation](registering-hooks.md) 단원으로 이동합니다.

# 에 사용자 지정 후크 등록 CloudFormation
<a name="registering-hooks"></a>

사용자 지정 후크를 생성한 후에는에 등록해야 사용할 CloudFormation 수 있습니다. 이 섹션에서는에서 사용할 후크를 패키징하고 등록하는 방법을 알아봅니다 AWS 계정.

## 후크 패키징(Java)
<a name="registering-hooks-package"></a>

Java로 후크를 개발한 경우 Maven을 사용하여 후크를 패키징합니다.

후크 프로젝트의 디렉터리에서 다음 명령을 실행하여 후크를 빌드하고, 단위 테스트를 실행하고, 프로젝트를 CloudFormation 레지스트리에 후크를 제출하는 데 사용할 수 있는 `JAR` 파일로 패키징합니다.

```
mvn clean package
```

## 사용자 지정 후크 등록
<a name="registering-hooks-register"></a>

**후크를 등록하려면**

1. (선택 사항) [https://docs.aws.amazon.com/cli/latest/reference/configure/](https://docs.aws.amazon.com/cli/latest/reference/configure/) 작업을 제출하여 기본 AWS 리전 이름을 `us-west-2`로 구성합니다.

   ```
   $ aws configure
   AWS Access Key ID [None]: <Your Access Key ID>
   AWS Secret Access Key [None]: <Your Secret Key>
   Default region name [None]: us-west-2
   Default output format [None]: json
   ```

1. (선택 사항) 다음 명령은 Hook 프로젝트를 등록하지 않고 빌드하고 패키징합니다.

   ```
   $ cfn submit --dry-run
   ```

1. CloudFormation CLI [https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-cli-submit.html](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-cli-submit.html) 작업을 사용하여 후크를 등록합니다.

   ```
   $ cfn submit --set-default
   ```

   이 명령은 다음 명령을 반환합니다.

   ```
   {‘ProgressStatus’: ‘COMPLETE’}
   ```

   *결과*: 후크를 성공적으로 등록했습니다.

## 계정에서 후크에 액세스할 수 있는지 확인
<a name="verifying-hooks"></a>

후크를 제출한 리전 AWS 계정 및에서 후크를 사용할 수 있는지 확인합니다.

1. 후크를 확인하려면 [https://docs.aws.amazon.com/cli/latest/reference/cloudformation/list-types.html](https://docs.aws.amazon.com/cli/latest/reference/cloudformation/list-types.html) 명령을 사용하여 새로 등록된 후크를 나열하고 요약 설명을 반환합니다.

   ```
   $ aws cloudformation list-types
   ```

   명령은 다음 출력을 반환하고 AWS 계정 및 리전에서 활성화할 수 있는 공개적으로 사용 가능한 후크도 표시합니다.

   ```
   {
       "TypeSummaries": [
           {
               "Type": "HOOK",
               "TypeName": "MyCompany::Testing::MyTestHook",
               "DefaultVersionId": "00000001",
               "TypeArn": "arn:aws:cloudformation:us-west-2:ACCOUNT_ID/type/hook/MyCompany-Testing-MyTestHook",
               "LastUpdated": "2021-08-04T23:00:03.058000+00:00",
               "Description": "Verifies S3 bucket and SQS queues properties before creating or updating"
           }
       ]
   }
   ```

1. 후크의 `list-type` 출력`TypeArn`에서를 검색하고 저장합니다.

   ```
   export HOOK_TYPE_ARN=arn:aws:cloudformation:us-west-2:ACCOUNT_ID/type/hook/MyCompany-Testing-MyTestHook
   ```

퍼블릭 용도로 후크를 게시하는 방법은 섹션을 참조하세요[퍼블릭 사용을 위한 후크 게시](hooks-publishing.md).

### 후크 구성
<a name="configure-hooks"></a>

후크를 개발하고 등록한 후 레지스트리에 게시 AWS 계정 하여에서 후크를 구성할 수 있습니다.
+ 계정에서 후크를 구성하려면 [https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_SetTypeConfiguration.html](https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_SetTypeConfiguration.html) 작업을 사용합니다. 이 작업은 후크의 스키마 `properties` 섹션에 정의된 후크의 속성을 활성화합니다. 다음 예제에서는 구성에서 `minBuckets` 속성이 `1` 로 설정됩니다.
**참고**  
계정에서 후크를 활성화하면 후크가의 정의된 권한을 사용하도록 승인하는 것입니다 AWS 계정. CloudFormation은 사용자의 권한을 후크에 전달하기 전에 필요하지 않은 권한을 제거합니다. CloudFormation에서는 고객 또는 후크 사용자에게 계정에서 후크를 활성화하기 전에 후크 권한을 검토하고 후크가 허용되는 권한을 인식할 것을 권장합니다.

  동일한 계정 및에서 등록된 후크 확장에 대한 구성 데이터를 지정합니다 AWS 리전.

  ```
  $ aws cloudformation set-type-configuration --region us-west-2 
    --configuration '{"CloudFormationConfiguration":{"HookConfiguration":{"HookInvocationStatus":"ENABLED","FailureMode":"FAIL","Properties":{"minBuckets": "1","minQueues": "1", "encryptionAlgorithm": "aws:kms"}}}}'
    --type-arn $HOOK_TYPE_ARN
  ```
**중요**  
후크가 스택의 구성을 사전에 검사할 수 있도록 하려면 계정에 후크를 등록하고 활성화한 후 `ENABLED` `HookConfiguration` 섹션에서를 `HookInvocationStatus`로 설정해야 합니다.

## 핸들러에서 AWS APIs
<a name="accessing-apis-in-handlers"></a>

후크가 핸들러에서 AWS API를 사용하는 경우 CFN-CLI는 IAM 실행 역할 템플릿인를 자동으로 생성합니다`hook-role.yaml`. `hook-role.yaml` 템플릿은 후크 스키마의 핸들러 섹션에 있는 각 핸들러에 지정된 권한을 기반으로 합니다. [https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-cli-generate.html](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-cli-generate.html) 작업 중에 `--role-arn` 플래그를 사용하지 않으면이 스택의 역할이 프로비저닝되어 후크의 실행 역할로 사용됩니다.

자세한 내용은 [리소스 유형에서 AWS APIs 액세스를 참조하세요.](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-develop.html#resource-type-develop-executionrole)

### hook-role.yaml 템플릿
<a name="resource-role.yaml"></a>

**참고**  
자체 실행 역할을 생성하기로 선택한 경우 `hooks.cloudformation.amazonaws.com` 및 만 허용하여 최소 권한 원칙을 연습하는 것이 좋습니다`resources.cloudformation.amazonaws.com`.

다음 템플릿은 IAM, Amazon S3 및 Amazon SQS 권한을 사용합니다.

```
AWSTemplateFormatVersion: 2010-09-09
Description: >
  This CloudFormation template creates a role assumed by CloudFormation during
  Hook operations on behalf of the customer.
Resources:
  ExecutionRole:
    Type: 'AWS::IAM::Role'
    Properties:
      MaxSessionDuration: 8400
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - resources.cloudformation.amazonaws.com
                - hooks.cloudformation.amazonaws.com
            Action: 'sts:AssumeRole'
            Condition:
              StringEquals:
                aws:SourceAccount: !Ref AWS::AccountId
              StringLike:
                aws:SourceArn: !Sub arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:type/hook/MyCompany-Testing-MyTestHook/*
      Path: /
      Policies:
        - PolicyName: HookTypePolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 's3:GetEncryptionConfiguration'
                  - 's3:ListBucket'
                  - 's3:ListAllMyBuckets'
                  - 'sqs:GetQueueAttributes'
                  - 'sqs:GetQueueUrl'
                  - 'sqs:ListQueues'
                Resource: '*'
Outputs:
  ExecutionRoleArn:
    Value: !GetAtt 
      - ExecutionRole
      - Arn
```

# 에서 사용자 지정 후크 테스트 AWS 계정
<a name="testing-hooks"></a>

호출 지점에 해당하는 핸들러 함수를 코딩했으므로 이제 CloudFormation 스택에서 사용자 지정 후크를 테스트할 차례입니다.

CloudFormation 템플릿이 다음을 사용하여 S3 버킷을 프로비저닝하지 않은 `FAIL` 경우 후크 실패 모드는 로 설정됩니다.
+ Amazon S3 버킷 암호화가 설정되어 있습니다.
+ 버킷에 대해 Amazon S3 버킷 키가 활성화됩니다.
+ Amazon S3 버킷에 설정된 암호화 알고리즘이 필요한 올바른 알고리즘입니다.
+  AWS Key Management Service 키 ID가 설정됩니다.

다음 예제에서는 스택 구성`my-failed-bucket-stack.yml`에 실패하고 리소스가 프로비저닝되기 전에 중지`my-hook-stack`되는 스택 이름이 인 라는 템플릿을 생성합니다.

## 스택을 프로비저닝하여 후크 테스트
<a name="testing-hooks-provision-stack"></a>

### 예제 1: 스택 프로비저닝
<a name="provision-a-stack-example-1"></a>

**규정 미준수 스택 프로비저닝**

1. S3 버킷을 지정하는 템플릿을 작성합니다. 예를 들어 `my-failed-bucket-stack.yml`입니다.

   ```
   AWSTemplateFormatVersion: 2010-09-09
   Resources:
     S3Bucket:
       Type: AWS::S3::Bucket
       Properties: {}
   ```

1. 스택을 생성하고 AWS Command Line Interface ()에서 템플릿을 지정합니다AWS CLI. 다음 예제에서는 스택 이름을 로 지정`my-hook-stack`하고 템플릿 이름을 로 지정합니다`my-failed-bucket-stack.yml`.

   ```
   $ aws cloudformation create-stack \
     --stack-name my-hook-stack \
     --template-body file://my-failed-bucket-stack.yml
   ```

1. (선택 사항) 스택 이름을 지정하여 스택 진행 상황을 확인합니다. 다음 예제에서는 스택 이름을 지정합니다`my-hook-stack`.

   ```
   $ aws cloudformation describe-stack-events \
     --stack-name my-hook-stack
   ```

   `describe-stack-events` 작업을 사용하여 버킷을 생성하는 동안 후크 실패를 확인합니다. 다음은 명령의 출력 예입니다.

   ```
   {
       "StackEvents": [
       ...
           {
               "StackId": "arn:aws:cloudformation:us-west-2:123456789012:stack/my-hook-stack/2c693970-f57e-11eb-a0fb-061a2a83f0b9",
               "EventId": "S3Bucket-CREATE_FAILED-2021-08-04T23:47:03.305Z",
               "StackName": "my-hook-stack",
               "LogicalResourceId": "S3Bucket",
               "PhysicalResourceId": "",
               "ResourceType": "AWS::S3::Bucket",
               "Timestamp": "2021-08-04T23:47:03.305000+00:00",
               "ResourceStatus": "CREATE_FAILED",
               "ResourceStatusReason": "The following hook(s) failed: [MyCompany::Testing::MyTestHook]",
               "ResourceProperties": "{}",
               "ClientRequestToken": "Console-CreateStack-abe71ac2-ade4-a762-0499-8d34d91d6a92"
           },
       ...
       ]
   }
   ```

   *결과*: 후크 호출이 스택 구성에 실패하고 리소스 프로비저닝이 중지되었습니다.

**CloudFormation 템플릿을 사용하여 후크 검증 통과**

1. 스택을 생성하고 후크 검증을 통과하려면 리소스가 암호화된 S3 버킷을 사용하도록 템플릿을 업데이트합니다. 이 예제에서는 `my-encrypted-bucket-stack.yml` 템플릿을 사용합니다.

   ```
   AWSTemplateFormatVersion: 2010-09-09
   Description: |
     This CloudFormation template provisions an encrypted S3 Bucket
   Resources:
     EncryptedS3Bucket:
       Type: AWS::S3::Bucket
       Properties:
         BucketName: !Sub encryptedbucket-${AWS::Region}-${AWS::AccountId}
         BucketEncryption:
           ServerSideEncryptionConfiguration:
             - ServerSideEncryptionByDefault:
                 SSEAlgorithm: 'aws:kms'
                 KMSMasterKeyID: !Ref EncryptionKey
               BucketKeyEnabled: true
     EncryptionKey:
       Type: AWS::KMS::Key
       DeletionPolicy: Retain
       Properties:
         Description: KMS key used to encrypt the resource type artifacts
         EnableKeyRotation: true
         KeyPolicy:
           Version: 2012-10-17
           Statement:
             - Sid: Enable full access for owning account
               Effect: Allow
               Principal:
                 AWS: !Ref AWS::AccountId
               Action: 'kms:*'
               Resource: '*'
   Outputs:
     EncryptedBucketName:
       Value: !Ref EncryptedS3Bucket
   ```
**참고**  
건너뛴 리소스에 대해서는 후크가 호출되지 않습니다.

1. 스택을 생성하고 템플릿을 지정합니다. 이 예제에서 스택 이름은 입니다`my-encrypted-bucket-stack`.

   ```
   $ aws cloudformation create-stack \
     --stack-name my-encrypted-bucket-stack \
     --template-body file://my-encrypted-bucket-stack.yml \
   ```

1. (선택 사항) 스택 이름을 지정하여 스택 진행 상황을 확인합니다.

   ```
   $ aws cloudformation describe-stack-events \
     --stack-name my-encrypted-bucket-stack
   ```

   `describe-stack-events` 명령을 사용하여 응답을 봅니다. 다음은 `describe-stack-events` 명령의 예입니다.

   ```
   {
       "StackEvents": [
       ...
           {
               "StackId": "arn:aws:cloudformation:us-west-2:123456789012:stack/my-encrypted-bucket-stack/82a97150-f57a-11eb-8eb2-06a6bdcc7779",
               "EventId": "EncryptedS3Bucket-CREATE_COMPLETE-2021-08-04T23:23:20.973Z",
               "StackName": "my-encrypted-bucket-stack",
               "LogicalResourceId": "EncryptedS3Bucket",
               "PhysicalResourceId": "encryptedbucket-us-west-2-123456789012",
               "ResourceType": "AWS::S3::Bucket",
               "Timestamp": "2021-08-04T23:23:20.973000+00:00",
               "ResourceStatus": "CREATE_COMPLETE",
               "ResourceProperties": "{\"BucketName\":\"encryptedbucket-us-west-2-123456789012\",\"BucketEncryption\":{\"ServerSideEncryptionConfiguration\":[{\"BucketKeyEnabled\":\"true\",\"ServerSideEncryptionByDefault\":{\"SSEAlgorithm\":\"aws:kms\",\"KMSMasterKeyID\":\"ENCRYPTION_KEY_ARN\"}}]}}",
               "ClientRequestToken": "Console-CreateStack-39df35ac-ca00-b7f6-5661-4e917478d075"
           },
           {
               "StackId": "arn:aws:cloudformation:us-west-2:123456789012:stack/my-encrypted-bucket-stack/82a97150-f57a-11eb-8eb2-06a6bdcc7779",
               "EventId": "EncryptedS3Bucket-CREATE_IN_PROGRESS-2021-08-04T23:22:59.410Z",
               "StackName": "my-encrypted-bucket-stack",
               "LogicalResourceId": "EncryptedS3Bucket",
               "PhysicalResourceId": "encryptedbucket-us-west-2-123456789012",
               "ResourceType": "AWS::S3::Bucket",
               "Timestamp": "2021-08-04T23:22:59.410000+00:00",
               "ResourceStatus": "CREATE_IN_PROGRESS",
               "ResourceStatusReason": "Resource creation Initiated",
               "ResourceProperties": "{\"BucketName\":\"encryptedbucket-us-west-2-123456789012\",\"BucketEncryption\":{\"ServerSideEncryptionConfiguration\":[{\"BucketKeyEnabled\":\"true\",\"ServerSideEncryptionByDefault\":{\"SSEAlgorithm\":\"aws:kms\",\"KMSMasterKeyID\":\"ENCRYPTION_KEY_ARN\"}}]}}",
               "ClientRequestToken": "Console-CreateStack-39df35ac-ca00-b7f6-5661-4e917478d075"
           },
           {
               "StackId": "arn:aws:cloudformation:us-west-2:123456789012:stack/my-encrypted-bucket-stack/82a97150-f57a-11eb-8eb2-06a6bdcc7779",
               "EventId": "EncryptedS3Bucket-6516081f-c1f2-4bfe-a0f0-cefa28679994",
               "StackName": "my-encrypted-bucket-stack",
               "LogicalResourceId": "EncryptedS3Bucket",
               "PhysicalResourceId": "",
               "ResourceType": "AWS::S3::Bucket",
               "Timestamp": "2021-08-04T23:22:58.349000+00:00",
               "ResourceStatus": "CREATE_IN_PROGRESS",
               "ResourceStatusReason": "Hook invocations complete.  Resource creation initiated",
               "ClientRequestToken": "Console-CreateStack-39df35ac-ca00-b7f6-5661-4e917478d075"
           },
       ...
       ]
   }
   ```

   *결과*: CloudFormation에서 스택을 성공적으로 생성했습니다. 후크의 로직은 `AWS::S3::Bucket` 리소스를 프로비저닝하기 전에 리소스에 서버 측 암호화가 포함되어 있는지 확인했습니다.

### 예제 2: 스택 프로비저닝
<a name="provision-a-stack-example-2"></a>

**규정 미준수 스택 프로비저닝**

1. S3 버킷을 지정하는 템플릿을 작성합니다. 예: `aes256-bucket.yml`.

   ```
   AWSTemplateFormatVersion: 2010-09-09
   Description: |
     This CloudFormation template provisions an encrypted S3 Bucket
   Resources:
     EncryptedS3Bucket:
       Type: AWS::S3::Bucket
       Properties:
         BucketName: !Sub encryptedbucket-${AWS::Region}-${AWS::AccountId}
         BucketEncryption:
           ServerSideEncryptionConfiguration:
             - ServerSideEncryptionByDefault:
                 SSEAlgorithm: AES256
               BucketKeyEnabled: true
   Outputs:
     EncryptedBucketName:
       Value: !Ref EncryptedS3Bucket
   ```

1. 스택을 생성하고에서 템플릿을 지정합니다 AWS CLI. 다음 예제에서는 스택 이름을 로 지정`my-hook-stack`하고 템플릿 이름을 로 지정합니다`aes256-bucket.yml`.

   ```
   $ aws cloudformation create-stack \
     --stack-name my-hook-stack \
     --template-body file://aes256-bucket.yml
   ```

1. (선택 사항) 스택 이름을 지정하여 스택 진행 상황을 확인합니다. 다음 예제에서는 스택 이름을 지정합니다`my-hook-stack`.

   ```
   $ aws cloudformation describe-stack-events \
     --stack-name my-hook-stack
   ```

   `describe-stack-events` 작업을 사용하여 버킷을 생성하는 동안 후크 실패를 확인합니다. 다음은 명령의 출력 예입니다.

   ```
   {
       "StackEvents": [
       ...
           {
               "StackId": "arn:aws:cloudformation:us-west-2:123456789012:stack/my-hook-stack/2c693970-f57e-11eb-a0fb-061a2a83f0b9",
               "EventId": "S3Bucket-CREATE_FAILED-2021-08-04T23:47:03.305Z",
               "StackName": "my-hook-stack",
               "LogicalResourceId": "S3Bucket",
               "PhysicalResourceId": "",
               "ResourceType": "AWS::S3::Bucket",
               "Timestamp": "2021-08-04T23:47:03.305000+00:00",
               "ResourceStatus": "CREATE_FAILED",
               "ResourceStatusReason": "The following hook(s) failed: [MyCompany::Testing::MyTestHook]",
               "ResourceProperties": "{}",
               "ClientRequestToken": "Console-CreateStack-abe71ac2-ade4-a762-0499-8d34d91d6a92"
           },
       ...
       ]
   }
   ```

   *결과*: 후크 호출이 스택 구성에 실패하고 리소스 프로비저닝이 중지되었습니다. S3 버킷 암호화가 잘못 구성되어 스택이 실패했습니다. 이 버킷이를 사용하는 `aws:kms` 동안 후크 유형 구성에가 필요합니다`AES256`.

**CloudFormation 템플릿을 사용하여 후크 검증 통과**

1. 스택을 생성하고 후크 검증을 통과하려면 리소스가 암호화된 S3 버킷을 사용하도록 템플릿을 업데이트합니다. 이 예제에서는 `kms-bucket-and-queue.yml` 템플릿을 사용합니다.

   ```
   AWSTemplateFormatVersion: 2010-09-09
   Description: |
     This CloudFormation template provisions an encrypted S3 Bucket
   Resources:
     EncryptedS3Bucket:
       Type: AWS::S3::Bucket
       Properties:
         BucketName: !Sub encryptedbucket-${AWS::Region}-${AWS::AccountId}
         BucketEncryption:
           ServerSideEncryptionConfiguration:
             - ServerSideEncryptionByDefault:
                 SSEAlgorithm: 'aws:kms'
                 KMSMasterKeyID: !Ref EncryptionKey
               BucketKeyEnabled: true
     EncryptedQueue:
       Type: AWS::SQS::Queue
       Properties:
         QueueName: !Sub encryptedqueue-${AWS::Region}-${AWS::AccountId}
         KmsMasterKeyId: !Ref EncryptionKey
     EncryptionKey:
       Type: AWS::KMS::Key
       DeletionPolicy: Retain
       Properties:
         Description: KMS key used to encrypt the resource type artifacts
         EnableKeyRotation: true
         KeyPolicy:
           Version: 2012-10-17
           Statement:
             - Sid: Enable full access for owning account
               Effect: Allow
               Principal:
                 AWS: !Ref AWS::AccountId
               Action: 'kms:*'
               Resource: '*'
   Outputs:
     EncryptedBucketName:
       Value: !Ref EncryptedS3Bucket
     EncryptedQueueName:
       Value: !Ref EncryptedQueue
   ```
**참고**  
건너뛴 리소스에 대해서는 후크가 호출되지 않습니다.

1. 스택을 생성하고 템플릿을 지정합니다. 이 예제에서 스택 이름은 입니다`my-encrypted-bucket-stack`.

   ```
   $ aws cloudformation create-stack \
     --stack-name my-encrypted-bucket-stack \
     --template-body file://kms-bucket-and-queue.yml
   ```

1. (선택 사항) 스택 이름을 지정하여 스택 진행 상황을 확인합니다.

   ```
   $ aws cloudformation describe-stack-events \
     --stack-name my-encrypted-bucket-stack
   ```

   `describe-stack-events` 명령을 사용하여 응답을 봅니다. 다음은 `describe-stack-events` 명령의 예입니다.

   ```
   {
       "StackEvents": [
       ...
           {
               "StackId": "arn:aws:cloudformation:us-west-2:123456789012:stack/my-encrypted-bucket-stack/82a97150-f57a-11eb-8eb2-06a6bdcc7779",
               "EventId": "EncryptedS3Bucket-CREATE_COMPLETE-2021-08-04T23:23:20.973Z",
               "StackName": "my-encrypted-bucket-stack",
               "LogicalResourceId": "EncryptedS3Bucket",
               "PhysicalResourceId": "encryptedbucket-us-west-2-123456789012",
               "ResourceType": "AWS::S3::Bucket",
               "Timestamp": "2021-08-04T23:23:20.973000+00:00",
               "ResourceStatus": "CREATE_COMPLETE",
               "ResourceProperties": "{\"BucketName\":\"encryptedbucket-us-west-2-123456789012\",\"BucketEncryption\":{\"ServerSideEncryptionConfiguration\":[{\"BucketKeyEnabled\":\"true\",\"ServerSideEncryptionByDefault\":{\"SSEAlgorithm\":\"aws:kms\",\"KMSMasterKeyID\":\"ENCRYPTION_KEY_ARN\"}}]}}",
               "ClientRequestToken": "Console-CreateStack-39df35ac-ca00-b7f6-5661-4e917478d075"
           },
           {
               "StackId": "arn:aws:cloudformation:us-west-2:123456789012:stack/my-encrypted-bucket-stack/82a97150-f57a-11eb-8eb2-06a6bdcc7779",
               "EventId": "EncryptedS3Bucket-CREATE_IN_PROGRESS-2021-08-04T23:22:59.410Z",
               "StackName": "my-encrypted-bucket-stack",
               "LogicalResourceId": "EncryptedS3Bucket",
               "PhysicalResourceId": "encryptedbucket-us-west-2-123456789012",
               "ResourceType": "AWS::S3::Bucket",
               "Timestamp": "2021-08-04T23:22:59.410000+00:00",
               "ResourceStatus": "CREATE_IN_PROGRESS",
               "ResourceStatusReason": "Resource creation Initiated",
               "ResourceProperties": "{\"BucketName\":\"encryptedbucket-us-west-2-123456789012\",\"BucketEncryption\":{\"ServerSideEncryptionConfiguration\":[{\"BucketKeyEnabled\":\"true\",\"ServerSideEncryptionByDefault\":{\"SSEAlgorithm\":\"aws:kms\",\"KMSMasterKeyID\":\"ENCRYPTION_KEY_ARN\"}}]}}",
               "ClientRequestToken": "Console-CreateStack-39df35ac-ca00-b7f6-5661-4e917478d075"
           },
           {
               "StackId": "arn:aws:cloudformation:us-west-2:123456789012:stack/my-encrypted-bucket-stack/82a97150-f57a-11eb-8eb2-06a6bdcc7779",
               "EventId": "EncryptedS3Bucket-6516081f-c1f2-4bfe-a0f0-cefa28679994",
               "StackName": "my-encrypted-bucket-stack",
               "LogicalResourceId": "EncryptedS3Bucket",
               "PhysicalResourceId": "",
               "ResourceType": "AWS::S3::Bucket",
               "Timestamp": "2021-08-04T23:22:58.349000+00:00",
               "ResourceStatus": "CREATE_IN_PROGRESS",
               "ResourceStatusReason": "Hook invocations complete.  Resource creation initiated",
               "ClientRequestToken": "Console-CreateStack-39df35ac-ca00-b7f6-5661-4e917478d075"
           },
       ...
       ]
   }
   ```

   *결과*: CloudFormation에서 스택을 성공적으로 생성했습니다. 후크의 로직은 `AWS::S3::Bucket` 리소스를 프로비저닝하기 전에 리소스에 서버 측 암호화가 포함되어 있는지 확인했습니다.

# 사용자 지정 후크 업데이트
<a name="updating-registered-hook"></a>

사용자 지정 후크를 업데이트하면 CloudFormation 레지스트리에서 후크의 개정을 사용할 수 있습니다.

사용자 지정 후크를 업데이트하려면 CloudFormation CLI [https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-cli-submit.html](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-cli-submit.html) 작업을 통해 개정을 CloudFormation 레지스트리에 제출합니다.

```
$ cfn submit
```

계정에서 후크의 기본 버전을 지정하려면 [https://docs.aws.amazon.com/cli/latest/reference/cloudformation/set-type-default-version.html](https://docs.aws.amazon.com/cli/latest/reference/cloudformation/set-type-default-version.html) 명령을 사용하고 유형, 유형 이름 및 버전 ID를 지정합니다.

```
$ aws cloudformation set-type-default-version \
    --type HOOK \
    --type-name MyCompany::Testing::MyTestHook \
    --version-id 00000003
```

후크 버전에 대한 정보를 검색하려면를 사용합니다[https://docs.aws.amazon.com/cli/latest/reference/cloudformation/list-type-versions.html](https://docs.aws.amazon.com/cli/latest/reference/cloudformation/list-type-versions.html).

```
$ aws cloudformation list-type-versions \
  --type HOOK \
  --type-name "MyCompany::Testing::MyTestHook"
```

# CloudFormation 레지스트리에서 사용자 지정 후크 등록 취소
<a name="deregistering-hooks"></a>

사용자 지정 후크의 등록을 취소하면 확장 또는 확장 버전이 CloudFormation 레지스트리`DEPRECATED`에서 로 표시되므로 활성 사용에서 제거됩니다. 더 이상 사용되지 않으면 CloudFormation 작업에서 사용자 지정 후크를 사용할 수 없습니다.

**참고**  
후크 등록을 취소하기 전에 해당 확장의 이전 활성 버전을 모두 개별적으로 등록 취소해야 합니다. 자세한 내용은 [https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_DeregisterType.html](https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_DeregisterType.html) 단원을 참조하십시오.

후크 등록을 취소하려면 [https://docs.aws.amazon.com/cli/latest/reference/cloudformation/deregister-type.html](https://docs.aws.amazon.com/cli/latest/reference/cloudformation/deregister-type.html) 작업을 사용하고 후크 ARN을 지정합니다.

```
$ aws cloudformation deregister-type \
    --arn HOOK_TYPE_ARN
```

이 명령은 출력을 생성하지 않습니다.

# 퍼블릭 사용을 위한 후크 게시
<a name="hooks-publishing"></a>

퍼블릭 타사 후크를 개발하려면 후크를 프라이빗 확장으로 개발합니다. 그런 다음 확장을 공개적으로 사용할 AWS 리전 수 있도록 하려는 각에서 다음을 수행합니다.

1. CloudFormation 레지스트리에 후크를 프라이빗 확장으로 등록합니다.

1. 후크를 테스트하여 CloudFormation 레지스트리에 게시하는 데 필요한 모든 요구 사항을 충족하는지 확인합니다.

1. 후크를 CloudFormation 레지스트리에 게시합니다.
**참고**  
지정된 리전에 익스텐션을 게시하기 전에 먼저 해당 리전에 익스텐션 게시자로 등록해야 합니다. 여러 리전에서 동시에이 작업을 수행하려면 * CloudFormation CLI 사용 설명서*[의 StackSets](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/publish-extension-stacksets.html).

후크를 개발하고 등록한 후에는 일반 CloudFormation 사용자가 타사 퍼블릭 익스텐션으로 CloudFormation 레지스트리에 *게시*하여 공개적으로 사용할 수 있도록 할 수 있습니다.

퍼블릭 타사 후크를 사용하면 프로비저닝 전에 AWS 리소스 구성을 사전에 검사할 수 있는 CloudFormation 사용자에게 제공할 수 있습니다. 프라이빗 후크와 마찬가지로 퍼블릭 후크는 CloudFormation AWS 내에서가 게시한 모든 후크와 동일하게 처리됩니다.

레지스트리에 게시된 후크는 해당 후크가 게시된 AWS 리전 의 모든 CloudFormation 사용자가 볼 수 있습니다. 그런 다음 사용자는 계정에서 확장을 *활성화*하여 템플릿에서 사용할 수 있습니다. 자세한 내용은 [사용 설명서의 CloudFormation 레지스트리에서 타사 퍼블릭 확장](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/registry-public.html) 사용을 참조하세요. *CloudFormation * 

# 퍼블릭 사용을 위한 사용자 지정 후크 테스트
<a name="hooks-testing-registered-hooks"></a>

등록된 사용자 지정 후크를 게시하려면 해당 후크에 대해 정의된 모든 테스트 요구 사항을 통과해야 합니다. 다음은 사용자 지정 후크를 타사 확장 프로그램으로 게시하기 전에 필요한 요구 사항 목록입니다.

각 핸들러와 대상은 두 번 테스트됩니다. 에 대해 한 번`SUCCESS`,에 대해 한 번`FAILED`.
+ `SUCCESS` 응답 사례의 경우:
  + 상태는 여야 합니다`SUCCESS`.
  + 오류 코드를 반환해서는 안 됩니다.
  + 지정된 경우 콜백 지연을 `0` 초로 설정해야 합니다.
+ `FAILED` 응답 사례의 경우:
  + 상태는 여야 합니다`FAILED`.
  + 오류 코드를 반환해야 합니다.
  + 응답에 메시지가 있어야 합니다.
  + 지정된 경우 콜백 지연을 `0` 초로 설정해야 합니다.
+ `IN_PROGRESS` 응답 사례의 경우:
  + 오류 코드를 반환해서는 안 됩니다.
  + `Result` 필드는 응답으로 설정해서는 안 됩니다.

# 계약 테스트에 사용할 입력 데이터 지정
<a name="hooks-input-data-contract-test"></a>

기본적으로는 후크 스키마에서 정의한 패턴에서 생성된 입력 속성을 사용하여 계약 테스트를 CloudFormation 수행합니다. 그러나 대부분의 후크는 프로비저닝 스택을 사전 생성하거나 사전 업데이트하기 위한 입력 속성에 프로비저닝되는 리소스를 이해해야 할 만큼 복잡합니다. 이를 해결하기 위해가 계약 테스트를 수행할 때 CloudFormation 사용하는 입력을 지정할 수 있습니다.

CloudFormation 는 계약 테스트를 수행할 때 사용할 입력 데이터를 지정하는 두 가지 방법을 제공합니다.
+ 파일을 재정의합니다.

  `overrides` 파일을 사용하면 `preCreate` `preUpdate` 및 `preDelete` 작업 테스트 중에에서 사용할 특정 속성에 대한 입력 데이터를 지정할 CloudFormation 수 있습니다.
+ 입력 파일

  다음과 같은 경우 여러 `input` 파일을 사용하여 계약 테스트 입력 데이터를 지정할 수도 있습니다.
  + 생성, 업데이트 및 삭제 작업을 위한 다른 입력 데이터 또는 테스트할 잘못된 데이터를 지정하거나 지정해야 합니다.
  + 여러 입력 데이터 세트를 지정하려고 합니다.

## 재정의 파일을 사용하여 입력 데이터 지정
<a name="hook-override-inputs"></a>

다음은 `overrides` 파일을 사용하는 Amazon S3 후크의 입력 데이터의 예입니다.

```
{
    "CREATE_PRE_PROVISION": {
        "AWS::S3::Bucket": {
            "resourceProperties": {
                "/BucketName": "encryptedbucket-us-west-2-contractor",
                "/BucketEncryption/ServerSideEncryptionConfiguration": [
                    {
                        "BucketKeyEnabled": true,
                        "ServerSideEncryptionByDefault": {
                            "KMSMasterKeyID": "KMS-KEY-ARN",
                            "SSEAlgorithm": "aws:kms"
                        }
                    }
                ]
            }
        },
        "AWS::SQS::Queue": {
            "resourceProperties": {
                "/QueueName": "MyQueueContract",
                "/KmsMasterKeyId": "hellocontract"
            }
        }
    },
    "UPDATE_PRE_PROVISION": {
        "AWS::S3::Bucket": {
            "resourceProperties": {
                "/BucketName": "encryptedbucket-us-west-2-contractor",
                "/BucketEncryption/ServerSideEncryptionConfiguration": [
                    {
                        "BucketKeyEnabled": true,
                        "ServerSideEncryptionByDefault": {
                            "KMSMasterKeyID": "KMS-KEY-ARN",
                            "SSEAlgorithm": "aws:kms"
                        }
                    }
                ]
            },
            "previousResourceProperties": {
                "/BucketName": "encryptedbucket-us-west-2-contractor",
                "/BucketEncryption/ServerSideEncryptionConfiguration": [
                    {
                        "BucketKeyEnabled": true,
                        "ServerSideEncryptionByDefault": {
                            "KMSMasterKeyID": "KMS-KEY-ARN",
                            "SSEAlgorithm": "aws:kms"
                        }
                    }
                ]
            }
        }
    },
    "INVALID_UPDATE_PRE_PROVISION": {
        "AWS::S3::Bucket": {
            "resourceProperties": {
                "/BucketName": "encryptedbucket-us-west-2-contractor",
                "/BucketEncryption/ServerSideEncryptionConfiguration": [
                    {
                        "BucketKeyEnabled": true,
                        "ServerSideEncryptionByDefault": {
                            "KMSMasterKeyID": "KMS-KEY-ARN",
                            "SSEAlgorithm": "AES256"
                        }
                    }
                ]
            },
            "previousResourceProperties": {
                "/BucketName": "encryptedbucket-us-west-2-contractor",
                "/BucketEncryption/ServerSideEncryptionConfiguration": [
                    {
                        "BucketKeyEnabled": true,
                        "ServerSideEncryptionByDefault": {
                            "KMSMasterKeyID": "KMS-KEY-ARN",
                            "SSEAlgorithm": "aws:kms"
                        }
                    }
                ]
            }
        }
    },
    "INVALID": {
        "AWS::SQS::Queue": {
            "resourceProperties": {
                "/QueueName": "MyQueueContract",
                "/KmsMasterKeyId": "KMS-KEY-ARN"
            }
        }
    }
}
```

## 입력 파일을 사용하여 입력 데이터 지정
<a name="hook-test-inputs"></a>

`input` 파일을 사용하여 입력`preCreate`, 입력, 잘못된 `preUpdate` 입력 등 CloudFormation 에서 사용할 다양한 종류의 입력 데이터를 지정합니다. 각 종류의 데이터는 별도의 파일에 지정됩니다. 계약 테스트를 위해 여러 입력 데이터 세트를 지정할 수도 있습니다.

계약 테스트에 CloudFormation 사용할의 `input` 파일을 지정하려면 Hooks 프로젝트의 루트 디렉터리에 `inputs` 폴더를 추가합니다. 그런 다음 입력 파일을 추가합니다.

다음 명명 규칙을 사용하여 파일에 포함된 입력 데이터의 종류를 지정합니다. 여기서 **n**은 정수입니다.
+ `inputs_n_pre_create.json`: `preCreate` 핸들러가 있는 파일을 사용하여 리소스를 생성하기 위한 입력을 지정합니다.
+ `inputs_n_pre_update.json`: `preUpdate` 핸들러가 있는 파일을 사용하여 리소스를 업데이트하기 위한 입력을 지정합니다.
+ `inputs_n_pre_delete.json`: `preDelete` 핸들러가 있는 파일을 사용하여 리소스를 삭제하기 위한 입력을 지정합니다.
+ `inputs_n_invalid.json`: 테스트할 잘못된 입력을 지정하는 데 사용됩니다.

계약 테스트를 위해 여러 입력 데이터 세트를 지정하려면 파일 이름에 정수를 증가시켜 입력 데이터 세트를 정렬합니다. 예를 들어 첫 번째 입력 파일 세트의 이름은 `inputs_1_pre_create.json`, `inputs_1_pre_update.json`및 이어야 합니다`inputs_1_pre_invalid.json`. 다음 세트의 이름은 `inputs_2_pre_create.json`, `inputs_2_pre_update.json`및 `inputs_2_pre_invalid.json`등입니다.

각 입력 파일은 테스트에 사용할 리소스 속성만 포함하는 JSON 파일입니다.

다음은 입력 파일을 사용하여 입력 데이터를 Amazon S3 지정하기 `inputs` 위한의 예제 디렉터리입니다.

`inputs_1_pre_create.json`  <a name="inputs_1_pre_create.json"></a>
다음은 `inputs_1_pre_create.json` 계약 테스트의 예입니다.  

```
{
    "AWS::S3::Bucket": {
        "resourceProperties": {
            "AccessControl": "BucketOwnerFullControl",
            "AnalyticsConfigurations": [],
            "BucketEncryption": {
                "ServerSideEncryptionConfiguration": [
                    {
                        "BucketKeyEnabled": true,
                        "ServerSideEncryptionByDefault": {
                            "KMSMasterKeyID": "KMS-KEY-ARN",
                            "SSEAlgorithm": "aws:kms"
                        }
                    }
                ]
            },
            "BucketName": "encryptedbucket-us-west-2"
        }
    },
    "AWS::SQS::Queue": {
        "resourceProperties": {
            "QueueName": "MyQueue",
            "KmsMasterKeyId": "KMS-KEY-ARN"
        }
    }
}
```

`inputs_1_pre_update.json`  <a name="inputs_1_pre_update.json"></a>
다음은 `inputs_1_pre_update.json` 계약 테스트의 예입니다.  

```
{
    "AWS::S3::Bucket": {
        "resourceProperties": {
            "BucketEncryption": {
                "ServerSideEncryptionConfiguration": [
                    {
                        "BucketKeyEnabled": true,
                        "ServerSideEncryptionByDefault": {
                            "KMSMasterKeyID": "KMS-KEY-ARN",
                            "SSEAlgorithm": "aws:kms"
                        }
                    }
                ]
            },
            "BucketName": "encryptedbucket-us-west-2"
        },
        "previousResourceProperties": {
            "BucketEncryption": {
                "ServerSideEncryptionConfiguration": [
                    {
                        "BucketKeyEnabled": true,
                        "ServerSideEncryptionByDefault": {
                            "KMSMasterKeyID": "KMS-KEY-ARN",
                            "SSEAlgorithm": "aws:kms"
                        }
                    }
                ]
            },
            "BucketName": "encryptedbucket-us-west-2"
        }
    }
}
```

`inputs_1_invalid.json`  <a name="inputs_1_invalid.json"></a>
다음은 `inputs_1_invalid.json` 계약 테스트의 예입니다.  

```
{
    "AWS::S3::Bucket": {
        "resourceProperties": {
            "AccessControl": "BucketOwnerFullControl",
            "AnalyticsConfigurations": [],
            "BucketEncryption": {
                "ServerSideEncryptionConfiguration": [
                    {
                        "ServerSideEncryptionByDefault": {
                            "SSEAlgorithm": "AES256"
                        }
                    }
                ]
            },
            "BucketName": "encryptedbucket-us-west-2"
        }
    },
    "AWS::SQS::Queue": {
        "resourceProperties": {
            "NotValid": "The property of this resource is not valid."
        }
    }
}
```

`inputs_1_invalid_pre_update.json`  <a name="inputs_1_invalid_pre_update.json"></a>
다음은 `inputs_1_invalid_pre_update.json` 계약 테스트의 예입니다.  

```
{
    "AWS::S3::Bucket": {
        "resourceProperties": {
            "BucketEncryption": {
                "ServerSideEncryptionConfiguration": [
                    {
                        "BucketKeyEnabled": true,
                        "ServerSideEncryptionByDefault": {
                            "KMSMasterKeyID": "KMS-KEY-ARN",
                            "SSEAlgorithm": "AES256"
                        }
                    }
                ]
            },
            "BucketName": "encryptedbucket-us-west-2"
        },
        "previousResourceProperties": {
            "BucketEncryption": {
                "ServerSideEncryptionConfiguration": [
                    {
                        "BucketKeyEnabled": true,
                        "ServerSideEncryptionByDefault": {
                            "KMSMasterKeyID": "KMS-KEY-ARN",
                            "SSEAlgorithm": "aws:kms"
                        }
                    }
                ]
            },
            "BucketName": "encryptedbucket-us-west-2"
        }
    }
}
```

자세한 내용을 알아보려면 * CloudFormation CLI 사용 설명서*의 [공개적으로 사용할 수 있도록 익스텐션 게시](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/publish-extension.html)를 참조하세요.

# CloudFormation 후크에 대한 스키마 구문 참조
<a name="hooks-schema"></a>

이 섹션에서는 CloudFormation 후크를 개발하는 데 사용하는 스키마의 구문을 설명합니다.

후크에는 JSON 스키마 및 후크 핸들러로 표현되는 후크 사양이 포함됩니다. 사용자 지정 후크를 생성하는 첫 번째 단계는 후크, 속성 및 속성을 정의하는 스키마를 모델링하는 것입니다. CloudFormation CLI [https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-cli-init.html](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-cli-init.html) 명령을 사용하여 사용자 지정 후크 프로젝트를 초기화하면 후크 스키마 파일이 생성됩니다. 이 스키마 파일을 사용자 지정 후크의 셰이프와 의미 체계를 정의하기 위한 시작점으로 사용합니다.

## 스키마 구문
<a name="schema-syntax"></a>

다음 스키마는 후크의 구조입니다.

```
{
"typeName": "string",
    "description": "string",
    "sourceUrl": "string",
    "documentationUrl": "string",
    "definitions": {
        "definitionName": {
          . . .
        }
    },
    "typeConfiguration": {
        "properties": {
             "propertyName": {
                "description": "string",
                "type": "string",
                 . . .
            },
        },
    "required": [
        "propertyName"
         . . .
            ],
    "additionalProperties": false
    },
    "handlers": {
        "preCreate": {
            "targetNames": [
            ],
            "permissions": [
            ]
        },
        "preUpdate": {
            "targetNames": [
            ],
            "permissions": [
            ]
        },
        "preDelete": {
            "targetNames": [
            ],
            "permissions": [
            ]
        }
   },
   "additionalProperties": false
}
```

`typeName`  <a name="hooks-properties-typeName"></a>
후크의 고유 이름입니다. 권장 패턴이 인 후크의 세 부분으로 구성된 네임스페이스를 지정합니다`Organization::Service::Hook`.  
다음 조직 네임스페이스는 예약되어 있으며 후크 유형 이름에 사용할 수 없습니다.  
+ `Alexa`
+ `AMZN`
+ `Amazon`
+ `ASK`
+ `AWS`
+ `Custom`
+ `Dev`
*필수 항목 여부:* 예  
 *Pattern*: `^[a-zA-Z0-9]{2,64}::[a-zA-Z0-9]{2,64}::[a-zA-Z0-9]{2,64}$`  
*최소*: `10`  
*최대*: `196`

`description`  <a name="hooks-properties-description"></a>
CloudFormation 콘솔에 표시되는 후크에 대한 간단한 설명입니다.  
*필수 항목 여부:* 예

`sourceUrl`  <a name="hooks-properties-sourceUrl"></a>
공개된 경우 후크에 대한 소스 코드의 URL입니다.  
*필수 항목 여부*: 아니요  
*최대*: `4096`

`documentationUrl`  <a name="hooks-properties-documentationurl"></a>
후크에 대한 자세한 설명서를 제공하는 페이지의 URL입니다.  
*필수 항목 여부:* 예  
*Pattern*: `^https\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])(\:[0-9]*)*([\?/#].*)?$`  
*최대*: `4096`  
후크 스키마에는 완전하고 정확한 속성 설명이 포함되어야 하지만 `documentationURL` 속성을 사용하여 사용자에게 예제, 사용 사례 및 기타 세부 정보를 비롯한 자세한 정보를 제공할 수 있습니다.

`definitions`  <a name="hooks-properties-definitions"></a>
`definitions` 블록을 사용하여 공유 후크 속성 스키마를 제공합니다.  
`definitions` 섹션을 사용하여 후크 유형 스키마의 여러 지점에서 사용할 수 있는 스키마 요소를 정의하는 것이 모범 사례로 간주됩니다. 그런 다음 JSON 포인터를 사용하여 후크 유형 스키마의 적절한 위치에서 해당 요소를 참조할 수 있습니다.  
*필수 항목 여부*: 아니요

`typeConfiguration`  <a name="hooks-properties-typeconfiguration"></a>
후크의 구성 데이터의 정의입니다.  
*필수 항목 여부:* 예

`properties`  <a name="hooks-properties-properties"></a>
후크의 속성입니다. 후크의 모든 속성은 스키마에서 표현되어야 합니다. 후크 스키마 속성을 후크 유형 구성 속성과 정렬합니다.  
중첩 속성은 허용되지 않습니다. 대신 `definitions` 요소에 중첩된 속성을 정의하고 `$ref` 포인터를 사용하여 원하는 속성에서 참조합니다.
현재 지원되는 속성은 다음과 같습니다.  
+ `default` - 속성의 기본값입니다.
+ `description` - 속성에 대한 설명입니다.
+ `pattern` - 입력을 검증하는 데 사용되는 정규식 패턴입니다.
+ `type` - 허용되는 속성 유형입니다.

`additionalProperties`  <a name="hooks-properties-additionalproperties"></a>
`additionalProperties`를 `false`로 설정해야 합니다. 후크의 모든 속성은 스키마로 표현되어야 합니다. 임의 입력은 허용되지 않습니다.  
*필수 항목 여부:* 예  
*유효한 값*: `false`

`handlers`  <a name="hooks-properties-handlers"></a>
핸들러는 후크 호출 지점과 같이 스키마에 정의된 후크를 시작할 수 있는 작업을 지정합니다. 예를 들어 `preUpdate` 핸들러의 지정된 모든 대상에 대한 업데이트 작업 전에 핸들러가 호출됩니다.  
*유효한 값*: `preCreate` \$1 `preUpdate` \$1 `preDelete`  
핸들러에 대해 하나 이상의 값을 지정해야 합니다.
상태가 인 스택 작업은 후크를 호출하지 `UpdateCleanup` 않습니다. 예를 들어 다음 두 시나리오에서는 후크의 `preDelete` 핸들러가 호출되지 않습니다.  
+ 템플릿에서 하나의 리소스를 제거하면 스택이 업데이트됩니다.
+ [대체](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) 업데이트 유형의 리소스가 삭제됩니다.

`targetNames`  <a name="hooks-properties-targetNames"></a>
후크가 대상으로 하는 유형 이름의 문자열 배열입니다. 예를 들어 `preCreate` 핸들러에 `AWS::S3::Bucket` 대상이 있는 경우 후크는 사전 프로비저닝 단계에서 Amazon S3 버킷에 대해 실행됩니다.  
+ `TargetName`

  구현된 각 핸들러에 대해 하나 이상의 대상 이름을 지정합니다.

  *Pattern*: `^[a-zA-Z0-9]{2,64}::[a-zA-Z0-9]{2,64}::[a-zA-Z0-9]{2,64}$`

  *최소*: `1`

  *필수 항목 여부:* 예
**주의**  
SSM SecureString 및 Secrets Manager 동적 참조는 후크로 전달되기 전에 확인되지 않습니다.

`permissions`  <a name="hooks-properties-permissions"></a>
핸들러를 호출하는 데 필요한 AWS 권한을 지정하는 문자열 배열입니다.  
*필수 항목 여부:* 예

`additionalProperties`  <a name="hooks-additional-properties"></a>
`additionalProperties`를 `false`로 설정해야 합니다. 후크의 모든 속성은 스키마로 표현되어야 합니다. 임의 입력은 허용되지 않습니다.  
*필수 항목 여부:* 예  
*유효한 값*: `false`

## 후크 스키마 예제
<a name="example-hooks"></a>

 **예제 1**.

Java 및 Python 연습에서는 다음 코드 예제를 사용합니다. 다음은 라는 후크의 예제 구조입니다`mycompany-testing-mytesthook.json`.

```
{
    "typeName":"MyCompany::Testing::MyTestHook",
    "description":"Verifies S3 bucket and SQS queues properties before create and update",
    "sourceUrl":"https://mycorp.com/my-repo.git",
    "documentationUrl":"https://mycorp.com/documentation",
    "typeConfiguration":{
        "properties":{
            "minBuckets":{
                "description":"Minimum number of compliant buckets",
                "type":"string"
            },
            "minQueues":{
                "description":"Minimum number of compliant queues",
                "type":"string"
            },
            "encryptionAlgorithm":{
                "description":"Encryption algorithm for SSE",
                "default":"AES256",
                "type":"string",
                "pattern": "[a-zA-Z]*[1-9]"
            }
        },
        "required":[
            
        ],
        "additionalProperties":false
    },
    "handlers":{
        "preCreate":{
            "targetNames":[
                "AWS::S3::Bucket",
                "AWS::SQS::Queue"
            ],
            "permissions":[
                
            ]
        },
        "preUpdate":{
            "targetNames":[
                "AWS::S3::Bucket",
                "AWS::SQS::Queue"
            ],
            "permissions":[
                
            ]
        },
        "preDelete":{
            "targetNames":[
                "AWS::S3::Bucket",
                "AWS::SQS::Queue"
            ],
            "permissions":[
                "s3:ListBucket",
                "s3:ListAllMyBuckets",
                "s3:GetEncryptionConfiguration",
                "sqs:ListQueues",
                "sqs:GetQueueAttributes",
                "sqs:GetQueueUrl"
            ]
        }
    },
    "additionalProperties":false
}
```

 **예제 2**.

다음 예제는 `STACK` 및를 사용하여 스택 템플릿 및 변경 세트 작업을 `CHANGE_SET` `targetNames` 대상으로 지정하는 스키마입니다.

```
{
    "typeName":"MyCompany::Testing::MyTestHook",
    "description":"Verifies Stack and Change Set properties before create and update",
    "sourceUrl":"https://mycorp.com/my-repo.git",
    "documentationUrl":"https://mycorp.com/documentation",
    "typeConfiguration":{
        "properties":{
            "minBuckets":{
                "description":"Minimum number of compliant buckets",
                "type":"string"
            },
            "minQueues":{
                "description":"Minimum number of compliant queues",
                "type":"string"
            },
            "encryptionAlgorithm":{
                "description":"Encryption algorithm for SSE",
                "default":"AES256",
                "type":"string",
                "pattern": "[a-zA-Z]*[1-9]"
            }
        },
        "required":[
        ],
        "additionalProperties":false
    },
    "handlers":{
        "preCreate":{
            "targetNames":[
                "STACK",
                "CHANGE_SET"
            ],
            "permissions":[  
            ]
        },
        "preUpdate":{
            "targetNames":[
                "STACK"
            ],
            "permissions":[
            ]
        },
        "preDelete":{
            "targetNames":[
                "STACK"
            ],
            "permissions":[
                
            ]
        }
    },
    "additionalProperties":false
}
```