2024/12/07

AWS CloudFormation

[AWS CloudFormation とは何ですか?](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/Welcome.html)

## CloudFormationの動作概要
ポイントは**StackSets**でクロスリージョンやクロスアカウントのリソースを作成できる点。必要であればオリジナルな処理を**カスタムリソース**で定義できる点。クロスアカウントでは管理者とターゲットアカウントの間に信頼関係を設定する。
### CloudFormation テンプレートの操作
動的なパラメータ参照、テンプレートのスニペットなど
* [CloudFormation テンプレートの使用](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-guide.html)
* [CloudFormation テンプレートスニペット](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-snippets.html)

### スタックの作成と管理
* [AWS CloudFormation スタックを使用した単一ユニットとしての AWS リソースの管理](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/stacks.html)

### 組込関数リファレンス
* [組み込み関数リファレンス](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html)
```
# 使用例
  UserData:
    Fn::Base64: 
      Fn::Sub:
      - |
        #!/bin/bash
        sed -i -e 's/APP_URL=.*/APP_URL=${APP_URL}/' /tmp/.env
        sed -i -e 's/DB_HOST=.*/DB_HOST=${DB_HOST}/' /tmp/.env
        sed -i -e 's/DB_DATABASE=.*/DB_DATABASE=${DbName}/' /tmp/.env
        sed -i -e 's/DB_USERNAME=.*/DB_USERNAME=${UserName}/' /tmp/.env
        sed -i -e 's/DB_PASSWORD=.*/DB_PASSWORD=${Password}/' /tmp/.env
        sed -i -e 's/AWS_S3_BUCKET=.*/AWS_S3_BUCKET=${S3_BUCKET}/' /tmp/.env
      - APP_URL:
          Fn::ImportValue:
            !Sub ${Prefix}::${Client}::LoadBalancerDNSName
        DB_HOST:
          Fn::ImportValue:
            !Sub ${Prefix}::${Client}::EndpointAddress
        S3_BUCKET:
          Fn::ImportValue:
            !Sub ${Prefix}::${Client}::DataBucket
```

### 疑似パラメータ
* [擬似パラメータ参照](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/pseudo-parameter-reference.html)

### AWS リソースとプロパティタイプのリファレンス
* [AWS リソースおよびプロパティタイプのリファレンス](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html)

### StackSets 
信頼関係を構築すれば、Organizationsを利用していなくても利用可能
* [StackSets を使用したアカウントとリージョン全体でのスタックの管理](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/what-is-cfnstacksets.html)
* [AWS CloudFormation StackSets のサンプルテンプレート](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/stacksets-sampletemplates.html)
* [セルフマネージド型のアクセス許可を付与する](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/stacksets-prereqs-self-managed.html)
```
# セルフマネージド型のアクセス許可
#
# 管理者アカウント側
#
AWSTemplateFormatVersion: 2010-09-09
Description: Configure the AWSCloudFormationStackSetAdministrationRole to enable use of AWS CloudFormation StackSets.

Parameters:
  AdministrationRoleName:
    Type: String
    Default: AWSCloudFormationStackSetAdministrationRole
    Description: "The name of the administration role. Defaults to 'AWSCloudFormationStackSetAdministrationRole'."
  ExecutionRoleName:
    Type: String
    Default: AWSCloudFormationStackSetExecutionRole
    Description: "The name of the execution role that can assume this role. Defaults to 'AWSCloudFormationStackSetExecutionRole'."

Resources:
  AdministrationRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Ref AdministrationRoleName
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: cloudformation.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: AssumeRole-AWSCloudFormationStackSetExecutionRole
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - sts:AssumeRole
                Resource:
                  - !Sub 'arn:*:iam::*:role/${ExecutionRoleName}'

#
# ターゲットアカウント側
# 
AWSTemplateFormatVersion: 2010-09-09
Description: Configure the AWSCloudFormationStackSetExecutionRole to enable use of your account as a target account in AWS CloudFormation StackSets.

Parameters:
  AdministratorAccountId:
    Type: String
    Description: AWS Account Id of the administrator account (the account in which StackSets will be created).
    MaxLength: 12
    MinLength: 12
  ExecutionRoleName:
    Type: String
    Default: AWSCloudFormationStackSetExecutionRole
    Description: "The name of the execution role. Defaults to 'AWSCloudFormationStackSetExecutionRole'."

Resources:
  ExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Ref ExecutionRoleName
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              AWS:
                - !Ref AdministratorAccountId
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - !Sub arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess
```

