Host private static website qua cloudfront ở tài khoản khác

Tình huống

Có 2 tài khoản:

  • App: Là app static build ra từ React

  • Tài khoản App: chứa cụm EKS, chỉ tạo private ALB

  • Tài khoản network, chứa public ALB, Cloudfront, proxy và Transit gateway

Static website: là phần thư mục build/ của câu lệnh npm build, reactjs

Bây giờ muốn host các trang web static này, và để các trang static này trong cụm EKS, người dùng sẽ truy cập vào thông qua Cloudfront ở tài khoản network

Assumption: Đã có sẵn:

Cách 1: Host qua EKS

Bước 1: Tạo app sample

Cấu trúc thư mục:

my-react-app/
├── src/
   ├── App.js           <- File code 
   ├── index.js
   ├── index.css        <- File chứa Tailwind imports
   └── components/      <- Thư mục chứa các components phụ (nếu cần)
├── public/
   └── index.html       <- Ảnh: refer thành S3 link 
├── package.json
└── tailwind.config.js

Để tạo nhanh một dự án React mới với cấu trúc này:

npx create-react-app my-react-app
cd my-react-app

Thay thế nội dung của src/App.js:

import React from 'react';
import { useState } from 'react';
import { Heart, Home, User, Settings, Menu, X } from 'lucide-react';

const App = () => {
  const [isMenuOpen, setIsMenuOpen] = useState(false);
  const [activeTab, setActiveTab] = useState('home');
  const [likes, setLikes] = useState(0);

  const menuItems = [
    { id: 'home', label: 'Trang chủ', icon: <Home className="w-5 h-5" /> },
    { id: 'profile', label: 'Hồ sơ', icon: <User className="w-5 h-5" /> },
    { id: 'settings', label: 'Cài đặt', icon: <Settings className="w-5 h-5" /> }
  ];

  const toggleMenu = () => setIsMenuOpen(!isMenuOpen);

  const renderContent = () => {
    switch(activeTab) {
      case 'home':
        return (
          <div className="p-6">
            <h1 className="text-2xl font-bold mb-4">Chào mừng đến với ứng dụng mẫu</h1>
            <div className="bg-white rounded-lg shadow p-6 mb-4">
              <h2 className="text-xl font-semibold mb-2">Bài viết mẫu</h2>
              <p className="text-gray-600 mb-4">
                Đây  một bài viết mẫu để kiểm tra giao diện. Bạn  thể thêm nội dung tùy ý vào đây.
              </p>
              <button 
                onClick={() => setLikes(prev => prev + 1)}
                className="flex items-center gap-2 px-4 py-2 bg-red-100 text-red-600 rounded-lg hover:bg-red-200"
              >
                <Heart className="w-5 h-5" />
                <span>{likes} lượt thích</span>
              </button>
            </div>
          </div>
        );
      case 'profile':
        return (
          <div className="p-6">
            <h1 className="text-2xl font-bold mb-4">Hồ  người dùng</h1>
            <div className="bg-white rounded-lg shadow p-6">
              <div className="flex items-center gap-4 mb-4">
                <div className="w-16 h-16 bg-gray-200 rounded-full"></div>
                <div>
                  <h2 className="text-xl font-semibold">Người dùng mẫu</h2>
                  <p className="text-gray-600">user@example.com</p>
                </div>
              </div>
              <p className="text-gray-600">
                Đây  trang hồ  mẫu. Bạn  thể thêm thông tin người dùng  đây.
              </p>
            </div>
          </div>
        );
      case 'settings':
        return (
          <div className="p-6">
            <h1 className="text-2xl font-bold mb-4">Cài đặt</h1>
            <div className="bg-white rounded-lg shadow p-6">
              <div className="space-y-4">
                <div className="flex items-center justify-between">
                  <span className="font-medium">Thông báo</span>
                  <div className="w-12 h-6 bg-gray-200 rounded-full relative cursor-pointer"></div>
                </div>
                <div className="flex items-center justify-between">
                  <span className="font-medium">Chế độ tối</span>
                  <div className="w-12 h-6 bg-gray-200 rounded-full relative cursor-pointer"></div>
                </div>
              </div>
            </div>
          </div>
        );
      default:
        return null;
    }
  };

  return (
    <div className="min-h-screen bg-gray-50">
      {/* Header */}
      <header className="bg-white shadow">
        <div className="max-w-7xl mx-auto px-4">
          <div className="flex items-center justify-between h-16">
            <div className="flex items-center">
              <span className="text-xl font-bold">Logo</span>
            </div>
            <button
              onClick={toggleMenu}
              className="md:hidden p-2 rounded-md hover:bg-gray-100"
            >
              {isMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
            </button>
          </div>
        </div>
      </header>

      {/* Mobile menu */}
      {isMenuOpen && (
        <div className="md:hidden">
          <div className="bg-white shadow-lg">
            {menuItems.map(item => (
              <button
                key={item.id}
                onClick={() => {
                  setActiveTab(item.id);
                  setIsMenuOpen(false);
                }}
                className={`w-full flex items-center gap-3 px-4 py-3 hover:bg-gray-50
                  ${activeTab === item.id ? 'text-blue-600 bg-blue-50' : 'text-gray-700'}`}
              >
                {item.icon}
                {item.label}
              </button>
            ))}
          </div>
        </div>
      )}

      {/* Desktop navigation */}
      <div className="hidden md:flex max-w-7xl mx-auto px-4 py-4">
        <nav className="space-x-4">
          {menuItems.map(item => (
            <button
              key={item.id}
              onClick={() => setActiveTab(item.id)}
              className={`flex items-center gap-2 px-4 py-2 rounded-lg
                ${activeTab === item.id 
                  ? 'bg-blue-50 text-blue-600' 
                  : 'text-gray-700 hover:bg-gray-50'}`}
            >
              {item.icon}
              {item.label}
            </button>
          ))}
        </nav>
      </div>

      {/* Main content */}
      <main className="max-w-7xl mx-auto">
        {renderContent()}
      </main>
    </div>
  );
};

export default App;

Cài đặt các dependencies cần thiết:

npm install lucide-react
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

build react app:

npm run build

Sau câu lệnh trên, 1 thư mục buid sẽ được tạo ra, cấu trúc thư mục sản phẩm build:

.
├── asset-manifest.json
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
├── robots.txt
└── static
    ├── css
       ├── main.e6c13ad2.css
       └── main.e6c13ad2.css.map
    └── js
        ├── 453.ad6eb26e.chunk.js
        ├── 453.ad6eb26e.chunk.js.map
        ├── main.0ff13fa2.js
        ├── main.0ff13fa2.js.LICENSE.txt
        └── main.0ff13fa2.js.map

3 directories, 14 files

Để host static website này sẽ có 2 cách:

Ở bài này sẽ thử làm theo cách 2. Nếu bạn muốn completely serverless và không quản lý servers/containers, thì nên dùng theo cách 1.

Bước 2: Tạo ECR repo và image (tài khoản app):

Tạo Dockerfile:

FROM nginx:alpine
COPY react-build/ /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/conf.d/default.conf

nginx.conf:

server {
    listen 80;
    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /static/ {
        try_files $uri =404;
    }

    # Prevent caching for index.html
    location = /index.html {
        add_header Cache-Control "no-cache";
    }

    # Proper MIME types
    types {
        text/html                             html;
        text/css                              css;
        application/javascript                js;
        image/x-icon                          ico;
        image/png                             png;
        application/json                      json;
    }
}

Tạo ECR repository:

aws ecr create-repository --repository-name static-react-app

Login vào ECR:

aws ecr get-login-password --region ap-southeast-1 | docker login --username AWS --password-stdin $(aws sts get-caller-identity --query Account --output text).dkr.ecr.ap-southeast-1.amazonaws.com

Build image:

docker build -t static-react-app .

Tag image:

docker tag static-react-app:latest $(aws sts get-caller-identity --query Account --output text).dkr.ecr.ap-southeast-1.amazonaws.com/static-react-app:latest

Push image:

docker push $(aws sts get-caller-identity --query Account --output text).dkr.ecr.ap-southeast-1.amazonaws.com/static-react-app:latest

Note trường hợp nếu có chỉnh sửa và cần build lại:

# Rebuild:
docker build -t static-react-app .

# Tag và push
docker tag static-react-app:latest $(aws sts get-caller-identity --query Account --output text).dkr.ecr.ap-southeast-1.amazonaws.com/static-react-app:latest
docker push $(aws sts get-caller-identity --query Account --output text).dkr.ecr.ap-southeast-1.amazonaws.com/static-react-app:latest

# Restart deployment
kubectl rollout restart deployment/react-static-1 -n web-landingpage
kubectl rollout restart deployment/react-static-2 -n web-landingpage

Bước 3: Tạo các file YAML (để apply trên tài khoản app)

1-namespace.yaml

apiVersion: v1
kind: Namespace
metadata:
  name: web-landingpage

2-app1-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: react-static-1
  namespace: web-landingpage
spec:
  replicas: 2
  selector:
    matchLabels:
      app: react-static-1
  template:
    metadata:
      labels:
        app: react-static-1
    spec:
      containers:
      - name: react-static
        image: <app-account-id>.dkr.ecr.ap-southeast-1.amazonaws.com/static-react-app:latest
        ports:
        - containerPort: 80

3-app2-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: react-static-2
  namespace: web-landingpage
spec:
  replicas: 2
  selector:
    matchLabels:
      app: react-static-2
  template:
    metadata:
      labels:
        app: react-static-2
    spec:
      containers:
      - name: react-static
        image: <app-account-id>.dkr.ecr.ap-southeast-1.amazonaws.com/static-react-app:latest
        ports:
        - containerPort: 80

4-app1-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: react-static-service-1
  namespace: web-landingpage
spec:
  selector:
    app: react-static-1
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

5-app2-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: react-static-service-2
  namespace: web-landingpage
spec:
  selector:
    app: react-static-2
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

6-ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-landingpage-ingress
  namespace: web-landingpage
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internal
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-southeast-1:<app-account-id>:certificate/<cert-id>
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'
    alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}'
