Help us understand the problem. What is going on with this article?

AWS CDKとGithub ActionsでIAMユーザーを管理してみた【CI/CD】

adv_cal.png

本記事はうるる Advent Calendar 2019 6日目の記事です。

TL;DR

先日こちらの記事で書いたものの続きと言いますか、運用に導入してみたのでそのお話です。
今まではIAMユーザー作成に関してTerraformで管理をしていたのですが、新しいアカウントができたのでタイミング的にちょうどいいこともありCDKでIAMユーザーの管理をしてみようと思いました。
またデプロイに関しては、Github Actionsを利用することにしました。
AWS CDKの実装やGithub Actionsに至った理由などをお伝えしていきたいと思います。

IAM管理リソース

現在Terraformで管理されているIAM周りのリソースに関して、至ってシンプルなものです。

  • IAMユーザー
  • IAMロール
  • IAMポリシー
  • アカウントパスワードポリシー

なんの変哲も無い構成だと思います。
ちなみに既存のTerraformはこんな感じになっています。

$ tree
.
├── README.md
├── build
│   ├── Jenkinsfile.deploy
│   └── Jenkinsfile.test
├── iam_user
│   ├── account_password_policy.tf
│   ├── backend.tf
│   ├── iam_group.tf
│   ├── iam_policy.tf
│   ├── iam_role.tf
│   ├── iam_user.tf
│   ├── provider.tf
│   └── variables.tf
└── modules
    ├── iam_group
    │   ├── main.tf
    │   ├── output.tf
    │   └── variables.tf
    ├── iam_policy
    │   ├── main.tf
    │   ├── output.tf
    │   └── variables.tf
    └── iam_user
        ├── main.tf
        ├── output.tf
        └── variables.tf

ソースコードはこちら
基本的にはiam_user/iam_user.tfにユーザー用のmoduleを追加していき、JenkinsかCircleCIでCI/CDを回すといったような運用です。
これらをAWS CDKに移管してみました。

AWS CDKで書いてみる

