AWS: CloudFormation – пример создания S3 корзины, IAM пользователя, Glacier Lifecycle и SNS Notification

By | 03/07/2018
 

Ещё один пример работы с CloudFormation.

Задача – добавить создание AWS S3 корзины, со следующими параметрами:

  • AccessControl: полный доступ владельцу (документация тут>>>) и IAM пользователю
  • LifecycleConfiguration: архивировать данные в AWS Glacier по истечении заданного кол-ва дней (документация тут>>>)
  • Tags: само собой
  • NotificationConfiguration: отправлять сообщения через AWS SNS при удалении объектов из корзины (документация тут>>>)
  • VersioningConfiguration: вкусно, но не сейчас (документация тут>>>)

Кроме ресурса самой корзины – добавим IAM пользователя, группу и политику доступа с разрешением доступа к этой корзине.

Приступим.

AWS::S3::Bucket

Сначала создадим минимальный шаблон:

{
  "AWSTemplateFormatVersion": "2010-09-09",

  "Description": "AWS CloudFormation S3 testing stack",

  "Parameters": {

    "BucketName": {
      "Description": "Bucket name",
      "Type": "String",
      "Default": "ads-test-bucket"
    },

    "ClientTag": {
      "Description": "Client tag value",
      "Type": "String",
      "Default": "TestClient"
    }
  },

  "Resources" : {

    "myS3Bucket" : {
      "Type" : "AWS::S3::Bucket",
      "Properties": {
        "AccessControl": "BucketOwnerFullControl",
        "Tags" : [
          {"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "s3-bucket"] ] } },
          {"Key" : "Env", "Value" : {"Ref" : "AWS::StackName"} },
          {"Key" : "Client", "Value" : {"Ref" : "ClientTag"} }
        ]
      }
      }
  }
}

Проверяем его:

aws --profile ads cloudformation validate-template --template-body file://ads-s3.json
{
"Parameters": [
{
"ParameterKey": "BucketName",
"DefaultValue": "ads-test-bucket",
"NoEcho": false,
"Description": "Bucket name"
},
{
"ParameterKey": "ClientTag",
"DefaultValue": "TestClient",
"NoEcho": false,
"Description": "Client tag value"
}
],
"Description": "AWS CloudFormation S3 testing stack"
}

ОК – теперь можно создавать сам стек, запускаем:

aws --profile ads cloudformation create-stack --stack-name ads-s3-test --template-body file://ads-s3.json
{
"StackId": "arn:aws:cloudformation:us-west-1:159***787:stack/ads-s3-test/9c2232b0-208b-11e8-aa51-503f2f3941c5"
}

Проверяем:

aws --profile ads cloudformation describe-stacks --stack-name ads-s3-test --query 'Stacks[*].StackStatus' --output text
CREATE_COMPLETE

И корзины:

aws --profile ads s3api list-buckets --query 'Buckets[*]' --output table | grep s3-test
|  2018-03-05T15:41:09.000Z |  ads-s3-test-mys3bucket-e7fz1ix67ukt                         |

ОК, работает, продолжаем.

AWS::IAM::User

Следующим шагом добавим IAM пользователя для работы с корзиной.

Тут потребуются:

  1. пользователь с доступом и в AWS Console, для пользователей, и через API, для скриптов – AWS::IAM::User
  2. Access и Secret ключи для API – AWS::IAM::AccessKey
  3. группа – AWS::IAM::Group
  4. что бы избежать “Circular dependency between resources” – отдельным ресурсом выполним добавление пользователя к группе, ресурс AWS::IAM::UserToGroupAddition
  5. политика доступа к корзине – AWS::IAM::Policy

В Parameters добавляем параметр Password, указываем NoEcho == True:

...
    "AdsTestS3UserPassword": {
      "Description": "User password",
      "Type": "String",
      "NoEcho": "true",
      "MinLength": "8",
      "MaxLength": "41",
      "AllowedPattern" : "[a-zA-Z0-9]*",
      "ConstraintDescription" : "must contain only alphanumeric characters.",
      "Default": "Gaquoe9ziu0p"
    }
  },
...

В Resources добавляем создание пользователя:

