AWS CloudFormation

2025. 5. 27. 16:01·Side Tech Notes

Coupon Infra 구축

쿠폰 발급 서비스를 구현하면서 단순히 동시성을 잘 처리하는 것이 아닌 얼마나 효과적으로 빠르게 처리하는지를 확인해보기 위해 k6를 사용해 성능 테스트를 하려고 했습니다. 이왕에 테스트하는 거 로컬에서 스프링 애플리케이션을 실행시킨 뒤 테스트하는 것보단 실제 서비스와 같이 aws에 인프라를 구축해서 테스트하면 더 재밌을 것 같다고 생각했습니다. 이왕에 구축하는 거 로드 밸런싱이나 auto scaling도 구축해보고 싶었지만 그랬다간 요금 폭탄을 맞을 것 같았습니다. 그렇다고 테스트를 하는 동안에만 켰다가 사용 안 할때는 요금이 나가는 것들을 다시 지우는 과정을 반복하기엔 너무 번거로울 것 같고 혹시 까먹고 안 지우면 그대로 돈을 지불해야 되서 포기했습니다. 좋은 방법이 없을까 찾아보던 중 AWS의 CloudFormation을 사용하면 인프라 구조를 빠르게 구축하고 빠르게 삭제할 수 있다는 정보를 얻었고 해당 기능을 사용해 테스트를 진행할 수 있었습니다.

CloudFormation이란

CloudFormation은 AWS에서 제공하는 IaC 도구입니다. IaC란 Infrastructure as Code의 약자로 인프라를 수동으로 설정하는 대신 코드를 사용해서 인프라를 관리하는 것을 의미합니다. 인프라를 코드를 통해 관리하기 때문에 수동으로 관리하면서 생기는 오류를 줄일 수 있고 IaC 구성 파일을 재사용하여 쉽게 인프라를 복제할 수도 있습니다. 또한 코드로 되어있기 때문에 협업 시 문서화의 역할을 하며 구성 내용을 공유하기에도 유리합니다.

이런 IaC 도구에는 여러가지가 있지만 저는 그 중 AWS의 CloudFormation을 사용했습니다. 이는 CloudFormation을 IaC 도구 중 유일하게 예전에 들었던 강의에서 한 번 다뤄봤기에 가장 익숙했기 때문입니다.

CloudFormation은 YAML 또는 JSON 형식으로 Template 코드를 만들어 사용합니다. 이 Template을 실행하면 실제 인프라 리소스의 집합체인 Stack을 생성합니다. 이 Stack을 삭제하면 해당 Stack에 포함된 인프라 리소스들도 한번에 삭제됩니다.

저는 코드로 바로 작성했지만 굳이 코드로 작성할 필요없이 AWS 콘솔의 CloudFormation -> 인프라 컴포저에 들어가면 블럭코딩과 같은 형식으로 쉽게 제작이 가능합니다.

Coupon Infra

 

현재 구축한 쿠폰 발급 서비스의 인프라 구조입니다. Server에서 실행될 스프링 애플리케이션은 S3에 jar파일로 저장한 뒤 저장된 파일을 다운로드해서 실행하도록 했습니다. 우선 전체 코드는 아래의 토글로 첨부했습니다. 보안 때문에 일부 코드는 가렸습니다.

전체 코드
AWSTemplateFormatVersion: '2010-09-09'
Description: Coupon Service Infrastructure with Auto Scaling Spring Boot, Redis, and MySQL

Parameters:
  KeyName:
    Type: String
    Default: {key 이름}
    Description: EC2 Key Pair for SSH access