### カスタムリソース
* [CloudFormation が提供するリソースタイプの使用によるテンプレートの機能の拡張](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cloudformation-supplied-resource-types.html)
#### 削除イベントの時にS3バケットを掃除するカスタムリソース
```
AWSTemplateFormatVersion: 2010-09-09
Description: If you use S3 bucket, you need to clean up used S3 bucket. so this template is cleanup example

Resources:
  #==========================
  # S3 Bucket
  #==========================
  DataBucket:
    Type: "AWS::S3::Bucket"
    Properties:
      BucketName: !Sub 'data-bucket'
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

  #==========================
  # カスタムリソース
  #==========================
  # S3バケット掃除のカスタムリソース
  CleanoutBucketOnDelete:
    Type: Custom::cleanupBucket
    Properties:
      ServiceToken: !GetAtt CleanoutBucketFunction.Arn
    DependsOn: 
      - CleanoutBucketFunction

  # 掃除用ファンクション
  CleanoutBucketFunction:
    Type: AWS::Lambda::Function
    DependsOn: 
      - DataBucket
      - CleanoutBucketRole
    Properties:
      Description: Clean out Bucket on delete.
      Handler: index.handler
      Runtime: python3.12
      Role: !GetAtt CleanoutBucketRole.Arn
      Timeout: 120
      Environment:
        Variables: 
          BUCKET_NAME: !Ref DataBucket
      Code:
        ZipFile: |
          import cfnresponse
          import logging
          import boto3
          import time, os
          status = cfnresponse.SUCCESS
          logger = logging.getLogger(__name__)
          logging.basicConfig(format='%(asctime)s %(message)s',level=logging.DEBUG)

          def handler(event, context):
              logger.debug(event)

              if event['RequestType'] == 'Delete':
                try:
                  BUCKETNAME = os.environ['BUCKET_NAME']
                  s3 = boto3.resource('s3')
                  bucket = s3.Bucket(BUCKETNAME)
                  bucket_versioning = s3.BucketVersioning(BUCKETNAME)
                  if bucket_versioning.status == 'Enabled':
                    bucket.object_versions.delete()
                  else:
                    bucket.objects.all().delete()
                  cfnresponse.send(event, context, status, {}, None)
                except Exception as e:
                  print(e)
                  cfnresponse.send(event, context, cfnresponse.FAILED, {}, None)
              else:
                cfnresponse.send(event, context, status, {}, None)

  # 掃除用ロール
  CleanoutBucketRole:
    Type: AWS::IAM::Role
    Properties:
      PermissionsBoundary: !Sub 'arn:aws:iam::${AWS::AccountId}:policy/DevelopUserBoundary'
      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: lambda-bucketcleaner
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:DeleteObject
                  - s3:DeleteObjectVersion
                Resource: !Sub 'arn:${AWS::Partition}:s3:::${DataBucket}/*'
              - Effect: Allow
                Action:
                  - s3:ListBucket
                  - s3:GetBucketVersioning
                Resource: !Sub 'arn:${AWS::Partition}:s3:::${DataBucket}'
```
#### インスタンスのネットワークインターフェイスを取得するカスタムリソース
```
AWSTemplateFormatVersion: 2010-09-09
Description: Get the network interfaces of an instance.

Resources:
  CustomResource:
    Type: AWS::CloudFormation::CustomResource
    DependsOn: 
      - Function
    Properties:
      ServiceToken: !GetAtt Function.Arn
      ServiceTimeout: 60
      INSTANCEID: i-1234567890abcdef0

  Function:
    Type: "AWS::Lambda::Function"
    Properties:
      Handler: index.handler
      Runtime: python3.8
      Timeout: 60
      Role: !GetAtt LambdaExecutionRole.Arn
      Code:
        ZipFile: |
          import cfnresponse
          import boto3
          status = cfnresponse.SUCCESS
          def handler(event, context):
              try:
                INSTANCEID = event['ResourceProperties']['INSTANCEID']
                ec2 = boto3.client('ec2')
                response = ec2.describe_network_interfaces(
                    Filters=[
                        {
                            'Name': 'attachment.instance-id',
                            'Values': [INSTANCEID]
                        }
                    ]
                )
                network_interface_id = response['NetworkInterfaces'][0]['NetworkInterfaceId']
                responseData = {}
                responseData['Data'] = network_interface_id
                cfnresponse.send(event, context, status, responseData, None)
              except Exception as e:
                print(e)
                cfnresponse.send(event, context, cfnresponse.FAILED, {}, None)

  LambdaExecutionRole:
    Type: "AWS::IAM::Role"
    Properties: 
      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: "LambdaEC2Policy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "ec2:DescribeNetworkInterfaces"
                Resource: "*"

  LogGroup:
    DeletionPolicy: Delete
    UpdateReplacePolicy: Delete
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub '${AWS::StackName}-LogGroup'
      RetentionInDays: 1

  FlowLogPolicy:
    Type: "AWS::IAM::ManagedPolicy"
    Properties:
      ManagedPolicyName: !Sub '${AWS::StackName}-FlowLogPolicy'
      Path: "/"
      PolicyDocument: |
          {
              "Version": "2012-10-17",
              "Statement": [
                  {
                      "Effect": "Allow",
                      "Action": [
                          "logs:CreateLogGroup",
                          "logs:CreateLogStream",
                          "logs:PutLogEvents",
                          "logs:DescribeLogGroups",
                          "logs:DescribeLogStreams"
                      ],
                      "Resource": "*"
                  }
              ]
          }

  FlowLogRole:
    Type: "AWS::IAM::Role"
    DependsOn: 
      - FlowLogPolicy
    Properties:
      Path: "/"
      RoleName: !Sub '${AWS::StackName}-FlowLogRole'
      AssumeRolePolicyDocument: 
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action:
              - sts:AssumeRole
            Principal:
              Service:
                - vpc-flow-logs.amazonaws.com
      MaxSessionDuration: 3600
      ManagedPolicyArns: 
        - !Ref FlowLogPolicy

  EC2FlowLog:
    Type: "AWS::EC2::FlowLog"
    DependsOn: 
      - CustomResource
    Properties:
      DeliverLogsPermissionArn: !GetAtt FlowLogRole.Arn
      LogGroupName: !Ref LogGroup
      ResourceId: !GetAtt CustomResource.Data
      TrafficType: "ALL"
      LogDestinationType: "cloud-watch-logs"
      ResourceType: "NetworkInterface"
      LogFormat: "${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status}"
      MaxAggregationInterval: 60
```
#### 削除イベントの時にAMIを作成してSSMパラメータにAMIIDを保存するカスタムリソース
```
AWSTemplateFormatVersion: 2010-09-09
Description: Create AMI on stack deletion, update SSM, delete old AMI and snapshots

Resources:
  # Description: Create AMI on stack deletion, update SSM, delete old AMI and snapshots
  AmiCreatorRole:
    Type: AWS::IAM::Role
    Properties:
      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: AmiCreatorPolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ec2:CreateImage
                  - ec2:DeregisterImage
                  - ec2:DeleteSnapshot
                  - ec2:DescribeImages
                Resource: "*"
              - Effect: Allow
                Action:
                  - ssm:GetParameter
                  - ssm:PutParameter
                Resource: "*"
      PermissionsBoundary: !Sub 'arn:aws:iam::${AWS::AccountId}:policy/DevelopUserBoundary'

  AmiCreatorCustomResource:
    Type: Custom::AmiCreator
    Properties:
      ServiceToken: !GetAtt AmiCreatorFunction.Arn
      InstanceId: i-1234567890abcdef0
      SsmParameterName: /testweb/latest-ami

  AmiCreatorFunction:
    Type: AWS::Lambda::Function
    Properties:
      Runtime: python3.12
      Handler: index.handler
      Timeout: 300
      Role: !GetAtt AmiCreatorRole.Arn
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import datetime

          ec2 = boto3.client('ec2')
          ssm = boto3.client('ssm')

          def handler(event, context):
              print("Event:", event)
              request_type = event['RequestType']

              # Delete のときだけ実行
              if request_type != 'Delete':
                  cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
                  return

              instance_id = event['ResourceProperties']['InstanceId']
              ssm_param = event['ResourceProperties']['SsmParameterName']

              # 既存 AMI の取得(削除は後で行う)
              try:
                  param = ssm.get_parameter(Name=ssm_param)
                  old_ami = param['Parameter']['Value']
                  print(f"Existing AMI in SSM: {old_ami}")
              except ssm.exceptions.ParameterNotFound:
                  old_ami = None
                  print("No existing AMI in SSM")

              # 新しい AMI 名
              timestamp = datetime.datetime.utcnow().strftime('%Y%m%d-%H%M%S')
              ami_name = f"stack-delete-ami-{instance_id}-{timestamp}"

              # 新しい AMI を作成
              print(f"Creating AMI: {ami_name}")
              response = ec2.create_image(
                  InstanceId=instance_id,
                  Name=ami_name,
                  NoReboot=True
              )
              new_ami = response['ImageId']
              print(f"Created AMI ID: {new_ami}")

              # SSM パラメータを更新(先に新しい AMI を保存)
              ssm.put_parameter(
                  Name=ssm_param,
                  Value=new_ami,
                  Type='String',
                  Overwrite=True
              )
              print(f"Updated SSM parameter {ssm_param} with {new_ami}")

              # 古い AMI とスナップショット削除
              if old_ami:
                  try:
                      print(f"Describing old AMI: {old_ami}")
                      images = ec2.describe_images(ImageIds=[old_ami])['Images']

                      snapshot_ids = []
                      for bd in images[0].get('BlockDeviceMappings', []):
                          if 'Ebs' in bd and 'SnapshotId' in bd['Ebs']:
                              snapshot_ids.append(bd['Ebs']['SnapshotId'])

                      # AMI 削除
                      print(f"Deregistering old AMI: {old_ami}")
                      ec2.deregister_image(ImageId=old_ami)

                      # スナップショット削除
                      for snap in snapshot_ids:
                          try:
                              print(f"Deleting snapshot: {snap}")
                              ec2.delete_snapshot(SnapshotId=snap)
                          except Exception as e:
                              print(f"Failed to delete snapshot {snap} (ignored): {e}")

                  except Exception as e:
                      print(f"Failed to clean old AMI (ignored): {e}")

              cfnresponse.send(event, context, cfnresponse.SUCCESS, {
                  "AmiId": new_ami
              })
```

0 件のコメント:

コメントを投稿

人気の投稿