PackerとCodeBuildでAuto Scalingの起動設定を更新してみる

2020.01.31

はじめに

おはようございます、もきゅりんです。

あけましておめでとうございます。

前回 はMackerelエージェント導入済みAMIをPackerで作成しました。

このAMI、ゴールデンイメージとなれば Auto Scalingにはすぐ反映したい ですよねえ。

Auto Scalingの起動設定の切り替えは地味にめんどくさい作業です。

ということで、CodeCommit -> CodeBuild -> 起動設定の作成 -> Auto Scalingに起動設定の反映 をしてみました。

以下のような図となります。

codebuild-ami

かなり古い記事ですが、下記を参考にして進めていきます。

AWS CodeBuild と HashiCorp Packer を用いた AMI ビルダーの構築方法

前提条件

  • 利用できるAuto Scaling環境があること
  • CodeCommitが利用できる状態であること
  • IAM権限を設定・更新できること

CodeCommitリポジトリの作成については、『CodeCommit ユーザーガイド』の「セットアップ」を参照してください。

CodeCommitには前回で利用したPackerのTemplateが保存されているとします。

CodeBuildがビルドするのに、mfaの記述は必要ないので削除しておきましょう。

# packer-apache-mackerel.json
{
  "builders": [
    {
      "type": "amazon-ebs",
      "ami_name": "apache-mackerel",
      "region": "ap-northeast-1",
      "source_ami_filter": {
        "filters": {
          "name": "amzn2-ami-hvm-*-x86_64-gp2"
        },
        "owners": ["137112412989"],
        "most_recent": true
      },
      "instance_type": "t2.micro",
      "ssh_username": "ec2-user"
    }
  ],
  "provisioners": [
    {
      "type": "shell",
      "inline": [
        "sudo yum -y update",
        "sudo yum -y install httpd",
        "sudo systemctl start httpd && sudo systemctl enable httpd",
        "curl -fsSL https://mackerel.io/file/script/amznlinux/setup-all-yum-v2.sh | MACKEREL_APIKEY='YOUR_MACKEREL_APIKEY' sh",
        "sudo sed -i -e '/^#\\s*\\[host_status\\]/s/^#\\s*//' /etc/mackerel-agent/mackerel-agent.conf",
        "sudo sed -i -e '/^#\\s*on_start\\s*=\\s*\"working\"/s/^#\\s*//' /etc/mackerel-agent/mackerel-agent.conf",
        "sudo sed -i -e '/^#\\s*AUTO_RETIREMENT\\s*=\\s*1/s/^#\\s*//' /etc/sysconfig/mackerel-agent"
      ]
    }
  ]
}

やること

  1. コンソールで AWS CodeBuild のプロジェクトを作成する
  2. ビルドスペックを作成する
  3. AWS CodeBuild プロジェクトを実行する
  4. CodePipelineを作成する

1. コンソールで AWS CodeBuild のプロジェクトを作成する

AWS CodeBuild のコンソールからプロジェクトを開始します。

適当な名前を入れます。

build1

今回のソースは CodeCommit です。

ブランチを選択します。

codebuild2

記事では Ubuntu コンテナを利用していますが、ここでは Amazon Linux2 のコンテナを利用しています。(特に理由はないです。)

build3

新しいサービスロールを作成していますが、既存のロールが環境にあればそれを使っても支障ありません。

後ほど権限を追加するため、利用するロール名は控えておきます。

その他はデフォルト設定で問題で進めます。

控えたIAMロールを検索してPackerに必要となる新しいポリシーを適当な名前で追加します。

(e.g. codebuild-AMI_Builder-ec2-permissions)

searchRole

Amazon AMI Builder / IAM Task or Instance Role

{
  "Version": "2012-10-17",
  "Statement": [{
      "Effect": "Allow",
      "Action" : [
        "ec2:AttachVolume",
        "ec2:AuthorizeSecurityGroupIngress",
        "ec2:CopyImage",
        "ec2:CreateImage",
        "ec2:CreateKeypair",
        "ec2:CreateSecurityGroup",
        "ec2:CreateSnapshot",
        "ec2:CreateTags",
        "ec2:CreateVolume",
        "ec2:DeleteKeyPair",
        "ec2:DeleteSecurityGroup",
        "ec2:DeleteSnapshot",
        "ec2:DeleteVolume",
        "ec2:DeregisterImage",
        "ec2:DescribeImageAttribute",
        "ec2:DescribeImages",
        "ec2:DescribeInstances",
        "ec2:DescribeInstanceStatus",
        "ec2:DescribeRegions",
        "ec2:DescribeSecurityGroups",
        "ec2:DescribeSnapshots",
        "ec2:DescribeSubnets",
        "ec2:DescribeTags",
        "ec2:DescribeVolumes",
        "ec2:DetachVolume",
        "ec2:GetPasswordData",
        "ec2:ModifyImageAttribute",
        "ec2:ModifyInstanceAttribute",
        "ec2:ModifySnapshotAttribute",
        "ec2:RegisterImage",
        "ec2:RunInstances",
        "ec2:StopInstances",
        "ec2:TerminateInstances"
      ],
      "Resource" : "*"
  }]
}