spec:
  rules:
  - http:
      paths:
      - path: /app1
        pathType: Prefix
        backend:
          service:
            name: react-static-service-1
            port:
              number: 80
      - path: /app2
        pathType: Prefix
        backend:
          service:
            name: react-static-service-2
            port:
              number: 80

Apply các manifest theo thứ tự đã đặt ở tên file. Kiểm tra:

kubectl get pods -n web-landingpage
kubectl get ingress -n web-landingpage

Bước 4: Add các DNS records

static.app.devopslearning.co.uk, CNAME, value là DNS của ALB private ở tài khoản app

public-alb.devopslearning.co.uk, Alias, value là DNS của public ALB trên tài khoản network

static-web.devopslearning.co.uk, Alias, value là DNS của Cloudfront distribution tài khoản network

Bước 5: Tạo proxy ở tài khoản network

Mặc định thì sẽ không thể có target cho ALB cross account, nên không thể add IP của ALB private bên tài khoản app làm target → cần 1 proxy, ở đây sẽ sử dụng nginx cho đơn giản

Tạo 1 EC2 instance và:

sudo apt update
sudo apt install nginx -y

sudo bash -c 'cat <<EOL > /etc/nginx/sites-available/default
server {
    listen 80;
    location / {
        proxy_pass http://static.app.devopslearning.co.uk;
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
    }
}
EOL'

Kiểm tra trạng thái của nginx:

sudo nginx -t
sudo systemctl restart nginx
$ cat /etc/nginx/sites-available/default
server {
    listen 80;
    location / {
        proxy_pass http://static.app.devopslearning.co.uk;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Sau đó, từ trình duyệt: https://<public-ip-proxy>/app1, và https://<public-ip-proxy>/app2 sẽ truy cập được app static nằm trong tài khoản app.

Note: Proxy để cho phép tạo public IP chỉ đơn giản là để debug dễ hơn, quan sát behavior dễ hơn, còn thực tế thì không cần!

Tiếp theo, gắn EC2 này vào làm target của ALB public. Thay public-ip-proxy bằng https://public-alb.devopslearning.co.uk/app1 và https://public-alb.devopslearning.co.uk/app2 cũng sẽ truy cập được app bình thường, dạng như sau:

Bước 6: Tạo CloudFront trên tài khoản network

Note: đảm bảo cloudfront 1 alias là static-web.devopslearning.co.uk (là giá trị đã register vào Route53)

Origin name và DNS của cloudfront sẽ là public-alb.devopslearning.co.uk (DNS record trỏ đến DNS của public ALB)

Có thể setup original path nếu thích

Truy cập vào cloudfront thì sẽ truy cập được thông qua: https://static-web.devopslearning.co.uk/app1 và https://static-web.devopslearning.co.uk/app2

Cách 2: Host qua S3

Bước 1: Tạo 1 số tài nguyên trên tài khoản app

Tạo mới VPC


vpc_id=$(aws ec2 create-vpc \
    --cidr-block 10.0.0.0/16 \
    --instance-tenancy default \
    --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=network-vpc}]' \
    --query 'Vpc.VpcId' \
    --output text)

# Enable DNS hostnames
aws ec2 modify-vpc-attribute \
    --vpc-id $vpc_id \
    --enable-dns-hostnames

# Tạo private subnet 1 trong AZ đầu tiên
private_subnet_1=$(aws ec2 create-subnet \
    --vpc-id $vpc_id \
    --cidr-block 10.0.1.0/24 \
    --availability-zone ap-southeast-1a \
    --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=private-subnet-1}]' \
    --query 'Subnet.SubnetId' \
    --output text)

