AWSTemplateFormatVersion: '2010-09-09'
Description: >
  3-Tier Architecture with Aurora MySQL Cluster (HA)
  ALB + ASG + Aurora MySQL Multi-AZ (Writer + Reader) + Bastion + Dual NAT HA
  - Aurora Writer 삭제 시 Reader가 자동으로 Writer로 승격
  - PHP는 Writer/Reader 엔드포인트를 분리하여 사용
  - Web Server checks Aurora connection status every 5 seconds via PHP.

Parameters:
  KeyName:
    Type: AWS::EC2::KeyPair::KeyName
    Description: EC2 접속을 위한 기존 키 페어 이름을 입력하세요.

  DBMasterUsername:
    Type: String
    Default: admin

  DBMasterUserPassword:
    Type: String
    NoEcho: true
    MinLength: 8
    Description: DB 마스터 비밀번호 (최소 8자)

  LatestAmiId:
    Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
    Default: '/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64'

Resources:

# ---------------- VPC & Networking ----------------
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true

  IGW:
    Type: AWS::EC2::InternetGateway

  AttachIGW:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref IGW

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24
      MapPublicIpOnLaunch: true
      AvailabilityZone: !Select [0, !GetAZs '']

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.2.0/24
      MapPublicIpOnLaunch: true
      AvailabilityZone: !Select [1, !GetAZs '']

  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.11.0/24
      AvailabilityZone: !Select [0, !GetAZs '']

  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.12.0/24
      AvailabilityZone: !Select [1, !GetAZs '']

  DBSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.21.0/24
      AvailabilityZone: !Select [0, !GetAZs '']

  DBSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.22.0/24
      AvailabilityZone: !Select [1, !GetAZs '']

# ---------------- NAT Gateways (High Availability) ----------------
  EIP1:
    Type: AWS::EC2::EIP
    DependsOn: AttachIGW
    Properties:
      Domain: vpc

  EIP2:
    Type: AWS::EC2::EIP
    DependsOn: AttachIGW
    Properties:
      Domain: vpc

  NAT1:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt EIP1.AllocationId
      SubnetId: !Ref PublicSubnet1

  NAT2:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt EIP2.AllocationId
      SubnetId: !Ref PublicSubnet2

# ---------------- Routing ----------------
  PublicRT:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC

  PublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRT
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref IGW

  PubAssoc1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRT

  PubAssoc2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRT

  PrivateRT1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC

  PrivateRoute1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRT1
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NAT1

  PriAssoc1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateRT1

  PrivateRT2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC

  PrivateRoute2:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRT2
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NAT2

  PriAssoc2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet2
      RouteTableId: !Ref PrivateRT2

# ---------------- Security Groups ----------------
  ALBSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !Ref VPC
      GroupDescription: Allow HTTP from everywhere
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0

  WebSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !Ref VPC
      GroupDescription: Allow HTTP from ALB
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          SourceSecurityGroupId: !Ref ALBSG

  BastionSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !Ref VPC
      GroupDescription: Allow SSH from everywhere
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0

  DBSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !Ref VPC
      GroupDescription: Allow MySQL from Web and Bastion
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          SourceSecurityGroupId: !Ref WebSG
        - IpProtocol: tcp
          FromPort: 3306
          ToPort: 3306
          SourceSecurityGroupId: !Ref BastionSG

# ---------------- Load Balancer ----------------
  ALB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Subnets: [!Ref PublicSubnet1, !Ref PublicSubnet2]
      SecurityGroups: [!Ref ALBSG]

  TG:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Port: 80
      Protocol: HTTP
      VpcId: !Ref VPC
      TargetType: instance
      HealthCheckPath: /index.php
      HealthCheckIntervalSeconds: 30

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

