tech.guitarrapc.cóm

Technical updates

CircleCI のOrb をPull Request を通じて学ぶ

エンジニア同士で話していると、CIどうしよう、今何がいいかなぁという話にたびたびなります。

CIサービスは複数ありますが、サーバーサイドビルドでSaaS 型CI なら CircleCI が今のところいいい感じです。(2.1を前提とする)

circleci.com

あるいはGitHub Actions もとてもいい感触です。 現状Beta で push イベント駆動なのでlintやビルドにはいい感じで利用できます。他のTagなどのイベントを利用できるようになるのが楽しみです。

github.com

今回は、CircleCI 2.1 で利用できる Orb についてPR送ることを通して学んでみます。

目次

TL;DR

PRを出したくて仕組みを理解していくと何かと捗るのでいいですよ。 実情は、適切に動かすためにコンセプトと仕様を理解することに徹するだけです。 モチベーションだいじ。

Orb とは

CircleCI 2.1 から利用できる機能で、JobsやCommand に定義していた処理を公開し、それを利用できる機能です。

circleci.com

利用方法はいたって簡単です。

circleci.com

YAML で version: 2.1 を宣言し、orbs で利用するorbを定義、command/jobs/workflow のいずれかでOrbに定義されたcommandやjob を namespace/コマンド のような記述で利用するだけです。 ドキュメントにあるミニマムな例が分かりやすいでしょう。

version: 2.1

orbs:
    hello: circleci/hello-build@0.0.5

workflows:
    "Hello Workflow":
        jobs:
          - hello/hello-build

いざ使うときに気になる「どんな Orbがあるのか、使い方は?」は、Orb Explorer を使うことで探すことができます。

circleci.com

Orbレジストリから検索

Orbを利用するにあたり困らない例と困る例

さて公開されているOrbですが、いい感じで使える一方であと一歩これが足りない、というケースがあります。

例えば、circleci/aws-s3@1.0.6 を見てみましょう。

https://circleci.com/orbs/registry/orb/circleci/aws-s3

このOrbがよく考えられているのが、arguments パラメーターを公開しておりOrbに定義がないパラメーターを受付可能にしていることです。 このパラメーターがあるおかげで、aws cli にOrbが想定していないパラメーターを渡すことができます。

version: 2.1
orbs:
  aws-s3: circleci/aws-s3@1.0.0
jobs:
  build:
    docker:
      - image: 'circleci/python:2.7'
    steps:
      - checkout
      - run: mkdir bucket && echo "lorum ipsum" > bucket/build_asset.txt
      - aws-s3/sync:
          from: bucket
          to: 's3://my-s3-bucket-name/prefix'
          arguments: |
            --acl public-read \
            --cache-control "max-age=86400"
          overwrite: true
      - aws-s3/copy:
          from: bucket/build_asset.txt
          to: 's3://my-s3-bucket-name'
          arguments: '--dryrun'

一方で、circleci/aws-code-deploy@0.0.7 を見るとこの arguments パラメーターがないために、ごにょごにょできません。

version: 2.1
orbs:
  aws-code-deploy: circleci/aws-code-deploy@1.0.0
workflows:
  deploy_application:
    jobs:
      - aws-code-deploy/deploy:
          application-name: myApplication
          deployment-group: myDeploymentGroup
          service-role-arn: myDeploymentGroupRoleARN
          bundle-bucket: myApplicationS3Bucket
          bundle-key: myS3BucketKey

AWS CLI を Circle CI で使うときの注意

aws関連のOrbに arguments を渡す口がなくて困る例として、aws cliの処理を assume role による認証で行いたい場合があります。

docs.aws.amazon.com

assume role を使うことで、認証は1つで、assume 先のrole定義でアクセス先を切り替えることができます。 また、アクセス先の認証も assume role の policy で縛ることができるため、AWS のアクセス制御的にも相性が良くなっています。 assume role を利用するには、そのプロファイル定義と role_arnsource_profile を定義します。

[profile marketingadmin]
role_arn = arn:aws:iam::123456789012:role/marketingadmin
source_profile = user1

あとは、環境変数でAWS_DEFAULT_PROFILE を定義するかAWS_PROFILE で暗黙的に解決させる、あるいはコマンド実行時に --profile プロファイル名 が必要です。

aws s3 cp .... s3://.... --marketingadmin

CircleCIの事情を考えると、circleci の aws 系のOrbは circleci/aws-cli を必ず利用しています。

https://circleci.com/orbs/registry/orb/circleci/aws-cli

この Orbは非常に使い勝手が良く、AWS_ACCESS_KEYAWS_SECRET_ACCESS_KEYAWS_DEFAULT_REGION をCircle CI のProject にあるEnvironment Variable に定義しておくだけで aws configure が行われます。