基本的な流れは一緒です。
今回もTypeScriptで書いていきます。
※AWS CDKを始めるのであれば TypeScriptを強くおすすめします!

  • cdk initを実行
  • cdk configを変更
  • lib/*.tsに書いていく
  • cdk bootstrapを実行
  • npm run buildでコンパイル
  • cdk diffでdry-run
  • cdk deployでリソース作成

以下が実装する上でハマりポイントでした。

  • アカウントパスワードポリシーはJavaScriptaws-sdkで定義してexportしてあげる必要がある
  • テストスクリプトの作成

Stack全体はこんな感じです。

lib/advent-calendar-2019-stack.ts
import { Construct, Stack, StackProps }  from '@aws-cdk/core';
import { Group, Policy, PolicyStatement, ManagedPolicy, User } from '@aws-cdk/aws-iam';
import AWS = require('aws-sdk');

const admins = 'testAdminGroup';
const adminUsers = [
  'demo01',
];

const developers = 'testDevGroup';
const devUsers = [
  'demo02',
  'demo03'
];

export class AdventCalendar2019Stack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // Configure account policy
    const accountPasswordPolicy = new AWS.IAM({});
    accountPasswordPolicy.updateAccountPasswordPolicy({
      "MinimumPasswordLength": 9,
      "RequireSymbols": false,
      "RequireNumbers": true,
      "RequireUppercaseCharacters": true,
      "RequireLowercaseCharacters": true,
      "AllowUsersToChangePassword": true,
    }, function(){});

    // Allow get account password policy
    const getAccountPassword = new PolicyStatement({
        resources: ["*"],
        actions: [
          "iam:GetAccountPasswordPolicy"
        ]
    });

    // Allow change user password by itself
    const changePassword = new PolicyStatement({
      resources: ["arn:aws:iam::account-id-without-hyphens:user/${aws:username}"],
      actions: [
        "iam:ChangePassword",
      ]
    });

    // Allow IAM Role access
    const iamPassRoleAccess = new PolicyStatement({
      resources: ["*"],
      actions: [
        "iam:Get*",
        "iam:List*",
        "iam:PassRole"
        ],
    });

    // AWS managed policy
    const adminPolicy = ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess');
    const powerUserPolicy = ManagedPolicy.fromAwsManagedPolicyName('PowerUserAccess');

    // Developer policy
    const devPolicy = new Policy(this, 'iamPassRoleAccess', { 
      policyName: "iamPassRoleAccess",
      statements: [iamPassRoleAccess],
    });

    // Common policy
    const commonPolicy = new Policy(this, 'changePassword', { 
      policyName: "changePassword",
      statements: [changePassword, getAccountPassword],
    });

    // Admin group
    const adminGroup = new Group(this, admins, { groupName: admins });
    adminGroup.addManagedPolicy(adminPolicy);
    adminGroup.attachInlinePolicy(commonPolicy);

    // Developer group
    const devGroup = new Group(this, developers, { groupName: developers });
    devGroup.addManagedPolicy(powerUserPolicy);
    devGroup.attachInlinePolicy(devPolicy);
    devGroup.attachInlinePolicy(commonPolicy);

    // Create users
    adminUsers.forEach(adminUser => {
      new User(this, adminUser, {
        userName: adminUser,
        groups: [adminGroup],
      });
    });

    devUsers.forEach(devUser => {
      new User(this, devUser, {
        userName: devUser,
        groups: [devGroup]
      });
    });
  }
}

リポジトリ

アカウントポリシーのexport

アカウントのパスワードポリシーに関してはaws-sdkを利用する必要があるので、lib/advent-calendar-2019-stack.d.ts内で型定義して上げる必要があります。
これらはaws-sdk-jsのリポジトリ内にあるのでそこからコピペしました。
https://github.com/aws/aws-sdk-js/blob/master/clients/iam.d.ts

lib/advent-calendar-2019-stack.d.ts
declare class IAM extends Service {
    /**
     * Constructs a service object. This object has one method for each API operation.
     */
    constructor(options?: IAM.Types.ClientConfiguration)
    config: Config & IAM.Types.ClientConfiguration;
  /**
   * Updates the password policy settings for the AWS account.    This operation does not support partial updates. No parameters are required, but if you do not specify a parameter, that parameter's value reverts to its default value. See the Request Parameters section for each parameter's default value. Also note that some parameters do not allow the default parameter to be explicitly set. Instead, to invoke the default value, do not include that parameter when you invoke the operation.     For more information about using a password policy, see Managing an IAM Password Policy in the IAM User Guide.
   */
  updateAccountPasswordPolicy(params: IAM.Types.UpdateAccountPasswordPolicyRequest, callback?: (err: AWSError, data: {}) => void): Request<{}, AWSError>;
  /**
   * Updates the password policy settings for the AWS account.    This operation does not support partial updates. No parameters are required, but if you do not specify a parameter, that parameter's value reverts to its default value. See the Request Parameters section for each parameter's default value. Also note that some parameters do not allow the default parameter to be explicitly set. Instead, to invoke the default value, do not include that parameter when you invoke the operation.     For more information about using a password policy, see Managing an IAM Password Policy in the IAM User Guide.
   */
  updateAccountPasswordPolicy(callback?: (err: AWSError, data: {}) => void): Request<{}, AWSError>;
}

export interface UpdateAccountPasswordPolicyRequest {
    /**
     * The minimum number of characters allowed in an IAM user password. If you do not specify a value for this parameter, then the operation uses the default value of 6.
     */
    MinimumPasswordLength?: minimumPasswordLengthType;
    /**
     * Specifies whether IAM user passwords must contain at least one of the following non-alphanumeric characters: ! @ # $ % ^ &amp; * ( ) _ + - = [ ] { } | ' If you do not specify a value for this parameter, then the operation uses the default value of false. The result is that passwords do not require at least one symbol character.
     */
    RequireSymbols?: booleanType;
    /**
     * Specifies whether IAM user passwords must contain at least one numeric character (0 to 9). If you do not specify a value for this parameter, then the operation uses the default value of false. The result is that passwords do not require at least one numeric character.
     */
    RequireNumbers?: booleanType;
    /**
     * Specifies whether IAM user passwords must contain at least one uppercase character from the ISO basic Latin alphabet (A to Z). If you do not specify a value for this parameter, then the operation uses the default value of false. The result is that passwords do not require at least one uppercase character.
     */
    RequireUppercaseCharacters?: booleanType;
    /**
     * Specifies whether IAM user passwords must contain at least one lowercase character from the ISO basic Latin alphabet (a to z). If you do not specify a value for this parameter, then the operation uses the default value of false. The result is that passwords do not require at least one lowercase character.
     */
    RequireLowercaseCharacters?: booleanType;
    /**
     *  Allows all IAM users in your account to use the AWS Management Console to change their own passwords. For more information, see Letting IAM Users Change Their Own Passwords in the IAM User Guide. If you do not specify a value for this parameter, then the operation uses the default value of false. The result is that IAM users in the account do not automatically have permissions to change their own password.
     */
    AllowUsersToChangePassword?: booleanType;
    /**
     * The number of days that an IAM user password is valid. If you do not specify a value for this parameter, then the operation uses the default value of 0. The result is that IAM user passwords never expire.
     */
    MaxPasswordAge?: maxPasswordAgeType;
    /**
     * Specifies the number of previous passwords that IAM users are prevented from reusing. If you do not specify a value for this parameter, then the operation uses the default value of 0. The result is that IAM users are not prevented from reusing previous passwords.
     */
    PasswordReusePrevention?: passwordReusePreventionType;
    /**
     * Prevents IAM users from setting a new password after their password has expired. The IAM user cannot be accessed until an administrator resets the password. If you do not specify a value for this parameter, then the operation uses the default value of false. The result is that IAM users can change their passwords after they expire and continue to sign in as the user.
     */
    HardExpiry?: booleanObjectType;
}

