from 30

30歳からwebエンジニアになったけど、思ったよりも苦しいので、その苦闘の記録をば

CDK Pipelines(Python)を使ってマルチアカウントへデプロイできるパイプラインを作成する

0. パイプラインをつくって試行回数を増やしたい

さて、マルチアカウントでSSOを設定する方法について前回の記事でまとめました。

今度はマルチアカウントにデプロイするパイプラインを作りたいと思います。

数パターンを考えましたが、

  • devアカウント、stgアカウント、prodアカウントそれぞれにパイプラインを作成する
  • manageアカウントにパイプラインを作成して各アカウントへクロスアカウントデプロイ

stgにデプロイして、確認してからApprovalするというパターンを取りたかったので、以下の構成を目指しました。

f:id:kohski:20210214220146p:plain

マネコンぽちぽちで試しにつくっていましたが、CodePipelineのDeployステージのCloudFormationで苦戦して断念。

そこでこちらのAWS公式ブログ を参考にCDK pipelinesを試しました。

aws.amazon.com

ブログはTypescriptですが今回はpythonで試してみます。

サンプルのプロジェクトはこちらにて公開しています。


以下の手順で進めていきます

1. account idを調べる

マネコンからならこちらを確認。

f:id:kohski:20210214222216p:plain

2. secrets managerにgithub-tokenを登録する

githubのアクセストークンの取得方法は以下を参照

docs.github.com

AWSのmanageアカウントのSecretManagerにtokenを登録

f:id:kohski:20210214223158p:plain

3. cdk init

ひとまず今回はHelloAppという名前のプロジェクトということで作成します。

$ mkdir HelloApp && cd HelloApp/
$ cdk init --language=python
$ source .venv/bin/activate
$ pip install -r requirements.txt

4. remote repositoryとしてgithubを登録する

$ git remote add origin <git-hubのhttps url>

5. .envの設定等

今回の例では影響が少ないですが、アカウントIDやバケット名などの情報は環境変数で管理していきます。

$ pip install python-dotenv
$ touch .env
$ echo 'DEV_ACCOUNT_ID="<AWSアカウントID>"' >> .env
$ echo 'STG_ACCOUNT_ID="<AWSアカウントID>"' >> .env
$ echo 'PROD_ACCOUNT_ID="<AWSアカウントID>"' >> .env
$ echo 'MANAGE_ACCOUNT_ID="<AWSアカウントID>"' >> .env

また、cdk独自の設定項目も追加

{
  "app": "python3 app.py",
  "context": {
    "@aws-cdk/core:newStyleStackSynthesis": "true" <= これを追加
  }
}

また、今回使用するcdkのモジュール類をpip installする setup.pyのsetuptools.setup()のinstall_requiresに以下を追加。 バージョンは適宜修正してください。

[
        "aws-cdk.core==1.88.0",
        "aws-cdk.aws-codepipeline==1.88.0",
        "aws-cdk.aws-codepipeline-actions==1.88.0",
        "aws-cdk.aws-codecommit==1.88.0",
        "aws-cdk.aws-codebuild==1.88.0",
        "aws-cdk.pipelines==1.88.0",
        "aws_cdk.aws_lambda==1.88.0",
        "aws-cdk.aws_logs==1.88.0"
]
$ pip install -r requirements.txt

6. クロスアカウントの信頼関係を登録

クロスアカウントでCloudFormationデプロイをする際に、このコマンドで信頼関係を定義することができる

$ cdk bootstrap --profile <manageアカウントのprofile> --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess aws://<manageアカウントのID>/ap-northeast-1
$ cdk bootstrap --profile <devアカウントのprofile> --trust <manageアカウントのID> --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess aws://<devアカウントのID>/ap-northeast-1
$ cdk bootstrap --profile <stgアカウントのprofile> --trust <manageアカウントのID> --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess aws://<stgアカウントのID>/ap-northeast-1
$ cdk bootstrap --profile <prodアカウントのprofile> --trust <manageアカウントのID> --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess aws://<prodアカウントのID>/ap-northeast-1

7. lambdaのstackの追加

今回は簡単なLambdaをサンプルでデプロイします。

mkdir -p functions/hello 
touch functions/hello/index.py 

lambda自体は簡単なreturnをするだけ

# functions/hello/index.py 

def lambda_handler(event, context):
    return {
        "statusCode": 200,
        "body": "hello from lambda"
    }

そして、これらのコードをデプロイするためのCDKを記載します。

$ touch hello_app/hello_app_stack.py
# hello_app/hello_app_stack.py

from aws_cdk import (
    core,
    aws_lambda as lambda_,
    aws_logs as logs,
)