# Tạo private subnet 2 trong AZ thứ hai  
private_subnet_2=$(aws ec2 create-subnet \
    --vpc-id $vpc_id \
    --cidr-block 10.0.2.0/24 \
    --availability-zone ap-southeast-1b \
    --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=private-subnet-2}]' \
    --query 'Subnet.SubnetId' \
    --output text)

# Tạo route table cho private subnet
private_rt=$(aws ec2 create-route-table \
    --vpc-id $vpc_id \
    --tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=private-rt}]' \
    --query 'RouteTable.RouteTableId' \
    --output text)

# Liên kết route table với private subnet 1
aws ec2 associate-route-table \
    --route-table-id $private_rt \
    --subnet-id $private_subnet_1

# Liên kết route table với private subnet 2  
aws ec2 associate-route-table \
    --route-table-id $private_rt \
    --subnet-id $private_subnet_2

echo "Private Subnet 1 ID: $private_subnet_1"
echo "Private Subnet 2 ID: $private_subnet_2"
echo "Private Route Table ID: $private_rt"
echo "VPC: $vpc_id"

Tạo S3 bucket

aws s3api create-bucket \
    --bucket static-web.devopslearning.co.uk \
    --region ap-southeast-1 \
    --create-bucket-configuration LocationConstraint=ap-southeast-1

Tạo các security cần thiết

# Security Group cho ALB
aws ec2 describe-security-groups \
    --filters Name=group-name,Values=alb-sg Name=vpc-id,Values=$vpc_id

alb_sg_id=$(aws ec2 create-security-group \
    --group-name alb-sg \
    --description "Security group for Internal ALB" \
    --vpc-id $vpc_id \
    --query 'GroupId' \
    --output text)

echo "Security Group ID: $alb_sg_id"

# Security Group ID: sg-021c42e85129bd984

aws ec2 authorize-security-group-ingress \
    --group-id $alb_sg_id \
    --protocol tcp \
    --port 443 \
    --cidr 172.31.0.0/16

aws ec2 authorize-security-group-ingress \
    --group-id $alb_sg_id \
    --protocol tcp \
    --port 443 \
    --cidr 10.0.0.0/16

# Security group cho VPC Endpoint
endpoint_sg_id=$(aws ec2 create-security-group \
    --group-name s3-endpoint-sg \
    --description "Security group for S3 VPC Endpoint" \
    --vpc-id $vpc_id \
    --output text --query 'GroupId')

aws ec2 authorize-security-group-ingress \
    --group-id $endpoint_sg_id \
    --protocol tcp \
    --port 443 \
    --source-group $alb_sg_id

Tạo S3 VPC endpoint (Interface)

vpc_endpoint_id=$(aws ec2 create-vpc-endpoint \
    --vpc-id $vpc_id \
    --vpc-endpoint-type Interface \
    --service-name com.amazonaws.ap-southeast-1.s3 \
    --subnet-ids $sn1 $sn2 \
    --security-group-ids $endpoint_sg_id \
    --no-private-dns-enabled \
    --output text --query 'VpcEndpoint.VpcEndpointId')

echo $vpc_endpoint_id

# Lấy IPs của endpoint
eni_ids=$(aws ec2 describe-vpc-endpoints \
    --vpc-endpoint-ids $vpc_endpoint_id \
    --query 'VpcEndpoints[0].NetworkInterfaceIds' \
    --output text)

endpoint_ips=$(aws ec2 describe-network-interfaces \
    --network-interface-ids $eni_ids \
    --query 'NetworkInterfaces[*].PrivateIpAddress' \
    --output text)

Tạo internal ALB

target_group_arn=$(aws elbv2 create-target-group \
    --name s3-static-tg \
    --protocol HTTPS \
    --port 443 \
    --vpc-id $vpc_id \
    --target-type ip \
    --health-check-protocol HTTPS \
    --health-check-port 443 \
    --health-check-path "/api/card/v2/index.html" \
    --health-check-interval-seconds 30 \
    --health-check-timeout-seconds 5 \
    --healthy-threshold-count 2 \
    --unhealthy-threshold-count 2 \
    --matcher '{"HttpCode": "200,301,307,405"}' \
    --output text --query 'TargetGroups[0].TargetGroupArn')

# Register targets
for ip in $endpoint_ips; do
    echo "Registering IP: $ip"
    aws elbv2 register-targets \
        --target-group-arn $target_group_arn \
        --targets Id=$ip,Port=443
done

# Tạo ALB
alb_arn=$(aws elbv2 create-load-balancer \
    --name s3-internal-alb \
    --subnets $sn1 $sn2 \
    --security-groups $alb_sg_id \
    --scheme internal \
    --type application \
    --output text --query 'LoadBalancers[0].LoadBalancerArn')

export app_cert=arn:aws:acm:ap-southeast-1:<app-account-id>:certificate/<cert-id>
# Tạo HTTPS listener
listener_arn=$(aws elbv2 create-listener \
    --load-balancer-arn $alb_arn \
    --protocol HTTPS \
    --port 443 \
    --certificates CertificateArn=$app_cert \
    --default-actions Type=forward,TargetGroupArn=$target_group_arn \
    --output text --query 'Listeners[0].ListenerArn')

Tạo listener cho ALB internal

aws elbv2 create-rule \
    --listener-arn $listener_arn \
    --priority 1 \
    --conditions '[{"Field":"path-pattern","Values":["/api/card/v2/*"]}]' \
    --actions '[{"Type":"forward","TargetGroupArn":"'$target_group_arn'"}]'

aws elbv2 create-rule \
    --listener-arn $listener_arn \
    --priority 2 \
    --conditions '[{"Field":"path-pattern","Values":["/api/gateway/v2/*"]}]' \
    --actions '[{"Type":"forward","TargetGroupArn":"'$target_group_arn'"}]'

Tạo S3 bucket policy

aws s3api put-bucket-policy \
    --bucket static-web.devopslearning.co.uk \
    --policy '{
        "Version": "2012-10-17",
        "Statement": [{
            "Sid": "VPCEAccess",
            "Effect": "Allow",
            "Principal": "*",
            "Action": ["s3:GetObject", "s3:ListBucket"],
            "Resource": [
                "arn:aws:s3:::static-web.devopslearning.co.uk",
                "arn:aws:s3:::static-web.devopslearning.co.uk/*"
            ],
            "Condition": {
                "StringEquals": {
                    "aws:SourceVpce": "'$vpc_endpoint_id'"
                }
            }
        }]
    }'

Thiết lập CORS cho S3 bucket:

[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "GET"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": []
    }
]

Sample app sẽ tận dụng lại từ hướng dẫn bên trên, nếu chưa có hãy tham khảo cách tạo app bên trên.

Upload content


aws s3 cp build/ s3://static-web.devopslearning.co.uk/api/card/v2/ --recursive
aws s3 cp build/ s3://static-web.devopslearning.co.uk/api/gateway/v2/ --recursive

Lấy DNS của internal ALB:

aws elbv2 describe-load-balancers --load-balancer-arns $alb_arn --query 'LoadBalancers[0].DNSName' --output text

Setup DNS internal ALB trên Route 53 public hosted zone

aws route53 change-resource-record-sets \
    --hosted-zone-id <hosted-zone-id> \
    --change-batch '{
        "Changes": [{
            "Action": "CREATE",
            "ResourceRecordSet": {
                "Name": "static-web.devopslearning.co.uk",
                "Type": "CNAME",
                "TTL": 300,
                "ResourceRecords": [{
                    "Value": "'$app_internal_alb_dns'"
                }]
            }
        }]
    }'

Command kiểm tra:

# Test command:
curl -k -v https://static-web.devopslearning.co.uk/api/gateway/v2/index.html
curl -k -v https://static-web.devopslearning.co.uk/api/card/v2/index.html

Lưu ý hiện tại đang là ALB internal nên chỉ có thể resolve trên EC2 chung VPC với ALB!

Tạo transit gateway attachment

(Phần này chỉ làm được khi đã tạo xong transit gateway trên tài khoản network:

tgw_attach_net_id=$(aws ec2 create-transit-gateway-vpc-attachment \
    --transit-gateway-id $tgw_id \
    --vpc-id $app_vpc_id \
    --subnet-ids $sn1 $sn2 \
    --tag-specifications 'ResourceType=transit-gateway-attachment,Tags=[{Key=Name,Value=app-attachment}]' \
    --query 'TransitGatewayVpcAttachment.TransitGatewayAttachmentId' \
    --output text)

export app_pri_rtb_id=rtb-aaa6be169d663aaa

aws ec2 create-route \
    --route-table-id $app_pri_rtb_id \
    --destination-cidr-block 172.31.0.0/16 \
    --transit-gateway-id $tgw_id

Bước 2: Thiết lập trên tài khoản network

Tạo transit gateway

# Tạo transit gateway: 
tgw_id=$(aws ec2 create-transit-gateway \
    --description "TGW between network and app accounts" \
    --options "AmazonSideAsn=65000,AutoAcceptSharedAttachments=enable,DefaultRouteTableAssociation=enable,DefaultRouteTablePropagation=enable,VpnEcmpSupport=enable,DnsSupport=enable,MulticastSupport=disable" \
    --tag-specifications 'ResourceType=transit-gateway,Tags=[{Key=Name,Value=cross-account-tgw}]' \
    --query 'TransitGateway.TransitGatewayId' \
    --output text)

# Share TGW với tài khoản app thông qua Resource Access Manager (RAM)
aws ram create-resource-share \
    --name "tgw-share" \
    --principals <app-account-id> \
    --resource-arns arn:aws:ec2:ap-southeast-1:<network-account-id>:transit-gateway/$tgw_id

Tạo TransitGateway attachment

(tài khoản network attach vào)

# Attach VPC network account (private subnet)
tgw_attach_net_id=$(aws ec2 create-transit-gateway-vpc-attachment \
    --transit-gateway-id $tgw_id \
    --vpc-id $netvpc \
    --subnet-ids $netprn1 $netprn2 $netprn3 \
    --tag-specifications 'ResourceType=transit-gateway-attachment,Tags=[{Key=Name,Value=network-attachment}]' \
    --query 'TransitGatewayVpcAttachment.TransitGatewayAttachmentId' \
    --output text)

export pri_rtb_id=rtb-0764aef742ecceaaa

aws ec2 create-route \
    --route-table-id $pri_rtb_id \
    --destination-cidr-block 10.0.0.0/16 \
    --transit-gateway-id $tgw_id

Tạo EC2 instance làm proxy trên tài khoản network

# Security group cho EC2 proxy
proxy_sg_id=$(aws ec2 create-security-group \
    --group-name proxy-sg \
    --description "Security group for Proxy EC2" \
    --vpc-id $netvpc \
    --output text --query 'GroupId')

# Allow inbound HTTP/HTTPS từ ALB
aws ec2 authorize-security-group-ingress \
    --group-id $proxy_sg_id \
    --protocol tcp \
    --port 80 \
    --cidr 0.0.0.0/0

# Allow inbound HTTP/HTTPS từ ALB
aws ec2 authorize-security-group-ingress \
    --group-id $proxy_sg_id \
    --protocol tcp \
    --port 443 \
    --cidr 0.0.0.0/0

# Tạo EC2 với nginx proxy config
proxy_instance_id=$(aws ec2 run-instances \
    --image-id ami-078c1149d8ad719a7 \
    --instance-type t3.micro \
    --subnet-id subnet-e707d7afdda \
    --security-group-ids $proxy_sg_id \
    --iam-instance-profile Arn=arn:aws:iam::<network-account-id>:instance-profile/proxy-ssm-profile \
    --associate-public-ip-address

echo "New instance ID: $proxy_instance_id"

Chờ instance running, ssh vào rồi chạy:

sudo apt update
sudo apt install nginx -y

sudo tee /etc/nginx/sites-available/default << "EOF"
server {
    listen 80;

    location /health {
        return 200;
    }

    # Lấy service name từ request URI
    set $service_name "";
    if ($request_uri ~ "^/api/([^/]+)/") {
        set $service_name $1;
    }

    # Dynamic sub_filter dựa trên service_name
    sub_filter '/static/' '/api/$service_name/v2/static/';
    sub_filter_once off;

    # Direct access cho images (dùng cho email)
    location /images/ {
        proxy_pass https://static-web.devopslearning.co.uk/images/;
        proxy_set_header Host static-web.devopslearning.co.uk;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_ssl_verify off;
    }

    # API endpoint cho web app
    location ~ ^/api/([^/]+)/v2/images/(.+)$ {
        set $app_name $1;
        set $image_path $2;
        proxy_pass https://static-web.devopslearning.co.uk/images/$app_name/$image_path;
        proxy_set_header Host static-web.devopslearning.co.uk;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_ssl_verify off;
    }

    location / {
        proxy_pass https://static-web.devopslearning.co.uk;
        proxy_set_header Host static-web.devopslearning.co.uk;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_ssl_verify off;
    }
}
EOF

sudo nginx -t
sudo systemctl restart nginx

Register với target group

(health check path /)

aws elbv2 register-targets \
    --target-group-arn $pub_tg_arn \
    --targets Id=$proxy_instance_id,Port=80

aws elbv2 describe-target-health \
    --target-group-arn $pub_tg_arn

Tạo public ALB

# Security group cho public ALB
pub_alb_sg_id=$(aws ec2 create-security-group \
    --group-name public-alb-sg \
    --description "Security group for Public ALB" \
    --vpc-id $netvpc \
    --output text --query 'GroupId')


aws ec2 authorize-security-group-ingress \
    --group-id $pub_alb_sg_id \
    --protocol tcp \
    --port 443 \
    --cidr 0.0.0.0/0

# Tạo target group
pub_tg_arn=$(aws elbv2 create-target-group \
    --name proxy-tg \
    --protocol HTTP \
    --port 80 \
    --vpc-id $netvpc \
    --target-type instance \
    --health-check-path "/" \
    --output text --query 'TargetGroups[0].TargetGroupArn')

# Register EC2 proxy
aws elbv2 register-targets \
    --target-group-arn $pub_tg_arn \
    --targets Id=$proxy_instance_id

# Tạo public ALB
pub_alb_arn=$(aws elbv2 create-load-balancer \
    --name public-alb \
    --subnets $networkpublic1 $$networkpublic2 \
    --security-groups $pub_alb_sg_id \
    --scheme internet-facing \
    --type application \
    --output text --query 'LoadBalancers[0].LoadBalancerArn')

# Tạo HTTPS listener
pub_listener_arn=$(aws elbv2 create-listener \
    --load-balancer-arn $pub_alb_arn \
    --protocol HTTPS \
    --port 443 \
    --certificates CertificateArn=arn:aws:acm:ap-southeast-1:<network-account-id>:certificate/<cert-id> \
    --default-actions Type=forward,TargetGroupArn=$pub_tg_arn \
    --output text --query 'Listeners[0].ListenerArn')


# Get ALB DNS name
pub_alb_dns=$(aws elbv2 describe-load-balancers \
    --load-balancer-arns $pub_alb_arn \
    --query 'LoadBalancers[0].DNSName' \
    --output text)

# Create Route53 record for ALB
aws route53 change-resource-record-sets \
    --hosted-zone-id <hostedzoneid> \
    --change-batch '{
        "Changes": [{
            "Action": "CREATE",
            "ResourceRecordSet": {
                "Name": "public-alb.devopslearning.co.uk",
                "Type": "CNAME",
                "TTL": 300,
                "ResourceRecords": [{
                    "Value": "'$pub_alb_dns'"
                }]
            }
        }]
    }'

Tạo cloudfront

# Tạo file config cho CloudFront
cat > distribution-config.json << EOF
{
    "CallerReference": "cli-$(date +%s)",
    "Aliases": {
        "Quantity": 1,
        "Items": ["web.devopslearning.co.uk"]
    },
    "DefaultRootObject": "index.html",
    "Origins": {
        "Quantity": 1,
        "Items": [
            {
                "Id": "PublicALB",
                "DomainName": "public-alb.devopslearning.co.uk",
                "CustomOriginConfig": {
                    "HTTPPort": 80,
                    "HTTPSPort": 443,
                    "OriginProtocolPolicy": "https-only",
                    "OriginSslProtocols": {
                        "Quantity": 1,
                        "Items": ["TLSv1.2"]
                    }
                }
            }
        ]
    },
    "DefaultCacheBehavior": {
        "TargetOriginId": "PublicALB",
        "ForwardedValues": {
            "QueryString": true,
            "Cookies": {
                "Forward": "all"
            },
            "Headers": {
                "Quantity": 1,
                "Items": ["Host"]
            }
        },
        "TrustedSigners": {
            "Enabled": false,
            "Quantity": 0
        },
        "ViewerProtocolPolicy": "redirect-to-https",
        "MinTTL": 0,
        "AllowedMethods": {
            "Quantity": 7,
            "Items": ["HEAD", "DELETE", "POST", "GET", "OPTIONS", "PUT", "PATCH"],
            "CachedMethods": {
                "Quantity": 2,
                "Items": ["HEAD", "GET"]
            }
        },
        "SmoothStreaming": false,
        "DefaultTTL": 300,
        "MaxTTL": 3600,
        "Compress": true,
        "LambdaFunctionAssociations": {
            "Quantity": 0
        },
        "FieldLevelEncryptionId": ""
    },
    "CacheBehaviors": {
        "Quantity": 0
    },
    "CustomErrorResponses": {
        "Quantity": 0
    },
    "Comment": "Distribution for web.devopslearning.co.uk",
    "Logging": {
        "Enabled": false,
        "IncludeCookies": false,
        "Bucket": "",
        "Prefix": ""
    },
    "PriceClass": "PriceClass_All",
    "Enabled": true,
    "ViewerCertificate": {
        "ACMCertificateArn": "arn:aws:acm:us-east-1:<network-account-id>:certificate/<cert-id>",
        "SSLSupportMethod": "sni-only",
        "MinimumProtocolVersion": "TLSv1.2_2021",
        "Certificate": "arn:aws:acm:us-east-1:<network-account-id>:certificate/<cert-id>",
        "CertificateSource": "acm"
    },
    "Restrictions": {
        "GeoRestriction": {
            "RestrictionType": "none",
            "Quantity": 0
        }
    },
    "WebACLId": "",
    "HttpVersion": "http2",
    "IsIPV6Enabled": true
}
EOF

# Tạo CloudFront distribution
aws cloudfront create-distribution \
    --distribution-config file://distribution-config.json


# Get CloudFront domain name
cf_domain=$(aws cloudfront list-distributions \
    --query "DistributionList.Items[?Aliases.Items[?contains(@, 'web.devopslearning.co.uk')]].DomainName" \
    --output text)

echo $cf_domain

# Create Route53 record for CloudFront
aws route53 change-resource-record-sets \
    --hosted-zone-id <hosted-zone-id> \
    --change-batch '{
        "Changes": [{
            "Action": "CREATE",
            "ResourceRecordSet": {
                "Name": "web.devopslearning.co.uk",
                "Type": "CNAME",
                "TTL": 300,
                "ResourceRecords": [{
                    "Value": "'$cf_domain'"
                }]
            }
        }]
    }'

Bước 3: Test kết quả

Chờ 1 lúc cho CF tạo xong

curl -k https://public-alb.devopslearning.co.uk/api/card/v2/index.html 
curl -k https://web.devopslearning.co.uk/api/card/v2/index.html

Terraform cho cách 2

Với cách 1, chủ yếu bạn chỉ cần làm việc với EKS và các file YAML, còn cách 2 phức tạp hơn nên mình có tạo code Terraform để deploy nhanh hơn cho những ai biết Terraform. Lưu ý Terraform tạo với mục đích testing là chính nên sẽ không tối ưu, cần customize lại.

Tài khoản app:

main.tf:

provider "aws" {
  region = "ap-southeast-1"
}

# VPC and Networking
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  instance_tenancy     = "default"
  enable_dns_hostnames = true

  tags = {
    Name = "network-vpc"
  }
}

resource "aws_subnet" "private_1" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.1.0/24"
  availability_zone = "ap-southeast-1a"

  tags = {
    Name = "private-subnet-1"
  }
}

resource "aws_subnet" "private_2" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.2.0/24"
  availability_zone = "ap-southeast-1b"

  tags = {
    Name = "private-subnet-2"
  }
}

resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "private-rt"
  }
}

resource "aws_route_table_association" "private_1" {
  subnet_id      = aws_subnet.private_1.id
  route_table_id = aws_route_table.private.id
}

resource "aws_route_table_association" "private_2" {
  subnet_id      = aws_subnet.private_2.id
  route_table_id = aws_route_table.private.id
}

# S3 Bucket
resource "aws_s3_bucket" "static_web" {
  bucket = "static-web.devopslearning.co.uk"
}

resource "aws_s3_bucket_policy" "allow_vpce_access" {
  bucket = aws_s3_bucket.static_web.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid       = "VPCEAccess"
        Effect    = "Allow"
        Principal = "*"
        Action    = ["s3:GetObject", "s3:ListBucket"]
        Resource = [
          aws_s3_bucket.static_web.arn,
          "${aws_s3_bucket.static_web.arn}/*"
        ]
        Condition = {
          StringEquals = {
            "aws:SourceVpce" = aws_vpc_endpoint.s3.id
          }
        }
      },
      {
        Sid    = "AllowALBAccess"
        Effect = "Allow"
        Principal = {
          AWS = "*"
        }
        Action   = "s3:GetObject"
        Resource = "${aws_s3_bucket.static_web.arn}/*"
        Condition = {
          StringLike = {
            "aws:SourceArn" : "${aws_lb.internal.arn}"
          }
        }
      }
    ]
  })
}

# Security Groups
resource "aws_security_group" "alb" {
  name        = "alb-sg"
  description = "Security group for Internal ALB"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["100.64.0.0/16", "10.0.0.0/16"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_security_group" "endpoint" {
  name        = "s3-endpoint-sg"
  description = "Security group for S3 VPC Endpoint"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["100.64.0.0/16", "10.0.0.0/16"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# VPC Endpoint
resource "aws_vpc_endpoint" "s3" {
  vpc_id              = aws_vpc.main.id
  service_name        = "com.amazonaws.ap-southeast-1.s3"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = [aws_subnet.private_1.id, aws_subnet.private_2.id]
  security_group_ids  = [aws_security_group.endpoint.id]
  private_dns_enabled = false
}


# Lấy IP của VPC Endpoint
data "aws_network_interface" "s3_endpoint" {
  count = 2 # Vì có 2 subnet

  depends_on = [aws_vpc_endpoint.s3,
    aws_security_group.endpoint,
  aws_security_group.alb]

  filter {
    name   = "vpc-id"
    values = [aws_vpc.main.id]
  }

  filter {
    name   = "subnet-id"
    values = [count.index == 0 ? aws_subnet.private_1.id : aws_subnet.private_2.id]
  }

  filter {
    name   = "group-id"
    values = [aws_security_group.endpoint.id]
  }
}

# ALB and Target Group
resource "aws_lb_target_group" "s3_static" {
  name        = "s3-static-tg"
  port        = 443
  protocol    = "HTTPS"
  vpc_id      = aws_vpc.main.id
  target_type = "ip"

  health_check {
    protocol            = "HTTPS"
    port                = "443"
    path                = "/"
    interval            = 30
    timeout             = 5
    healthy_threshold   = 2
    unhealthy_threshold = 2
    matcher             = "200,301,307,405"
  }

  tags = {
    Name = "s3-static-tg"
  }
}

# Register IP vào target group sử dụng aws_lb_target_group_attachment
resource "aws_lb_target_group_attachment" "s3_endpoint" {
  count = 2

  target_group_arn = aws_lb_target_group.s3_static.arn
  target_id        = data.aws_network_interface.s3_endpoint[count.index].private_ip
  port             = 443

  lifecycle {
    ignore_changes = [target_id] # Ignore changes to target_id after creation
  }
}

resource "aws_lb" "internal" {
  name               = "s3-internal-alb"
  internal           = true
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = [aws_subnet.private_1.id, aws_subnet.private_2.id]
}

resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.internal.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-2016-08"
  certificate_arn   = "arn:aws:acm:ap-southeast-1:<app-account-id>:certificate/<cert-id>"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.s3_static.arn
  }
}

# Listener Rules
resource "aws_lb_listener_rule" "card" {
  listener_arn = aws_lb_listener.https.arn
  priority     = 1

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.s3_static.arn
  }

  condition {
    path_pattern {
      values = ["/api/card/v2/*"]
    }
  }
}