テストの実施

書いたtsファイルに対して実際にコンパイルしたファイルをテストすることが必要になります。
下記ドキュメントにしたがって設定していきます。
https://docs.aws.amazon.com/cdk/latest/guide/testing.html
AWS CDKはデフォルトでjestを利用してテストを実施することが可能です。
jestの機能を利用してまずはSnapshot Testsを実施します。

Snapshot Tests
import '@aws-cdk/assert/jest'
import { SynthUtils } from '@aws-cdk/assert'
import { Stack, App } from '@aws-cdk/core'  

import { AdventCalendar2019Stack } from '../lib/advent-calendar-2019-stack'

test('IAM Resource Stack', () => {
  const app = new App();
  const stack = new AdventCalendar2019Stack(app, 'IAMTestStack');
  expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot();
});

ソースコードに変更がない限りはエラーは発生しません。

$ npm run test

> poc-actions@0.1.0 test /Users/user/Documents/GitHub/advent-calendar-2019
> jest

 PASS  test/advent-calendar-2019.test.ts
   IAM Resource Stack (96ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 passed, 1 total
Time:        1.585s, estimated 2s
Ran all test suites.

次にFine-Grained Assertionsを実施します。
構成要素の一部機能を抜き出してテストを実施します。
この際、CloudFormationの関数(Ref::Fn::)が必要になってくるのでCFnに慣れていない方はドキュメントを参考にしながら実施するとよいと思います。

Fine-Grained Assertions
import '@aws-cdk/assert/jest'
import { SynthUtils } from '@aws-cdk/assert'
import { Stack, App } from '@aws-cdk/core'  

import { AdventCalendar2019Stack } from '../lib/advent-calendar-2019-stack'

test('IAM users fine', () => {
  const app = new App();
  const stack = new AdventCalendar2019Stack(app, 'usersFineGrainedTestStack');
  expect(stack).toHaveResource('AWS::IAM::User', {
    UserName: 'demo01',
    Groups: [
      {
        "Ref": "testAdminGroupA356E014"
      }
    ]
  })
})

test('IAM group fine', () => {
  const app = new App();
  const stack = new AdventCalendar2019Stack(app, 'groupFineGrainedTestStack');
  expect(stack).toHaveResource('AWS::IAM::Group', {
    GroupName: 'testAdminGroup',
    "ManagedPolicyArns": [
      {
        "Fn::Join": [
          "",
          [
            "arn:",
            {
              "Ref": "AWS::Partition"
            },
            ":iam::aws:policy/AdministratorAccess"
          ]
        ]
      }
    ]
  })
})

test('IAM policy fine', () => {
  const app = new App();
  const stack = new AdventCalendar2019Stack(app, 'policyFineGrainedTestStack');
  expect(stack).toHaveResource('AWS::IAM::Policy', {
    PolicyName: 'iamPassRoleAccess',
    Groups: [
        // {"Ref": "testAdminGroupA356E014"},
        {"Ref": "testDevGroup93FABFEE"}
    ],
    PolicyDocument: {
      "Statement": [
        {
          "Action": [
              "iam:Get*",
              "iam:List*",
              "iam:PassRole",
            ],
          "Effect": "Allow",
          "Resource": "*",
        },
      ],
      "Version": "2012-10-17",
    }
  })
})

こちらは書き方などに問題がなければ、エラー無く返ってきます。

success
$ npm run test

> poc-actions@0.1.0 test /Users/user/Documents/GitHub/advent-calendar-2019
> jest

 PASS  test/advent-calendar-2019.test.ts
   IAM users fine (100ms)
   IAM group fine (42ms)
   IAM policy fine (37ms)
   IAM Resource Stack (39ms)

Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   1 passed, 1 total
Time:        3.884s
Ran all test suites.

エラーがある場合はでわかるのでエラー発見が簡単です。

$ npm run test

> poc-actions@0.1.0 test /Users/user/Documents/GitHub/advent-calendar-2019
> jest

 FAIL  test/advent-calendar-2019.test.ts
   IAM users fine (107ms)
   IAM group fine (55ms)
   IAM policy fine (40ms)
   IAM Resource Stack (38ms)

   IAM group fine

    None of 2 resources matches resource 'AWS::IAM::Group' with properties {
      "GroupName": "testAdminGroup",
      "ManagedPolicyArns": [
        {
          "Fn::Join": [
            "arn:",
            {
              "Ref": "AWS::Partition"
            },
            ":iam::aws:policy/AdministratorAccess"
          ]
        }
      ]
    }.
    - Field ManagedPolicyArns mismatch in:
        {
          "Type": "AWS::IAM::Group",
          "Properties": {
            "GroupName": "testAdminGroup",
            "ManagedPolicyArns": [
              {
                "Fn::Join": [
                  "",
                  [
                    "arn:",
                    {
                      "Ref": "AWS::Partition"
                    },
                    ":iam::aws:policy/AdministratorAccess"
                  ]
                ]
              }
            ]
          }
        }
    - Field GroupName mismatch,Field ManagedPolicyArns mismatch in:
        {
          "Type": "AWS::IAM::Group",
          "Properties": {
            "GroupName": "testDevGroup",
            "ManagedPolicyArns": [
              {
                "Fn::Join": [
                  "",
                  [
                    "arn:",
                    {
                      "Ref": "AWS::Partition"
                    },
                    ":iam::aws:policy/PowerUserAccess"
                  ]
                ]
              }
            ]
          }
        }

      21 |   const app = new App();
      22 |   const stack = new AdventCalendar2019Stack(app, 'groupFineGrainedTestStack');
    > 23 |   expect(stack).toHaveResource('AWS::IAM::Group', {
         |                 ^
      24 |     GroupName: 'testAdminGroup',
      25 |     ManagedPolicyArns: [
      26 |       {

      at Object.<anonymous> (test/advent-calendar-2019.test.ts:23:17)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 3 passed, 4 total
Snapshots:   1 passed, 1 total
Time:        2.885s, estimated 4s
Ran all test suites.

最後はValidation Testsが必要だとなっています。
プロパティの値が正常値 / 異常値でちゃんと検知できるかをテストする必要があります。
今回私は無しでやってしまったのですが、次回以降は実施してみます。
参考

// lib/sample.ts
export interface DeadLetterQueueProps {
    /**
     * The amount of days messages will live in the dead letter queue
     *
     * Cannot exceed 14 days.
     *
     * @default 14
     */
    retentionDays?: number;
}

export class DeadLetterQueue extends sqs.Queue {
  public readonly messagesInQueueAlarm: cloudwatch.IAlarm;

  constructor(scope: Construct, id: string, props: DeadLetterQueueProps = {}) {
    if (props.retentionDays !== undefined && props.retentionDays > 14) {
      throw new Error('retentionDays may not exceed 14 days');
    }

    super(scope, id, {
        // Given retention period or maximum
        retentionPeriod: Duration.days(props.retentionDays || 14)
    });
    // ...
  }
}

============================================================================

// test/sample.test.ts
test('retention period can be configured', () => {
  const stack = new Stack();

  new dlq.DeadLetterQueue(stack, 'DLQ', {
    retentionDays: 7
  });

  expect(stack).toHaveResource('AWS::SQS::Queue', {
    MessageRetentionPeriod: 604800
  });
});

test('configurable retention period cannot exceed 14 days', () => {
  const stack = new Stack();

  expect(() => {
    new dlq.DeadLetterQueue(stack, 'DLQ', {
      retentionDays: 15
    });
  }).toThrowError(/retentionDays may not exceed 14 days/);
});

AWS CDKはCloudFormationが裏で動いているため、エラーがあった際にはロールバックされて既存リソースまで巻き込む可能性があるので十分なテストが必要になります。

CI / CD

CI/CDを回していく上でどのツールを利用するかを検討しました。
以下が当初候補に上がりました。

  • Jenkins
    • 筆者がJenkinsおじさんなため
  • CircleCI
    • アプリケーションのデプロイやDockerイメージの更新など既に多くが実運用に乗っている実績あり
  • CodeBuild
    • 実績は少ないが、冒頭のTerraformでのアカウント管理などは既に運用している
  • Github Actions
    • 実績がなく、利用事例も少なく社内に知見者もいない

と言った感じの状況でした。
まあタイトルから既にネタバレではあるのですが、Github Actionsで運用してみることにしました。
理由は下記からです。

  • Buildが早い
  • 設定の自由度が高い
  • 有り物が多い
  • 新しそうだから←

一番は実際に運用してみて不便あれば戻せば良いか、くらいの温度感で始められたのが決め手でした。

Jenkins

Jenkinsで運用する場合どのように運用するか。
基本的に常時必要なものではないので、Dockerイメージを利用してFargateに乗っけるといった運用を考えていました。
またCDKのpluginがJenkinsにはないので、いわゆるDooDやDinDで実施するパターンとして

  1. nodeイメージを引っ張ってきてnpm install aws-cdk -gする
  2. あらかじめnpm install aws-cdk -gしたイメージを登録しておいて呼び出す

の2パターンを想定していました。
1の場合ビルド時間がかかってしまうため避けたい気持ちがありました。
また、2の場合CDKのCI/CD以外にイメージのCI/CDも考慮しないといけないためこちらもイマイチな感じがしました。
個人的にはやるとすれば2ではあるのですが、今回はどちらもあまりピンとこなかったため検証に至りませんでした。

CircleCI

CircleCIにはOrbsという強力な機能があります。
公式や多くの有志の方々が提供している便利なイメージ群です。
Orbs とは
Explore Orbs

しかし残念ながら現時点でAWS CDKのOrbsは確認できませんでしたので、

  1. 自前でOrbsを作る
  2. パイプライン内でnpm install aws-cdk -gする

の2パターンでの運用になると考えました。
1に関しては着想から実装まで期間が短く、他のプロダクトとの兼ね合いもあったため断念しました。
2に関しては現在運用しているものの中でも同様にインストールなどを実施している運用実績があったため、まあ無くはないなといった感覚でした。
なのでとりあえず2で検証を進めてみました。
結論から言うと遅くはないけど、、、良くもない、、、と言った感じで微妙でした。

comfig.yml
version: 2.1

orbs:
  slack: slack: circleci/slack@3.2.0

executors:

  node:
    docker:
      - image: circleci/node:12.8.1-stretch

workflows:

  cdk-test:
    jobs:
      - initialize:
          filters:
            branches:
              only:
                - develop
                - master

      - lint-check:
          requires:
            - initialize
          filters:
            branches:
              only:
                - develop
                - master

      - document-test:
          requires:
            - lint-check
          filters:
            branches:
              only:
                - develop
                - master

      - diff-check:
          requires:
            - document-test
          filters:
            branches:
              only:
                - develop
                - master

      - send-approval-link:
          requires:
            - diff-check
          filters:
            branches:
              only:
                - master

      - approval:
          type: approval
          requires:
            - diff-check

      - cdk-deploy:
          requires:
            - approval
          filters:
            branches:
              only:
                - master

      - cdk-synth:
          requires:
            - cdk-deploy
          filters:
            branches:
              only:
                - master

jobs:

  initialize:
    executor: node
    steps:
      - checkout
      - run: npm install tslint typescript aws-cdk -g
      - run: npm ci
      - save_cache:
          key: {{ .Branch }}-{{ checksum "package.json" }}
          paths:
            - node_modules

  lint-check:
    executor: node
    steps:
      - checkout:
      - restore_cache:
          key: {{ .Branch }}-{{ checksum "package.json" }}
      - run: tslint './lib/*.ts'
      - save_cache:
          key: {{ .Branch }}-{{ checksum "package.json" }}
          paths:
            - node_modules

  document-test:
    executor: node
    steps:
      - checkout:
      - restore_cache:
          key: {{ .Branch }}-{{ checksum "package.json" }}
      - run: npm run build
      - run: npm run test
      - save_cache:
          key: {{ .Branch }}-{{ checksum "package.json" }}
          paths:
            - node_modules

  diff-check:
    executor: node
    steps:
      - checkout:
      - restore_cache:
          key: {{ .Branch }}-{{ checksum "package.json" }}
      - run: cdk diff AdventCalendar2019Stack
      - save_cache:
          key: {{ .Branch }}-{{ checksum "package.json" }}
          paths:
            - node_modules

  send-approval-link:
    executor: node
    steps:
      - slack/approval:
          message: "cdk diff has done"
          mentions: "here"
          color: "#3dc105"

  cdk-deploy:
    executor: node
    steps:
      - checkout:
      - restore_cache:
          key: {{ .Branch }}-{{ checksum "package.json" }}
      - run: cdk deploy AdventCalendar2019Stack
      - save_cache:
          key: {{ .Branch }}-{{ checksum "package.json" }}
          paths:
            - node_modules

  cdk-synth:
    executor: node
    steps:
      - checkout:
      - restore_cache:
          key: {{ .Branch }}-{{ checksum "package.json" }}
      - run: cdk synth AdventCalendar2019Stack
      - save_cache:
          key: {{ .Branch }}-{{ checksum "package.json" }}
          paths:
            - node_modules

CodeBuild

CodeBuildはイメージの利用方法として、

  1. あらかじめ用意してあるものから持ってきてごにょごにょするか
  2. Docker Hubからpullして利用する
  3. ECRに登録しておいてpullして利用する

の3パターンになります。
2に関してはDocker Hubを調べたのですが、AWS CDKのバージョンが古いものしかなくそれを元にカスタムするのも1とあまり変わりがないためやめました。

docker search cdk
$ docker search cdk
NAME                                    DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
cdkbot/hostpath-provisioner-amd64                                                       0                                       
cdkbot/addon-resizer-amd64                                                              0                                       
cdkbot/hostpath-provisioner                                                             0                                       
cdkbot/registry-amd64                                                                   0                                       
calvinhartwell/cdk-cats                 Small docker image for cdk-cats                 0                                       
cdkbot/nginx-ingress-controller-s390x                                                   0                                       
cdkbot/microbot-amd64                                                                   0                                       
cdkbot/hostpath-provisioner-arm64                                                       0                                       
amarkwalder/cdk-ntp                     Docker NTP image used for time synchronizati…   0                                       [OK]
amarkwalder/cdk-java-jre                ti&m channel suite - CDK Java (JRE) Base Ima…   0                                       [OK]
cdkbot/node-arm64                                                                       0                                       
amarkwalder/cdk-java-jdk                ti&m channel suite - CDK Java (JDK) Base Ima…   0                                       [OK]
ventx/cdk-k8s-helm-image                A Docker Image containing the AWS CDK, Kubec…   0                                       
laozhuforever/cdkey-promotion                                                           0                                       
laozhuforever/cdkey-mall                                                                0                                       
laozhuforever/cdkey-product                                                             0                                       
amarkwalder/cdk-tomcat                  ti&m channel suite - CDK Apache Tomcat Base …   0                                       [OK]
cdkbot/addon-resizer-arm64                                                              0                                       
cdkbot/registry-arm64                                                                   0                                       
cdkbot/microbot-arm64                                                                   0                                       
amarkwalder/cdk-nginx                   ti&m channel suite - CDK NGINX Base Image       0                                       [OK]
laozhuforever/cdkey-stockapi                                                            0                                       
laozhuforever/cdkey-erp                                                                 0                                       
kmrudolphm/cdk_ci_cd                                                                    0                                       
laozhuforever/cdkey-searchapi                                                           0                                       

3に関してはJenkinsの理由と同様、現時点ではイメージを管理する別のCI/CD構築する余裕がなかったので今回はやめました。
そのため、CIrcleCI同様パイプライン内でセットアップして実施することにしました。
結論から言うと今回は無しだと思いました。
必要とするリソースの準備が他に比べ多いことと、ビルド時間がやはり突出したものではなかったためです。

Github Actions

タイトルでもわかる通り結果的にGithub Actionsで一旦運用してみることにしました。
一番大きかったのは特に難しいセットアップが不要でPR時点でのテストやマージ即ち実行が簡単に実現できたことです。
正直ドキュメント読んですぐに設定できるレベルで簡単でした。また豊富なトリガーがあるため今後も様々なシーンで活用できる気がしています。

Github Actionsの設定

Github Actionsの設定ファイルですが、ルートディレクトリ直下に.github/workflowを作成しその配下にYAML形式でおきます。
元々はhashicorp社のHCLという言語が利用されていたのですが、2019年9月末ごろから完全にその記述ができなくなりました。
なので、調べていると古い記法にぶつかることはままありますのでご注意ください。
※内部的にはそのままHCLを利用して動いているらしい。

起動トリガー

上述したとおり起動トリガーの種類は非常に多いです。
https://help.github.com/ja/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows

今回はmasterブランチへのpull_request / pushをトリガーとしてテストとデプロイが走るように下記のような.github/workflows/test.yml.github/workflows/deploy.ymlを用意しました。

test.yml
name: check cdk diff

on:
  pull_request:
    branches:
      - master

jobs:
  aws_cdk_diff:
    runs-on: ubuntu-18.04
    steps:
      - name: Checkout
        uses: actions/checkout@v1

      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: '12.x'

      - name: Setup TypeScript
        run: npm install tslint typescript -g

      - name: Lint check with tslint
        run: tslint './lib/*.ts'

      - name: Setup dependencies
        run: |
          npm ci
          npm run build

      # https://docs.aws.amazon.com/cdk/latest/guide/testing.html
      - name: Unit tests
        run: |
          npm run build
          npm run test

      - name: cdk diff
        uses: youyo/aws-cdk-github-actions@master
        with:
          cdk_subcommand: 'diff'
          cdk_stack: 'AdventCalendar2019Stack'
          actions_comment: false
          cdk_version: '1.17.1'
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: 'us-east-1'

      - name: error notification to Slack
        if: failure()
        uses: bryan-nice/slack-notification@master
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
          SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }}
          SLACK_COLOR: '#cb2431'
          SLACK_ICON: 'https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png?size=48'
          SLACK_USERNAME: Github Actions
          SLACK_TITLE: PR Test ${{ github.head_ref }}
          SLACK_MESSAGE: 'PR test has been returning error!'

      - name: passed notification to Slack
        if: success()
        uses: bryan-nice/slack-notification@master
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
          SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }}
          SLACK_COLOR: '#3dc105'
          SLACK_ICON: 'https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png?size=48'
          SLACK_USERNAME: Github Actions
          SLACK_TITLE: PR Test ${{ github.head_ref }}
          SLACK_MESSAGE: Test result has been returning SUCCESS!
deploy.yml
name: deploy resource

on:
  push:
    branches:
      - master

jobs:
  aws_cdk:
    runs-on: ubuntu-18.04
    steps:
      - name: Checkout
        uses: actions/checkout@v1

      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: '12.x'

      - name: Setup TypeScript
        run: npm install typescript -g

      - name: Setup dependencies
        run: |
          npm ci
          npm run build

      - name: cdk deploy
        uses: youyo/aws-cdk-github-actions@master
        with:
          cdk_subcommand: 'deploy'
          cdk_stack: 'AdventCalendar2019Stack'
          cdk_version: '1.17.1'
          actions_comment: false
          args: '--require-approval never'
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: 'us-east-1'

      - name: error notification to Slack
        if: failure()
        uses: bryan-nice/slack-notification@master
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
          SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }}
          SLACK_ICON: github
          SLACK_COLOR: '#cb2431'
          SLACK_USERNAME: Github Actions
          SLACK_TITLE: DEPLOYMENT ERROR
          SLACK_MESSAGE: ${{ github.head_ref }} deployment has been fail!

      - name: cdk synth
        uses: youyo/aws-cdk-github-actions@master
        with:
          cdk_subcommand: 'synth'
          cdk_version: '1.17.1'
          cdk_stack: 'AdventCalendar2019Stack'
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: 'us-east-1'

      - name: passed notification to Slack
        if: success()
        uses: bryan-nice/slack-notification@master
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
          SLACK_CHANNEL: ${{ secrets.SLACK_CHANNEL }}
          SLACK_ICON: github
          SLACK_COLOR: '#3dc105'
          SLACK_USERNAME: Github Actions
          SLACK_TITLE: DEPLOYMENT SUCCESS
          SLACK_MESSAGE: ${{ github.head_ref }} deployment has been success!

