Jenkins pipelineでAWS CloudFormationを利用しVPCを構築する

### はじめに

JenkinsからAWS CloudFormationを利用しVPCを構築するサンプルです。JenkinsからAWS CLIを利用するやり方はたくさんあります。これはあくまでひとつの例です。

### 検証環境

OS: CentOS7
Jenkinsのバージョン: 2.1以降
必要なプラグイン: Jenkinsの初期設定画面でインストールされるプラグイン + [Pipeline: AWS Steps](https://jenkins.io/doc/pipeline/steps/pipeline-aws/)

### Jenkinsの設定

#### 必要なパッケージ

Jenkinsサーバに以下のパッケージがインストールされている必要があります。

- Git
- AWS CLI

[こちら](https://github.com/ansible-centos7/jenkins_install.git)のAnsible PlaybookでJenkinsを構築することも可能です。

#### [Pipeline: AWS Steps](https://jenkins.io/doc/pipeline/steps/pipeline-aws/)をインストールする

「Jenkinsの管理」→「プラグインの管理」→「利用可能タブ」から　**AWS CodePipeline** を選択しインストールする（Jenkinsサーバ側ではプラグイン名が「Pipeline: AWS Steps」ではないので注意！）

このプラグインをインストールすると他の依存関係にあるプラグインもインストールされる

<img width="1087" alt="スクリーンショット 2019-08-29 20.49.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79373/caafa3b9-6986-28ee-7f80-16a6d7702462.png">


#### AWS Credentialsを登録する

- AWSのIAMのコンソールでEC2とCloudFormationの実行権限を付与したアクセスキーを作成する

- 「認証情報」→「システム」→「 認証情報の追加」→「種類」から「AWS Credentials」を選択し「Access Key ID」と「Secret Access Key」を入力する

- IDの部分は入力しない

<img width="1025" alt="スクリーンショット 2019-08-29 20.24.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79373/a55f5419-3a30-6f05-f440-999248373c39.png">

#### JenkinsへPipelineファイルを登録する

[こちら](https://github.com/jenkinsfiles-aws/cloudformation)のPipelineファイルを登録していきます。

```groovy:create-stack/Jenkinsfile
#!groovy

pipeline {

  agent any

  parameters {
    string(
      name: 'AWS_CREDENTIALS_ID',
      defaultValue: '',
      description: 'AWS credentials id, stored in Jenkins credentials'
    )
    string(
      name: 'EXTRA_ARGS',
      defaultValue: '',
      description: 'aws cloudformation create-stack command extra arguments'
    )
    string(
      name: 'GIT_BRANCHES_CFN',
      defaultValue: '*/master',
      description: "Git branch or tag name or commit id to retrieve of GIT_URL of CloudFormation template file"
    )
    string(
      name: 'GIT_URL',
      defaultValue: '',
      description: "GitHub URL to retrieve CloudFormation template"
    )
    string(
      name: 'REGION',
      defaultValue: '',
      description: 'AWS CLI region name'
    )
    string(
      name: 'STACK_NAME',
      defaultValue: '',
      description: 'CloudFormation stack name'
    )
    string(
      name: 'TEMPLATE_FILE_PATH',
      defaultValue: '',
      description: 'CloudFormation template file path'
    )
    string(
      name: 'WORKING_DIR',
      defaultValue: 'cfn',
      description: 'Job working directory'
    )
  }

  stages {
    stage('Initialize wokring directory') {
      steps {
        dir ("${params.WORKING_DIR}") {
          cleanWs()
        }
      }
    }

    stage('Retrieve CloudFormation template file from Github') {
      steps {
        checkout(
          [
            $class: 'GitSCM',
            branches: [
              [
                name: "${params.GIT_BRANCHES_CFN}"
              ]
            ],
            extensions: [
              [
                $class: 'RelativeTargetDirectory',
                relativeTargetDir: "${params.WORKING_DIR}"
              ]
            ],
            doGenerateSubmoduleConfigurations: false,
            submoduleCfg: [],
            userRemoteConfigs: [
              [
                url: "${params.GIT_URL}"
              ]
            ]
          ]
        )
      }
    }

    stage('Create Stack') {
      steps {
        withAWS(credentials:"${params.AWS_CREDENTIALS_ID}", region:"${params.REGION}") {
          dir ("${params.WORKING_DIR}") {
             // Create Stack
             sh "aws cloudformation create-stack \
                --stack-name ${params.STACK_NAME} \
                --template-body file://${params.TEMPLATE_FILE_PATH} \
                ${params.EXTRA_ARGS}"
  
             // Wait until Stack is created completely
             sh "aws cloudformation wait stack-create-complete \
                --stack-name ${params.STACK_NAME}" 
  
             // Print CloudFormation create command resutls
             sh "aws cloudformation describe-stacks \
                --stack-name ${params.STACK_NAME}"
          }
        }
      }
    }
  }
}
```

- 「新規ジョブ作成」→パイプライン名を入力して「パイプライン」をクリック

<img width="1009" alt="スクリーンショット 2019-08-29 21.20.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79373/413706a2-cc3c-5ffb-4d67-bc000fc0a888.png">

- パイプラインで以下を設定

リポジトリURL
>https://github.com/jenkinsfiles-aws/cloudformation.git

Script Path
>create-stack/Jenkinsfile

<img width="1440" alt="スクリーンショット 2019-08-29 21.17.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79373/71419f37-541b-7bdf-7c02-c042b58992cf.png">

### Jobを実行する

作成したJobを一回空実行します。一度実行するとパラメータが指定できる様になります。

<img width="1048" alt="スクリーンショット 2019-08-29 21.25.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79373/b70fc004-84df-2cc8-633a-76f9e8bd3183.png">

パラメータ設定例です。

AWS_CREDENTIALS_IDはJenkinsで発行される一意のIDです。

利用したCloudFormationテンプレートは[こちら](https://github.com/aws-cloudformation-tm/ec2.git)です。

```yaml:centos7-hvm.yaml
AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  InstanceType:
    Description: EC2 instance type
    Type: String
    Default: t2.micro
    ConstraintDescription: must be a valid EC2 instance type.
  KeyName:
    Description: Name of an existing EC2 KeyPair to enable SSH access to the instances
    Type: AWS::EC2::KeyPair::KeyName
    ConstraintDescription: must be the name of an existing EC2 KeyPair.

Mappings:
  StackConfigs:
    VPC:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: 'true'
      EnableDnsHostnames: 'true'
    PublicSubnet:
      AvailabilityZone: "ap-northeast-1a"
      CidrBlock: 10.0.1.0/24
      Name: "10.0.1.0 - ap-northeast-1a"

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !FindInMap [StackConfigs, VPC, CidrBlock]
      EnableDnsSupport: !FindInMap [StackConfigs, VPC, EnableDnsSupport]
      EnableDnsHostnames: !FindInMap [StackConfigs, VPC, EnableDnsHostnames]
      Tags:
      - Key: Name
        Value: !Ref AWS::StackName

  GatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId:
        Ref: VPC
      InternetGatewayId:
        Ref: InternetGateway

  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !FindInMap [StackConfigs, PublicSubnet, AvailabilityZone]
      CidrBlock: !FindInMap [StackConfigs, PublicSubnet, CidrBlock]
      MapPublicIpOnLaunch: 'true'
      VpcId:
        Ref: VPC
      Tags:
      - Key: Name
        Value: !FindInMap [StackConfigs, PublicSubnet, Name]

  centos7:
    Type: AWS::EC2::Instance
    DependsOn: InternetGateway
    Properties:
      BlockDeviceMappings:
        - DeviceName: /dev/sda1
          Ebs:
            DeleteOnTermination: true
            VolumeType: gp2
      ImageId: ami-045f38c93733dd48d
      InstanceType: !Ref 'InstanceType'
      KeyName: !Ref 'KeyName'
      SecurityGroupIds:
        - !Ref sshSG
        - !Ref httpSG
      SubnetId:
        Ref: PublicSubnet
      Tags:
      - Key: Name
        Value: !Ref AWS::StackName
      - Key: ansible_inventory_group_name
        Value: centos7
      - Key: ansible_user
        Value: centos
      - Key: ansible_port
        Value: 22

  sshSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: ssh
      GroupDescription: SSH access rule
      SecurityGroupIngress:
      - CidrIp: 0.0.0.0/0
        FromPort: 22
        IpProtocol: tcp
        ToPort: 22
      VpcId:
        Ref: VPC

  httpSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: http
      GroupDescription: http access rule
      SecurityGroupIngress:
      - CidrIp: 0.0.0.0/0
        FromPort: 80
        IpProtocol: tcp
        ToPort: 80
      VpcId:
        Ref: VPC

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
      - Key: Name
        Value: InternetGateway

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: VPC
      Tags:
      - Key: Name
        Value: public

  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: InternetGateway
    Properties:
      RouteTableId:
        Ref: PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: InternetGateway

  PublicSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId:
        Ref: PublicSubnet
      RouteTableId:
        Ref: PublicRouteTable

Outputs:
  VpcId:
    Value: !Ref VPC
```

Jobを実行するとスタックが作成されます。

<img width="1436" alt="スクリーンショット 2019-08-29 21.30.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79373/ac8b3501-3463-c3f6-d75a-b4ec9ceba600.png">