0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

CloudFormationをDeleteStackしたときACMと一緒にCNAMEも消す方法

Last updated at Posted at 2023-07-05

概要

CloudFormationでDNS検証方式のACM証明書を作成した場合、Route53に検証用CNAMEレコードが自動的に追加されますが、Delete StackしてもこのCNAMEレコードは自動的には削除されません。

しかしCloudFormationのカスタムリソースを使えば、Delete Stack時にCNAMEも一緒に消すことが可能です。
今回はそれを実現する方法を紹介したいと思います。

カスタムリソースとは

CloudFormationのカスタムリソースは、CloudFormationテンプレート内で定義される独自のリソースです。通常のCloudFormationリソースでは提供されていない機能やサービスをカバーするために使用されます。

カスタムリソースはLambda関数を使用して作成され、スタックの作成、更新、削除をトリガーにAWSリソースの作成、更新、削除などの処理を実行できます。つまりカスタムリソースを使用することで、CloudFormationの範囲を超えた任意の操作や自動化が可能になります。

参考: カスタムリソース (公式ドキュメント)

注意事項

1つ目のCNAMEしか消せません

1つの証明書に複数のドメインを入れることがありますが、今回のサンプルだと1つ目のCNAMEしか消せません。

自動でCNAMEが消えないのには訳がある

ACMの検証用CNAMEはドメイン名に基づいて作成されます。
複数のACM証明書で同一のドメインを設定する場合、それぞれの検証用CNAMEは同一になります。
つまり、削除する検証用CNAMEが他の証明書で使われている可能性があります。
スタック毎に一意な検証用CNAMEが作られることが保証されている環境でのみ、この手法を使うようにしてください。

ゴミを消したかったのに新たなゴミが

この仕組みを考えた動機は、スタック削除時に不要なCNAMEレコードが残って邪魔だったり手動で削除するのが面倒だからですが、Lambdaを使ったせいで今度はLambdaのロググループがゴミとして残ってしまいます。
(Lambdaを実行すると自動的にCloudWatchのロググループが作成されます)

今回の例ではスタック専用のLambdaを作って、スタック削除時にこのLambdaも一緒に消えるようにしていますが、CNAMEを削除する用の共通のLambdaを用意して、各スタックのカスタムリソースでは共通Lambdaを呼び出すようにすべきだったかも知れません。

XXXXX_IN_PROGRESSのまま止まってしまったら

1時間と少しでタイムアウトしてFAILEDになるので、それから手動でスタックを削除してください。
これが発生するのはカスタムリソースのLabmdaが応答オブジェクトをCloudFormationに返せていないときです。
サンプル通りにやれば大丈夫なはずです。

UpdateStackは考慮してるけどしてない

今回のサンプルだとUpdateStackすると新しい証明書を発行して、古い証明書は削除されます。
その場合も古いCNAMEが削除されるようになっていますが、それはUpdate用の処理をしているわけでなく、UpdateのときにDeleteが同時に呼び出されるからです。
Update時にDeleteされてはまずいケースがあるはずなので、サンプルを改変して使う場合は必ず各パラメータのUpdateStackの動作確認をしてください。

Update時の挙動に関してはこちらの記事が詳しいです。

サンプルテンプレート

AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  DomainName:
    Type: String
    Description: "証明書のドメイン名"
    Default: "foo.example.com"
  HostedZoneId:
    Type: String
    Description: "ドメインを管理しているRoute53のホステッドゾーンID"
    Default: "ZXXXXXXXXXXXXXXXXXXX"