シークレット設定

対象リポジトリのSetteingから左ペインのSecretsを選んで追加していくだけです。
sc_01.png

テスト(pull_requestトリガー)

早速テストを実施してみましょう。
PRを上げるとすぐに走りました。
sc_02.png
他のCI/CDツール同様途中経過も確認できます。
sc_03.png
無事にテスト通過してSlack通知も確認できました。

sc_05.png
sc_04.png

デプロイ(pushトリガー)

ではテストも通ったので、マージしてみます。
すぐにデプロイがはじまりました。
sc_06.png
無事に成功しました。
sc_07.png
(アイコンの設定忘れてた。。)
sc_08.png

4分くらいで終わりました。
もっと使い慣れてくればキャッシュだったり、なんだりでもう少し早くなりそうです。
sc_09.png

残課題

正直突貫で仕上げたので、粗が目立つと思います。
TypeScript自体AWS CDKを通して初めて触りましたので、指摘等あればぜひお願いします。
主に下記課題が直近で修正が必要なものだと感じています。

  • マルチアカウントへの対応
  • cdk.jsonの活用
  • テストスクリプトの自動生成
  • エラー発生時のロールバック

など課題はまだまだあります。
また既存リソースをどう扱っていくかなど、社内で横展開をするに当たって色々と考慮が必要そうです。
引き続き新規リソースから扱っていき、色々な知見を貯めていきたいです。

