BLOG

Chef Automate 인프라를 위해 AWS OpsWorks 배포하기
작성일: 2018년 3월 26일

지리적 AWS 리전에 걸쳐 많은 노드를 관리하는 조직에서는 해당 AWS OpsWorks for Chef 자동화 구현 시 레이턴시를 줄이고 노드 간에 로드를 줄이기를 희망할 수 있습니다. 여러 서버 간에 노드를 배포함으로써 조직은 하나 이상의 지역에 상주하는 둘 이상의 Chef Server에서 Cookbook 들과 기타 구성을 일관되게 구축할 수 있는 방법을 찾을 수 있습니다. 이를 위해 고객은 하나 이상의 Chef Automate 인스턴스에 Cookbook을 배포하는 프로세스를 추진하는 몇 가지 추가적인 AWS서비스를 사용할 수 있습니다.

 

개요
대규모의 Chef사용자들이 이같은 상황을 접할 수 있게 되는 상황은 그들의 OWCA (OpsWorks for Chef  Automate)서버의 OpWoorks로 관리되는 노드 수가 그 용량을 초과할 때 입니다. 고객은 가장 최근의 백업을 사용하여 더 큰 Amazon EC2인스턴스로 전환할 수 있지만 이 또한 제한에 걸릴 수도 있습니다. 또한, 세계적으로 분산된 환경에서는 통신 레이턴시가 발생할 수 있습니다.

 

이는 노드 관리 기능을 여러  OpsWorks for Chef Automate 인스턴스에 대해 관리함으로써 해결할 수 있는 문제입니다. 그러나 이러한 접근 방식은 여러 OWCA 인스턴스 간에 Cookbook을 동기화하는 문제를 제기합니다. 이 작업은 AWS CodeCommit, AWS CodeBuild, AWS CodePipeline및 옵션 AWS Lambda를 사용하여 수행할 수 있습니다. 이 블로그 게시물에서는 고객이 두 지역에 걸쳐 Chef 관리 인프라를 확장할 수 있는 방법을 보여 줍니다.

 

 

 

CodeCommit 과 CodePipeline을 사용하면 쿡북의 개발자가 중앙 저장소에 변화를 줄 수 있습니다. CodePipeline은 이 커밋을 트리거하고 업데이트된 저장소 컨텐츠를 CodeBuild로 전송하여 처리하도록 합니다. 간단한 스크립팅을 사용하여 CodeBuild는 chef supermarket의 의존성을 제거하고 필요한 cookbook 계정(또는 계정)의 각 Chef Automate 인스턴스에 업로드합니다. chef-client cookbook을 사용해서 노드는 사전 구성된 일정에 따라 자동으로 체크인 하게 될 것입니다. 이 블로그 포스트의 파이프 라인의 호출 단계에서는 AWS Lambda 및 AWS Systems Manager를 사용하여 환경의 모든 노드에 대해 chef-client 를 실행할 수 있습니다. 이 단계는 선택 사항이지만 더 많은 변경 사항을 적용하는 시나리오에 유용합니다.

Setup

이 값을 설정하려면 AWS Cloud Formation템플릿을 사용합니다. 템플릿에 추가할 각 리소스를 살펴보겠습니다. 이 작업을 수행하기 전에 Chef Automation인스턴스의 OpsWorks를 하나 이상 생성해야 합니다. 각 인스턴스의 스타터 키트에는 [Starter-Kit]/.chef/ 디렉토리의 개인 키가 포함되어 있습니다. 이 키를 Code Build의 인증에 사용할 수도 있지만, Chef  Automate 콘솔에서 별도의 사용자를 생성하고 이를 공용/전용 키 쌍으로 할당하는 것이 좋습니다. 참조를 위해 Chef.io에 있는 가이드라인을 따르시길 바랍니다.  적어도 이 사용자 계정에는 committer 권한이 필요합니다. 이 사용자의 개인 키는 계정의 Amazon S3 버킷에 저장할 수 있으며, CodeBuild에서  cookbook 업로드 프로세스 중에 Chef  Automate 서버를 인증하는 데 액세스 합니다. 이 버킷에 대한 액세스를 적절한 버킷 정책으로 엄격히 제어하는 것 또한 중요합니다. 또 다른 고려 사항은 AWS KMS (AWS Key Management Service)에 키를 저장하는 것입니다.

 