...
  "Resources" : {

    "AdsTestS3User" : {
      "Type" : "AWS::IAM::User",
      "Properties" : {
        "LoginProfile": {
          "Password": { "Ref" : "AdsTestS3UserPassword" }
        }
      }
    },
...

Имя пользователя (параметр UserName) можно указать, но:

If you specify a name, you cannot perform updates that require replacement of this resource. You can perform updates that require no or some interruption. If you must replace the resource, specify a new name.

Так что не будем.

Ключи доступа:

...
    "AdsTestS3UserKeys": {
      "Type": "AWS::IAM::AccessKey",
      "Properties": {
        "UserName": {
          "Ref": "AdsTestS3User"
        }
      }
    },
...

Далее – группа:

...
    "AdsTestS3Group" : {
      "Type" : "AWS::IAM::Group"
    },
...

Добавляем пользователя к группе:

...
    "AddTestS3UserToTestS3Group": {
      "Type": "AWS::IAM::UserToGroupAddition",
      "Properties": {
        "GroupName": { "Ref": "AdsTestS3Group" },
        "Users": [{ "Ref": "AdsTestS3User" }]
      }
    },
...

Описываем политику доступа к корзине:

...
    "AdsTestS3Policy": {
      "Type" : "AWS::IAM::Policy",
      "Properties" : {
        "PolicyName" : "AdsTestS3Policy",
        "PolicyDocument" : {
          "Statement": [ {
            "Effect": "Allow",
            "Action": [
              "s3:ListBucket"
            ],
            "Resource": { "Fn::Join": [ "", [ "arn:aws:s3:::", {"Ref" : "AdsTestS3Bucket"} ]]}
            },
            {
            "Effect": "Allow",
            "Action": [
              "s3:PutObject",
              "s3:GetObject"
            ],
            "Resource": { "Fn::Join": [ "", [ "arn:aws:s3:::", {"Ref" : "AdsTestS3Bucket"}, "/*" ]]}
            }
          ]
        },
        "Groups" : [ { "Ref" : "AdsTestS3Group" } ]
      }
    },
...

Проверяем шаблон:

aws --profile ads cloudformation validate-template --template-body file://ads-s3.json
{
"Parameters": [
{
"ParameterKey": "BucketName",
"DefaultValue": "ads-test-bucket",
"NoEcho": false,
"Description": "Buclet name"
},
{
"ParameterKey": "AdsTestS3UserPassword",
"DefaultValue": "Gaquoe9ziu0p",
"NoEcho": true,
"Description": "User password"
},
{
"ParameterKey": "ClientTag",
"DefaultValue": "TestClient",
"NoEcho": false,
"Description": "Client tag value"
}
],
"Description": "AWS CloudFormation S3 testing stack",
"Capabilities": [
"CAPABILITY_IAM"
],
"CapabilitiesReason": "The following resource(s) require capabilities: [AWS::IAM::User]"
}

Создаём стек:

aws --profile ads cloudformation create-stack --capabilities CAPABILITY_IAM --stack-name ads-s3-test --template-body file://ads-s3.json
{
"StackId": "arn:aws:cloudformation:us-west-1:159***787:stack/ads-s3-test/ab947ef0-2131-11e8-bb11-50a686e19fae"
}

Используем “--capabilities CAPABILITY_IAM“, что бы избежать ошибки “(InsufficientCapabilitiesException) when calling the CreateStack operation: Requires capabilities : [CAPABILITY_IAM]“.

Проверяем доступ к корзине.

Добавим именованный профиль AWS CLI:

aws configure --profile ads-s3-test
AWS Access Key ID [None]: AKIA***NHNQ
AWS Secret Access Key [None]: PZsM***5LKFr
Default region name [None]: us-west-1
Default output format [None]: json

Проверяем доступы.

Копируем файл в корзину (Action:s3:PutObject):

touch test.file
aws --profile ads-s3-test s3 cp test.file s3://ads-s3-test-adstests3bucket-132udo8uein4x
upload: ./test.file to s3://ads-s3-test-adstests3bucket-132udo8uein4x/test.file

Проверяем содержимое корзины (Action:s3:ListBucket):

aws --profile ads-s3-test s3 ls s3://ads-s3-test-adstests3bucket-132udo8uein4x
2018-03-06 14:00:44          0 test.file

Загружаем файл из корзины к себе на машину (Action:s3:GetObject):

aws --profile ads-s3-test s3 cp s3://ads-s3-test-adstests3bucket-132udo8uein4x/test.file test2.file
download: s3://ads-s3-test-adstests3bucket-132udo8uein4x/test.file to ./test2.file
ls -l | grep test
-rw-r--r-- 1 setevoy setevoy     0 Mar  6 14:00 test2.file
-rw-r--r-- 1 setevoy setevoy     0 Mar  6 13:50 test.file

А вот удаление я забыл:

aws --profile ads-s3-test s3 rm s3://ads-s3-test-adstests3bucket-132udo8uein4x/test.file
delete failed: s3://ads-s3-test-adstests3bucket-132udo8uein4x/test.file An error occurred (AccessDenied) when calling the DeleteObject operation: Access Denied

Обновляем полиси:

...
            "Action": [
              "s3:PutObject",
              "s3:GetObject",
              "s3:DeleteObject",
            ],
...

Обновляем стек:

aws --profile ads cloudformation update-stack --stack-name ads-s3-test --capabilities CAPABILITY_IAM --template-body file://ads-s3.json
{
"StackId": "arn:aws:cloudformation:us-west-1:159***787:stack/ads-s3-test/0340d7b0-2133-11e8-a777-500c28cbde61"
}

Пробуем ещё раз:

aws --profile ads-s3-test s3 rm s3://ads-s3-test-adstests3bucket-132udo8uein4x/test.file
delete: s3://ads-s3-test-adstests3bucket-132udo8uein4x/test.file

ОК, с этим вроде всё.

AWS::SNS::Topic

Начнём с добавления SNS.

В Parameters добавляем NotificationEmail:

...
    "NotificationEmail": {
      "Description": "Email to use for S3 deletion notifications",
      "Default": "test@domain.kiev.ua",
      "Type": "String"
    }
...

В Resources добавляем сам SNS топик:

...
    "AdsTestSNSTopicpic": {
      "Type": "AWS::SNS::Topic",
      "Properties": {
        "DisplayName": {
          "Fn::Join": [
            "",
            [
              {
                "Ref": "AdsTestS3Bucket"
              },
              "-deletions"
            ]
          ]
        },
        "Subscription": [
          {
            "Protocol": "email",
            "Endpoint": {
              "Ref": "NotificationEmail"
            }
          }
        ]
      }
    },
...

AWS::SNS::TopicPolicy

Добавляем политику доступа к этому топику – какой ресурс может обращаться к нему, что бы отправлять уведомления:

...
    "SNSTopicPolicy": {
      "Type": "AWS::SNS::TopicPolicy",
      "Properties": {
        "PolicyDocument": {
          "Id": "S3BucketPolicy",
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Principal": {
                "Service": "s3.amazonaws.com"
              },
              "Action": [
                "SNS:Publish"
              ],
              "Resource": "*",
              "Condition": {
                "ArnLike": {
                  "aws:SourceArn": {
                    "Fn::Join": [
                      "",
                      [
                        "arn:aws:s3:*:*:",
                        {
                          "Ref": "BucketName"
                        }
                      ]
                    ]
                  }
                }
              }
            }
          ]
        },
        "Topics": [
          {
            "Ref": "AdsTestSNSTopic"
          }
        ]
      }
    },