Resources:

  CouponEC2Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: {IAM 역할 이름}
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess

  CouponEC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref CouponEC2Role

  CouponVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: CouponVPC

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref CouponVPC
      CidrBlock: 10.0.3.0/24
      MapPublicIpOnLaunch: true
      AvailabilityZone: !Select [0, !GetAZs '']
      Tags:
        - Key: Name
          Value: CouponPublicSubnet1

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref CouponVPC
      CidrBlock: 10.0.2.0/24
      MapPublicIpOnLaunch: true
      AvailabilityZone: !Select [1, !GetAZs '']
      Tags:
        - Key: Name
          Value: CouponPublicSubnet2

  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref CouponVPC
      CidrBlock: 10.0.1.0/24
      MapPublicIpOnLaunch: false
      AvailabilityZone: !Select [0, !GetAZs '']
      Tags:
        - Key: Name
          Value: CouponPrivateSubnet1

  NATGatewayEIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

  NATGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NATGatewayEIP.AllocationId
      SubnetId: !Ref PublicSubnet1

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref CouponVPC

  PrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NATGateway

  PrivateSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateRouteTable

  InternetGateway:
    Type: AWS::EC2::InternetGateway

  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref CouponVPC
      InternetGatewayId: !Ref InternetGateway

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref CouponVPC

  PublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnetRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable

  PublicSubnetRouteTableAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRouteTable

  CouponSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for Coupon service
      VpcId: !Ref CouponVPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 8080
          ToPort: 8080
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 6379
          ToPort: 6379
          CidrIp: 0.0.0.0/0

  RedisInstance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: t3.micro
      KeyName: !Ref KeyName
      SubnetId: !Ref PrivateSubnet1
      PrivateIpAddress: 10.0.1.101
      ImageId: !Sub "{{resolve:ssm:/aws/service/canonical/ubuntu/server/20.04/stable/current/amd64/hvm/ebs-gp2/ami-id}}"
      IamInstanceProfile: !Ref CouponEC2InstanceProfile
      SecurityGroupIds:
        - !Ref CouponSecurityGroup
      Tags:
        - Key: Name
          Value: RedisInstance
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          apt update -y
          apt install -y redis-server

          sed -i 's/^bind .*/bind 0.0.0.0/' /etc/redis/redis.conf
          sed -i 's/^protected-mode .*/protected-mode no/' /etc/redis/redis.conf

          systemctl enable redis-server
          systemctl restart redis-server

  MySQLInstance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: t3.micro
      KeyName: !Ref KeyName
      SubnetId: !Ref PrivateSubnet1
      PrivateIpAddress: 10.0.1.100
      ImageId: !Sub "{{resolve:ssm:/aws/service/canonical/ubuntu/server/20.04/stable/current/amd64/hvm/ebs-gp2/ami-id}}"
      IamInstanceProfile: !Ref CouponEC2InstanceProfile
      SecurityGroupIds:
        - !Ref CouponSecurityGroup
      Tags:
        - Key: Name
          Value: MySQLInstance
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          apt update -y
          apt install -y mysql-server

          sed -i "s/^bind-address.*/bind-address = 0.0.0.0/" /etc/mysql/mysql.conf.d/mysqld.cnf

          systemctl restart mysql
          systemctl enable mysql

          until mysqladmin ping >/dev/null 2>&1; do
            echo "⏳ Waiting for MySQL to be ready..."
            sleep 2
          done

          mysql -u root <<EOF
          ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY {비밀번호};
          CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED WITH mysql_native_password BY {비밀번호};
          GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
          FLUSH PRIVILEGES;
          CREATE DATABASE IF NOT EXISTS {database 이름};
          EOF

  LaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateName: CouponSpringTemplate
      LaunchTemplateData:
        InstanceType: t3.micro
        KeyName: !Ref KeyName
        ImageId: !Sub "{{resolve:ssm:/aws/service/canonical/ubuntu/server/20.04/stable/current/amd64/hvm/ebs-gp2/ami-id}}"
        IamInstanceProfile: 
          Name: !Ref CouponEC2InstanceProfile
        SecurityGroupIds:
          - !Ref CouponSecurityGroup
        UserData:
          Fn::Base64: !Sub |
            #!/bin/bash
            apt update -y
            apt install -y openjdk-21-jdk unzip curl awscli
            mkdir -p /home/ubuntu/app
            cd /home/ubuntu/app
            aws s3 cp {jar 파일의 s3 주소} .
            nohup java -jar {jar 파일 이름} > coupon.log 2>&1 &

  SpringAutoScalingGroup:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      VPCZoneIdentifier:
        - !Ref PublicSubnet1
        - !Ref PublicSubnet2
      LaunchTemplate:
        LaunchTemplateId: !Ref LaunchTemplate
        Version: !GetAtt LaunchTemplate.LatestVersionNumber
      MinSize: '2'
      MaxSize: '10'
      DesiredCapacity: '2'
      TargetGroupARNs:
        - !Ref ALBTargetGroup

  ALB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: CouponALB
      Scheme: internet-facing
      Subnets:
        - !Ref PublicSubnet1
        - !Ref PublicSubnet2
      SecurityGroups:
        - !Ref CouponSecurityGroup

  ALBTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: CouponTargetGroup
      Port: 8080
      Protocol: HTTP
      VpcId: !Ref CouponVPC
      TargetType: instance
      HealthCheckPath: /

  ALBListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref ALB
      Port: 80
      Protocol: HTTP
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref ALBTargetGroup

  SpringScaleOutAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmDescription: "Scale out if CPU > 50% for 1 minutes"
      Namespace: AWS/EC2
      MetricName: CPUUtilization
      Dimensions:
        - Name: AutoScalingGroupName
          Value: !Ref SpringAutoScalingGroup
      Statistic: Average
      Period: 60
      EvaluationPeriods: 1
      Threshold: 50
      ComparisonOperator: GreaterThanThreshold
      AlarmActions:
        - !Ref SpringScaleOutPolicy

  SpringScaleOutPolicy:
    Type: AWS::AutoScaling::ScalingPolicy
    Properties:
      AutoScalingGroupName: !Ref SpringAutoScalingGroup
      PolicyType: SimpleScaling
      AdjustmentType: ChangeInCapacity
      ScalingAdjustment: 1
      Cooldown: 100

  SpringScaleInAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmDescription: "Scale in if CPU < 30% for 2 minutes"
      Namespace: AWS/EC2
      MetricName: CPUUtilization
      Dimensions:
        - Name: AutoScalingGroupName
          Value: !Ref SpringAutoScalingGroup
      Statistic: Average
      Period: 60
      EvaluationPeriods: 2
      Threshold: 30
      ComparisonOperator: LessThanThreshold
      AlarmActions:
        - !Ref SpringScaleInPolicy

  SpringScaleInPolicy:
    Type: AWS::AutoScaling::ScalingPolicy
    Properties:
      AutoScalingGroupName: !Ref SpringAutoScalingGroup
      PolicyType: SimpleScaling
      AdjustmentType: ChangeInCapacity
      ScalingAdjustment: -1
      Cooldown: 200