구성 요소들

파라미터
이 템플릿에는 하나의 입력 매개 변수인 KeyBucket 만 필요하며, 이는 이 cookbook 저장소와 동기화할 각 Chef Automate 인스턴스에 대한 개인 키가 들어 있는 Amazon S3 버켓에 해당합니다.

 

[Json]

{

    “Parameters”: {

        “KeyBucket”: {

            “Type”: “String”,

            “Description”: “Name of S3 bucket which contains the OWCA private keys.”,

            “AllowedPattern”: “^[a-z0-9][a-z0-9-.]*$”,

            “MinLength”: 3,

            “MaxLength”: 63,

            “ConstraintDescription”: “Please provide a valid S3 bucket name.”

        }

    }

}

 

IAM사용 권한
파이프 라인이 제대로 작동하려면 두개의 AWS ID및 액세스 관리(IAM)역할이 필요합니다.

첫번째 IAM역할인 BuildRole은 빌드 작업 중에 CodeBuild에서 사용되며 CodeBuild컨테이너에 대한 기본 사용 권한이 필요합니다. 또한 이 역할은 각각의 Chef Automate 인스턴스에 대한 인증을 위한 개인 키를 포함하고 있는 Amazon S3 버킷(템플릿의 Parameters 섹션 참조)에 액세스 해야 합니다.

두번째 IAM역할인 FunctionRole는 AWS Lambda 환경에서 각 인스턴스에 대해 chef-client를 실행하는 데 사용되며,  Lambda 실행 역할에 필요한 Amazon CloudWatch 로그 권한 외에 이 역할에는 AWS Systems Manager를 통해 명령을 전송하는 기능이 필요합니다.

 

[Json]