ポリシーを作成したら、ロールにアタッチします。

さらにもう1つ、ASGのポリシーも必要です。

今回は若干雑な処置ですが、AutoScalingFullAccess をアタッチしてしまいます。

asg-policy

2. ビルドスペックを作成する

ビルドで必要な対応を buildspec.yml に記載します。

やっていることは、PackerでAMIを生成し、起動設定を作成し、指定されたAuto Scaling Group(以下ASG)の起動設定を更新し、現在使用している起動設定を破棄します。

過去の起動設定を破棄したくない場合や各環境変数は環境に応じて編集して下さい。

なお、どこかでコケてもロールバックを行うような処理は考慮されていません。

Packerが生成するAMIのIDを取得するのにこちらを参考としました。

PackerをCIツールに組み込むためのTipsあれこれ

version: 0.2

env:
  variables:
    ENV: 'dev'
    NameTagPrefix: 'ProjectName'
    SG: 'SECURITY_GROUP_ID'
    InstanceType: 'INSTANCE_TYPE'
    VolumeSize: 8
    VolumeType: 'gp2'
    KeyName: 'SECRET_KEY_NAME'
    ASGName: 'AUTO_SCALING_GROUP_NAME'

phases:
  install:
    runtime-versions:
      docker: 18
  pre_build:
    commands:
      - echo "HashiCorp Packer をインストール中..."
      - curl -qL -o packer.zip https://releases.hashicorp.com/packer/0.12.3/packer_0.12.3_linux_amd64.zip && unzip packer.zip
      - echo "jq をインストール中..."
      - curl -qL -o jq https://stedolan.github.io/jq/download/linux64/jq && chmod +x ./jq
      - echo "packer-apache-mackerel.json をバリデーションします"
      - ./packer validate packer-apache-mackerel.json
  build:
    commands:
      ### HashiCorp Packer cannot currently obtain the AWS CodeBuild-assigned role and its credentials
      ### Manually capture and configure the AWS CLI to provide HashiCorp Packer with AWS credentials
      ### More info here: https://github.com/mitchellh/packer/issues/4279
      - echo "AWS credentials を設定"
      - curl -qL -o aws_credentials.json http://169.254.170.2/$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI > aws_credentials.json
      - aws configure set region $AWS_REGION
      - aws configure set aws_access_key_id `./jq -r '.AccessKeyId' aws_credentials.json`
      - aws configure set aws_secret_access_key `./jq -r '.SecretAccessKey' aws_credentials.json`
      - aws configure set aws_session_token `./jq -r '.Token' aws_credentials.json`
      - echo "HashiCorp Packer のテンプレート amazon-linux_packer-template.json をビルド"
      - AMIID=`./packer build -machine-readable packer-apache-mackerel.json
        | awk -F , '$3 ~ /^artifact$/ && $5 ~ /^id$/ { print $6 };'
        | cut -d ":" -f 2`
      - echo $AMIID
  post_build:
    commands:
      - echo "HashiCorp Packer によるビルドが完了しました。 `date`"
      - echo "AutoScaling Launch Configuration を設定"
      - aws autoscaling create-launch-configuration
        --launch-configuration-name ${ENV}-${NameTagPrefix}-$(date +%Y%m%d%H%M)
        --image-id $AMIID --security-groups $SG --key-name $KeyName
        --instance-type $InstanceType
        --block-device-mappings "DeviceName=/dev/xvda,Ebs={VolumeSize=8,VolumeType=gp2}"
        --instance-monitoring Enabled=false
      - echo "AutoScaling Launch Configuration が完了しました"
      - echo "AutoScaling の起動設定 を更新します"
      - ASG=`aws autoscaling describe-auto-scaling-groups --auto-scaling-group-name ${ASGName}`
      - PreLaunchConfigName=`echo $ASG | jq -r .AutoScalingGroups[].LaunchConfigurationName`
      - aws autoscaling update-auto-scaling-group
        --auto-scaling-group-name $ASGName
        --launch-configuration-name ${ENV}-${NameTagPrefix}-$(date +%Y%m%d%H%M)
      - echo "AutoScaling の起動設定 を更新が完了しました"
      - aws autoscaling delete-launch-configuration
        --launch-configuration-name $PreLaunchConfigName
      - echo "古いAutoScaling Launch Configuration を削除しました"