個人的にはCircleCIよりも細かいコントロールが効くので好きです。
あとはちょっと嬉しかったのはPRの*.ymlを読み込んでくれるところです。
パイプラインの修正がマージしなくても反映されるので、これは地味に嬉しかったです。

なぜInfrastructure as Codeをやるか?

ここまでAWS CDKを利用したCI/CDについて色々と書いてきましたが、みなさまはふと上記のようなことを思うことはありませんか?
Infrastructure as Codeを推進するにあたって色々と意見を頂戴することがあります。
下記はほんの一部だと思いますが、私なりの見解をお伝えできればと思います。

構築時の心理的安全性

  • なんでインフラってコード化するの?
  • GUIの方が楽じゃない?
  • 学習コストの方が結果的に高くつくのでは?

こういった声を聞くこともあります。
正直に申し上げると事実だと思います。
タイトルのユーザー管理に関しても、複数ユーザーをGUIで作成するのは正直大した手間ではありませんし、EC2インスタンス1台立ち上げることだってほんの数クリックでできてしまいます。
しかし、それと全く同じことを100回実施したらどうでしょう?
人間なのでエラーはあって然るべきだと私は思います。
そのたった一度の「あ、やべ…。」が取り返しつかないかもしれません。
そうなってしまった時に戻せるよう、いわゆる冪等性を実現して心理的安全性を確保しましょうよ、というお話しです。