Resources:
  Certificate:
    Type: AWS::CertificateManager::Certificate
    Properties:
      DomainName: !Ref DomainName
      ValidationMethod: DNS
      DomainValidationOptions:
        - DomainName: !Ref DomainName
          HostedZoneId: !Ref HostedZoneId

  CustomResourceLambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub "${AWS::StackName}-custom-resource-handler"
      Handler: index.handler
      Runtime: python3.10
      Timeout: 30
      Role: !GetAtt CustomResourceLambdaRole.Arn
      Code:
        ZipFile: |
          import cfnresponse
          import boto3

          def get_cname_record(certificate_arn):
              try:
                  client = boto3.client('acm')
                  response = client.describe_certificate(CertificateArn=certificate_arn)
                  certificate_details = response['Certificate']
                  cname_record = certificate_details.get('DomainValidationOptions', [{}])[0].get('ResourceRecord', {})
                  return cname_record
              except Exception as e:
                  print(f'Error getting CNAME record: {str(e)}')

          def delete_cname_record(hosted_zone_id, record_name):
              try:
                  client = boto3.client('route53')
                  response = client.list_resource_record_sets(
                      HostedZoneId=hosted_zone_id,
                      StartRecordName=record_name,
                      MaxItems='1'
                  )
                  record_sets = response['ResourceRecordSets']
                  for record_set in record_sets:
                      if record_set['Name'] == record_name and record_set['Type'] == 'CNAME':
                          changes = [
                              {
                                  'Action': 'DELETE',
                                  'ResourceRecordSet': {
                                      'Name': record_set['Name'],
                                      'Type': record_set['Type'],
                                      'TTL': record_set['TTL'],
                                      'ResourceRecords': record_set['ResourceRecords']
                                  }
                              }
                          ]
                          client.change_resource_record_sets(
                              HostedZoneId=hosted_zone_id,
                              ChangeBatch={'Changes': changes}
                          )
                          print(f"Record '{record_name}' deleted successfully.")
                          break
              except Exception as e:
                  print(f'Error deleting CNAME record: {str(e)}')

          def handler(event, context):
              domain_name = event['ResourceProperties']['DomainName']
              hosted_zone_id = event['ResourceProperties']['HostedZoneId']
              certificate_arn = event['ResourceProperties']['CertificateArn']
              stack_name = event['ResourceProperties']['StackName']

              if event['RequestType'] == 'Delete':
                  record_name = get_cname_record(certificate_arn)
                  delete_cname_record(hosted_zone_id, record_name['Name'])

              # 応答オブジェクトの作成
              response_data = {
                  'DomainName': domain_name,
                  'HostedZoneId': hosted_zone_id,
                  'CertificateArn': certificate_arn,
                  'StackName': stack_name
              }
              cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)

  CustomResourceLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${AWS::StackName}-custom-resource-handler-role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: Route53DeleteRecordPolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "acm:DescribeCertificate"
                Resource: "*"
              - Effect: Allow
                Action:
                  - "route53:*"
                Resource:
                  - !Sub "arn:aws:route53:::hostedzone/${HostedZoneId}"

  CustomResource:
    Type: AWS::CloudFormation::CustomResource
    Properties:
      ServiceToken: !GetAtt CustomResourceLambda.Arn
      DomainName: !Ref DomainName
      HostedZoneId: !Ref HostedZoneId
      CertificateArn: !Ref Certificate
      StackName: !Ref "AWS::StackName"

解説

パラメータ

  • DomainName
    • ACMで発行する証明書に埋め込むドメイン名です
  • HostedZoneId
    • 上記ドメインを管理しているRoute53のホストゾーンIDです
    • 自動的にCNAMEレコードを作成するにはACMと同じAWSでドメインを管理している必要があります

リソース

  • Certificate
    • ACM証明書です
  • CustomResourceLambda
    • カスタムリソースの実体となるLambdaです。ここに処理を書きます
      • get_cname_record
        • ACMのARNを受け取り、CNAMEのレコード情報を返します
      • delete_cname_record
        • ゾーンIDとCNAMEのレコード名を受け取り、対象を削除します
      • handler
        • メイン関数です
        • get_cname_recordでCNAMEのレコード情報を取得します
        • delete_cname_recordでCNAMEを削除します
        • 応答オブジェクトを作成してCloudFormationに返します。これをしないとCloudFormationがXXXXX_IN_PROGRESSのまま止まってしまいます
        • 削除に失敗してもCloudFormationにはSUCCESSを返すようにしています。そうしないとDeleteStackが進まないので
        • 実行されるとCloudWatchにLambdaと同名のロググループが作成されます。デバッグしたいときはそこを見てください
  • CustomResourceLambdaRole
    • Lambdaでさせたい処理に必要な権限を与えてます
  • CustomResource
    • これがカスタムリソースです
    • 対応するLambdaとプロパティを定義しているだけで、リソースとしての実体は持ちません
    • このリソースが作成、更新、削除されると対応するLambdaが起動します
    • プロパティは呼び出されたLambdaが参照できます
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?