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
これらを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
でリソース作成
以下が実装する上でハマりポイントでした。
- アカウントパスワードポリシーは
JavaScript
のaws-sdk
で定義してexportしてあげる必要がある - テストスクリプトの作成
Stack全体はこんな感じです。
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
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: ! @ # $ % ^ & * ( ) _ + - = [ ] { } | ' 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を実施します。
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に慣れていない方はドキュメントを参考にしながら実施するとよいと思います。
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": "testDevGroup93FABFEE"}
],
PolicyDocument: {
"Statement": [
{
"Action": [
"iam:Get*",
"iam:List*",
"iam:PassRole",
],
"Effect": "Allow",
"Resource": "*",
},
],
"Version": "2012-10-17",
}
})
})
こちらは書き方などに問題がなければ、エラー無く返ってきます。
$ 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で実施するパターンとして
- nodeイメージを引っ張ってきて
npm install aws-cdk -g
する - あらかじめ
npm install aws-cdk -g
したイメージを登録しておいて呼び出す
の2パターンを想定していました。
1の場合ビルド時間がかかってしまうため避けたい気持ちがありました。
また、2の場合CDKのCI/CD以外にイメージのCI/CDも考慮しないといけないためこちらもイマイチな感じがしました。
個人的にはやるとすれば2ではあるのですが、今回はどちらもあまりピンとこなかったため検証に至りませんでした。
CircleCI
CircleCIにはOrbs
という強力な機能があります。
公式や多くの有志の方々が提供している便利なイメージ群です。
Orbs とは
Explore Orbs
しかし残念ながら現時点でAWS CDK
のOrbsは確認できませんでしたので、
- 自前でOrbsを作る
- パイプライン内で
npm install aws-cdk -g
する
の2パターンでの運用になると考えました。
1に関しては着想から実装まで期間が短く、他のプロダクトとの兼ね合いもあったため断念しました。
2に関しては現在運用しているものの中でも同様にインストールなどを実施している運用実績があったため、まあ無くはないなといった感覚でした。
なのでとりあえず2で検証を進めてみました。
結論から言うと遅くはないけど、、、良くもない、、、と言った感じで微妙でした。
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はイメージの利用方法として、
- あらかじめ用意してあるものから持ってきてごにょごにょするか
- Docker Hubからpullして利用する
- ECRに登録しておいてpullして利用する
の3パターンになります。
2に関してはDocker Hubを調べたのですが、AWS CDK
のバージョンが古いものしかなくそれを元にカスタムするのも1とあまり変わりがないためやめました。
$ 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
を用意しました。
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!
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を選んで追加していくだけです。
テスト(pull_requestトリガー)
早速テストを実施してみましょう。
PRを上げるとすぐに走りました。
他のCI/CDツール同様途中経過も確認できます。
無事にテスト通過してSlack通知も確認できました。
デプロイ(pushトリガー)
ではテストも通ったので、マージしてみます。
すぐにデプロイがはじまりました。
無事に成功しました。
(アイコンの設定忘れてた。。)
4分くらいで終わりました。
もっと使い慣れてくればキャッシュだったり、なんだりでもう少し早くなりそうです。
残課題
正直突貫で仕上げたので、粗が目立つと思います。
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ツールを提供したのであれば些細なことからでも便利にできると思います。
小さなものからでよいので少し試してみてはいかがでしょうか?