...

Обновляем ресурс AdsTestS3Bucket – в Properties добавляем параметр NotificationConfiguration:

...
          "NotificationConfiguration": {
            "TopicConfigurations": [ {
              "Event": "s3:ObjectRemoved:Delete",
              "Topic": {
                "Ref": "AdsTestSNSTopic"
              }
            }
          ]
        }
...

И используем созданный в самом начале параметр BucketName:

...
      "Properties": {
        "BucketName": { "Ref": "BucketName" },
...

В результате ресурс корзины сейчас выглядит так:

...
    "AdsTestS3Bucket" : {
      "Type" : "AWS::S3::Bucket",
      "Properties": {
        "BucketName": { "Ref": "BucketName" },
        "AccessControl": "BucketOwnerFullControl",
          "NotificationConfiguration": {
            "TopicConfigurations": [ {
              "Event": "s3:ObjectRemoved:Delete",
              "Topic": {
                "Ref": "AdsTestSNSTopic"
              }
            }
          ]
        },
        "Tags" : [
          {"Key" : "Name", "Value" : { "Fn::Join": [ "-", [ {"Ref" : "AWS::StackName"}, "s3-bucket"] ] } },
          {"Key" : "Env", "Value" : {"Ref" : "AWS::StackName"} },
          {"Key" : "Client", "Value" : {"Ref" : "ClientTag"} }
        ]
      }
    }
...

Теперь проверим синтаксис шаблона, обновим стек, проверим ресурсы, если всё ОК – перейдём к LifecycleConfiguration.

Проверяем:

aws --profile ads cloudformation validate-template --template-body file://ads-s3.json
{
"Parameters": [
{
"ParameterKey": "NotificationEmail",
"DefaultValue": "test@domain.kiev.ua",
"NoEcho": false,
"Description": "Email to use for S3 deletion notifications"
},
{
"ParameterKey": "BucketName",
"DefaultValue": "ads-test-bucket",
"NoEcho": false,
"Description": "Bucket name"
},
{
"ParameterKey": "AdsTestS3UserPassword",
"DefaultValue": "Gaquoe9ziu0p",
"NoEcho": true,
"Description": "User password"
},
{
"ParameterKey": "ClientTag",
"DefaultValue": "TestClient",
"NoEcho": false,
"Description": "Client tag value"
}
],
"Description": "AWS CloudFormation S3 testing stack",
"Capabilities": [
"CAPABILITY_IAM"
],
"CapabilitiesReason": "The following resource(s) require capabilities: [AWS::IAM::User]"
}

Обновляем стек:

aws --profile ads cloudformation update-stack --stack-name ads-s3-test --capabilities CAPABILITY_IAM --template-body file://ads-3.json

Amazon S3 Bucket LifecycleConfiguration

Последнее, что добавим в этот шаблон – это Lifecycle.

Про Glacier постараюсь написать отдельный пост, сейчас добавим одну политику, которая будет указывать на смену типа хранилища для объектов старше 0 дней со Standart на Glacier.

В Parameters шаблона добавим параметр, в котором будем определять – через сколько дней выполнять архивирование:

...
    "ArchiveTransitionInDays": {
      "Description": "Days becfore change storage type from Standart to Glacier",
      "Type": "Number",
      "Default": 0
    },
...

В Parameters ресурса корзины добавляем LifecycleConfiguration:

...
        "LifecycleConfiguration": {
          "Rules": [
            {
              "Id": "GlacierArchive",
              "Status": "Enabled",
              "Transitions": [
                {
                  "StorageClass": "GLACIER",
                  "TransitionInDays": { "Ref": "ArchiveTransitionInDays" }
                }
              ]
            }
          ]
        },
...

Теперь ресурс корзины выглядит так:

...
    "AdsTestS3Bucket" : {
      "Type" : "AWS::S3::Bucket",
      "Properties": {
        "BucketName": { "Ref": "BucketName" },
        "AccessControl": "BucketOwnerFullControl",
          "NotificationConfiguration": {
            "TopicConfigurations": [ {
              "Event": "s3:ObjectRemoved:Delete",
              "Topic": {
                "Ref": "AdsTestSNSTopic"
              }
            }
          ]
        },
        "LifecycleConfiguration": {
          "Rules": [
            {
              "Id": "GlacierArchive",
              "Status": "Enabled",
              "Transitions": [
                {
                  "StorageClass": "GLACIER",
                  "TransitionInDays": { "Ref": "ArchiveTransitionInDays" }
                }
              ]
            }
          ]
        },
        "Tags" : [
          {"Key" : "Name", "Value" : { "Fn::Join": [ "-", [ {"Ref" : "AWS::StackName"}, "s3-bucket"] ] } },
          {"Key" : "Env", "Value" : {"Ref" : "AWS::StackName"} },
          {"Key" : "Client", "Value" : {"Ref" : "ClientTag"} }
        ]
      }
    }
...

Проверяем:

aws --profile ads cloudformation validate-template --template-body file://ads-s3.json
{
"Parameters": [
{
"ParameterKey": "NotificationEmail",
"DefaultValue": "test@domain.kiev.ua",
"NoEcho": false,
"Description": "Email to use for S3 deletion notifications"
},
{
"ParameterKey": "BucketName",
"DefaultValue": "ads-test-bucket",
"NoEcho": false,
"Description": "Bucket name"
},
{
"ParameterKey": "AdsTestS3UserPassword",
"DefaultValue": "Gaquoe9ziu0p",
"NoEcho": true,
"Description": "User password"
},
{
"ParameterKey": "ArchiveTransitionInDays",
"DefaultValue": "0",
"NoEcho": false,
"Description": "Days becfore change storage type from Standart to Glacier"
},
{
"ParameterKey": "ClientTag",
"DefaultValue": "TestClient",
"NoEcho": false,
"Description": "Client tag value"
}
],
"Description": "AWS CloudFormation S3 testing stack",
"Capabilities": [
"CAPABILITY_IAM"
],
"CapabilitiesReason": "The following resource(s) require capabilities: [AWS::IAM::User]"
}

Обновляем:

!561
aws --profile ads cloudformation update-stack --stack-name ads-s3-test --capabilities CAPABILITY_IAM --template-body file://ads-s3.json
{
"StackId": "arn:aws:cloudformation:us-west-1:159***787:stack/ads-s3-test/0340d7b0-2133-11e8-a777-500c28cbde61"
}

Проверяем корзину:

ОК – архивирование готово.

Вроде всё?

Удалим стек полностью:

aws --profile ads cloudformation delete-stack --stack-name ads-s3-test

Создаём его заново:

aws --profile ads cloudformation create-stack --stack-name ads-s3-test --capabilities CAPABILITY_IAM --template-body file://ads-s3.json

Проверяем, всё есть, всё работает – готово.

Шаблон, немного изменённый в плане некоторых имён, можно взять тут>>>.

Ссылки по теме

S3 Lifecycle Policies, Versioning & Encryption: AWS Security

Lifecycle Configuration Elements

Transitioning Objects: General Considerations

Expiring Objects: General Considerations

How to Move Amazon S3 Data to Glacier

SophosUserCreation.template

aws-cloudformation/IAM/IAM.template