2020年11月12日木曜日
AWS ECRのライフサイクルポリシーを設定して自動クリーンアップ
今回は軽く、AWS ECR (Elastic Container Registry) の小ネタです。
AWSでコンテナを使って何かする場合ECRにコンテナイメージを保存しておくことが多いと思いますが、日ごろからイメージをよくビルド&プッシュしているとどんどんイメージが増えていきますね。
イメージサイズがものすごく大きいとかでなければそれほど料金を食うわけでもないのですが、まず使わない古いイメージをずっと保存しておくこともないよなーと思ったのでそのあたりを設定してみました。
ECRではライフサイクルポリシーという機能があり、いろいろな条件を定めてイメージを自動クリーンアップすることができるんですね。
Amazon ECRのライフサイクルポリシーでコンテナイメージのクリーンアップ | Amazon Web Services ブログ
設定できる条件は
- タグ
- イメージ数
- プッシュされてからの経過日数
今回は、「タグが latest
ではない」かつ「プッシュされて90日以上」の条件に合うイメージを削除するポリシーを設定してみました。
AWSコンソールで設定する場合は、ECRのコンソールでリポジトリを選択して左のメニューの Lifecycle Policy
をクリックすると設定画面があります。
「テストルールの編集」ボタンを押すとポリシーを適用した結果が確認できる(実際に保存されているイメージには何もしない)テスト用ページに移動するので、まずはそこで試してみるのがいいですね。
さて、ライフサイクルポリシーは「ルール」を優先順位つきで好きな数設定することで構成します。ルール作成画面はこうなっていて、一致条件は「イメージをプッシュしてから」「次の数値を超えるイメージ数」が選べます。
今回作りたい条件は「タグが latest
ではない」かつ「プッシュされて90日以上」ということで、
- タグ付け済("latest")、次の数値を超えるイメージ数(1)
- すべて、イメージをプッシュしてから(90日)
という2つのルールを作ってこのようになります。
ルールを作ればあとは自動でそれに従ってイメージが毎日掃除されるので、とても楽ですね😊
ここまではコンソールで操作してきましたが、リポジトリが多い場合やコンソール面倒だと言う場合はもちろんCLIが使えます。
put-lifecycle-policy — AWS CLI 2.1.0 Command Reference
まずは先ほどのルールをJSON化して policy.json
とでもファイルを作ります。
{
"rules": [
{
"rulePriority": 1,
"description": "keep latest image",
"selection": {
"tagStatus": "tagged",
"tagPrefixList": ["latest"],
"countType": "imageCountMoreThan",
"countNumber": 1
},
"action": {
"type": "expire"
}
},
{
"rulePriority": 2,
"description": "expire images older than 90 days",
"selection": {
"tagStatus": "any",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 90
},
"action": {
"type": "expire"
}
}
]
}
今回は全リポジトリに適用したかったのでシェルスクリプトを作りました。
#!/bin/sh
# ECRリポジトリにライフサイクルポリシーを設定する
for REPO in $(aws ecr describe-repositories --query "repositories[*].{name:repositoryName}" --output text)
do
echo "repository: $REPO"
aws ecr put-lifecycle-policy --lifecycle-policy-text "file://policy.json" --repository-name $REPO
done
これで一括適用ができますね💪
比較的簡単に設定ができるので、「うわっ…ECRのイメージ、多すぎ…?」と気づいた際には設定してみてはいかがでしょうか。
ちなみにterraformだとaws_ecr_lifecycle_policy
リソースで設定できるようです。
aws_ecr_lifecycle_policy | Resources | hashicorp/aws | Terraform Registry
2020年9月4日金曜日
GCPの料金アラートをSlackに出してみる
AWSやGCPの料金、たまに気になりますよね。
そういえば今月どれくらい料金食ってるんだ?と思ったとき、もちろん各サービスのコンソールで課金管理のページを開けば確認できますね。
が、日常的にはお知らせが届くほうが簡単ですむよね、ということでslackに通知をさせてみました。
AWSは簡単だったのですがGCPはちょっと手間だったのでまとめておきたいと思います。
まあエンジニアとしては課金の具合を気にせず開発に集中できれば一番いいのですが、現実には会社の予算とかいろいろありますし私は課金周りの把握をする役回りもあるので、これはやっておきたいところです。
2020年7月10日金曜日
GCPでGitHub Actionsのセルフホストランナーを作った
https://github.co.jp/pricing.html
Teamプランは1ユーザーあたり4ドルになったのですが、さりげなく同時にGitHub Actionsの利用時間(organizationのプライベートリポジトリ全体で)が月3000分まで、と減ってしまいました 😥 (前は10000分)
以前の記事(GitHub ActionsとCypressでサイト監視してみる|sitateru tech blog)で紹介したようなCypressを使った監視をActionsで動かしていたので、月3000分は少ないなーと思って料金を見てみたところ・・・
https://github.co.jp/features/actions#pricing
セルフホストはタダなのか!しかしセルフホストって何だ?🤔
ということでやってみました。
セルフホストランナーについて - GitHub Docs
セルフホストランナーというのは、GitHub Actionsを実行するマシンのことです。自分の管理下にあるサーバーやPCでActionsが実行できちゃいます。前述のように、セルフホストランナーで実行する分にはGitHubに対して課金は発生しません。
・・・おっ、ということはGCPの永久無料枠のCompute Engineインスタンスを使えばタダで回し放題やんけ!( ^ω^)
Google Cloud の無料枠 | Google Cloud Platform の無料枠
と思ったのですが、先に結論を書きます、コンテナ上でCypressテストを実行するには無料枠の
f1-micro
では処理能力的に無理でした。もっと軽い処理なら足りるのかもしれませんが・・・e2-micro
ならなんとか動いたのでこれで良しとしましょう。さて、セルフホストランナーをクラウド上のLinuxインスタンスで構築する手順に行きたいと思います。
セルフホストランナーの追加 - GitHub Docs
organizationで使えるランナーを追加するには、 まず
https://github.com/organizations/{organization名}/settings/actions
で Add Runner
をクリック。するとインストールするコマンドが出てくるので、それを実行すればOKです。簡単ですね。インストールコマンドを実行するとこんな感じです。AAがいいですね。
途中でこのランナーにつける名前やラベルを聞かれますが、とりあえずは初期値でも大丈夫そうです。ランナーを複数登録する想定ならわかりやすいものをつけるとよさそうですね。
ちなみに、DockerイメージをAction上で使う場合はランナーマシンにDockerをインストールしておく必要があります。
インストール後、ランナーを起動するにはコマンドで起動する方法とサービスとして動かす方法があります。
コマンドの場合はランナーのパッケージ内の
run.sh
を実行するだけ。サービスの場合は下ドキュメントにありますが
svc.sh
でサービスを実行するactions-runner/bin/actions.runner.service.template
ファイルを編集して/etc/systemd/system/
あたりにうまいこと設置して systemctl でコントロールする
今回は後者の方法で、
/etc/systemd/system/actions.service
を設置しました。[Unit]
Description=GitHub Actions Runner
After=network.target
[Service]
ExecStart=/usr/local/actions-runner/runsvc.sh
User=dev
WorkingDirectory=/usr/local/actions-runner
KillMode=process
KillSignal=SIGTERM
TimeoutStopSec=5min
[Install]
WantedBy=multi-user.target
インストールと起動がうまくいけば、このようにGitHubのページ上にも出てきます。
Actionsをセルフホストランナーで実行するには、ワークフローのyamlで以下のように書きます。
runs-on: [self-hosted, linux, x64]
ワークフローでのセルフホストランナーの利用 - GitHub Docs
このセルフホストランナー、今回やってみたようにサーバ上で常時稼働してもいいですが、ちょっとactionsを動かしていろいろデバッグとかしたいけど使用量の枠を食うのがやだなあ・・・というときにも便利そうですね。開発用マシンでその時だけランナーを動かすということもできるので。
なおセルフホストランナーですが、セキュリティ上の理由でパブリックリポジトリでは利用しないようにと公式ドキュメントに書かれています。
パブリックリポジトリではActionsが無料で使えるようなので、そちらでやればよさそうですね。
2020年4月30日木曜日
Flood Element でブラウザベースの負荷テスト
少し前に負荷テストツールのFlood Elementというものを使ってみたので、紹介しようと思います。
まずはCLIをインストールします。npmかHomeBrewでインストールできます。
$ npm install -g @flood/element-cli
$ brew install flood-io/taps/element
CLIを使ってプロジェクトを作成、テストファイルを追加します。
$ element init
$ element generate some-test.ts
テストファイルにはブラウザ上での操作を書いていきます。elementはpuppeteerを利用しているので、書き方はかなりpuppeteerに近いですね。
googleのトップページにアクセスして"flood element"と検索するならこのようなコードになります。
import { step, TestSettings, By, Until } from '@flood/element'
export const settings: TestSettings = {
// userAgent: 'flood-chrome-test',
loopCount: 10,
screenshotOnFailure: true,
clearCache: true,
clearCookies: true,
actionDelay: 1,
stepDelay: 1,
waitTimeout: 180
}
export default () => {
step('Sample', async browser => {
await browser.visit('https://google.com')
await browser.click(By.css('form input[type="text"]'))
await browser.sendKeys('flood element', Key.ENTER)
await browser.wait(Until.elementIsVisible(By.css('div#search')))
await browser.takeScreenshot()
})
}
$ element run some-test.ts
でOKです。
スクリーンショット等の実行結果は、
tmp/element-results/<テストファイル名>/<実行タイムスタンプ>
以下に記録されます。ファイル前半で定義しているのは動作時の設定なのですが、そのあたり他詳しいことは公式ドキュメントを参照してください。
さて、テストファイルができたら負荷テストを実行してみます。負荷テストSaaSのFloodの出番です。
Scalable software starts here - Flood
elementはFloodがテストを書くために作ったライブラリなんですね。
ログインしたら"Stream"タブの"CREATE STREAM"をクリック、先ほどのテストファイルをアップロードします。
"CONFIGURE LAUNCH"をクリックすると実行設定に移ります。
テストファイルを実行するリージョンをまず選びましょう。
"Users per Region"がリージョンごとの並列実行数で、"Duration"は実行時間の上限のようです。(試した限りでは、設定したDurationより早くテストファイルの実行が終わったらその時点でテスト完了になりました)
あとは"LAUNCH TES"を押せば負荷テストが始まります。
結果はこのようにグラフで見られます。
ちなみにFloodの料金ですが、VUHという単位の使用量によって決まります。
Flood Load Testing Pricing
VUH = Virtual User Hourであり、 (実行時に設定したUser数) x (テストを実行した時間(15分単位)) というものです。例えば200ユーザーで30分間(=0.5時間)動かしたら 200x0.5=100VUH 、という要領ですね。
ひと月あたり500VUHまでは無料で、それ以降は500VUHごとに$22程度かかる従量課金制になっています。うっかり使いすぎないよう気をつけないといけないですね。
実際のブラウザアクセスを想定した負荷テストって難しそうだなと思っていたのですが、これで無料でもちょっとした実験はすることができました。
好きなサイトに大量のアクセスをかけることができてしまうので悪用厳禁なツールですが、簡単な負荷テストやってみたいという際には試してみてはいかがでしょうか。
2020年3月19日木曜日
Cypress on GitHub Actions のいろいろなテクニック
先日CypressとGitHubを使う記事を書いたのですが、今回はもう少し突っ込んだ話として、ActionsでCypressを使う上で引っかかったところなどをまとめてみたいと思います。
Cypressのテストが止まったままになる??
Dockerコンテナ上でCypress(ブラウザはChrome)を実行していると途中で止まってしまうことがあります。調べてみたところChromeの共有メモリが足りなくなっているようです。
そんなときは cypress/plugin/index.js にこの記述で直りました。
module.exports = (on, config) => {
on('before:browser:launch', (browser, launchOptions) => {
if (browser.name === 'chrome') {
launchOptions.args.push('--disable-dev-shm-usage')
}
return launchOptions
})
}
--disable-dev-shm-usage
オプションをつけているのですが、これによって共有メモリファイルを /tmp
以下に配置するので充分な容量が確保できるということだそうです。:hover
CypressにはCSSの:hover状態を発生させる機能が無いようです🙄hover | Cypress Documentation
If cy.hover() is used, an error will display and redirect you to this page.Cypressでは非表示状態の要素はクリックなどの操作ができないのですが、じゃあ:hover時に表示される要素をクリックしたいときはどうするの?という話になりますね。
上記のドキュメントでは
cy.get('.hidden').invoke('show').click()
cy.get('.hidden').click({ force: true })
ちょっと試したところ
invoke('show')
はうまくいかなかったのですが click({ force: true })
はできたので、これでいいかなということにしました。XPath
ページに表示されているテキストを検索して要素を選択したいときなどにはXPathを使いたくなりますが、その場合はプラグインを追加するという方法になります。package.jsonに cypress-xpath を追加し、cypress/support/index.js に
require('cypress-xpath')
cy.xpath('//div[contains(text(), "ここをクリック")]')
のように要素を取得できます。ESLint
ESLintを使っている場合、Cypressのコードはそのままだと引っかかってエラーが出てしまいます。そんなときはlintのcypressプラグインを使いましょう。
eslint-plugin-cypress を追加し、cypress/.eslintrc.jsonを作成します。
最低限の設定をするならこんなもんです。
{
"plugins": [
"cypress"
],
"env": {
"cypress/globals": true
},
"extends": [
"plugin:cypress/recommended"
]
}
Cypressだけインストールする
これはCypressではなくnpmのテクニックなのですが、Actions内でCypressを実行するだけなら、Cypress以外のnpmパッケージをインストールする必要はないですよね。package.jsonやpackage-lock.jsonの中身を無視してCypressだけインストールするならこうする手もあります。
$ npm install cypress@4.1.0 --no-save --no-package-lock
npm-install | npm Documentation
本当はnode_modulesをキャッシュしたいんですが、
依存関係をキャッシュしてワークフローのスピードを上げる - GitHub ヘルプ
によると
pull_requestのclosedイベントの場合を除く、push及びpull_requestイベントで起動されたワークフロー内のキャッシュにのみアクセスできます。ということなので、スケジュール実行だとできないのです・・・
というわけで、CypressとActions関係のさまざまなTipsを書いてみました。
何かの参考になれば幸いです。
2020年2月5日水曜日
GitHub ActionsとCypressでサイト監視してみる
皆さんはGithub Actions使ってますか?
シタテルではCI/CDはCircleCIを使っているのですが、せっかくなのでActionsも何かに使いたいなあと思って、簡単なサイト監視のようなものを作ってみたのでそのことを書いてみます。
監視に使うのは、E2EテストフレームワークのCypressです。
ブラウザの自動化やテスト用に使っている方も多いのではないかと思います。
サクッと動かすなら
- テストコードを書いて
cypress/integration/
ディレクトリに入れておく - npmでcypressをインストール
-
$ npx cypress run
で実行
たとえば https://sitateru.com/ のページが表示されることをチェックするコードをCypressで書くとこのようになります。
describe('Check page', function() {
it('top', function() {
// URLを開く
cy.visit('https://sitateru.com/')
// ページタイトルをチェック
cy.title().should('eq', 'sitateru - シタテル - | その服は、つくれる。')
// ページ内に特定の文字列があることを確認する
cy.contains('かんたん無料登録')
// CSSセレクタを使って、要素が存在することを確認する
cy.get('.top-main-visual').should('exist')
})
})
また、ログイン操作であればこんなふうになります。
describe('Login', function() {
it('login', function() {
cy.visit('https://atelier.sitateru.com/login')
// フォームに入力
cy.get('#email').type(Cypress.env('LOGIN_EMAIL'))
cy.get('#password').type(Cypress.env('LOGIN_PASSWORD'))
// ログインボタンをクリック
cy.get('input[type="submit"]').click()
// 遷移先のURLパスをチェック
cy.location('pathname').should('eq', '/topics')
})
})
パスワードなどの情報は環境変数に入れておくのがいいですね。CYPRESS_SOMEENV
のように CYPRESS_ プレフィックスをつけた環境変数を定義しておくと、コード内で Cypress.env('SOMEENV')
と書いて呼び出すことができます。GitHub Actionsは
.github/workflows/
にやりたいことを書いたyamlファイルを置けば実行されるので簡単ですね。ざっと作ってみたのがこちら。
name: e2e test
on:
# 毎時0分に実行
schedule:
- cron: '0 * * * *'
jobs:
build:
name: monitoring
runs-on: ubuntu-latest
# cypress公式のDockerイメージを使う
container:
image: cypress/browsers:node12.14.0-chrome79-ff71
steps:
- name: checkout
uses: actions/checkout@master
with:
ref: 'master'
- name: install
run: npm install
- name: run test
env:
LANG: "ja_JP.UTF-8"
CYPRESS_LOGIN_EMAIL: ${{ secrets.CYPRESS_LOGIN_EMAIL }}
CYPRESS_LOGIN_PASSWORD: ${{ secrets.CYPRESS_LOGIN_PASSWORD }}
run: npx cypress run --browser chrome
# 失敗したときはスクリーンショットをダウンロードできるように
- name: gather artifact on failure
uses: actions/upload-artifact@v1
if: failure()
with:
name: cypress-screenshots
path: cypress/screenshots
# 結果をslackに通知 (失敗したときはメンション)
- name: post to slack
uses: homoluctus/slatify@master
if: always()
with:
type: ${{ job.status }}
job_name: 'e2e test'
mention: 'here'
mention_if: 'failure'
channel: '#notification-actions'
icon_emoji: ':github:'
url: ${{ secrets.SLACK_WEBHOOK_URL }}
実行時の環境変数は
env:
で定義できます。yaml内に直接書きたくないパスワードなどは、https://github.com/USERNAME/REPOSITORY/settings/secrets で登録しておけば、
${{ secrets.SECRET_NAME }}
で参照することができます。このyamlでは、ソースコードをチェックアウトしてcypressをインストール&実行した後、失敗した場合のスクリーンショットを見られるようにupload-artifact、結果をslackに送るslatifyを使ってみました。
また、Actionsでテストを実行すると、何もしなければブラウザのデフォルト言語が英語になるようです。今回は日本語表示を監視したかったので、環境変数LANGで日本語を設定しています。
というわけで、1時間に一度サイトをチェックすることができるようになりました。
slackに監視OKのメッセージが来るのがちょっとした安心感がありますね。
ここで紹介したコードはだいぶ基本的な部分で現在はもう少しいろいろ入れて運用していますが、さらに高度なレベルに持っていきたいですね💪🏻!
2019年12月19日木曜日
安全にkubectl applyするコマンドを作った
最近 本番環境でやらかしちゃった人 Advent Calendar が話題ですね。 私も楽しく読ませてもらっています。
かくいう私も先日、kubernetes環境で作成中のアプリケーションを間違って別アプリケーション用のnamespaceにデプロイしてしまうという事故をやってしまいました😇
何をどうミスしたかというと、
- 新しく作るnamespace以下にkubectl applyでアプリケーションをデプロイしようとした
- 対象のyamlファイルは別プロジェクトのソースファイルをコピーして作ったものだった
- yaml内に書かれているapply先namespaceが別プロジェクトのnamespaceのままだった
普段kubernetes環境へのapplyは原則CircleCI内で行っているのですが、今回は環境構築中なアプリケーションだったので手動で実行しようとしていたのでした。
実行前に確認しなかった私が悪いのですが、これは何とかして防げるようにしたいなあということで、applyする前にyaml内のnamespaceをチェックしておかしかったら止める、というシェルスクリプトを作りました。
作るにあたっては以下のような前提があります。
- 環境の切り替え管理にkustomizeを使っている
- 各環境のyamlは k8s/overlays/{環境名} ディレクトリに置いて、
kubectl apply -k k8s/overlays/{環境名}
でデプロイ - デプロイ先namespaceは {リポジトリ名}-{環境名} に統一する
#!/bin/sh
# check whether namespace is {repository}-{env} correctly or not
kubectl kustomize $1 > /dev/null 2> /dev/null
if [ $? != 0 ]; then
/bin/echo 'usage: kubeapply [kustomize_dir]'
exit 1
fi
PROCEED=0
REPOSITORY_NAME=$(git rev-parse --show-toplevel | rev | cut -f1 -d'/' | rev)
NAMESPACES=$(kubectl kustomize $1 | grep namespace | sed -E 's/namespace:(.*)/\1/' | tr -d ' ' | sort | uniq)
OVERLAY_DIR=$(echo $1/ | tr -s '/' | rev | cut -f2 -d'/' | rev)
FOUND=()
for NAMESPACE in $NAMESPACES
do
if [ ${NAMESPACE} != ${REPOSITORY_NAME}-${OVERLAY_DIR} ]; then
FOUND+=("${NAMESPACE}")
PROCEED=0
else
PROCEED=1
fi
done
if [ ${PROCEED} = 0 ]; then
/bin/echo "expected namespace is '${REPOSITORY_NAME}-${OVERLAY_DIR}'"
/bin/echo "but, namaspace '${FOUND[@]}' found in kustomize"
/bin/echo -n "proceed apply? (yes/No): "
read ANSWER
if [ ${ANSWER} = 'yes' ]; then
PROCEED=1
fi
fi
if [ ${PROCEED} = 1 ]; then
kubectl apply -k $@
else
echo "stop applying"
fi
これを私はkubeapplyという名前にしてPATHが通っているところに置いてます。
$ kubeapply k8s/overlays/dev
のように実行すればOKです。やっていることは、
- 引数がkustomizeディレクトリとして正しいかチェック
- kustomize結果内のnamespaceを抽出
- 抽出されたnamespaceが{リポジトリ名}-{環境名(=引数のディレクトリ名)}と一致しているか確認
- すべて一致していれば
kubectl apply -k {引数}
を実行 - 一致していないものがあればプロンプトを表示、yesと回答したらapplyを実行