# ---------------- Aurora MySQL Cluster ----------------
# HA 핵심:
#   - AuroraCluster : 클러스터 엔드포인트(Writer)와 Reader 엔드포인트를 영구 보유
#   - AuroraWriter  : 첫 번째 인스턴스 (Writer/Primary), 삭제되어도 Reader가 자동 승격
#   - AuroraReader  : 두 번째 인스턴스 (Reader/Replica), 다른 AZ에 배치
#   - 인스턴스가 삭제/교체되어도 클러스터 엔드포인트는 변하지 않으므로
#     PHP 코드의 접속 주소 변경 없이 서비스 지속 가능

  DBSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: Aurora Multi-AZ Subnet Group
      SubnetIds: [!Ref DBSubnet1, !Ref DBSubnet2]

  AuroraCluster:
    Type: AWS::RDS::DBCluster
    DeletionPolicy: Snapshot
    Properties:
      Engine: aurora-mysql
      EngineVersion: "8.0.mysql_aurora.3.10.4"
      MasterUsername: !Ref DBMasterUsername
      MasterUserPassword: !Ref DBMasterUserPassword
      DBSubnetGroupName: !Ref DBSubnetGroup
      VpcSecurityGroupIds: [!Ref DBSG]
      BackupRetentionPeriod: 1          # 최소 1일 백업 유지 (0은 Aurora 미지원)
      DeletionProtection: false
      StorageEncrypted: true

  # Writer 인스턴스 — AZ-a
  AuroraWriter:
    Type: AWS::RDS::DBInstance
    Properties:
      DBClusterIdentifier: !Ref AuroraCluster
      DBInstanceClass: db.t3.medium    # Aurora는 db.t3.micro 미지원
      Engine: aurora-mysql
      AvailabilityZone: !Select [0, !GetAZs '']
      PubliclyAccessible: false

  # Reader 인스턴스 — AZ-b
  # Writer 삭제 시 이 인스턴스가 자동으로 Writer로 승격됨
  AuroraReader:
    Type: AWS::RDS::DBInstance
    DependsOn: AuroraWriter
    Properties:
      DBClusterIdentifier: !Ref AuroraCluster
      DBInstanceClass: db.t3.medium
      Engine: aurora-mysql
      AvailabilityZone: !Select [1, !GetAZs '']
      PubliclyAccessible: false

