I'm trying to build a CICD pipeline that supports the very simple process laid out below. I am trying to do this all in AWS (ie, avoiding GitHub Actions), and I do not want to have to manually zip code or transfer anything.
Target process:
Git Push code to GitHub Repository.
AWS Updates code within existing Lambda function and updates the $latest alias accordingly.
Progress so far
I have been able to link AWS CodePipeline to GitHub. When code is pushed to the repository, the pipeline triggers and a compressed file that contains the contents from GitHub is added to an S3 bucket.
Long term I will likely be interested in pre- and post-deployment testing, approvals, etc etc... but for now I just want a simple setup as described above.
Challenge
I cannot fathom how then to actually update the Lamda function now I have this compressed file in S3. I've tried various Build/Deploy things from within the CodeDeloy Pipeline, but I get various errors. I'm not even entirely sure if this entire approach is the best way to go about what I want to do?!
Ask
Is this a valid approach to implementing this kind of CICD pipeline? If no, please suggest alternative and justify why you think it's better.
How do you automatically take the code from within the compressed S3 file and get it in to the Lambda function?
Thanks for your help!
Richard
What you could do, is include an AWS SAM (CloudFormation) template in your repository. You could then in a build step, use the build/package step of AWS SAM, which will create a packaged.yaml CloudFormation template. This template is then usable with the CloudFormation deployment actions.
This is part of a CloudFormation template that sets up such a flow, some things are omitted for brevity:
CodeBuildProject:
Type: AWS::CodeBuild::Project
Properties:
Name: codebuildproject
Description: Package and Deploy
Artifacts:
Type: CODEPIPELINE
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0
EnvironmentVariables:
- Name: IAC_BUCKET
Type: PLAINTEXT
Value: !Sub iac-${AWS::Region}-${AWS::AccountId} # Bucket needed for SAM deployment
ServiceRole: !Ref CodeBuildServiceRole
Source:
Type: CODEPIPELINE
BuildSpec: |
version: 0.2
phases:
install:
runtime-versions:
python: 3.8
commands:
- 'pip install --upgrade --user aws-sam-cli'
build:
commands:
- sam build
- sam package --s3-bucket $IAC_BUCKET --output-template-file packaged.yaml
artifacts:
files:
- 'packaged.yaml'
Pipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
ArtifactStore:
Location: !Sub "codepipeline-${AWS::Region}-${AWS::AccountId}"
Type: S3
Name: deployment-pipeline
RoleArn: !GetAtt PipelineExecutionRole.Arn
Stages:
- Name: Source
Actions:
- YourGithubSourceAction
- Name: Package
Actions:
- Name: SamPackage
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: '1'
Configuration:
ProjectName: !Ref CodeBuildProject
InputArtifacts:
- Name: SourceZip
OutputArtifacts:
- Name: samArtifact
RunOrder: 1
- Name: Deployment
Actions:
- Name: CreateChangeSet
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Version: '1'
Configuration:
ActionMode: "CHANGE_SET_REPLACE"
ChangeSetName: !Sub "${ApplicationName}-${Environment}-changeset"
Capabilities: CAPABILITY_NAMED_IAM
StackName: your-stack-name
RoleArn: !GetAtt PipelineExecutionRole.Arn
ParameterOverrides: !Sub '{ "Environment" : "${Environment}" }'
TemplatePath: 'samArtifact::packaged.yaml'
InputArtifacts:
- Name: samArtifact
RunOrder: 1
- Name: ExecuteChangeSet
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Version: '1'
Configuration:
ActionMode: CHANGE_SET_EXECUTE
ChangeSetName: !Sub ${ApplicationName}-${Environment}-changeset
StackName: your-stack-name
RunOrder: 2
Be sure to have a look at AWS SAM if you're not familiar with it to see all the possibilities and how to construct your template itself.
Related
I am using the following configuration to deploy a couple lambda functions to different stages prod and dev on AWS. Both stages should be protected with an api key which is stored in SSM.
serverless.yml
service: my-service
frameworkVersion: "3"
provider:
name: aws
runtime: nodejs16.x
region: eu-central-1
apiGateway:
apiKeys:
- name: my-apikey
value: ${ssm:my-apikey}
functions:
v1_myfunc:
handler: src/api/myfunc/get_func.get
events:
- http:
path: /v1/myfunc
method: get
private: true
plugins:
- serverless-esbuild
- serverless-offline
- serverless-dotenv-plugin
My deployment scripts look like this:
package.json
"scripts": {
"deploy:dev": "serverless deploy --stage dev",
"deploy:prod": "serverless deploy --stage prod"
}
The problem:
When I deploy one of the stages then everything works fine. But if I deploy the other one afterwards, I always get the following error (in this case I deployed prod first, and then dev):
Deploying my-service to stage dev (eu-central-1)
✖ Stack my-service-dev failed to deploy (46s)
Environment: darwin, node 16.15.0, framework 3.23.0, plugin 6.2.2, SDK 4.3.2
Credentials: Local, "default" profile
Error:
Invalid API Key identifier specified
error Command failed with exit code 1.
Looking into AWS console, I noticed that the generated api key has the same id for both stacks (dev and prod). So, I'm guessing this is where the problem is: Both stacks sharing the same api key instance.
So, I tried to fix this by setting different api key names for each stage:
- name: my-apikey-${self:provider.stage}
value: ${ssm:my-apikey}
But this doesn't solve the problem, as I'm still getting this error:
Invalid API Key identifier specified
Question: How do I have to change my serverless.yml config to fix the issue?
The api key values for both stages need to be different:
service: my-service
frameworkVersion: "3"
provider:
name: aws
runtime: nodejs16.x
region: eu-central-1
apiGateway:
apiKeys:
${self:custom.apiTest.${sls:stage}}
functions:
v1_myfunc:
handler: src/api/myfunc/get_func.get
events:
- http:
path: /v1/myfunc
method: get
private: true
plugins:
- serverless-esbuild
- serverless-offline
- serverless-dotenv-plugin
custom:
apiTest:
dev:
- name: api-key-dev
value: 123 # Needs to be different vs prod!
prod:
- name: api-key-prod
value: 456 # Needs to be different vs dev!
After deploying a Lambda through SAM, I was getting a 403 inside the Lambda when attempting to download from S3. I checked the Lambda's Role in IAM Management Console, and I saw that the Role only had AWSLambdaBasicExecutionRole. However, it should also have the policies from the SAM template for S3 read/write.
Here is a snippet from my SAM template (with some things renamed):
MyFunction:
Type: AWS::Serverless::Function
Properties:
PackageType: Image
Metadata:
DockerTag: my-tag
DockerContext: ./src/stuff
Dockerfile: Dockerfile
Policies:
- Version: '2012-10-17'
- S3ReadPolicy:
BucketName: !Ref MyBucket
- S3WritePolicy:
BucketName: !Ref MyBucket
Shouldn't the S3ReadPolicy and S3WritePolicy be a part of the Lambda's Role?
Is there something I'm missing?
I know I could manually add the policies needed, but obviously I want as much as possible to be happening automatically via SAM.
Other details: I'm not sure if it matters, but for the sake of additional context, the Lambda is part of a Step Function state machine. I'm using the boto3 library for making the request to download from S3. I get a {'Code': '403', 'Message': 'Forbidden'} error from boto3.
(answering my own question). The issue was the Policies section needed to be under Properties.
MyFunction:
Type: AWS::Serverless::Function
Properties:
PackageType: Image
Policies:
- Version: '2012-10-17'
- S3ReadPolicy:
BucketName: !Ref MyBucket
- S3WritePolicy:
BucketName: !Ref MyBucket
Metadata:
DockerTag: my-tag
DockerContext: ./src/stuff
Dockerfile: Dockerfile
I'm trying to use Cloudformation to package and deploy a simple "hello world" serverless app that uses a single Lambda Layer. The issue I'm having is that the LayerVersion section in my CF template file doesn't seem to like the fact that I'm using a !Ref to specify the S3Bucket and S3Key values. I don't want to hard-code these; nothing I've found in the documentation suggests that what I'm trying to do won't work, but it doesn't work :(
Here's the output of the deploy command that's failing:
aws cloudformation deploy --template-file out.yml --stack-name cftest-lambda --parameter-overrides S3BucketNameParameter=cftest-0eddf3f0b289f2c2 S3LambdaLayerNameParameter=cftest-lambda-layer-1602434332.zip --capabilities CAPABILITY_NAMED_IAM
Waiting for changeset to be created..
Failed to create the changeset: Waiter ChangeSetCreateComplete failed: Waiter encountered a terminal failure state Status: FAILED. Reason: Transform AWS::Serverless-2016-10-31 failed with: Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [libs] is invalid. property Content not defined for resource of type AWS::Serverless::LayerVersion
Here is the full CF template file:
cat template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: An AWS Lambda application
Parameters:
S3BucketNameParameter:
Type: String
Description: Bucket name for deployment artifacts
S3LambdaLayerNameParameter:
Type: String
Description: Object name for lambda layer deployment artifact
Resources:
helloworldfunction:
Type: AWS::Serverless::Function
Properties:
Handler: lambda_function.lambda_handler
Runtime: python3.8
CodeUri: hello-world-with-layer/.
Description: Hello world function to test cf using layers
Timeout: 10
# Function's execution role
Policies:
- AWSLambdaBasicExecutionRole
- AWSLambdaReadOnlyAccess
- AWSXrayWriteOnlyAccess
Tracing: Active
Layers:
- !Ref libs
libs:
Type: AWS::Serverless::LayerVersion
Properties:
Content:
S3Bucket: !Ref S3BucketNameParameter
S3Key: !Ref S3LambdaLayerNameParameter
CompatibleRuntimes:
- python3.8
LayerName: hello-world-lib
Description: Dependencies for the hello-world-with-layer app.
Any suggestions on how to approach this correctly?
The correct properties for LayerContent are:
Bucket: String
Key: String
Version: String
However, you are using (different names):
S3Bucket: String
S3Key: String
Have recently setup a basic CodePipeline on AWS (following this guide: https://docs.aws.amazon.com/lambda/latest/dg/build-pipeline.html) which is triggered when there is a new commit on the CodeCommit repository.
But even though after successful execution of the pipeline the lambda function is not updated.
My buildspec.yml:
version: 0.2
phases:
install:
runtime-versions:
nodejs: 12
build:
commands:
- npm install
- export BUCKET=xx-test
- aws cloudformation package --template-file template.yaml --s3-bucket $BUCKET --output-template-file outputtemplate.yml
artifacts:
type: zip
files:
- template.yml
- outputtemplate.yml
My template.yaml:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
helloWorld
API Gateway connectivity helloWorld
Globals:
Function:
Timeout: 3
Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./
Handler: app.lambdaHandler
Runtime: nodejs12.x
Events:
HelloWorld:
Type: Api
Properties:
Path: /hello
Method: get
Is there any additional configuration that need to be done?
From what you've posted, it seems that your last action is CHANGE_SET_REPLACE? If so this would explain why there are no updates to your lambda function. Namely, this only creates a changeset, but doesn't not execute it. It other words, it does not apply it.
You need to add new action after CHANGE_SET_REPLACE action, which is called CHANGE_SET_EXECUTE. This action will take the changes created by the CHANGE_SET_REPLACE, and actually apply it to your stack.
How to add such an action is described in Complete the deployment stage of the tutorial you've provided:
Change sets let you preview the changes that are made before making them, and add approval stages. Add a second action that executes the change set to complete the deployment.
Currently I am able to deploy a lambda by pushing to github. I also automatically deploy a lambda but only because the api gateway is an event in the lambda yaml file
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: Identifies paragraphs in documents and links to the law
Resources:
LambdaParagraphLinker:
Type: 'AWS::Serverless::Function'
Properties:
Handler: LambdaParagraphLinker.lambda_handler
Runtime: python3.6
CodeUri: ./
Description: Identifies paragraphs in documents and links to the
law
MemorySize: 512
Timeout: 10
Events:
Api:
Type: Api
Properties:
Path: /LambdaParagraphLinker
Method: ANY
How can I deploy an api gateway using a swagger file ?
Hands down the best way to do this in codepipeline is by using https://serverless.com/ framework. This replaces every super complicated hack-job and workaround I've previously used. Way less complicated IMO.
Create a codepipeline, link it to src & a codebuild project, set a few permissions, done.
//serverless.yml
service: my-api
provider:
name: aws
runtime: python2.7
functions:
hello:
handler: handler.hello
events:
- http:
path: api/v1/message
method: post
//buildspec.yml
version: 0.2
phases:
install:
commands:
#BUILD
- sudo apt-get update -y
build:
commands:
- echo $environment
- serverless package --stage $environment --region us-east-1
- serverless deploy --stage $environment --region us-east-1
Or torture yourself by doing one of the options below...
You can do this in cloudformation from within code pipeline. Export the swagger spec from within the gatewayapi console and place in the cloudformation template.
AWSTemplateFormatVersion: '2010-09-09'
Resources:
PlayersAPI:
Type: AWS::ApiGateway::RestApi
Properties:
Name: MyApi
Description: API Description
Body:
"SWAGGER HERE"
Hooking this up to lambda is a little bit cumbersome but I can describe the steps. First create a codepipeline project with source, build, and deploy steps.
src should be standard from github or codecommit
build should output a zip file and use buildspec.yml Something like this...
//buildspec.yml
version: 0.1
phases:
install:
commands:
#BUILD
- zip -r lambda.zip . -x *.git*
artifacts:
files:
- '**/*.zip'
- '**/*.yml'
discard-paths: no
Have the build step export an artifact MyAppBuild (or whatever you want to call it)
The final pipeline step is creating the lambda function in this repo as a standalone function through the console(its reusable):
https://github.com/tkntobfrk/codepipeline-lambda-s3
This lambda function downloads the pipeline artifact/zipped lambda function and updates it using boto.
After these steps you could add another step as a cloudformation deploy step. Connect it to the lambda function that you've just deployed.
If you are dealing with multiple environments you could create lambda functions and gatewayapi cloudformation template for each environment then run them in sequence.
stage 1: src
stage 2: build
stage 3: deploy lambda test, deploy gateway api cloudformation test
stage 4: validate test
stage 5: deploy lambda prod, deploy gateway api cloudformation prod
Using straight AWS serverless like this works too. However, you need to use a standard artifact location for the uri's. The DefinitionUri: for the API can be the exported swagger from the gatewayapi console.
//cloudformation.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
MySimpleFunction:
Type: AWS::Serverless::Function
Properties:
Handler: app.lambda_handler
Runtime: python2.7
CodeUri: s3://somebucket/somezip.zip
MyAPI:
Type: AWS::Serverless::Api
Properties:
StageName: prod
DefinitionUri: s3://somebucket/somezip.zip
AWS::Serverless::Api
https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessapi
You can find Swagger docs all over the place, and docs on API Gateway extensions are in the developer guide. I would start by going into the API Gateway console and look at the API that Lambda creates for you. You can go to the 'Stages' page and for any stage, you can Export the API as Swagger.