{

    “Resources”: {

        “BuildRole”: {

            “Type”: “AWS::IAM::Role”,

            “Properties”: {

                “AssumeRolePolicyDocument”: {

                    “Version”: “2012-10-17”,

                    “Statement”: [ {

                        “Effect”: “Allow”,

                        “Principal”: {

                            “Service”: [ “codebuild.amazonaws.com” ]

                        },

                        “Action”: [ “sts:AssumeRole” ]

                    } ]

                },

                “Path”: “/”,

                “Policies”: [ {

                    “PolicyName”: “CodeBuildS3WithCWL”,

                    “PolicyDocument”: {

                        “Version”: “2012-10-17”,

                        “Statement”: [

                            {

                                “Effect”: “Allow”,

                                “Action”: [

                                    “s3:Get*”,

                                    “s3:List*”

                                ],

                                “Resource”: [

                                    { “Fn::GetAtt”: [ “ArtifactBucket”, “Arn” ] },

                                    {

                                        “Fn::Join”: [ “”, [

                                            { “Fn::GetAtt”: [ “ArtifactBucket”, “Arn” ] },

                                            “/*”

                                        ] ]

                                    },

                                    {

                                        “Fn::Join”: [ “”, [

                                            “arn:aws:s3:::”,

                                            { “Ref”: “KeyBucket” }

                                        ] ]

                                    },

                                    {

                                        “Fn::Join”: [ “”, [

                                            “arn:aws:s3:::”,

                                            { “Ref”: “KeyBucket” },

                                            “/*”

                                        ] ]

                                    }

                                ]

                            },

                            {

                                “Effect”: “Allow”,

                                “Resource”: [

                                    {

                                        “Fn::Join”: [ “”, [

                                            “arn:aws:logs:”,

                                            { “Ref”: “AWS::Region” },

                                            “:”,

                                            { “Ref”: “AWS::AccountId” },

                                            “:log-group:/aws/codebuild/*”

                                        ] ]

                                    },

                                    {

                                        “Fn::Join”: [ “”, [

                                            “arn:aws:logs:”,

                                            { “Ref”: “AWS::Region” },

                                            “:”,

                                            { “Ref”: “AWS::AccountId” },

                                            “:log-group:/aws/codebuild/*:*”

                                        ] ]

                                    }

                                ],

                                “Action”: [

                                    “logs:CreateLogGroup”,

                                    “logs:CreateLogStream”,

                                    “logs:PutLogEvents”

                                ]

                            },

                            {

                                “Effect”: “Allow”,

                                “Resource”: [ “arn:aws:s3:::codepipeline-*” ],

                                “Action”: [

                                    “s3:PutObject”,

                                    “s3:GetObject”,

                                    “s3:GetObjectVersion”

                                ]

                            },

                            {

                                “Effect”: “Allow”,

                                “Action”: [ “ssm:GetParameters” ],

                                “Resource”: {

                                    “Fn::Join”: [ “”, [

                                        “arn:aws:ssm:”,

                                        { “Ref”: “AWS::Region” },

                                        “:”,

                                        { “Ref”: “AWS::AccountId” },

                                        “:parameter/CodeBuild/*”

                                    ] ]

                                }

                            }

                        ]

                    }

                } ]

            }

        },

        “FunctionRole”: {

            “Type”: “AWS::IAM::Role”,

            “Properties”: {

                “AssumeRolePolicyDocument”: {

                    “Version”: “2012-10-17”,

                    “Statement”: [ {

                        “Effect”: “Allow”,

                        “Principal”: {

                            “Service”: [ “lambda.amazonaws.com” ]

                        },

                        “Action”: [ “sts:AssumeRole” ]

                    } ]

                },

                “Path”: “/”,

                “Policies”: [ {

                    “PolicyName”: “LambdaBasicExecutionWithSSM”,

                    “PolicyDocument”: {

                        “Version”: “2012-10-17”,

                        “Statement”: [

                            {

                                “Effect”: “Allow”,

                                “Action”: [

                                    “ssm:SendCommand”,

                                    “ssm:GetCommandInvocation”

                                ],

                                “Resource”: “*”

                            },

                            {

                                “Effect”: “Allow”,

                                “Action”: [

                                    “logs:CreateLogGroup”,

                                    “logs:CreateLogStream”,

                                    “logs:PutLogEvents”

                                ],

                                “Resource”: [

                                    {

                                        “Fn::Join”: [ “”, [

                                            “arn:aws:logs:”,

                                            { “Ref”: “AWS::Region” },

                                            “:”,

                                            { “Ref”: “AWS::AccountId” },

                                            “:log-group:/aws/lambda/*”

                                        ] ]

                                    },

                                    {

                                        “Fn::Join”: [ “”, [

                                            “arn:aws:logs:”,

                                            { “Ref”: “AWS::Region” },

                                            “:”,

                                            { “Ref”: “AWS::AccountId” },

                                            “:log-group:/aws/lambda/*:*”

                                        ] ]

                                    }

                                ]

                            },

                            {

                                “Effect”: “Allow”,

                                “Action”: [

                                    “codepipeline:PutJobSuccessResult”,

                                    “codepipeline:PutJobFailureResult”

                                ],

                                “Resource”: “*”

                            }

                        ]

                    }

                } ]

            }

        }

    }

}

 

Amazon S3 Bucket

Codepipeline 에 artifact 를 저장하기 위해 파이프라인 자체가 만들어질 때 ArtifactBucket 자원이 생성되고 참조가 됩니다.

 

[Json]

{

    “Resources”: {

        “ArtifactBucket”: {

            “Type”: “AWS::S3::Bucket”

        }

    }

}

 

CodeCommit repository

생성되는 CodeCommit 저장소는 cookbook을 저장하기 위한 Chef Repo 역할을 합니다. 저장소 구조는 다음 형식을 준수해야 합니다.

 

 

[Json]

{

    “Resources”: {

        “Repo”: {

            “Type”: “AWS::CodeCommit::Repository”,

            “Properties”: {

                “RepositoryDescription”: “Cookbook repository for multiple region OWCA deployment.”,

                “RepositoryName”: “owca-multi-region-repo”

            }

        }

    }

}

 

.chef

.chef/디렉토리 구조는 Chef Automate 스타터 키트의 OpsWorks에 일반적으로 포함되어 있는 것과 약간 다릅니다. 이후의 수정을 통해 나이프 유틸리티는 통신할 Chef Automate 인스턴스와 사용할 인증서 파일을 결정할 때 환경 변수를 사용할 수 있게 만들어줍니다.

아래의 knife.rb 파일은 yaml을 사용하여 config.yml의 구성 데이터를 구문 분석합니다. 올바른 구성 파일은 CHEF_SERVER 환경 변수의 값을 기준으로 결정됩니다.

 

[YAML]

require ‘yaml’

 

CHEF_SERVER = ENV[‘CHEF_SERVER’] || “NONE”

current_dir = File.dirname(__FILE__)

base_dir = File.join(File.dirname(File.expand_path(__FILE__)), ‘..’)

env_config = YAML.load_file(“#{current_dir}/#{CHEF_SERVER}/config.yml”)

 

log_level                :info

log_location             STDOUT

node_name                ‘pivotal’

client_key               “#{current_dir}/#{CHEF_SERVER}/private.pem”

syntax_check_cache_path  File.join(base_dir, ‘.chef’, ‘syntax_check_cache’)

cookbook_path            [File.join(base_dir, ‘cookbooks’)]

 

chef_server_url          env_config[“server”]

ssl_ca_file              File.join(base_dir, ‘.chef’, ‘ca_certs’, ‘opsworks-cm-ca-2016-root.pem’)

trusted_certs_dir        File.join(base_dir, ‘.chef’, ‘ca_certs’)

 

커뮤니케이션 할 올바른 Chef Automate 인스턴스를 결정하려면 각 인스턴스의 .chef/ 아래에 인스턴스 이름에 해당하는 고유한 디렉토리가 있어야 합니다. 이 디렉토리 내에서 config.yml 파일은 아래 형식을 따라야 합니다.

server: ‘https://[SERVER_FQDN]/organizations/default’

 

현재 각각의 Chef Automate 인스턴스 디렉토리에는 서버와 통신하는 데 필요한 개인 키가 포함되어 있지 않습니다. SSL/API키 또는 암호와 같은 인증 정보를 소스 제어 시스템에 커밋 된 파일에 포함하지 않는 것이 좋습니다. 대신 S3에서 필요한 키를 포함하기 위해 Build process에 단계를 추가했습니다.

 

Berksfile

Berksfile내에서, cookbooks/디렉토리에 포함된 모든 cookbook은 다음과 같은 형식으로 참조되어야 합니다. 이는 것이 해당 위치의 저장소에서 찾을 수 있다는 것을 Berkshelf에게 알려 줄 것입니다.

 

 

Chef Supermarket에서 가져온 Cookbook은 일반적으로 포함될 수 있습니다.

 

buildspec.yml

buildspec.yml파일은 CodeBuild에 종속성을 다운로드하고 각 Chef Automate 인스턴스로 cookbook을 업로드하기 위한 지침을 제공합니다. Berks명령을 사용하려면 빌드 프로세스 중에 ChefDK를 설치해야 합니다. 이 예에서는 설치 패키지를 Chef.io.에서 다운로드합니다. 또는 ChefDK를 패키징 하여 사용자 지정 빌드 환경에 미리 설치할 수 있으므로 빌드 시간을 줄일 수 있습니다. 다음 예와 같이 빌드 프로세스의 주요 단계는 다음과 같습니다.

 

1. ChefDK를 다운로드하여 설치합니다.
2. S3에서 자동으로 인스턴스 두개를 인증하는 개인 키를 복사합니다.
3. Berks install을 실행하여 종속성을 다운로드하고 설치합니다.
4. 두 Chef Automate 인스턴스에 cookbook을 업로드하세요.

 

[YAML]

version: 0.2

 

phases:

  install:

    commands:

      – “wget https://packages.chef.io/files/stable/chefdk/1.5.0/ubuntu/14.04/chefdk_1.5.0-1_amd64.deb”

      – “dpkg -i ./chefdk_1.5.0-1_amd64.deb”

  build:

    commands:

      – “aws s3 cp s3://[KEY_BUCKET]/[CHEF_SERVER_1]/private.pem ./.chef/[CHEF_SERVER_1]/private.pem”

      – “aws s3 cp s3://[KEY_BUCKET]/[CHEF_SERVER_2/private.pem ./.chef/[CHEF_SERVER_2]/private.pem”

      – “CHEF_SERVER=[CHEF_SERVER_1] berks install”

      – “CHEF_SERVER=[CHEF_SERVER_2] berks install”

      – “CHEF_SERVER=[CHEF_SERVER_1] berks upload –no-ssl-verify”

      – “CHEF_SERVER=[CHEF_SERVER_2] berks upload –no-ssl-verify”

  post_build:

    commands:

      – “echo ‘Complete'”

 

이 빌드 사양은 파이프 라인 구축 단계 중에 CodeBuild에 의해 제공됩니다.

 

[Json]

{

    “Resources”: {

        “BuildProject”: {

            “Type”: “AWS::CodeBuild::Project”,

            “Properties”: {

                “Artifacts”: { “Type”: “CODEPIPELINE” },

                “Description”: “Installs cookbook dependencies from Berksfile and uploads to one or more OWCA servers.”,

                “Environment”: {

                    “ComputeType”: “BUILD_GENERAL1_SMALL”,

                    “Image”: “aws/codebuild/ubuntu-base:14.04”,

                    “Type”: “LINUX_CONTAINER”

                },

                “ServiceRole”: {

                    “Fn::GetAtt”: [ “BuildRole”, “Arn” ]

                },

                “Source”: { “Type”: “CODEPIPELINE” }

            }

        }

    }

}

 

Lambda 기능

Lambda기능(ChefClientfunction)은 Boto3을 통해 AWS Systems Manager를 사용하여 기능 코드에 제공된 인스턴스 목록에 sudo chef-client를 호출합니다. 아래의 예에서는 목록에서 전달되는 두 개의 인스턴스 ID를 사용합니다. 그러나 Boto3은 태그 또는 기타 관련 속성별로 노드 목록을 생성하는 데 추가로 활용할 수 있습니다. 이것은 독자들을 위한 연습장으로 남겨놓았습니다.

 

[Json]

{

    “Resources”: {

        “ChefClientFunction”: {

            “Type”: “AWS::Lambda::Function”,

            “Properties”: {

                “Code”: {

                    “ZipFile”: {

                        “Fn::Join”: [ “\n”, [

                            “from __future__ import print_function”,

                            “”,

                            “import boto3”,

                            “”,

                            “ssm = boto3.client(‘ssm’)”,

                            “code_pipeline = boto3.client(‘codepipeline’)”,

                            “”,

                            “def lambda_handler(event,context):”,

                            ”    job_id = event[‘CodePipeline.job’][‘id’]”,

                            “”,

                            ”    try:”,

                            ”        response = ssm.send_command(“,

                            ”            InstanceIds=[“,

                            ”                ‘[INSTANCE_ID_1]’,”,

                            ”                ‘[INSTANCE_ID_2]'”,

                            ”            ],”,

                            ”            DocumentName=’AWS-RunShellScript’,”,

                            ”            Comment=’chef-client’,”,

                            ”            Parameters={“,

                            ”                ‘commands’: [‘sudo chef-client’]”,

                            ”            }”,

                            ”        )”,

                            “”,

                            ”        command_id = response[‘Command’][‘CommandId’]”,

                            ”        print(‘SSM Command ID: ‘ + command_id)”,

                            ”        print(‘Command Status: ‘ + response[‘Command’][‘Status’])”,

                            “”,

                            ”        # Include monitoring of job success/failure as needed.”,

                            “”,

                            ”        code_pipeline.put_job_success_result(jobId=job_id)”,

                            “”,

                            ”        return”,

                            ”    except Exception as e:”,

                            ”        print(e)”,

                            “”,

                            ”        code_pipeline.put_job_failure_result(jobId=job_id, failureDetails={‘message’: e.message, ‘type’: ‘JobFailed’})”,

                            “”,

                            ”        raise e”

                        ] ]

                    }

                },

                “Description”: “Executes chef-client on specified nodes.”,

                “Handler”: “index.lambda_handler”,

                “Role”: {

                    “Fn::GetAtt”: [ “FunctionRole”, “Arn” ]

                },

                “Runtime”: “python2.7”,

                “Timeout”: “10”

            }

        }

    }

}

 

Pipeline

마지막으로, 파이프 라인이 배포될 때 각 구성 요소를 사용하여 두 Chef Automate 서버에 동시에 배포합니다.

 

[Json]

{

    “Resources”: {

        “OWCAPipeline”: {

            “Type”: “AWS::CodePipeline::Pipeline”,

            “Properties”: {

                “ArtifactStore”: {

                    “Location”: { “Ref”: “ArtifactBucket” },

                    “Type”: “S3”

                },

                “RoleArn”: {

                    “Fn::Join”: [ “”, [

                        “arn:aws:iam::”,

                        { “Ref”: “AWS::AccountId” },

                        “:role/AWS-CodePipeline-Service”

                    ] ]

                },

                “Stages”: [

                    {

                        “Actions”: [

                            {

                                “ActionTypeId”: {

                                    “Category”: “Source”,

                                    “Owner”: “AWS”,

                                    “Provider”: “CodeCommit”,

                                    “Version”: “1”

                                },

                                “Configuration”: {

                                    “BranchName”: “master”,

                                    “RepositoryName”: {

                                        “Fn::GetAtt”: [ “Repo”, “Name” ]

                                    }

                                },

                                “Name”: “ChefRepo”,

                                “OutputArtifacts”: [

                                    { “Name”: “Cookbooks” }

                                ]

                            }

                        ],

                        “Name”: “Source”

                    },

                    {

                        “Actions”: [

                            {

                               “ActionTypeId”: {

                                    “Category”: “Build”,

                                    “Owner”: “AWS”,

                                    “Provider”: “CodeBuild”,

                                    “Version”: “1”

                                },

                                “Configuration”: {

                                    “ProjectName”: { “Ref”: “BuildProject” }

                                },

                                “InputArtifacts”: [

                                    { “Name”: “Cookbooks” }

                                ],

                                “Name”: “Berkshelf”

                            }

                        ],

                        “Name”: “Build”

                    },

                    {

                        “Actions”: [

                            {

                                “ActionTypeId”: {

                                    “Category”: “Invoke”,

                                    “Owner”: “AWS”,

                                    “Provider”: “Lambda”,

                                    “Version”: “1”

                                },

                                “Configuration”: {

                                    “FunctionName”: { “Ref”: “ChefClientFunction” }

                                },

                                “Name”: “LambdaChefClient”

                            }

                        ],

                        “Name”: “Invoke”

                    }

                ]

            }

        }

    }

}

 

요약

Chef Automate 인스턴스를 위한 여러 OpsWorks에 노드를 배포함으로써 인스턴스당 평균 워크 로드를 줄일 수 있으므로, 인프라의 확장에 따라 훨씬 더 많은 인스턴스를 관리할 수 있습니다. 여러 AWS서비스를 사용하면 서버, 지역 및 AWS계정 전반에서 cookbook의 일관성과 관리 용이성을 보장할 수 있습니다.

 

원문 URL: https://aws.amazon.com/ko/blogs/mt/distributing-your-aws-opsworks-for-chef-automate-infrastructure/

** 메가존 TechBlog는 AWS BLOG 영문 게재글중에서 한국 사용자들에게 유용한 정보 및 콘텐츠를 우선적으로 번역하여 내부 엔지니어 검수를 받아서, 정기적으로 게재하고 있습니다. 추가로 번역및 게재를 희망하는 글에 대해서 관리자에게 메일 또는 SNS페이지에 댓글을 남겨주시면, 우선적으로 번역해서 전달해드리도록 하겠습니다.