Outputs:
  ALBDNS:
    Description: ALB DNS name
    Value: !GetAtt ALB.DNSName

 

이제 각 파트별로 확인해보겠습니다. 블럭 코딩으로 쉽게 만들 수 있기에 각 문법을 자세히 설명하기 보단 해당 코드가 어느 부분을 의미하고 왜 구성했는지 설명하는 방식으로 진행하겠습니다.

기본 메타 정보

AWSTemplateFormatVersion: '2010-09-09'
Description: Coupon Service Infrastructure with Auto Scaling Spring Boot, Redis, and MySQL

AWSTemplateFormatVersion은 CloudFormation의 템플릿 버전을 명시하는 것으로 보통 '2010-09-09'를 사용합니다.

Description은 템플릿에 대한 설명입니다.

Parameters

Parameters:
  KeyName:
    Type: String
    Default: {key 이름}
    Description: EC2 Key Pair for SSH access

EC2에 SSH 접속할 때 사용할 키 페어 이름 입니다. 해당 키는 이미 생성되어 있는 키의 이름이어야 합니다.

IAM Role 및 인스턴스 프로파일

CouponEC2Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: {IAM 역할 이름}
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess

  CouponEC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref CouponEC2Role

CouponEC2Role은 S3에 대한 접근 권한을 주기 위한 Role입니다. 이는 EC2에서 스프링 애플리케이션을 S3에서 jar파일을 다운받아 실행하기 때문에 EC2에 S3의 접근 권한을 주기 위해 만들었습니다.

CouponEC2InstanceProfile은 생성한 CouponEC2Role을 EC2에 연결하기 위해 만들었습니다.

네트워크 구성 (VPC, Subnets, NAT, 라우팅)

CouponVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: CouponVPC

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref CouponVPC
      CidrBlock: 10.0.3.0/24
      MapPublicIpOnLaunch: true
      AvailabilityZone: !Select [0, !GetAZs '']
      Tags:
        - Key: Name
          Value: CouponPublicSubnet1

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref CouponVPC
      CidrBlock: 10.0.2.0/24
      MapPublicIpOnLaunch: true
      AvailabilityZone: !Select [1, !GetAZs '']
      Tags:
        - Key: Name
          Value: CouponPublicSubnet2

  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref CouponVPC
      CidrBlock: 10.0.1.0/24
      MapPublicIpOnLaunch: false
      AvailabilityZone: !Select [0, !GetAZs '']
      Tags:
        - Key: Name
          Value: CouponPrivateSubnet1

  NATGatewayEIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

  NATGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NATGatewayEIP.AllocationId
      SubnetId: !Ref PublicSubnet1

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref CouponVPC

  PrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NATGateway

  PrivateSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateRouteTable

  InternetGateway:
    Type: AWS::EC2::InternetGateway

  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref CouponVPC
      InternetGatewayId: !Ref InternetGateway

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref CouponVPC

  PublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnetRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable

  PublicSubnetRouteTableAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRouteTable

위 코드는 VPC와 Subnet, NATGateway, InternetGateway를 구성하는 코드입니다.

VPC는 AWS의 리소스를 배치할 수 있는 논리적 네트워크 영역입니다. 이 템플릿에서 정의되는 모든 Subnet, EC2, ALB 등은 전부 이 VPC 안에서 동작합니다. IP 주소로는 10.0.0.0/16을 할당하여 총 65,536개의 IP 주소를 사용할 수 있습니다.

Subnet은 Public Subnet 2개와 Private Subnet 1개를 생성했습니다. Public Subnet의 EC2에는 스프링 애플리케이션을, Private Subnet의 EC2에서는 Redis와 MySQL을 설치했습니다. 이는 MapPublicIpOnLaunch의 값에 따라 Public Subnet인지 Private Subnet인지 결정할 수 있습니다.

NATGateway는 Private Subnet에 있는 EC2가 외부 인터넷과 통신할 수 있도록 해줍니다. NAT Gateway는 단방향 연결만 가능하므로 외부에서는 Private Subnet에 접근하지 못합니다. 그런데 가격이 비싸서(한달에 약 46$) 다른 프로젝트에서는 bastion 서버를 구축했지만 여기선 테스트할 때만 사용할 것이기 때문에 구축했습니다. NATGateway를 이용해서 Private Subnet의 EC2에서 Redis와 MySQL을 설치하도록 했습니다.

InternetGateway는 퍼블릭 서브넷에서 외부와 통신할 수 있도록 해줍니다. InternetGateway를 VPC에 연결해서 통신이 가능하도록 해줍니다.

보안 그룹

CouponSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for Coupon service
      VpcId: !Ref CouponVPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 8080
          ToPort: 8080
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 6379
          ToPort: 6379
          CidrIp: 0.0.0.0/0

원래는 보안을 위해 더 세부적으로 설정해줘야 하지만 테스트할 때만 사용할 거라서 러프하게 구축했습니다.

22는 ssh 통신을 위함이고 3306은 mysql, 6379는 redis를 위해서 허용해줬습니다.

Redis, MySQL EC2 인스턴스

RedisInstance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: t3.micro
      KeyName: !Ref KeyName
      SubnetId: !Ref PrivateSubnet1
      PrivateIpAddress: 10.0.1.101
      ImageId: !Sub "{{resolve:ssm:/aws/service/canonical/ubuntu/server/20.04/stable/current/amd64/hvm/ebs-gp2/ami-id}}"
      IamInstanceProfile: !Ref CouponEC2InstanceProfile
      SecurityGroupIds:
        - !Ref CouponSecurityGroup
      Tags:
        - Key: Name
          Value: RedisInstance
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          apt update -y
          apt install -y redis-server

          sed -i 's/^bind .*/bind 0.0.0.0/' /etc/redis/redis.conf
          sed -i 's/^protected-mode .*/protected-mode no/' /etc/redis/redis.conf

          systemctl enable redis-server
          systemctl restart redis-server

  MySQLInstance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: t3.micro
      KeyName: !Ref KeyName
      SubnetId: !Ref PrivateSubnet1
      PrivateIpAddress: 10.0.1.100
      ImageId: !Sub "{{resolve:ssm:/aws/service/canonical/ubuntu/server/20.04/stable/current/amd64/hvm/ebs-gp2/ami-id}}"
      IamInstanceProfile: !Ref CouponEC2InstanceProfile
      SecurityGroupIds:
        - !Ref CouponSecurityGroup
      Tags:
        - Key: Name
          Value: MySQLInstance
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          apt update -y
          apt install -y mysql-server

          sed -i "s/^bind-address.*/bind-address = 0.0.0.0/" /etc/mysql/mysql.conf.d/mysqld.cnf

          systemctl restart mysql
          systemctl enable mysql

          until mysqladmin ping >/dev/null 2>&1; do
            echo "⏳ Waiting for MySQL to be ready..."
            sleep 2
          done

          mysql -u root <<EOF
          ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY {비밀번호};
          CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED WITH mysql_native_password BY {비밀번호};
          GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
          FLUSH PRIVILEGES;
          CREATE DATABASE IF NOT EXISTS {database 이름};
          EOF

