이 글은 총 4개의 파트로 나뉘며 각 파트별 다루는 내용은 위와 같습니다.
AWS EKS 위에 배포할 워크로드에 대한 CI/CD pipeline을 AWS 리소스로 만들어보자.
특히, 여기서 사용하는 AWS 리소스들의 배포는 Terraform을 사용해보자.
Source는 CodeCommit을 사용.
CI는 CodeBuild를 사용.
CD는 CodeBuild를 사용하여 Delivery 기능 까지만 구현하자. AWS는 EKS에 대한 Continuous Deployment를 제공하지 않기 때문.
이러한 CI/CD 작업은 CodePipeline으로 이어주자.
일반적으로 CI와 CD 각각 git 저장소와 파이프라인을 구축하는데 여기서는 간단한 테스트를 다루므로 CI, CD를 하나의 git 저장소로 관리하고 파이프라인도 하나만 두자.
또한 CD는 Delivery 까지만 구현할 것이므로 CD 단계 전에 사용자의 수동 승인 단계를 추가할 것임.
파이프라인의 시작 기준인 Source는 CodeCommit으로 하며 git 저장소의 브랜치 전략은 CI/CD에 적합하다는 trunk-based development로써 master 브랜치 하나만 두자. 그리고 배포 환경별로 git tag에 prefix를 지정해(e.g. dev-v0.0.1, stg-v0.0.1, prod-v0.0.1) 릴리즈를 push했을 때 이를 인지하고 CodePipeline을 시작하도록 EventBridge를 구성하자.
CI 작업에 대한 내역이 담긴 buildspec.yml에는 source가 되는 CodeCommit의 git 저장소를 통째로 다운받고 최신 git tag명을 읽어와 환경변수로 사용하며 이를 빌드하는 컨테이너 이미지의 태그로 활용하자.
CD에서도 역시 git tag명을 읽어와 환경변수로 사용하는데 워크로드를 배포하기 전에 deployment.yaml에 image tag명을 git tag명으로 바꿔주어 delivery할 수 있게 하자.
더 자세한 내용은 아래에서 순서대로 다루자.
특정 IAM User의 자격증명(이하 tf-uesr)으로 terraform을 실행해 관련 aws 리소스들을 생성할 예정. 그러므로 이 tf-user에게 적절한 aws 권한이 필요함. 예를 들면, IAM 리소스 생성 권한, CodeCommit이나 ECR에 대한 사용 권한, S3 생성 및 사용 권한, logGroup 생성 및 사용 권한, CodeBuild나 CodePipeline 생성 및 사용 권한 등. 이렇게 필요한 권한은 아래와 같이 생성해서 사용하면 terraform 실행 시에 권한 오류는 생기지 않음.
create-attach-policy-to-tfuser-ci.sh
#!/bin/bash
SUBJECT="sample-pipeline"
REGION="<리전명>"
ACCOUNT_ID="<적용할 aws account id>"
POLICY_NAME="<사용할 policy 이름>"
USER_NAME="<사용할 tf-user 이름>"
aws iam create-policy --policy-name $POLICY_NAME --policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3List",
"Effect": "Allow",
"Action": [
"s3:ListAllMyBuckets"
],
"Resource": [
"arn:aws:s3:::*"
]
},
{
"Sid": "Ec2List",
"Effect": "Allow",
"Action": [
"ec2:DescribeVpcs",
"ec2:DescribeSubnets",
"ec2:DescribeSecurityGroups",
"ec2:DescribeVpcAttribute"
],
"Resource": "*"
},
{
"Sid": "LogsList",
"Effect": "Allow",
"Action": [
"logs:DescribeLogGroups"
],
"Resource": [
"arn:aws:logs:'$REGION':'$ACCOUNT_ID':log-group::log-stream:"
]
},
{
"Sid": "Iam",
"Effect": "Allow",
"Action": [
"iam:GetUser",
"iam:GetRole",
"iam:GetPolicy",
"iam:GetPolicyVersion",
"iam:ListRolePolicies",
"iam:ListAttachedRolePolicies",
"iam:ListPolicyVersions",
"iam:ListInstanceProfilesForRole",
"iam:DetachRolePolicy",
"iam:CreateRole",
"iam:CreatePolicy",
"iam:CreatePolicyVersion",
"iam:AttachRolePolicy",
"iam:PassRole",
"iam:DeletePolicy",
"iam:DeletePolicyVersion",
"iam:DeleteRole",
"iam:TagRole",
"iam:TagPolicy"
],
"Resource": [
"arn:aws:iam::'$ACCOUNT_ID':user/'$SUBJECT'-*",
"arn:aws:iam::'$ACCOUNT_ID':role/'$SUBJECT'-*",
"arn:aws:iam::'$ACCOUNT_ID':policy/'$SUBJECT'-*"
]
},
{
"Sid": "CodeCommit",
"Effect": "Allow",
"Action": "codecommit:GetRepository",
"Resource": "arn:aws:codecommit:'$REGION':'$ACCOUNT_ID':'$SUBJECT'"
},
{
"Sid": "Ecr",
"Effect": "Allow",
"Action": [
"ecr:ListTagsForResource",
"ecr:DescribeRepositories",
"ecr:DescribeImages"
],
"Resource": [
"arn:aws:ecr:'$REGION':'$ACCOUNT_ID':repository/'$SUBJECT'"
]
},
{
"Sid": "S3",
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::'$SUBJECT'-*",
"arn:aws:s3:::'$SUBJECT'-*/*"
]
},
{
"Sid": "Logs",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:TagResource",
"logs:ListTagsLogGroup",
"logs:DeleteLogGroup"
],
"Resource": [
"arn:aws:logs:'$REGION':'$ACCOUNT_ID':log-group:'$SUBJECT'-*:log-stream:*"
]
},
{
"Sid": "CodeBuild",
"Effect": "Allow",
"Action": [
"codebuild:BatchGetProjects",
"codebuild:CreateProject",
"codebuild:DeleteProject",
"codebuild:ListBuildsForProject",
"codebuild:StopBuild"
],
"Resource": [
"arn:aws:codebuild:'$REGION':'$ACCOUNT_ID':project/'$SUBJECT'-*"
]
},
{
"Sid": "CodePipeline",
"Effect": "Allow",
"Action": [
"codepipeline:GetPipeline",
"codepipeline:CreatePipeline",
"codepipeline:DeletePipeline",
"codepipeline:UpdatePipeline",
"codepipeline:StopPipelineExecution",
"codepipeline:ListTagsForResource",
"codepipeline:ListPipelineExecutions",
"codepipeline:TagResource"
],
"Resource": [
"arn:aws:codepipeline:'$REGION':'$ACCOUNT_ID':'$SUBJECT'-*",
"arn:aws:codepipeline:'$REGION':'$ACCOUNT_ID':'$SUBJECT'-*/*"
]
},
{
"Sid": "EventBridge",
"Effect": "Allow",
"Action": [
"events:PutRule",
"events:DescribeRule",
"events:ListTagsForResource",
"events:DeleteRule",
"events:PutTargets",
"events:ListTargetsByRule",
"events:RemoveTargets"
],
"Resource": [
"arn:aws:events:'$REGION':'$ACCOUNT_ID':rule/'$SUBJECT'-*"
]
}
]
}' 2>&1
aws iam attach-user-policy --user-name $USER_NAME --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/$POLICY_NAME
적절한 IAM 자격증명이 있는 aws cli에서 이 스크립트를 아래와 같이 실행하면, policy가 생성되고 연결됨.
$ ./create-attach-policy-to-tfuser-cicd.sh
{
"Policy": {
"PolicyName": "****",
"PolicyId": "ANPA****",
"Arn": "arn:aws:iam::<aws account id>:policy/****",
"Path": "/",
"DefaultVersionId": "v1",
"AttachmentCount": 0,
"PermissionsBoundaryUsageCount": 0,
"IsAttachable": true,
"CreateDate": "2023-10-05T11:50:54+00:00",
"UpdateDate": "2023-10-05T11:50:54+00:00"
}
}
추가적으로 나는 tfstate를 s3에 저장하고 활용하는데 이를 위해서는 tf-user에게 아래에 권한이 필요함.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3tfstate",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::<tfstate를 저장할 s3 bucket 이름>",
"arn:aws:s3:::<tfstate를 저장할 s3 bucket 이름>/*"
]
}
]
}
여기서는 ECR과 CodeCommit을 terraform으로 관리하지 않았음. git 저장소와 레지스트리를 실수로 지우고 싶지 않기 때문이며 크게 설정할 부분도 없어서 굳이 terraform을 쓸 이유가 없어보임.
CI 파이프라인에 의해 빌드된 도커 이미지를 저장할 레지스트리를 생성하자. 이름은 sample-pipeline
으로 했음.
aws 콘솔 로그인 → ECR Repositories → Create repository
CodeCommit도 ECR과 마찬가지로 직접 생성함. 이름도 sample-pipeline
으로 함.
aws 콘솔 로그인 → CodeCommit → repositories → Create repository
이제, 기본 전제는 완료됐음. 다음으로 워크로드로 띄워 서비스할 웹앱, CI/CD 작업에 대한 명세서 buildspec.yml, eks에 배포할 매니페스트 yaml들에 대한 소스 코드를 작성해보자.