一方で、このOrbは default profile を使う前提であるため任意の Profile を追加で生成できません。そのためassume role を使うときには別途プロファイルを追加する必要があります。

      - run: 
          command: |
            mkdir -p ~/.aws
            echo "[assume_role]" >> ~/.aws/credentials
            echo "source_profile = default" >> ~/.aws/credentials
            echo "role_arn = << parameters.role_arn >>" >> ~/.aws/credentials

circleci/aws-cli を自分で実行しない場合は、事前に ~/.awsを作ってあげましょう。もしcircleci/aws-cli/installcircleci/aws-cli/configure を自分で実行するなら不要です。 あとはコマンドに Profile を渡せばいいです。 で、前節のcircleci/aws-s3 の場合は、arguments: '--profile assume_role' とOrb実行時に渡せばいいのでok、となります。

余談ですがcircle ci のaws-cli/configureを使うなら、AWS_DEFAULT_PROFILEAWS_PROFILE は使えません。 これらを定義しているとaws-cli/configure でコケます、AWS_DEFAULT_PROFILEaws configureが終わっている前提なのでまぁシカタナイ。

Traceback (most recent call last):
  File "/usr/local/bin/aws", line 27, in <module>
    sys.exit(main())
  File "/usr/local/bin/aws", line 23, in main
    return awscli.clidriver.main()
  File "/usr/local/lib/python2.7/site-packages/awscli/clidriver.py", line 59, in main
    rc = driver.main()
  File "/usr/local/lib/python2.7/site-packages/awscli/clidriver.py", line 193, in main
    command_table = self._get_command_table()
  File "/usr/local/lib/python2.7/site-packages/awscli/clidriver.py", line 102, in _get_command_table
    self._command_table = self._build_command_table()
  File "/usr/local/lib/python2.7/site-packages/awscli/clidriver.py", line 122, in _build_command_table
    command_object=self)
  File "/usr/local/lib/python2.7/site-packages/botocore/session.py", line 671, in emit
    return self._events.emit(event_name, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/botocore/hooks.py", line 356, in emit
    return self._emitter.emit(aliased_event_name, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/botocore/hooks.py", line 228, in emit
    return self._emit(event_name, kwargs)
  File "/usr/local/lib/python2.7/site-packages/botocore/hooks.py", line 211, in _emit
    response = handler(**kwargs)
  File "/usr/local/lib/python2.7/site-packages/awscli/customizations/preview.py", line 69, in mark_as_preview
    service_name=original_command.service_model.service_name,
  File "/usr/local/lib/python2.7/site-packages/awscli/clidriver.py", line 318, in service_model
    return self._get_service_model()
  File "/usr/local/lib/python2.7/site-packages/awscli/clidriver.py", line 335, in _get_service_model
    api_version = self.session.get_config_variable('api_versions').get(
  File "/usr/local/lib/python2.7/site-packages/botocore/session.py", line 233, in get_config_variable
    logical_name)
  File "/usr/local/lib/python2.7/site-packages/botocore/configprovider.py", line 226, in get_config_variable
    return provider.provide()
  File "/usr/local/lib/python2.7/site-packages/botocore/configprovider.py", line 323, in provide
    value = provider.provide()
  File "/usr/local/lib/python2.7/site-packages/botocore/configprovider.py", line 382, in provide
    config = self._session.get_scoped_config()
  File "/usr/local/lib/python2.7/site-packages/botocore/session.py", line 334, in get_scoped_config
    raise ProfileNotFound(profile=profile_name)
botocore.exceptions.ProfileNotFound: The config profile (assume_role) could not be found
Exited with code 1

circleci/aws-code-deploy で arguments 対応をPRする

このあたりを理解した上で、一時対処として自分の Commands で対応していたのですが、公式にあった方がいいので対応しました。

circleci/aws-code-deploy@0.0.9 から arguments が利用できます。

github.com

なお、#140 で対応漏れがあったので #144 をすぐに出しましたが... はずかしい、Orbのテストがないと厳しいですね。

github.com

対応は単純で arguments パラメーターをつけて回るだけです。この時、when が string型に対しては '' をfalse と認識することを利用しています。

Empty strings are treated as a falsy value in evaluation of when clauses, and all other strings are treated as truthy. Using an unquoted string value that YAML interprets as a boolean will result in a type error.

circleci.com

加えて、<<# parameters.xxxxx >> <</parameters.xxxxx>> によってパラメーターが true の場合にのみ埋め込まれるのを使うとこう書けばいいことが分かります。

<<# parameters.arguments >> << parameters.arguments >><</parameters.arguments >>

これで、argument を使って --profile を指定可能になりましたとさ、めでたしめでたし。

version: 2.1
orbs:
  aws-code-deploy: circleci/aws-code-deploy@1.0.0
workflows:
  deploy_application:
    jobs:
      - aws-code-deploy/deploy:
          application-name: myApplication
          deployment-group: myDeploymentGroup
          service-role-arn: myDeploymentGroupRoleARN
          bundle-bucket: myApplicationS3Bucket
          bundle-key: myS3BucketKey
          arguments: '--profile assume_role'