Redis와 MySQL이 설치된 EC2에 대한 코드입니다. 주목할 부분은 ImageId와 PrivateIpAddress, UserData부분입니다.

ImageId는 AMI를 사용하기 위해 사용했습니다. AWS에서는 각 EC2의 기본적인 이미지가 세팅되어 있습니다. 여기서 저는 ubuntu에 amd64, gp2를 사용하는 AMI를 사용하도록 세팅했습니다.

PrivateIpAddress는 각각의 EC2의 private ip를 고정하기 위해 사용했습니다. 이는 스프링 애플리케이션에서 mysql과 redis의 주소를 가지고 있어야 하는데 지정을 안 해주면 매 생성마다 달라져 연결할 수가 없기 때문에 지정해줬습니다.

UserData는 EC2 시작 시 실행할 스크립트를 작성하는 부분입니다. UserData를 통해 redis와 mysql을 설치하고 기본세팅을 진행했습니다. 여기서 bind-address를 0.0.0.0/으로 설정해줬는데 이는 모든 IP에서 redis와 mysql에 접근이 가능하도록 하는 설정입니다. 실제 운영환경에선 보안의 문제로 세부적으로 설정해줘야합니다.

Launch Template

LaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateName: CouponSpringTemplate
      LaunchTemplateData:
        InstanceType: t3.micro
        KeyName: !Ref KeyName
        ImageId: !Sub "{{resolve:ssm:/aws/service/canonical/ubuntu/server/20.04/stable/current/amd64/hvm/ebs-gp2/ami-id}}"
        IamInstanceProfile: 
          Name: !Ref CouponEC2InstanceProfile
        SecurityGroupIds:
          - !Ref CouponSecurityGroup
        UserData:
          Fn::Base64: !Sub |
            #!/bin/bash
            apt update -y
            apt install -y openjdk-21-jdk unzip curl awscli
            mkdir -p /home/ubuntu/app
            cd /home/ubuntu/app
            aws s3 cp {jar 파일의 s3 주소} .
            nohup java -jar {jar 파일 이름} > coupon.log 2>&1 &

스프링 애플리케이션이 구동될 EC2의 템플릿입니다. 여기서 aws s3 cp 명령어를 통해 S3에 저장된 jar파일을 다운받아 실행하도록 설정했습니다. 이제 Auto Scaling을 하면 이 템플릿의 EC2가 생성됩니다.

Auto Scaling

SpringAutoScalingGroup:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      VPCZoneIdentifier:
        - !Ref PublicSubnet1
        - !Ref PublicSubnet2
      LaunchTemplate:
        LaunchTemplateId: !Ref LaunchTemplate
        Version: !GetAtt LaunchTemplate.LatestVersionNumber
      MinSize: '2'
      MaxSize: '10'
      DesiredCapacity: '2'
      TargetGroupARNs:
        - !Ref ALBTargetGroup

Auto Scaling에 대한 설정입니다. 위에서 설정한 LaunchTemplate으로 EC2를 만들도록 설정했고 EC2는 최소 2개 최대 10개까지 생성하도록 설정했습니다.

Load Balancer (ALB)

ALB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: CouponALB
      Scheme: internet-facing
      Subnets:
        - !Ref PublicSubnet1
        - !Ref PublicSubnet2
      SecurityGroups:
        - !Ref CouponSecurityGroup

  ALBTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: CouponTargetGroup
      Port: 8080
      Protocol: HTTP
      VpcId: !Ref CouponVPC
      TargetType: instance
      HealthCheckPath: /

  ALBListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref ALB
      Port: 80
      Protocol: HTTP
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref ALBTargetGroup

로드 밸런싱을 해줄 ALB에 대한 설정입니다. 80으로 들어오는 요청을 Public Subnet에 있는 EC2에 8080 포트로 전달하도록 설정했습니다.

