HardПрактика4 min

CloudFormation и SAM

CloudFormation/SAM шаблоны для всей инфраструктуры CRC, CDK как альтернатива

Полный SAM-шаблон CRC

SAM (Serverless Application Model) -- расширение CloudFormation с упрощённым синтаксисом для serverless-ресурсов.

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Cloud Resume Challenge - Complete Infrastructure

Parameters:
  DomainName:
    Type: String
    Default: resume.ivanpetrov.com
  HostedZoneId:
    Type: String
    Description: Route53 Hosted Zone ID

Globals:
  Function:
    Runtime: python3.12
    Architectures: [arm64]
    Timeout: 10
    MemorySize: 128
    Environment:
      Variables:
        TABLE_NAME: !Ref VisitorCounterTable

Resources:
  # ========== BACKEND ==========

  VisitorCounterTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: visitor-counter
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH
      Tags:
        - Key: project
          Value: crc

  VisitorCounterFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: visitor-counter
      CodeUri: backend/
      Handler: lambda_function.lambda_handler
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref VisitorCounterTable
      Events:
        GetCount:
          Type: HttpApi
          Properties:
            Path: /count
            Method: GET
            ApiId: !Ref CrcApi

  CrcApi:
    Type: AWS::Serverless::HttpApi
    Properties:
      StageName: prod
      CorsConfiguration:
        AllowOrigins:
          - !Sub 'https://${DomainName}'
        AllowMethods:
          - GET
          - OPTIONS
        AllowHeaders:
          - Content-Type
        MaxAge: 86400

  # ========== FRONTEND ==========

  FrontendBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub '${DomainName}-frontend'
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      Tags:
        - Key: project
          Value: crc

  FrontendBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref FrontendBucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: AllowCloudFrontOAC
            Effect: Allow
            Principal:
              Service: cloudfront.amazonaws.com
            Action: s3:GetObject
            Resource: !Sub '${FrontendBucket.Arn}/*'
            Condition:
              StringEquals:
                AWS:SourceArn: !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${CdnDistribution}'

  OAC:
    Type: AWS::CloudFront::OriginAccessControl
    Properties:
      OriginAccessControlConfig:
        Name: crc-oac
        OriginAccessControlOriginType: s3
        SigningBehavior: always
        SigningProtocol: sigv4

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

  CdnDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Enabled: true
        DefaultRootObject: index.html
        HttpVersion: http2and3
        PriceClass: PriceClass_100
        Aliases:
          - !Ref DomainName
        ViewerCertificate:
          AcmCertificateArn: !Ref Certificate
          SslSupportMethod: sni-only
          MinimumProtocolVersion: TLSv1.2_2021
        Origins:
          - Id: S3Origin
            DomainName: !GetAtt FrontendBucket.RegionalDomainName
            OriginAccessControlId: !Ref OAC
            S3OriginConfig:
              OriginAccessIdentity: ''
        DefaultCacheBehavior:
          TargetOriginId: S3Origin
          ViewerProtocolPolicy: redirect-to-https
          Compress: true
          CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6
          AllowedMethods: [GET, HEAD]
          CachedMethods: [GET, HEAD]
        CustomErrorResponses:
          - ErrorCode: 403
            ResponsePagePath: /index.html
            ResponseCode: '200'
            ErrorCachingMinTTL: 300

  DnsRecord:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneId: !Ref HostedZoneId
      Name: !Ref DomainName
      Type: A
      AliasTarget:
        DNSName: !GetAtt CdnDistribution.DomainName
        HostedZoneId: Z2FDTNDATAQYW2

Outputs:
  ApiUrl:
    Value: !Sub 'https://${CrcApi}.execute-api.${AWS::Region}.amazonaws.com/prod/count'
  CloudFrontUrl:
    Value: !Sub 'https://${DomainName}'
  CloudFrontDistributionId:
    Value: !Ref CdnDistribution
  S3BucketName:
    Value: !Ref FrontendBucket

Деплой

# Build
sam build

# Первый деплой (guided)
sam deploy --guided
# Stack Name: crc-production
# Region: us-east-1
# Parameter DomainName: resume.ivanpetrov.com
# Parameter HostedZoneId: Z1234567890

# Последующие деплои
sam deploy

# Удаление
sam delete --stack-name crc-production

CDK как альтернатива

# cdk/crc_stack.py
from aws_cdk import (
    Stack, Duration, RemovalPolicy, CfnOutput,
    aws_dynamodb as dynamodb,
    aws_lambda as _lambda,
    aws_apigatewayv2 as apigwv2,
    aws_s3 as s3,
    aws_cloudfront as cloudfront,
    aws_cloudfront_origins as origins,
    aws_certificatemanager as acm,
    aws_route53 as route53,
    aws_route53_targets as targets,
)
from constructs import Construct

class CrcStack(Stack):
    def __init__(self, scope: Construct, id: str, domain: str, **kwargs):
        super().__init__(scope, id, **kwargs)

        # DynamoDB
        table = dynamodb.Table(self, 'Counter',
            table_name='visitor-counter',
            partition_key=dynamodb.Attribute(name='id', type=dynamodb.AttributeType.STRING),
            billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,
            removal_policy=RemovalPolicy.DESTROY
        )

        # Lambda
        fn = _lambda.Function(self, 'CounterFn',
            runtime=_lambda.Runtime.PYTHON_3_12,
            handler='lambda_function.lambda_handler',
            code=_lambda.Code.from_asset('backend/'),
            architecture=_lambda.Architecture.ARM_64,
            timeout=Duration.seconds(10),
            environment={'TABLE_NAME': table.table_name}
        )
        table.grant_read_write_data(fn)

        # S3
        bucket = s3.Bucket(self, 'Frontend',
            removal_policy=RemovalPolicy.DESTROY,
            auto_delete_objects=True
        )

        # CloudFront
        dist = cloudfront.Distribution(self, 'CDN',
            default_behavior=cloudfront.BehaviorOptions(
                origin=origins.S3BucketOrigin.with_origin_access_control(bucket)
            ),
            default_root_object='index.html'
        )

        CfnOutput(self, 'DistributionUrl', value=dist.distribution_domain_name)

Управление стеком

# Просмотр ресурсов стека
aws cloudformation describe-stack-resources --stack-name crc-production

# Просмотр outputs
aws cloudformation describe-stacks --stack-name crc-production \
  --query 'Stacks[0].Outputs'

# Detect drift
aws cloudformation detect-stack-drift --stack-name crc-production

# Rollback
aws cloudformation rollback-stack --stack-name crc-production

Проверь себя

Какая команда SAM покажет preview изменений перед деплоем?

Что такое DynamoDBCrudPolicy в SAM?

Чем CDK лучше SAM для сложных проектов?

Зачем в SAM-шаблоне используется секция Globals?

Что делает Transform: AWS::Serverless-2016-10-31 в SAM-шаблоне?