# ---------------- Launch Template & Auto Scaling ----------------
  LT:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateData:
        ImageId: !Ref LatestAmiId
        InstanceType: t3.micro
        SecurityGroupIds: [!Ref WebSG]
        # ----------------------------------------------------------------
        # Aurora 페일오버 대응 PHP 페이지
        #
        # [접속 전략]
        #   1차: 클러스터 Writer 엔드포인트 (일반 쓰기/읽기)
        #   2차: 클러스터 Reader 엔드포인트 (Writer 페일오버 중 읽기용 폴백)
        #
        # Aurora 페일오버 흐름:
        #   Writer 삭제 → Reader가 Writer로 자동 승격 (30초~1분)
        #   → 클러스터 Writer 엔드포인트가 새 Writer를 자동으로 가리킴
        #   → PHP 코드 수정 없이 자동 복구
        # ----------------------------------------------------------------
        UserData:
          Fn::Base64: !Sub |
            #!/bin/bash
            dnf update -y
            dnf install -y httpd php php-mysqli mariadb105
            systemctl enable httpd
            systemctl start httpd

            cat <<'PHPEOF' > /var/www/html/index.php
            <!DOCTYPE html>
            <html>
            <head>
                <title>Aurora HA Connectivity Monitor</title>
                <meta http-equiv="refresh" content="5">
                <style>
                    body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; text-align: center; padding: 50px; background-color: #f4f7f6; }
                    .container { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); display: inline-block; min-width: 500px; }
                    .status { font-weight: bold; padding: 15px; border-radius: 5px; margin: 10px 0; font-size: 1.1em; }
                    .ok      { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
                    .warn    { background-color: #fff3cd; color: #856404; border: 1px solid #ffc107; }
                    .fail    { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
                    .info    { color: #666; font-size: 0.85em; text-align: left; background: #eee; padding: 10px; border-radius: 5px; margin-top: 10px; }
                    .badge   { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.8em; font-weight: bold; margin-left: 6px; }
                    .badge-writer { background: #cce5ff; color: #004085; }
                    .badge-reader { background: #e2e3e5; color: #383d41; }
                </style>
            </head>
            <body>
                <div class="container">
                    <h1>☁️ Aurora HA Status Monitor</h1>
                    <p><strong>Web Server:</strong> <?php echo gethostname(); ?></p>
                    <p><strong>Last Checked:</strong> <?php echo date('Y-m-d H:i:s T'); ?> (Every 5s)</p>
                    <hr>

                    <?php
                    // ── 엔드포인트 설정 ──────────────────────────────────────────
                    // CloudFormation Sub로 주입된 Aurora 클러스터 엔드포인트
                    $writer_host = 'WRITER_ENDPOINT_PLACEHOLDER';
                    $reader_host = 'READER_ENDPOINT_PLACEHOLDER';
                    $user        = 'DB_USER_PLACEHOLDER';
                    $pass        = 'DB_PASS_PLACEHOLDER';
                    $timeout     = 3; // 초

                    // ── 공통 접속 함수 ────────────────────────────────────────────
                    function try_connect($host, $user, $pass, $timeout) {
                        mysqli_report(MYSQLI_REPORT_OFF);
                        $conn = mysqli_init();
                        mysqli_options($conn, MYSQLI_OPT_CONNECT_TIMEOUT, $timeout);
                        if (@mysqli_real_connect($conn, $host, $user, $pass)) {
                            $role_res = mysqli_query($conn, "SELECT @@innodb_read_only AS ro");
                            $role_row = mysqli_fetch_assoc($role_res);
                            $role     = ($role_row['ro'] == '0') ? 'WRITER' : 'READER(Promoted)';
                            mysqli_close($conn);
                            return ['ok' => true, 'role' => $role, 'error' => ''];
                        }
                        return ['ok' => false, 'role' => '', 'error' => mysqli_connect_error()];
                    }

                    // ── Writer 엔드포인트 체크 ────────────────────────────────────
                    $w = try_connect($writer_host, $user, $pass, $timeout);
                    if ($w['ok']) {
                        echo "<div class='status ok'>● WRITER ENDPOINT CONNECTED <span class='badge badge-writer'>{$w['role']}</span></div>";
                    } else {
                        // Writer 접속 실패 → 페일오버 진행 중일 가능성
                        echo "<div class='status warn'>⚠ WRITER ENDPOINT UNREACHABLE (Failover in progress?)</div>";
                        echo "<p style='color:#856404;font-size:0.9em'>Error: {$w['error']}</p>";
                    }

                    // ── Reader 엔드포인트 체크 (폴백) ────────────────────────────
                    $r = try_connect($reader_host, $user, $pass, $timeout);
                    if ($r['ok']) {
                        echo "<div class='status ok'>● READER ENDPOINT CONNECTED <span class='badge badge-reader'>{$r['role']}</span></div>";
                    } else {
                        echo "<div class='status fail'>○ READER ENDPOINT UNREACHABLE</div>";
                        echo "<p style='color:red;font-size:0.9em'>Error: {$r['error']}</p>";
                    }

                    // ── 전체 서비스 판정 ──────────────────────────────────────────
                    if (!$w['ok'] && !$r['ok']) {
                        echo "<div class='status fail' style='font-size:1.3em'>🔴 SERVICE UNAVAILABLE — Both endpoints unreachable</div>";
                    } elseif (!$w['ok'] && $r['ok']) {
                        echo "<div class='status warn'>🟡 DEGRADED — Serving via Reader only (Failover in progress)</div>";
                    } else {
                        echo "<div class='status ok'>🟢 SERVICE HEALTHY</div>";
                    }
                    ?>

                    <div class="info">
                        <strong>Writer Endpoint:</strong> <?php echo $writer_host; ?><br>
                        <strong>Reader Endpoint:</strong> <?php echo $reader_host; ?><br>
                        <br>
                        <strong>Failover Notes:</strong><br>
                        • Writer 삭제 시 Reader가 자동으로 Writer로 승격됩니다 (~30–60초)<br>
                        • 승격 완료 후 Writer 엔드포인트가 새 Writer를 자동으로 가리킵니다<br>
                        • 코드 변경 없이 서비스가 자동 복구됩니다
                    </div>
                </div>
            </body>
            </html>
            PHPEOF

            # CloudFormation Sub 치환값을 sed로 실제 엔드포인트로 교체
            # (Heredoc 내부에서 !Sub 중첩 회피를 위해 sed 사용)
            sed -i \
              -e "s|WRITER_ENDPOINT_PLACEHOLDER|${AuroraCluster.Endpoint.Address}|g" \
              -e "s|READER_ENDPOINT_PLACEHOLDER|${AuroraCluster.ReadEndpoint.Address}|g" \
              -e "s|DB_USER_PLACEHOLDER|${DBMasterUsername}|g" \
              -e "s|DB_PASS_PLACEHOLDER|${DBMasterUserPassword}|g" \
              /var/www/html/index.php

  ASG:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      VPCZoneIdentifier: [!Ref PrivateSubnet1, !Ref PrivateSubnet2]
      LaunchTemplate:
        LaunchTemplateId: !Ref LT
        Version: !GetAtt LT.LatestVersionNumber
      MinSize: 2
      MaxSize: 4
      DesiredCapacity: 2
      TargetGroupARNs: [!Ref TG]

# ---------------- Bastion ----------------
  Bastion:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: t3.micro
      KeyName: !Ref KeyName
      ImageId: !Ref LatestAmiId
      SubnetId: !Ref PublicSubnet1
      SecurityGroupIds: [!Ref BastionSG]
      Tags:
        - Key: Name
          Value: Bastion-Host

# ---------------- Outputs ----------------
Outputs:
  WebUrl:
    Description: ALB 주소 — 접속하여 Aurora HA 상태를 확인하세요.
    Value: !Sub "http://${ALB.DNSName}"

  BastionPublicIP:
    Description: Bastion 호스트 IP
    Value: !GetAtt Bastion.PublicIp

  AuroraWriterEndpoint:
    Description: Aurora 클러스터 Writer 엔드포인트
    Value: !GetAtt AuroraCluster.Endpoint.Address

  AuroraReaderEndpoint:
    Description: Aurora 클러스터 Reader 엔드포인트
    Value: !GetAtt AuroraCluster.ReadEndpoint.Address