3. AWS CodeBuild プロジェクトを実行する

ビルドを開始します。数分の時間がかかります。

BuildStart

成功です。

build-success

ASG、起動設定、AMIを確認していきます。

asg

起動設定の名前に利用されている時間はUTCです。

launchconfig

そしてAMI。

lastest-ami

特に問題なさそうですね。

ここまでの作業はご覧の通りですが、まぁなかなか面倒な作業ですよね。

ということで、CodePipelineを構築する簡易なCFnテンプレートを作りました。

4. CodePipelineを作成する

以下のテンプレートでは便宜上、SecurityGroupIDやASG名などをパラメータとして指定していますが、実際は別スタックの出力値を取得して利用されるのがよいかと思います。

aws cloudformation deploy --stack-name demo-packer-codepipeline --template-file sample.yml \
  --parameter-overrides \
  CodeCommitRepositoryName='DemoPacker' \
  CodeBuildProjectName='Demo-CodePipeline-AMI-Builder' \
  PipelineName='Demo-AMI-Builder-CodePipeline' \
  ASGName='ASG_NAME' \
  SecurityGroupID='SG_ID' \
  InstanceType='t3.micro' \
  KeyName='YOUR_KEY_NAME' \
  --capabilities CAPABILITY_NAMED_IAM

適宜、適当な名前をパラメータに代入して実行して下さい。

本筋とは関係ありませんが、このなかなかの量のパラメータ数とうまく対応したり、CFnとの付き合い方を語った実益ある弊社ブログを紹介しておきます。

CloudFormationの全てを味わいつくせ!「AWSの全てをコードで管理する方法〜その理想と現実〜」

# sample.yml
AWSTemplateFormatVersion: 2010-09-09
Description: CodePipeline For AMI Update with Packer
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Parameters:
  ENV:
    Type: String
    Default: 'demo'
  NameTagPrefix:
    Type: String
    Default: 'ProjectName'
  CodeCommitRepositoryName:
    Type: String
  CodeBuildProjectName:
    Type: String
  PipelineName:
    Type: String
  ASGName:
    Type: String
  SecurityGroupID:
    Type: 'AWS::EC2::SecurityGroup::Id'
  InstanceType:
    Type: String
  VolumeSize:
    Type: Number
    Default: 8
  KeyName:
    Type: 'AWS::EC2::KeyPair::KeyName'