resource "aws_lb_listener_rule" "gateway" {
  listener_arn = aws_lb_listener.https.arn
  priority     = 2

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.s3_static.arn
  }

  condition {
    path_pattern {
      values = ["/api/gateway/v2/*"]
    }
  }
}

output "s3_endpoint_ips" {
  value = data.aws_network_interface.s3_endpoint[*].private_ip
}

# Sau khi transit gateway ở tài khoản web được tạo, cần attach transit gateway vào VPC tài khoản app, và sau đó sửa route table cho route từ VPC tài khoản app sang VPC tài khoản network thông qua transit gateway!

s3.tf

# Tạo prefixes cho images trong bucket static web
resource "aws_s3_object" "images_prefix" {
  bucket       = aws_s3_bucket.static_web.id
  key          = "images/"
  content_type = "application/x-directory"
}

resource "aws_s3_object" "images_card_prefix" {
  bucket       = aws_s3_bucket.static_web.id
  key          = "images/card/"
  content_type = "application/x-directory"
}

resource "aws_s3_object" "images_gateway_prefix" {
  bucket       = aws_s3_bucket.static_web.id
  key          = "images/gateway/"
  content_type = "application/x-directory"
}

Tài khoản network:

vpc.tf

# VPC and Networking
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_config.network_vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = merge(var.tags, {
    Name = "${var.environment}-network-vpc"
  })
}

# Private Subnets
resource "aws_subnet" "private" {
  count             = length(var.vpc_config.azs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_config.network_vpc_cidr, 4, count.index)
  availability_zone = var.vpc_config.azs[count.index]

  tags = merge(var.tags, {
    Name = "${var.environment}-private-${count.index + 1}"
  })
}

# Public Subnets
resource "aws_subnet" "public" {
  count                   = length(var.vpc_config.azs)
  vpc_id                  = aws_vpc.main.id
  cidr_block              = cidrsubnet(var.vpc_config.network_vpc_cidr, 4, count.index + 3)
  availability_zone       = var.vpc_config.azs[count.index]
  map_public_ip_on_launch = true

  tags = merge(var.tags, {
    Name = "${var.environment}-public-${count.index + 1}"
  })
}

# Route Table Associations
resource "aws_route_table_association" "private" {
  count          = length(var.vpc_config.azs)
  subnet_id      = aws_subnet.private[count.index].id
  route_table_id = aws_route_table.private.id
}

resource "aws_route_table_association" "public" {
  count          = length(var.vpc_config.azs)
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

# Internet Gateway
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "main-igw"
  }
}

# Route Tables
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

  route {
    cidr_block         = var.vpc_config.app_vpc_cidr
    transit_gateway_id = aws_ec2_transit_gateway.main.id
  }

  tags = merge(var.tags, {
    Name = "${var.environment}-public-rt"
  })
}

resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block         = var.vpc_config.app_vpc_cidr
    transit_gateway_id = aws_ec2_transit_gateway.main.id
  }

  tags = merge(var.tags, {
    Name = "${var.environment}-private-rt"
  })
}

transit-gateway.tf

# Transit Gateway
resource "aws_ec2_transit_gateway" "main" {
  description = "TGW between network and app accounts"

  tags = {
    Name = "cross-account-tgw"
  }

  default_route_table_association = "enable"
  default_route_table_propagation = "enable"
  dns_support                     = "enable"
  vpn_ecmp_support                = "enable"
  auto_accept_shared_attachments  = "enable"
  amazon_side_asn                 = 65000
}

# Share TGW với app account
resource "aws_ram_resource_share" "tgw" {
  name = "tgw-share"

  tags = {
    Name = "tgw-share"
  }
}

resource "aws_ram_principal_association" "tgw" {
  principal          = var.app_account_id
  resource_share_arn = aws_ram_resource_share.tgw.arn
}

resource "aws_ram_resource_association" "tgw" {
  resource_share_arn = aws_ram_resource_share.tgw.arn
  resource_arn       = aws_ec2_transit_gateway.main.arn
}

# Attach network VPC to TGW
resource "aws_ec2_transit_gateway_vpc_attachment" "network" {
  subnet_ids         = aws_subnet.private[*].id
  transit_gateway_id = aws_ec2_transit_gateway.main.id
  vpc_id             = aws_vpc.main.id

  tags = merge(var.tags, {
    Name = "${var.environment}-network-attachment"
  })
}

route53.tf (để add record DNS ALB của tài khoản app vào Route53 public hosted zone quản lý trên tài khoản network)


# Route53 Record
resource "aws_route53_record" "static_web" {
  zone_id = "<hosted-zone-id"
  name    = "static-web.devopslearning.co.uk"
  type    = "CNAME"
  ttl     = "300"
  records = ["<app-alb-dns>"]
}

proxy.tf (Tạo ra EC2 proxy)

# Security Group for EC2 Proxy
resource "aws_security_group" "proxy" {
  name        = "proxy-sg"
  description = "Security group for Proxy EC2"
  vpc_id      = aws_vpc.main.id

  # Allow SSH from anywhere
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow SSH from anywhere"
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow HTTP"
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow HTTPS"
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow all outbound traffic"
  }

  tags = {
    Name = "proxy-sg"
  }
}