class HelloAppStack(core.Stack):

    def __init__(self, scope: core.Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        hello_app_func = lambda_.Function(self, "HelloAppFunction",
                                              code=lambda_.Code.from_asset('functions/hello'),
                                              handler="index.lambda_handler",
                                              runtime=lambda_.Runtime.PYTHON_3_8,
                                              tracing=lambda_.Tracing.ACTIVE,
                                              timeout=core.Duration.seconds(29),
                                              memory_size=128,
                                              )

        logs.LogGroup(self, 'HelloAppFunctionLogGroup',
                      log_group_name='/aws/lambda/' + hello_app_func.function_name,
                      retention=logs.RetentionDays.TWO_WEEKS
                      )

8. pipelineのstackを追加

(1)PipelineStageのStackでlambdaのStackを読み込み

PipelineStageクラスの中で7.で作成したLambdaStackのクラスを初期化します。

# pipeline_lib/pipeline_stage.py 

from aws_cdk import (
    core
)
from hello_app.hello_app_stack import HelloAppStack

class PipelineStage(core.Stage):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        HelloAppStack(self, self.node.try_get_context('service_name') + '-stack')

つづいて、必要に応じでステージに応じたpipelineのスタックを追加します。 Approvalつきのprodのpipelineをいかに例示します。

#  pipeline_lib/pipeline_master_stack.py

from aws_cdk import (
    aws_codepipeline as codepipeline,
    aws_codepipeline_actions as actions,
    aws_codecommit as codecommit,
    aws_codebuild as codebuild,
    pipelines,
    core
)
from pipeline_lib.pipeline_stage import PipelineStage
import os

STAGE = "stg"
STAGE2 = "prod"

class PipelineMasterStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)



        sourceArtifact = codepipeline.Artifact()
        cloudAssemblyArtifact = codepipeline.Artifact()

        pipeline = pipelines.CdkPipeline(self, 'Pipeline',
                                         pipeline_name=self.node.try_get_context(
                                             'repository_name') + "-{}-pipeline".format(STAGE),
                                         cloud_assembly_artifact=cloudAssemblyArtifact,
                                         source_action=actions.GitHubSourceAction(
                                            action_name='GitHub',
                                            output=sourceArtifact,
                                            oauth_token=core.SecretValue.secrets_manager('github-token'),
                                            owner=self.node.try_get_context(
                                             'owner'),
                                            repo=self.node.try_get_context(
                                             'repository_name'),
                                            branch=STAGE2
                                        ),
                                         synth_action=pipelines.SimpleSynthAction(
                                             synth_command="cdk synth",
                                             install_commands=[
                                                 "pip install --upgrade pip",
                                                 "npm i -g aws-cdk",
                                                 "pip install -r requirements.txt"
                                             ],
                                             source_artifact=sourceArtifact,
                                             cloud_assembly_artifact=cloudAssemblyArtifact,
                                             environment={
                                                 'privileged': True
                                             },
                                             environment_variables={
                                                 'DEV_ACCOUNT_ID': codebuild.BuildEnvironmentVariable(value=os.environ['DEV_ACCOUNT_ID']),
                                                 'STG_ACCOUNT_ID': codebuild.BuildEnvironmentVariable(value=os.environ['STG_ACCOUNT_ID']),
                                                 'PROD_ACCOUNT_ID': codebuild.BuildEnvironmentVariable(value=os.environ['PROD_ACCOUNT_ID']),
                                                 'MANAGE_ACCOUNT_ID': codebuild.BuildEnvironmentVariable(value=os.environ['MANAGE_ACCOUNT_ID'])
                                             }
                                         )
                                         )

        stg = PipelineStage(self, self.node.try_get_context('repository_name') + "-{}".format(STAGE),
                            env={
                                'region': "ap-northeast-1", 'account': os.environ['STG_ACCOUNT_ID']}
                            )

        stg_stage = pipeline.add_application_stage(stg)
        stg_stage.add_actions(actions.ManualApprovalAction(
            action_name="Approval",
            run_order=stg_stage.next_sequential_run_order()
        ))
        prod = PipelineStage(self, self.node.try_get_context('repository_name') + "-{}".format(STAGE2),
                             env={
                                 'region': "ap-northeast-1", 'account': os.environ['PROD_ACCOUNT_ID']}
                             )
        pipeline.add_application_stage(app_stage=prod)

一連の処理の中でcdkのcontextから値をとっている部分があるので、 sampleを動かすためには以下の設定も必要

$ touch cdk.context.json
{
  "owner": "<github repositoryの組織>",
  "repository_name": "<repository名>",
  "service_name": "hello-app"
}

9. app.pyを修正

cdkのエントリーポイントを修正して、pipelinesのstackを構成するように指定する

# app.py

from aws_cdk import core
from pipeline_lib.pipeline_master_stack import PipelineMasterStack
from dotenv import load_dotenv
import os

load_dotenv()

app = core.App()

PipelineMasterStack(
    app,
    "{}-master-pipeline".format(app.node.try_get_context('service_name')),
    env={
        'region': "ap-northeast-1",
        'account': app.account
    }
)

app.synth()

10. pipelineのstackをcdk deploy

ここまでてきていればいいので

$ cdk list --profile <manageアカウントのprofile>

でデプロイ可能なapp名を確認して、

$ cdk deploy --profile <maageアカウントのprofile> <デプロイするapp名>

11. githubにpush

プロジェクト全体をgithubにpushしてpipelineが動作するのを確認する。

まとめ

cdk pipelinesを使用してかなり簡単にパイプラインを作成することができました。 これを参考に雛形のプロジェクトをつくっておけばマイクロサービス * ステージごとなど、たくさんのパイプラインも手間なく作れますね。

以上です。