Auto Scaling 정책 및 경보

SpringScaleOutAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmDescription: "Scale out if CPU > 50% for 1 minutes"
      Namespace: AWS/EC2
      MetricName: CPUUtilization
      Dimensions:
        - Name: AutoScalingGroupName
          Value: !Ref SpringAutoScalingGroup
      Statistic: Average
      Period: 60
      EvaluationPeriods: 1
      Threshold: 50
      ComparisonOperator: GreaterThanThreshold
      AlarmActions:
        - !Ref SpringScaleOutPolicy

  SpringScaleOutPolicy:
    Type: AWS::AutoScaling::ScalingPolicy
    Properties:
      AutoScalingGroupName: !Ref SpringAutoScalingGroup
      PolicyType: SimpleScaling
      AdjustmentType: ChangeInCapacity
      ScalingAdjustment: 1
      Cooldown: 100

  SpringScaleInAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmDescription: "Scale in if CPU < 30% for 2 minutes"
      Namespace: AWS/EC2
      MetricName: CPUUtilization
      Dimensions:
        - Name: AutoScalingGroupName
          Value: !Ref SpringAutoScalingGroup
      Statistic: Average
      Period: 60
      EvaluationPeriods: 2
      Threshold: 30
      ComparisonOperator: LessThanThreshold
      AlarmActions:
        - !Ref SpringScaleInPolicy

  SpringScaleInPolicy:
    Type: AWS::AutoScaling::ScalingPolicy
    Properties:
      AutoScalingGroupName: !Ref SpringAutoScalingGroup
      PolicyType: SimpleScaling
      AdjustmentType: ChangeInCapacity
      ScalingAdjustment: -1
      Cooldown: 200

Auto Scaling 정책에 대한 코드입니다. SpringScaleOutAlarm에서 지정한 Auto Scaling Group을 모니터링합니다. Auto Scaling Group의 CPU 평균이 1분 동안 50% 이상일 경우 AlarmActions에서 지정한 정책을 수행합니다. 그러면 ScalingAdjustment에 지정한 수만큼 EC2를 늘리고 Cooldown 동안은 새로운 스케일링을 방지합니다. SpringScaleInAlarm도 비슷한 방식으로 동작합니다.

Outputs

Outputs:
  ALBDNS:
    Description: ALB DNS name
    Value: !GetAtt ALB.DNSName

이 부분은 스택이 생성된 후 사용자에게 어떤 값을 표시해줄지를 정하는 부분입니다. 이 값은 다른 스택에서 참조할 수도 있습니다. 저는 ALB의 DNSName을 반환하도록 설정했습니다.

'Side Tech Notes' 카테고리의 다른 글

분산 트랜잭션과 합의  (0) 2025.07.21
리액터 패턴 / 프로액터 패턴  (1) 2025.07.19
@Transactional과 동시성 제어를 위한 Lock의 관계  (0) 2025.05.23
빌더 패턴이 정말로 좋을까  (0) 2025.03.24
좋은 코드란 무엇일까 (feat. 객체지향)  (0) 2025.02.21
'Side Tech Notes' 카테고리의 다른 글
  • 분산 트랜잭션과 합의
  • 리액터 패턴 / 프로액터 패턴
  • @Transactional과 동시성 제어를 위한 Lock의 관계
  • 빌더 패턴이 정말로 좋을까
ggio
ggio
개발 공부를 하며 배운 내용을 기록합니다.
  • ggio
    기록을 하자
    ggio
  • 전체
    오늘
    어제
    • 분류 전체보기 (41)
      • SW마에스트로 (5)
      • System Architecture (8)
      • Algorithm (15)
      • Side Tech Notes (7)
      • CS (5)
      • 취준 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    매일메일
    코테
    리액터 패턴
    소프트웨어 마에스트로
    Programming
    fail back
    멀티 코어
    알고리즘
    프로액터 패턴
    코딩테스트
    fail over
    Algorithm
    소마
    메시지 큐
    SW마에스트로
    객체지향
    leetcode
    프로그래밍
    분산락
    시스템 아키텍쳐
    at-least-once
    토스 NEXT
    ha 아키텍처
    리트코드
    시스템 설계
    3PC
    지리적 분산
    비관락
    부트캠프
    다중화
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
ggio
AWS CloudFormation
상단으로

티스토리툴바