# EC2 Instance Role
resource "aws_iam_role" "proxy_role" {
  name = "proxy-ssm-role-v3"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "ssm_policy" {
  role       = aws_iam_role.proxy_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_instance_profile" "proxy_profile" {
  name = "proxy-ssm-profile-v3"
  role = aws_iam_role.proxy_role.name
}

# EC2 Instance
resource "aws_instance" "proxy" {
  ami                    = "ami-078c1149d8ad719a7" # Ubuntu 20.04 LTS
  instance_type          = "t3.micro"
  subnet_id              = aws_subnet.public[0].id
  vpc_security_group_ids = [aws_security_group.proxy.id]
  iam_instance_profile   = aws_iam_instance_profile.proxy_profile.name
  key_name               = "proxy-key" # Thêm SSH key pair name của bạn

  user_data = <<-EOF
              #!/bin/bash
              apt update
              apt install -y nginx
              cat > /etc/nginx/sites-available/default << "CONF"
              server {
                  listen 80;

                  location /health {
                      return 200;
                  }

                  # Lấy service name từ request URI
                  set $service_name "";
                  if ($request_uri ~ "^/api/([^/]+)/") {
                      set $service_name $1;
                  }

                  # Dynamic sub_filter dựa trên service_name
                  sub_filter '/static/' '/api/$service_name/v2/static/';
                  sub_filter_once off;

                  # Direct access cho images (dùng cho email)
                  location /images/ {
                      proxy_pass https://static-web.${var.domain_name}/images/;
                      proxy_set_header Host static-web.${var.domain_name};
                      proxy_set_header X-Real-IP $remote_addr;
                      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                      proxy_set_header X-Forwarded-Proto $scheme;
                      proxy_ssl_verify off;
                  }

                  # API endpoint cho web app
                  location ~ ^/api/([^/]+)/v2/images/(.+)$ {
                      set $app_name $1;
                      set $image_path $2;
                      proxy_pass https://static-web.${var.domain_name}/images/$app_name/$image_path;
                      proxy_set_header Host static-web.${var.domain_name};
                      proxy_set_header X-Real-IP $remote_addr;
                      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                      proxy_set_header X-Forwarded-Proto $scheme;
                      proxy_ssl_verify off;
                  }

                  location / {
                      proxy_pass https://static-web.${var.domain_name};
                      proxy_set_header Host static-web.${var.domain_name};
                      proxy_set_header X-Real-IP $remote_addr;
                      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                      proxy_set_header X-Forwarded-Proto $scheme;
                      proxy_ssl_verify off;
                  }
              }
              CONF
              nginx -t
              systemctl restart nginx
              EOF

  tags = merge(var.tags, {
    Name = "${var.environment}-proxy-instance"
  })

  # Enable public IP
  associate_public_ip_address = true

  root_block_device {
    volume_size = 8
    volume_type = "gp3"
    encrypted   = true
  }
}

# Output the public IP
output "proxy_public_ip" {
  value       = aws_instance.proxy.public_ip
  description = "The public IP of the proxy EC2 instance"
}

# Sau khi tạo xong EC2, cần register EC2 làm target group cho ALB

providers.tf

provider "aws" {
  region = "ap-southeast-1"
  # Sử dụng credentials của network account
}

# Provider cho Route53 và ACM
provider "aws" {
  alias  = "us_east_1"
  region = "us-east-1"
  # Needed for CloudFront certificate
}

public-alb.tf

# Security Group for Public ALB
resource "aws_security_group" "public_alb" {
  name        = "public-alb-sg"
  description = "Security group for Public ALB"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "public-alb-sg"
  }
}

# Target Group
resource "aws_lb_target_group" "proxy" {
  name        = "proxy-tg"
  port        = 80
  protocol    = "HTTP"
  vpc_id      = aws_vpc.main.id
  target_type = "instance"

  health_check {
    path                = "/"
    healthy_threshold   = 2
    unhealthy_threshold = 2
  }
}

# Target Group Attachment
resource "aws_lb_target_group_attachment" "proxy" {
  target_group_arn = aws_lb_target_group.proxy.arn
  target_id        = aws_instance.proxy.id
  port             = 80
}

# Public ALB
resource "aws_lb" "public" {
  name               = "${var.environment}-public-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.public_alb.id]
  subnets            = aws_subnet.public[*].id

  tags = merge(var.tags, {
    Name = "${var.environment}-public-alb"
  })
}

# HTTPS Listener
resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.public.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-2016-08"
  certificate_arn   = "arn:aws:acm:ap-southeast-1:<network-account-id:certificate/<cert-id>"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.proxy.arn
  }
}

# Route53 Record for ALB
resource "aws_route53_record" "public_alb" {
  zone_id = "<hosted-zone-id>"
  name    = "public-alb.${var.domain_name}"
  type    = "CNAME"
  ttl     = "300"
  records = [aws_lb.public.dns_name]
}

cloudfront.tf

# CloudFront Distribution
resource "aws_cloudfront_distribution" "main" {
  enabled             = true
  is_ipv6_enabled     = true
  comment             = "Distribution for web.${var.domain_name}"
  default_root_object = "index.html"
  price_class         = "PriceClass_All"

  aliases = ["web.${var.domain_name}"]

  origin {
    domain_name = "public-alb.${var.domain_name}"
    origin_id   = "PublicALB"

    custom_origin_config {
      http_port              = 80
      https_port             = 443
      origin_protocol_policy = "https-only"
      origin_ssl_protocols   = ["TLSv1.2"]
    }
  }

  default_cache_behavior {
    allowed_methods  = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "PublicALB"

    forwarded_values {
      query_string = true
      headers      = ["Host"]

      cookies {
        forward = "all"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 300
    max_ttl                = 3600
    compress               = true
  }

  viewer_certificate {
    acm_certificate_arn      = "arn:aws:acm:us-east-1:<network-account-id>:certificate/<cert-id>"
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2021"
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }
}

# Route53 Record for CloudFront
resource "aws_route53_record" "cloudfront" {
  zone_id = "<hosted-zone-id>"
  name    = "web.${var.domain_name}"
  type    = "CNAME"
  ttl     = "300"
  records = [aws_cloudfront_distribution.main.domain_name]
}

outputs.tf

output "transit_gateway_id" {
  value = aws_ec2_transit_gateway.main.id
}

output "vpc_id" {
  value = aws_vpc.main.id
}

output "proxy_instance_id" {
  value = aws_instance.proxy.id
}

output "public_alb_dns" {
  value = aws_lb.public.dns_name
}

output "cloudfront_domain" {
  value = aws_cloudfront_distribution.main.domain_name
}

terraform.tfvars

vpc_config = {
  network_vpc_cidr = "100.64.0.0/16"
  app_vpc_cidr     = "10.0.0.0/16"
  azs              = ["ap-southeast-1a", "ap-southeast-1b", "ap-southeast-1c"]
}