# ------------------------------------------------------------#
# Resources
# ------------------------------------------------------------#
Resources:
  # S3Bucket
  ArtifactBucket:
    Type: AWS::S3::Bucket

  # CodeWatchEventを実行できるIAMRole
  AmazonCloudWatchEventRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - events.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: cwe-pipeline-execution
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: codepipeline:StartPipelineExecution
                Resource: !Join
                  - ''
                  - - 'arn:aws:codepipeline:'
                    - !Ref 'AWS::Region'
                    - ':'
                    - !Ref 'AWS::AccountId'
                    - ':'
                    - !Ref 'PipelineName'

  # CodeBuildに適用するIAMRole
  CodeBuildServiceRole:
    Type: AWS::IAM::Role
    Properties:
      Path: /
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: codebuild.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonS3FullAccess
        - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
        - arn:aws:iam::aws:policy/AutoScalingFullAccess
      Policies:
        - PolicyName: SampleCodeBuildAccess
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Resource: '*'
                Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
              - Resource: arn:aws:s3:::codepipeline-${AWS::Region}-*
                Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:PutObject
                  - s3:GetObjectVersion
                  - s3:GetBucketAcl
                  - s3:GetBucketLocation
              - Resource: !Sub arn:aws:codecommit:${AWS::Region}:${AWS::AccountId}:${CodeCommitRepositoryName}
                Effect: Allow
                Action:
                  - codecommit:GitPull
              - Resource: !Sub arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:report-group/${CodeBuildProjectName}-*
                Effect: Allow
                Action:
                  - codebuild:CreateReportGroup
                  - codebuild:CreateReport
                  - codebuild:UpdateReport
                  - codebuild:BatchPutTestCases
              - Resource: '*'
                Effect: Allow
                Action:
                  - ec2:AttachVolume
                  - ec2:AuthorizeSecurityGroupIngress
                  - ec2:CopyImage
                  - ec2:CreateImage
                  - ec2:CreateKeypair
                  - ec2:CreateSecurityGroup
                  - ec2:CreateSnapshot
                  - ec2:CreateTags
                  - ec2:CreateVolume
                  - ec2:DeleteKeyPair
                  - ec2:DeleteSecurityGroup
                  - ec2:DeleteSnapshot
                  - ec2:DeleteVolume
                  - ec2:DeregisterImage
                  - ec2:DescribeImageAttribute
                  - ec2:DescribeImages
                  - ec2:DescribeInstances
                  - ec2:DescribeInstanceStatus
                  - ec2:DescribeRegions
                  - ec2:DescribeSecurityGroups
                  - ec2:DescribeSnapshots
                  - ec2:DescribeSubnets
                  - ec2:DescribeTags
                  - ec2:DescribeVolumes
                  - ec2:DetachVolume
                  - ec2:GetPasswordData
                  - ec2:ModifyImageAttribute
                  - ec2:ModifyInstanceAttribute
                  - ec2:ModifySnapshotAttribute
                  - ec2:RegisterImage
                  - ec2:RunInstances
                  - ec2:StopInstances
                  - ec2:TerminateInstances

  # CodePipelineに適用するIAMRole
  CodePipelineServiceRole:
    Type: AWS::IAM::Role
    Properties:
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: codepipeline.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: SamplePipeline
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Resource:
                  - !Sub arn:aws:s3:::${ArtifactBucket}/*
                Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketVersioning
              - Resource: '*'
                Effect: Allow
                Action:
                  - codecommit:GetRepository
                  - codecommit:ListBranches
                  - codecommit:GetUploadArchiveStatus
                  - codecommit:UploadArchive
                  - codecommit:CancelUploadArchive
                  - codebuild:StartBuild
                  - codebuild:StopBuild
                  - codebuild:BatchGet*
                  - codebuild:Get*
                  - codebuild:List*
                  - codecommit:GetBranch
                  - codecommit:GetCommit
                  - iam:PassRole

  # CloudWatchEventの実行ルール
  AmazonCloudWatchEventRule:
    Type: AWS::Events::Rule
    Properties:
      EventPattern:
        source:
          - aws.codecommit
        detail-type:
          - 'CodeCommit Repository State Change'
        resources:
          - !Join
            - ''
            - - 'arn:aws:codecommit:'
              - !Ref 'AWS::Region'
              - ':'
              - !Ref 'AWS::AccountId'
              - ':'
              - !Ref 'CodeCommitRepositoryName'
        detail:
          event:
            - referenceCreated
            - referenceUpdated
          referenceType:
            - branch
          referenceName:
            - master
      Targets:
        - Arn: !Join
            - ''
            - - 'arn:aws:codepipeline:'
              - !Ref 'AWS::Region'
              - ':'
              - !Ref 'AWS::AccountId'
              - ':'
              - !Ref 'PipelineName'
          RoleArn: !GetAtt AmazonCloudWatchEventRole.Arn
          Id: codepipeline-AppPipeline

  # CodeBuild
  CodeBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      Artifacts:
        Type: CODEPIPELINE
      Environment:
        PrivilegedMode: true
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/amazonlinux2-x86_64-standard:2.0
        Type: LINUX_CONTAINER
        EnvironmentVariables:
          - Name: AWS_DEFAULT_REGION
            Value: !Ref AWS::Region
          - Name: ENV
            Value: !Ref ENV
          - Name: NameTagPrefix
            Value: !Ref NameTagPrefix
          - Name: SecurityGroupID
            Value: !Ref SecurityGroupID
          - Name: InstanceType
            Value: !Ref InstanceType
          - Name: VolumeSize
            Value: !Ref VolumeSize
          - Name: VolumeType
            Value: 'gp2'
          - Name: KeyName
            Value: !Ref KeyName
          - Name: ASGName
            Value: !Ref ASGName
      Name: !Ref CodeBuildProjectName
      ServiceRole: !Ref CodeBuildServiceRole
      Source:
        Type: CODEPIPELINE
        BuildSpec: |
          version: 0.2
          phases:
            install:
              runtime-versions:
                docker: 18
            pre_build:
              commands:
                - echo "HashiCorp Packer をインストール中..."
                - curl -qL -o packer.zip https://releases.hashicorp.com/packer/0.12.3/packer_0.12.3_linux_amd64.zip && unzip packer.zip
                - echo "jq をインストール中..."
                - curl -qL -o jq https://stedolan.github.io/jq/download/linux64/jq && chmod +x ./jq
                - echo "packer-apache-mackerel.json をバリデーションします"
                - ./packer validate packer-apache-mackerel.json
            build:
              commands:
                ### HashiCorp Packer cannot currently obtain the AWS CodeBuild-assigned role and its credentials
                ### Manually capture and configure the AWS CLI to provide HashiCorp Packer with AWS credentials
                ### More info here: https://github.com/mitchellh/packer/issues/4279
                - echo "AWS credentials を設定"
                - curl -qL -o aws_credentials.json http://169.254.170.2/$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI > aws_credentials.json
                - aws configure set region $AWS_REGION
                - aws configure set aws_access_key_id `./jq -r '.AccessKeyId' aws_credentials.json`
                - aws configure set aws_secret_access_key `./jq -r '.SecretAccessKey' aws_credentials.json`
                - aws configure set aws_session_token `./jq -r '.Token' aws_credentials.json`
                - echo "HashiCorp Packer のテンプレート amazon-linux_packer-template.json をビルド"
                - echo "HashiCorp Packer のテンプレート amazon-linux_packer-template.json をビルド"
                - AMIID=`./packer build -machine-readable packer-apache-mackerel.json
                  | awk -F , '$3 ~ /^artifact$/ && $5 ~ /^id$/ { print $6 };'
                  | cut -d ":" -f 2`
                - echo $AMIID
            post_build:
              commands:
                - echo "HashiCorp Packer によるビルドが完了しました。 `date`"
                - echo "AutoScaling Launch Configuration を設定"
                - aws autoscaling create-launch-configuration
                  --launch-configuration-name ${ENV}-${NameTagPrefix}-$(date +%Y%m%d%H%M)
                  --image-id $AMIID --security-groups $SG --key-name $KeyName
                  --instance-type $InstanceType
                  --block-device-mappings "DeviceName=/dev/xvda,Ebs={VolumeSize=8,VolumeType=gp2}"
                  --instance-monitoring Enabled=false
                - echo "AutoScaling Launch Configuration が完了しました"
                - echo "AutoScaling の起動設定 を更新します"
                - ASG=`aws autoscaling describe-auto-scaling-groups --auto-scaling-group-name ${ASGName}`
                - PreLaunchConfigName=`echo $ASG | jq -r .AutoScalingGroups[].LaunchConfigurationName`
                - aws autoscaling update-auto-scaling-group
                  --auto-scaling-group-name $ASGName
                  --launch-configuration-name ${ENV}-${NameTagPrefix}-$(date +%Y%m%d%H%M)
                - echo "AutoScaling の起動設定 を更新が完了しました"
                - aws autoscaling delete-launch-configuration
                  --launch-configuration-name $PreLaunchConfigName
                - echo "古いAutoScaling Launch Configuration を削除しました"

  # CodePipeLine
  CodePipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      RoleArn: !GetAtt CodePipelineServiceRole.Arn
      Name: !Ref PipelineName
      ArtifactStore:
        Type: S3
        Location: !Ref ArtifactBucket
      Stages:
        - Name: Source
          Actions:
            - Name: SourceAction
              ActionTypeId:
                Category: Source
                Owner: AWS
                Version: 1
                Provider: CodeCommit
              Configuration:
                RepositoryName: !Ref CodeCommitRepositoryName
                PollForSourceChanges: false
                BranchName: master
              RunOrder: 1
              OutputArtifacts:
                - Name: App
        - Name: Build
          Actions:
            - Name: Build
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: 1
                Provider: CodeBuild
              Configuration:
                ProjectName: !Ref CodeBuildProject
              RunOrder: 1
              InputArtifacts:
                - Name: App
# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------#
Outputs:
  PipelinelogicalID:
    Description: logical ID.
    Value: !Ref CodePipeline

成功です。

success-pipeline

5. 最後に

実用する際はもう少々考慮、改善点がありそうですが、ソースリポジトリをCodeCommitをGitHubにしてもやることに大差ありません。

手前味噌で恐縮ですが、下記などお役に立つでしょう。

CFnでGitHub + Fargate + CodePipelineを構築してみる

以上です。

どなたかのお役に立てば幸いです。

参考: