정적인 웹 사이트, 기본적인 html+css+js 외에도 react 등의 프레임워크를 이용해서 개발할 수 있다.
많이 받는 질문 중 하나가 '프론트엔드로 웹 서비스를 만들었는데, 어떻게 배포하냐?'라는 부분이다. 그리고 과거의 나를 포함해서 많은 뉴비들이 실수하는 부분이 EC2와 같은 리눅스 서버를 구매해서 개발할 때 처럼 'npm start' 등으로 백그라운드에 실행해두는 방식이다.
그러면 속도도 느려지고, 서버 메모리도 많이 잡아먹고, 서버비용도 꼬박꼬박 내야하는 아주 안좋은 방식이다.
(심지어 경험상 Docker로 react를 한번 감싸서 EC2에 배포한다? 그러면 t*.micro 프리티어에선 돌아가지도 않는다.)
그래서
S3에 업로드하는 방식으로 정적 웹 사이트를 호스팅할 수 있는데 간단히 생각하면, react 등도 결국 build하면 html+css+js로 웹팩에 의해서 바뀌기 때문에 client 입장에서 html+css+js를 다운로드 받을 수 있는 인프라만 구축하면 되고 S3(Simple Storage Service)는 이런 역할을 해줄 수 있는 서비스인 것이다.
여기에 Cloudfront(CDN역할)를 연결해주면 속도+네트워크 비용최적화의 장점까지 가져갈 수 있다.
그런데
이걸 웹 콘솔로 구축하기 위해선 아래의 귀찮은 작업들이 필요하다.
(before)
1. S3 버킷 생성
2. S3 버킷에 대한 정책 설정 (접근 정책과 정적 웹 사이트 호스팅 정책)
3. Cloudfront를 S3에 연결
4. Cloudfront에 대한 대체도메인 설정 및 https를 위한 ssl 인증서 발급
5. Route53에서 ssl 인증서 인증 및 대체도메인을 레코드에 등록
6. S3에 웹 서비스 파일 업로드
웹 사이트 1개정도 페이지를 호스팅할 때에는 그냥 할 수도 있겠지만, 앱 서비스에서 쓰기 위한 웹 뷰가 여러개 필요한 상황에서는 상당히 귀찮은 작업이 아닐 수 없었다.
그래서 이를 테라폼으로 미리 짜두고 variable만 바꿔가면서 apply 해주면, (1)~(5)과정을 명령어 한 줄로 관리할 수 있을 것이고, (6)의 과정도 github action으로 지속적 배포가 가능하도록 설정하면 코드에만 집중할 수 있는 인프라 구성이 가능할 것으로 기대되었다.
(after)
1. terraform 변수 값 설정 및 apply
2. github action으로 코드 자동 배포
방법과 세팅 과정에 있었던 에러 트러블슈팅을 기록해보겠다.
// provider.tf
provider "aws" {
profile = "default"
}
provider "aws" {
alias = "virginia"
region = "us-east-1"
}
나중에 acm 설정을 위해서 alias를 버지니아동부로 provider를 등록해준다.
// variables.tf
variable "domain_name" {
type = string
default = "test.ddakzip.shop"
description = "Fully domain name to deploy Web services"
}
variable "validation_domain_name" {
type = string
default = "ddakzip.shop"
description = "Domain name for validation on acm"
}
variable "bucket_name" {
type = string
default = "s3-bucket-test"
description = "Bucket name for S3"
}
배포마다 바뀌는 도메인 이름, 도메인 원본 이름, 버킷 이름을 변수로 등록해준다.
// s3.tf
resource "aws_s3_bucket" "deploy_bucket" {
bucket = var.bucket_name
tags = {
Name = "terraform-web-deploy"
Environment = "Dev"
}
force_destroy = true
}
resource "aws_s3_bucket_ownership_controls" "example" {
bucket = aws_s3_bucket.deploy_bucket.id
rule {
object_ownership = "BucketOwnerPreferred"
}
}
resource "aws_s3_bucket_public_access_block" "example" {
bucket = aws_s3_bucket.deploy_bucket.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
resource "aws_s3_bucket_acl" "example" {
depends_on = [
aws_s3_bucket_ownership_controls.example,
aws_s3_bucket_public_access_block.example,
]
bucket = aws_s3_bucket.deploy_bucket.id
acl = "public-read"
}
data "aws_iam_policy_document" "s3_policy" {
statement {
actions = ["s3:GetObject"]
resources = ["${aws_s3_bucket.deploy_bucket.arn}/*"]
principals {
type = "AWS"
identifiers = ["*"]
}
}
}
resource "aws_s3_bucket_policy" "example" {
bucket = aws_s3_bucket.deploy_bucket.id
policy = data.aws_iam_policy_document.s3_policy.json
}
resource "aws_s3_bucket_website_configuration" "website-config" {
bucket = aws_s3_bucket.deploy_bucket.id
index_document {
suffix = "index.html"
}
}
resource "aws_s3_bucket"
- 버킷을 생성하는 리소스이다. 'force_destroy = true'에 관해서는 나중에 트러블슈팅에서 다뤄보겠다.
resource "aws_s3_bucket_ownership_controls"
- 버킷의 객체 소유권 설정이다. ACL은 비활성화하고, 소유권은 버킷 오너에게 준다.
resource "aws_s3_bucket_public_access_block" & resource "aws_s3_bucket_acl"
- 버킷의 퍼블릭 엑세스 관련이다. 다 false로 설정하면, 모든 퍼블릭 엑세스에 대해서 차단을 하지 않는다.
- 퍼블릭에서 읽기만 가능하도록 설정한다.
data "aws_iam_policy_document" & resource "aws_s3_bucket_policy"
- 버킷의 정책 설정이다. data로 json형식으로 만들어서 넣어주면되는데 GetObject에 대해서 모든 리소스에 대한 허용을 해준다.
- 참고 : https://registry.terraform.io/providers/hashicorp/aws/3.4.0/docs/data-sources/iam_policy_document
resource "aws_s3_bucket_website_configuration"
- 정적 웹 서비스 호스팅을 위한 설정으로 index.html, error.html 등을 설정할 수 있다.
// cloudfront.tf
resource "aws_cloudfront_origin_access_identity" "cloudfront_oai" {
comment = "cloudfront_origin_access_identity comment"
}
resource "aws_cloudfront_distribution" "s3_distribution" {
origin {
domain_name = aws_s3_bucket.deploy_bucket.bucket_domain_name
origin_id = aws_s3_bucket.deploy_bucket.id
s3_origin_config {
origin_access_identity = aws_cloudfront_origin_access_identity.cloudfront_oai.cloudfront_access_identity_path
}
origin_shield {
enabled = true
origin_shield_region = "ap-northeast-2"
}
}
enabled = true
is_ipv6_enabled = true
comment = "test comment"
default_root_object = "index.html"
aliases = [var.domain_name]
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = aws_s3_bucket.deploy_bucket.id
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
}
ordered_cache_behavior {
path_pattern = "/*"
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = aws_s3_bucket.deploy_bucket.id
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
min_ttl = 0
default_ttl = 86400
max_ttl = 31536000
compress = true
viewer_protocol_policy = "redirect-to-https"
}
price_class = "PriceClass_200"
restrictions {
geo_restriction {
restriction_type = "whitelist"
locations = ["US", "CA", "GB", "DE", "KR"]
}
}
tags = {
Environment = "production"
}
viewer_certificate {
acm_certificate_arn = aws_acm_certificate_validation.example.certificate_arn
ssl_support_method = "sni-only"
}
}
resource "aws_cloudfront_distribution"
- cloudfront 설정 값들을 포함한다.
- origin : 대상으로할 domain(여기서는 S3버킷)을 지정한다.
- origin_shield : origin shield 여부 설정
* Origin Shield는 원본의 부하를 줄이고 가용성을 보호하는 데 도움이 되는 추가 캐싱 계층입니다.
- aliases : 대체 도메인 목록
- default_cache_behavior : 캐시 동작 설정
- price_class : 아래 도표 참고해서 어디를 타켓으로 할 지 설정
- restrictions : 허용할 국가 설정, 차단도 가능 KR을 꼭 넣어줘야 막히지 않음
- viewer_certificate : https를 사용할 경우에 ssl 인증서 설정
// acm-certificate.tf
resource "aws_acm_certificate" "example" {
provider = aws.virginia
domain_name = var.domain_name
validation_method = "DNS"
validation_option {
domain_name = var.domain_name
validation_domain = var.validation_domain_name
}
tags = {
Environment = "test"
}
lifecycle {
create_before_destroy = true
}
}
data "aws_route53_zone" "example" {
name = var.validation_domain_name
private_zone = false
}
resource "aws_route53_record" "example" {
for_each = {
for dvo in aws_acm_certificate.example.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
allow_overwrite = true
name = each.value.name
records = [each.value.record]
ttl = 60
type = each.value.type
zone_id = data.aws_route53_zone.example.zone_id
}
resource "aws_acm_certificate_validation" "example" {
provider = aws.virginia
certificate_arn = aws_acm_certificate.example.arn
validation_record_fqdns = [for record in aws_route53_record.example : record.fqdn]
}
resource "aws_acm_certificate"
- ssl 인증서 생성, 이때 provider를 꼭 버지니아 동부로 재설정해줘야함
- route53과 연결해서 DNS에 CNAME 레코드를 등록하는 방식으로 인증하게 된다.
resource "aws_route53_record" & "aws_acm_certificate_validation"
- 인증을 위한 레코드 등록과 설정
// route53.tf
data "aws_route53_zone" "route53_host_zone" {
name = var.validation_domain_name
}
resource "aws_route53_record" "route53_record_a_eb_test" {
zone_id = data.aws_route53_zone.route53_host_zone.zone_id
name = var.domain_name
type = "A"
alias {
name = "${aws_cloudfront_distribution.s3_distribution.domain_name}"
zone_id = "${aws_cloudfront_distribution.s3_distribution.hosted_zone_id}"
evaluate_target_health = false
}
}
data "aws_route53_zone"
- 필자는 이미 route53에 호스팅영역에 등록되어 있어서 data로 받아왔고, 만약 새로 만드는거면 resource로 생성해야한다.
resource "aws_route53_record"
- cloudfront와 연결된 A 레코드 생성
이렇게 해주면 한 10분? 정도 배포 시간이 걸리고(cloudfront가 전세계에 뿌려야해서 배포가 오래걸린다) 도메인으로 접근할 수 있다.
+ 처음에 cloudfront가 완전히 배포 되지 않았을 때는 s3로 리디렉션하는 현상이 일어나는데, 몇시간 지나면 정상적으로 연결된다.
트러블 슈팅 1
이 부분은 꼭 인지하고 있어야해서 따로 정리해본다. S3 생성시에 'force_destroy = true'를 넣지 않으면 terraform 생성이후 terraform delete 명령어에서 다음과 같은 에러를 만날 수 있다. S3 버킷을 삭제해보았다면 알겠지만, S3 버킷은 기본값이 안에 남아있는 값이 있으면 삭제가 안되게 막혀있다.
안전하게 콘솔로 확인하고 내용물을 지우던지, 생성할 때부터 강제 삭제가 가능하도록 세팅하는 방법 중 선택을 해야한다.
필자는 어짜피 코드가 github 상에 저장되고, 배포만 하는 것이기 때문에(데이터 저장에 목적이 있지 않기 때문에) 관리의 편리성을 위해 강제 삭제를 허용했다.
트러블 슈팅2
S3를 생성하는데 이런 이슈가 발생했다. 분명 IAM에 모든 생성 권한을 줬는데 403 에러가 났다. 관련된 내용을 찾아보니 이미 알려진 issue였다. 해결 방법은 terraform apply를 한번 더 해주는 것이다.
(정확한 이유를 찾은 뒤 추가 예정)
- https://github.com/hashicorp/terraform-provider-aws/issues/762#issue-235683868
- https://github.com/hashicorp/terraform-provider-aws/issues?q=is%3Aissue+Error+putting+s3+policy