見知らぬリソースとの対峙

自分自身の参画しているサービスでも下記のような体験をしたことがありませんか?

  • このリソースは果たして使っているんだろうか…?
  • なんでこのポート空いているんだろうか…?
  • このIPはどこの子…?

色々な背景があってそのような状況が発生しているのは重々承知です。
それを証跡として追えないことがつらみなのです。(監査証跡から追う…みたいな荒業は除いて)
たとえドキュメントがなかったとしてもコミットメッセージひとつあるだけで意図が伝わります。
いる・いらないや、必要・不要を明確にできるようにしましょうよ、というお話しです。

インフラエンジニアだってナウくありたい!

ここは完全に個人の意見で少々エモくなってしまうのですが、インフラエンジニアだって未来に生きたいんです!

  • パラメータシートと突合しながらアーキテクチャー構築
  • 過剰な指差し確認
  • レビューといったらせいぜい初期設定スクリプト

そんな時代に取り残された方法はやりたくないのです。
まだまだこの業界で奮闘していきたいんです。
より便利に、より楽にを実現したい気持ちはインフラエンジニアも一緒だ、というお話しです。

まとめ

AWS CDKをCI/CDで回すのは可能ですが、それなりに自分で準備する必要があります。
しかし、アップデート頻度やコミュニティーの活性度合いをみれば今のうちから取り組んでおくべきものだと思います。
何より好みの言語で書けることは幸せですし、インフラへの理解が深まりよりよいサービスを生み出すのに役立つと思います。
また様々なCI/CDツールが存在する中で、我々が慣れ親しんだGithubがCI/CDツールを提供したのであれば些細なことからでも便利にできると思います。
小さなものからでよいので少し試してみてはいかがでしょうか?

あとがき

Advent Calendar 6日目でした。
明日7日目は rokumura さんによる記事を乞うご期待!
※ちなみに彼はAdvent Calendarのことを考え過ぎて 夜しか 眠れなかったらしいです 笑
https://adventar.org/calendars/4548

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away