diff --git a/aws/image-resizer/.terraform.lock.hcl b/aws/image-resizer/.terraform.lock.hcl new file mode 100644 index 0000000..4c4321e --- /dev/null +++ b/aws/image-resizer/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "4.66.1" + constraints = "~> 4.16" + hashes = [ + "h1:i5PN+Xs0DXLe6a512XELJcYvFN4oYOkg+WMT39AzgRw=", + "zh:001c707174b7d6bf89a96cf806f925bb852d1a285fb80b81222cbeb4743bcb79", + "zh:19bc6ac0a7fd1c564fd56c536f1743f71a5e7ca724e21ea51a6a79218939733d", + "zh:3dac5c27f40b511239e9fe6f97dc0b6c95f630ba328001820ddc764e766a5ca2", + "zh:49092c92e2565db4cd4c98ec6878386e6957525d3392b63f0d5df4c48a7c1913", + "zh:4f9e2e1d0c5365a4e6689096cc91ba88ca9c0dc7c633377ba674c1dd856b6a9f", + "zh:57e32bb454f2dc17d5631a9559e36188761d8ae95a452478f81f41bb568a3a42", + "zh:678b78ba629dd833f0705ac90630969f514a54013ab9713ce7ceda55fc5ea138", + "zh:8aab1d76348cf2a685f72382cb838a910b77353179e81ab5794b9c45c8fb36a3", + "zh:8b6791bf0948aa8b49258863992a8ad7e7332dcae1a889e86da0e5ab778dc3b6", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:a36f2777452c2cebdaa8a27378416d512ead367acc078a671bb12276dd4bc9dd", + "zh:c492e6f685882fad6481f4793e696d9e1b01aaae419225c2db0a484b632d1cac", + "zh:d4418e0d1d18e321db364a91d7a768e274bb0fb46df9f3cb5b9debb2bb6917b9", + "zh:d5b4310ef2b2ec22ae14cf909deb1231b56bdd79dc2b51e5db4e46a05e0110c4", + "zh:dedfb01e26b34fb61a52b7e953b8bf5d7a69971187e91697b67221298bbed377", + ] +} diff --git a/aws/image-resizer/README.md b/aws/image-resizer/README.md new file mode 100644 index 0000000..01c48bc --- /dev/null +++ b/aws/image-resizer/README.md @@ -0,0 +1,48 @@ +# Image resizer for S3 + +- Lambda Edge 기반의 이미지 리사이저 + +## 리소스 구성 + +1. Lambda +2. Cloudfront +3. CodePipeline(CodeBuild) +4. Lambda Edge +5. S3 (for CodeBuild) + +## 프로젝트 템플릿 + +- 현재는 Node.js 서버만 고려한 상태입니다. + +### Node.js + +- [템플릿](https://github.com/myyrakle/image_resizer_template) 프로젝트를 clone하거나 fork해서 사용합니다. + +## 준비물 + +- 다음 명령을 사용해서 사용할 codestar 정보를 조회합니다. + `aws codestar-connections list-connections` +- github에 템플릿을 참고해서 프로젝트를 생성합니다. + +--- + +## parameter 설정 + +- 자세한 것은 [](./variables.tf)에서 확인하거나 수정할 수 있습니다. + +### required parameter + +1. region: 리전 정보입니다. 서울이라면 ap-northeast-2 값을 넘겨줍니다. +2. environment: 환경 정보입니다. server_name과 조합되어 고유의 리소스 이름을 형성합니다. prod, stage, dev 등의 값을 설정하면 됩니다. +3. system_name: 시스템명입니다. environment와 조합해서 고유의 리소스 이름을 형성합니다. +4. github_user: github username or organization name입니다. +5. github_repository: 레포지토리명입니다. +6. github_branch: 트리거할 브랜치입니다. +7. codestar_arn: codestart connection ARN입니다. + +### optional parameter + +1. buildspec_path: 빌드에 사용할 buildspec.yml 위치입니다. +2. codebuild_compute_type: code build 컴퓨팅 타입입니다. +3. lambda_runtime: 람다 런타임. 현재는 node.js만 고려해둔 상태입니다. +4. lambda_layers: 레이어 목록입니다. diff --git a/aws/image-resizer/cicd.tf b/aws/image-resizer/cicd.tf new file mode 100644 index 0000000..2e6c178 --- /dev/null +++ b/aws/image-resizer/cicd.tf @@ -0,0 +1,113 @@ +// 아티팩트 버킷 +resource "aws_s3_bucket" "artifact_bucket" { + bucket = "${local.resource_id}-artifact-bucket" + + tags = local.tags +} + +// 빌드 캐시용 버킷 +resource "aws_s3_bucket" "cache_bucket" { + bucket = "${local.resource_id}-cache-bucket" + + tags = local.tags +} + +// code build +resource "aws_codebuild_project" "codebuild" { + name = local.resource_id + description = "code build" + build_timeout = "30" + service_role = aws_iam_role.codebuild_role.arn + + artifacts { + type = "S3" + location = aws_s3_bucket.artifact_bucket.bucket + } + + cache { + type = "S3" + location = aws_s3_bucket.cache_bucket.bucket + } + + environment { + compute_type = var.codebuild_compute_type + image = "aws/codebuild/amazonlinux2-x86_64-standard:4.0" + type = "LINUX_CONTAINER" + image_pull_credentials_type = "CODEBUILD" + privileged_mode = true + + environment_variable { + name = "AWS_ACCOUNT_ID" + value = local.account_id + } + + environment_variable { + name = "AWS_DEFAULT_REGION" + value = local.region + } + + environment_variable { + name = "EnvironmentName" + value = local.resource_id + } + } + + source { + type = "S3" + location = "${aws_s3_bucket.artifact_bucket.arn}/source.zip" + buildspec = var.buildspec_path + } + + tags = local.tags +} + +// code pipeline +resource "aws_codepipeline" "codepipeline" { + name = local.resource_id + role_arn = aws_iam_role.codepipeline_role.arn + + artifact_store { + location = aws_s3_bucket.artifact_bucket.bucket + type = "S3" + } + + stage { + name = "Source" + + action { + configuration = { + ConnectionArn : var.codestar_arn + FullRepositoryId : join("/", [var.github_user, var.github_repository]) + BranchName : var.github_branch + } + + name = "Source" + category = "Source" + owner = "AWS" + provider = "CodeStarSourceConnection" + version = "1" + + output_artifacts = ["Source"] + } + } + + stage { + name = "Build" + + action { + name = "Build" + category = "Build" + owner = "AWS" + provider = "CodeBuild" + input_artifacts = ["Source"] + output_artifacts = ["Build"] + version = "1" + + configuration = { + ProjectName = aws_codebuild_project.codebuild.name + } + } + } + + tags = local.tags +} diff --git a/aws/image-resizer/codes/origin/index.js b/aws/image-resizer/codes/origin/index.js new file mode 100644 index 0000000..c4c6359 --- /dev/null +++ b/aws/image-resizer/codes/origin/index.js @@ -0,0 +1,91 @@ +const http = require("http"); +const https = require("https"); +const querystring = require("querystring"); +const sharp = require("sharp"); +const urlencode = require("urlencode"); + +const aws = require("aws-sdk"); +const S3 = new aws.S3(); + +// 이미지 사이즈로 축소 +async function toSmallSize(image) { + const resized = image.resize({ width: 140, height: 140 }); + + return resized; +} + +const BUCKET_NAME = "metaverse2-community-image"; + +exports.handler = async (event, context, callback) => { + const response = event.Records[0].cf.response; + + try { + console.log("Response status code :%s", response.status); + + // 이미 리사이징된 파일이 저장되어있지 않을 경우 + if (response.status == 404 || response.status == 403) { + const request = event.Records[0].cf.request; + + const requestUri = urlencode.decode(request.uri).replace("/", ""); // 첫번째 슬래시 제거 + const uri = requestUri.replace("resize/", ""); // resize prefix 제거 + + console.log("requestUri: ", requestUri); + + const resizeType = uri.split("/")[0]; + const originUri = uri.replace(resizeType + "/", ""); + + console.log("resizeType: ", resizeType); + console.log("originUri: ", originUri); + + switch (resizeType) { + // /resize/small/... 케이스 처리 + case "small": { + const data = await S3.getObject({ + Bucket: BUCKET_NAME, + Key: originUri, + }).promise(); + + let image = sharp(data.Body); + const metadata = await image.metadata(); + const format = metadata.format; + + const mimeType = "image/" + format; + + image = await toSmallSize(image); + + const buffer = await image.toBuffer(); + + // 백업이 필요하다면 resize 경로에 저장. 하지 않더라도 캐싱 자체는 됨. 선택사항 + // await S3.upload({ + // Body: buffer, + // Bucket: '...', + // Key: requestUri, + // ContentType: mimeType, + // ACL: 'public-read', + // }).promise(); + + // generate a binary response with resized image + response.status = 200; + response.body = buffer.toString("base64"); + response.bodyEncoding = "base64"; + response.headers["content-type"] = [ + { key: "Content-Type", value: mimeType }, + ]; + callback(null, response); + + break; + } + default: { + callback(null, response); + break; + } + } + } else { + callback(null, response); + } + } catch (error) { + console.log("!! ERROR"); + console.error(error); + callback(null, response); + } +}; diff --git a/aws/image-resizer/locals.tf b/aws/image-resizer/locals.tf new file mode 100644 index 0000000..acc02b5 --- /dev/null +++ b/aws/image-resizer/locals.tf @@ -0,0 +1,14 @@ +data "aws_caller_identity" "current" {} + +locals { + region = "us-east-1" + + tags = { + Environment = var.environment + Application = var.server_name + } + + resource_id = join("-", [var.system_name, var.environment]) + + account_id = data.aws_caller_identity.current.account_id +} diff --git a/aws/image-resizer/main.tf b/aws/image-resizer/main.tf new file mode 100644 index 0000000..1399411 --- /dev/null +++ b/aws/image-resizer/main.tf @@ -0,0 +1,35 @@ +terraform { + required_providers { + # 일종의 라이브러리 로드 + aws = { + source = "hashicorp/aws" + version = "~> 4.16" + } + } + + required_version = ">= 1.2.0" +} + +provider "aws" { + region = local.region +} + +resource "aws_lambda_function" "lambda" { + description = "A lambda connect function for ${local.resource_id}" + function_name = local.resource_id + role = aws_iam_role.lambda_role.arn + layers = var.lambda_layers + runtime = var.lambda_runtime + handler = "index.handler" + filename = "codes/zip/connect.zip" + + environment { + variables = { + ServerName = var.server_name + ENVIRONMENT = var.environment + ConnectionTableName = "${local.resource_id}_connection" + } + } + + tags = local.tags +} diff --git a/aws/image-resizer/outputs.tf b/aws/image-resizer/outputs.tf new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/aws/image-resizer/outputs.tf @@ -0,0 +1 @@ + diff --git a/aws/image-resizer/roles.tf b/aws/image-resizer/roles.tf new file mode 100644 index 0000000..224d53e --- /dev/null +++ b/aws/image-resizer/roles.tf @@ -0,0 +1,175 @@ +// Lambda Role +resource "aws_iam_role" "lambda_role" { + name = "${local.resource_id}-lambda-role" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + "Effect" : "Allow", + "Principal" : { + "Service" : ["lambda.amazonaws.com"] + }, + "Action" : [ + "sts:AssumeRole" + ] + } + ] + }) + + inline_policy { + name = "root" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + "Sid" : "SpecificTable", + "Effect" : "Allow", + "Action" : [ + "dynamodb:BatchGet*", + "dynamodb:DescribeStream", + "dynamodb:DescribeTable", + "dynamodb:Get*", + "dynamodb:Query", + "dynamodb:Scan", + "dynamodb:BatchWrite*", + "dynamodb:CreateTable", + "dynamodb:Delete*", + "dynamodb:Update*", + "dynamodb:PutItem" + ], + "Resource" : [ + // 테이블을 추가할 때마다 여기에도 리소스를 추가해줍니다. + "arn:aws:dynamodb:*:*:table/${local.resource_id}_connection" + ] + } + ] + }) + } +} + +resource "aws_iam_role_policy_attachment" "lambda-basic-attach" { + role = aws_iam_role.lambda_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" +} + +data "aws_iam_policy_document" "update_code_policy_data" { + statement { + actions = [ + "lambda:UpdateFunctionCode", + ] + effect = "Allow" + resources = [ + aws_lambda_function.connect_lambda.arn, + aws_lambda_function.disconnect_lambda.arn, + aws_lambda_function.default_lambda.arn + ] + } +} + +resource "aws_iam_policy" "update_code_policy" { + name = "${local.resource_id}-update-code-policy" + path = "/" + policy = data.aws_iam_policy_document.update_code_policy_data.json +} + +// Code Build에 사용할 role +resource "aws_iam_role" "codebuild_role" { + name = join("-", [local.resource_id, "codebuild-role"]) + + # Terraform's "jsonencode" function converts a + # Terraform expression result to valid JSON syntax. + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + "Effect" : "Allow", + "Principal" : { + "Service" : "codebuild.amazonaws.com" + }, + "Action" : "sts:AssumeRole" + } + ] + }) + + path = "/" + + inline_policy { + name = "codebuild" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + "Effect" : "Allow", + "Action" : "*", + "Resource" : "*" + } + ] + }) + } + + tags = local.tags +} + +resource "aws_iam_role_policy_attachment" "update-code-attach" { + role = aws_iam_role.codebuild_role.name + policy_arn = aws_iam_policy.update_code_policy.arn +} + +// Code Pipeline에 사용할 role +resource "aws_iam_role" "codepipeline_role" { + name = join("-", [local.resource_id, "codepipeline-role"]) + + # Terraform's "jsonencode" function converts a + # Terraform expression result to valid JSON syntax. + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + "Effect" : "Allow", + "Principal" : { + "Service" : "codepipeline.amazonaws.com" + }, + "Action" : ["sts:AssumeRole"] + } + ] + }) + + path = "/" + + inline_policy { + name = "root" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + "Resource" : "${aws_s3_bucket.artifact_bucket.arn}/*", + "Effect" : "Allow", + "Action" : [ + "s3:PutObject", + "s3:GetObject", + "s3:GetObjectVersion", + "s3:GetBucketVersioning" + ] + }, + { + "Resource" : "*", + "Effect" : "Allow", + "Action" : [ + "ecs:DescribeServices", + "ecs:DescribeTaskDefinition", + "ecs:DescribeTasks", + "ecs:ListTasks", + "ecs:RegisterTaskDefinition", + "ecs:UpdateService", + "codebuild:StartBuild", + "codebuild:BatchGetBuilds", + "iam:PassRole", + "codestar-connections:*" + ] + } + ] + }) + } + + tags = local.tags +} diff --git a/aws/image-resizer/variables.tf b/aws/image-resizer/variables.tf new file mode 100644 index 0000000..029d63e --- /dev/null +++ b/aws/image-resizer/variables.tf @@ -0,0 +1,69 @@ +// 리전 +variable "region" { + description = "region" + type = string +} + +// tag 및 리소스 이름 구성에 사용됨 +variable "environment" { + description = "environment info. (e.g: prod, dev, stage, test)" + type = string +} + +// 서버명 (server_name-environment 형태로 구성됩니다.) +variable "system_name" { + description = "The name of the server machine you want to create." + type = string +} + +// Lambda runtime +// https://docs.aws.amazon.com/lambda/latest/dg/API_CreateFunction.html#SSS-CreateFunction-request-Runtime 참조 +// 커스텀 런타임은 provided.al2, provided +// nodejs | nodejs4.3 | nodejs6.10 | nodejs8.10 | nodejs10.x | nodejs12.x | nodejs14.x | nodejs16.x | java8 | java8.al2 | java11 | python2.7 | python3.6 | python3.7 | python3.8 | python3.9 | dotnetcore1.0 | dotnetcore2.0 | dotnetcore2.1 | dotnetcore3.1 | dotnet6 | nodejs4.3-edge | go1.x | ruby2.5 | ruby2.7 | provided | provided.al2 | nodejs18.x | python3.10 | java17 +variable "lambda_runtime" { + description = "lambda runtime" + type = string + default = "nodejs16.x" +} + +// Lambda layers +variable "lambda_layers" { + description = "layer arn list" + type = list(string) + default = [] +} + +variable "codestar_arn" { + description = "CodeStarConnection ARN" + type = string +} + +// 빌드에 사용할 buildspec.yml 위치입니다. +variable "buildspec_path" { + description = "BuildSpec file path (e.g: \"/prod/buildspec.yml\")" + type = string + default = "buildspec.yml" +} + +// code build 컴퓨팅 타입입니다. +// 다음 문서를 참고합니다. https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-compute-types.html +variable "codebuild_compute_type" { + description = "The compute type of the codebuild. (e.g: BUILD_GENERAL1_SMALL)" + type = string + default = "BUILD_GENERAL1_SMALL" +} + +variable "github_user" { + description = "The username of the github repository." + type = string +} + +variable "github_repository" { + description = "The name of the github repository." + type = string +} + +variable "github_branch" { + description = "The name of the github branch." + type = string +}