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ài đặt transit gateway giữa 2 tài khoản: https://docs.aws.amazon.com/vpc/latest/tgw/tgw-getting-started.html
Trên tài khoản network đã có sẵn public hosted zone: devopslearning.co.uk
Cert cho cloudfront và public ALB tại tài khoản network: *.devopslearning.co.uk
Cert cho private alb tại tài khoản app: *app.devopslearning.co.uk
Có sẵn 1 cụm EKS trên tài khoản app, đã cài đặt ALB Controller
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 là một bài viết mẫu để kiểm tra giao diện. Bạn có 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ồ sơ 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 là trang hồ sơ mẫu. Bạn có 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:
AWS S3 + CloudFront. Nếu muốn private thì kiến trúc như sau: https://aws.amazon.com/blogs/networking-and-content-delivery/hosting-internal-https-static-websites-with-alb-s3-and-privatelink/ Ta có thể thêm CloudFront để route traffic vào ALB, để tận dụng được caching.
Sử dụng container với minimal web server (ALB yêu cầu target pods phải có một HTTP endpoint để thực hiện health checks và forward requests)
Ở 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:
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!
# 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 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
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
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
}
# 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 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]
}
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"]
}